GUACAMOLE-623: Generate Kubernetes API endpoint dynamically.

This commit is contained in:
Michael Jumper 2018-09-10 18:39:06 -07:00
parent 34f8f8b30d
commit ed56093888
6 changed files with 311 additions and 8 deletions

View File

@ -29,6 +29,7 @@ libguac_client_kubernetes_la_SOURCES = \
pipe.c \ pipe.c \
settings.c \ settings.c \
kubernetes.c \ kubernetes.c \
url.c \
user.c user.c
noinst_HEADERS = \ noinst_HEADERS = \
@ -38,6 +39,7 @@ noinst_HEADERS = \
pipe.h \ pipe.h \
settings.h \ settings.h \
kubernetes.h \ kubernetes.h \
url.h \
user.h user.h
libguac_client_kubernetes_la_CFLAGS = \ libguac_client_kubernetes_la_CFLAGS = \

View File

@ -21,6 +21,7 @@
#include "common/recording.h" #include "common/recording.h"
#include "kubernetes.h" #include "kubernetes.h"
#include "terminal/terminal.h" #include "terminal/terminal.h"
#include "url.h"
#include <guacamole/client.h> #include <guacamole/client.h>
#include <guacamole/protocol.h> #include <guacamole/protocol.h>
@ -345,6 +346,28 @@ void* guac_kubernetes_client_thread(void* data) {
guac_kubernetes_settings* settings = kubernetes_client->settings; guac_kubernetes_settings* settings = kubernetes_client->settings;
pthread_t input_thread; pthread_t input_thread;
char endpoint_path[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
/* Verify that the pod name was specified (it's always required) */
if (settings->kubernetes_pod == NULL) {
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
"The name of the Kubernetes pod is a required parameter.");
goto fail;
}
/* Generate endpoint for attachment URL */
if (guac_kubernetes_endpoint_attach(endpoint_path, sizeof(endpoint_path),
settings->kubernetes_namespace,
settings->kubernetes_pod,
settings->kubernetes_container)) {
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
"Unable to generate path for Kubernetes API endpoint: "
"Resulting path too long");
goto fail;
}
guac_client_log(client, GUAC_LOG_DEBUG, "The endpoint for attaching to "
"the requested Kubernetes pod is \"%s\".", endpoint_path);
/* Set up screen recording, if requested */ /* Set up screen recording, if requested */
if (settings->recording_path != NULL) { if (settings->recording_path != NULL) {
@ -434,9 +457,9 @@ void* guac_kubernetes_client_thread(void* data) {
goto fail; goto fail;
} }
/* FIXME: Generate path dynamically */ /* Generate path dynamically */
connection_info.context = kubernetes_client->context; connection_info.context = kubernetes_client->context;
connection_info.path = "/api/v1/namespaces/default/pods/my-shell-68974bb7f7-rpjgr/attach?container=my-shell&stdin=true&stdout=true&tty=true"; connection_info.path = endpoint_path;
/* Open WebSocket connection to Kubernetes */ /* Open WebSocket connection to Kubernetes */
kubernetes_client->wsi = lws_client_connect_via_info(&connection_info); kubernetes_client->wsi = lws_client_connect_via_info(&connection_info);

View File

@ -32,6 +32,9 @@
const char* GUAC_KUBERNETES_CLIENT_ARGS[] = { const char* GUAC_KUBERNETES_CLIENT_ARGS[] = {
"hostname", "hostname",
"port", "port",
"namespace",
"pod",
"container",
"use-ssl", "use-ssl",
"client-cert-file", "client-cert-file",
"client-key-file", "client-key-file",
@ -67,6 +70,24 @@ enum KUBERNETES_ARGS_IDX {
*/ */
IDX_PORT, IDX_PORT,
/**
* The name of the Kubernetes namespace of the pod containing the container
* being attached to. If omitted, the default namespace will be used.
*/
IDX_NAMESPACE,
/**
* The name of the Kubernetes pod containing with the container being
* attached to. Required.
*/
IDX_POD,
/**
* The name of the container to attach to. If omitted, the first container
* in the pod will be used.
*/
IDX_CONTAINER,
/** /**
* Whether SSL/TLS should be used. If omitted, SSL/TLS will not be used. * Whether SSL/TLS should be used. If omitted, SSL/TLS will not be used.
*/ */
@ -215,11 +236,31 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user,
guac_kubernetes_settings* settings = guac_kubernetes_settings* settings =
calloc(1, sizeof(guac_kubernetes_settings)); calloc(1, sizeof(guac_kubernetes_settings));
/* Read parameters */ /* Read hostname */
settings->hostname = settings->hostname =
guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
IDX_HOSTNAME, ""); IDX_HOSTNAME, "");
/* Read port */
settings->port =
guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
IDX_PORT, GUAC_KUBERNETES_DEFAULT_PORT);
/* Read Kubernetes namespace */
settings->kubernetes_namespace =
guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
IDX_NAMESPACE, GUAC_KUBERNETES_DEFAULT_NAMESPACE);
/* Read name of Kubernetes pod (required) */
settings->kubernetes_pod =
guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
IDX_POD, NULL);
/* Read container of pod (optional) */
settings->kubernetes_container =
guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
IDX_CONTAINER, NULL);
/* Parse whether SSL should be used */ /* Parse whether SSL should be used */
settings->use_ssl = settings->use_ssl =
guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
@ -276,11 +317,6 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user,
settings->height = user->info.optimal_height; settings->height = user->info.optimal_height;
settings->resolution = user->info.optimal_resolution; settings->resolution = user->info.optimal_resolution;
/* Read port */
settings->port =
guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
IDX_PORT, GUAC_KUBERNETES_DEFAULT_PORT);
/* Read typescript path */ /* Read typescript path */
settings->typescript_path = settings->typescript_path =
guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv,
@ -341,6 +377,11 @@ void guac_kubernetes_settings_free(guac_kubernetes_settings* settings) {
/* Free network connection information */ /* Free network connection information */
free(settings->hostname); free(settings->hostname);
/* Free Kubernetes pod/container details */
free(settings->kubernetes_namespace);
free(settings->kubernetes_pod);
free(settings->kubernetes_container);
/* Free SSL/TLS details */ /* Free SSL/TLS details */
free(settings->client_cert_file); free(settings->client_cert_file);
free(settings->client_key_file); free(settings->client_key_file);

View File

@ -44,6 +44,12 @@
*/ */
#define GUAC_KUBERNETES_DEFAULT_PORT 8080 #define GUAC_KUBERNETES_DEFAULT_PORT 8080
/**
* The name of the Kubernetes namespace that should be used by default if no
* specific Kubernetes namespace is provided.
*/
#define GUAC_KUBERNETES_DEFAULT_NAMESPACE "default"
/** /**
* The filename to use for the typescript, if not specified. * The filename to use for the typescript, if not specified.
*/ */
@ -76,6 +82,24 @@ typedef struct guac_kubernetes_settings {
*/ */
int port; int port;
/**
* The name of the Kubernetes namespace of the pod containing the container
* being attached to.
*/
char* kubernetes_namespace;
/**
* The name of the Kubernetes pod containing with the container being
* attached to.
*/
char* kubernetes_pod;
/**
* The name of the container to attach to, or NULL to arbitrarily attach to
* the first container in the pod.
*/
char* kubernetes_container;
/** /**
* Whether SSL/TLS should be used. * Whether SSL/TLS should be used.
*/ */

View File

@ -0,0 +1,126 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "config.h"
#include "url.h"
#include <stdio.h>
#include <string.h>
static int guac_kubernetes_is_url_safe(char c) {
return (c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9')
|| strchr("-_.!~*'()", c) != NULL;
}
int guac_kubernetes_escape_url_component(char* output, int length,
const char* str) {
char* current = output;
while (*str != '\0') {
char c = *str;
/* Store alphanumeric characters verbatim */
if (guac_kubernetes_is_url_safe(c)) {
/* Verify space exists for single character */
if (length < 1)
return 1;
*(current++) = c;
length--;
}
/* Escape EVERYTHING else as hex */
else {
/* Verify space exists for hex-encoded character */
if (length < 4)
return 1;
snprintf(current, 4, "%%%02X", (int) c);
current += 3;
length -= 3;
}
/* Next character */
str++;
}
/* Verify space exists for null terminator */
if (length < 1)
return 1;
/* Append null terminator */
*current = '\0';
return 0;
}
int guac_kubernetes_endpoint_attach(char* buffer, int length,
const char* kubernetes_namespace, const char* kubernetes_pod,
const char* kubernetes_container) {
int written;
char escaped_namespace[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
char escaped_pod[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
char escaped_container[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
/* Escape Kubernetes namespace */
if (guac_kubernetes_escape_url_component(escaped_namespace,
sizeof(escaped_namespace), kubernetes_namespace))
return 1;
/* Escape name of Kubernetes pod */
if (guac_kubernetes_escape_url_component(escaped_pod,
sizeof(escaped_pod), kubernetes_pod))
return 1;
/* Generate attachment endpoint URL */
if (kubernetes_container != NULL) {
/* Escape container name */
if (guac_kubernetes_escape_url_component(escaped_container,
sizeof(escaped_container), kubernetes_container))
return 1;
written = snprintf(buffer, length,
"/api/v1/namespaces/%s/pods/%s/attach"
"?container=%s&stdin=true&stdout=true&tty=true",
escaped_namespace, escaped_pod, escaped_container);
}
else {
written = snprintf(buffer, length,
"/api/v1/namespaces/%s/pods/%s/attach"
"?stdin=true&stdout=true&tty=true",
escaped_namespace, escaped_pod);
}
/* Endpoint URL was successfully generated if it was written to the given
* buffer without truncation */
return !(written < length - 1);
}

View File

@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#ifndef GUAC_KUBERNETES_URL_H
#define GUAC_KUBERNETES_URL_H
#include "config.h"
/**
* The maximum number of characters allowed in the full path for any Kubernetes
* endpoint.
*/
#define GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH 1024
/**
* Escapes the given string such that it can be included safely within a URL.
* This function duplicates the behavior of JavaScript's encodeURIComponent(),
* escaping all but the following characters: A-Z a-z 0-9 - _ . ! ~ * ' ( )
*
* @param output
* The buffer which should receive the escaped string. This buffer may be
* touched even if escaping is unsuccessful.
*
* @param length
* The number of bytes available in the given output buffer.
*
* @param str
* The string to escape.
*
* @return
* Zero if the string was successfully escaped and written into the
* provided output buffer without being truncated, including null
* terminator, non-zero otherwise.
*/
int guac_kubernetes_escape_url_component(char* output, int length,
const char* str);
/**
* Generates the full path to the Kubernetes API endpoint which handles
* attaching to running containers within specific pods. Values within the path
* will be URL-escaped as necessary.
*
* @param buffer
* The buffer which should receive the endpoint path. This buffer may be
* touched even if the endpoint path could not be generated.
*
* @param length
* The number of bytes available in the given buffer.
*
* @param kubernetes_namespace
* The name of the Kubernetes namespace of the pod containing the container
* being attached to.
*
* @param kubernetes_pod
* The name of the Kubernetes pod containing with the container being
* attached to.
*
* @param kubernetes_container
* The name of the container to attach to, or NULL to arbitrarily attach
* to the first container in the pod.
*
* @return
* Zero if the endpoint path was successfully written to the provided
* buffer, non-zero if insufficient space exists within the buffer.
*/
int guac_kubernetes_endpoint_attach(char* buffer, int length,
const char* kubernetes_namespace, const char* kubernetes_pod,
const char* kubernetes_container);
#endif