Add support for executing commands within a container (#63)
* Add support for executing commands within a container * Add WebSocketNamespacedPodPortForwardAsync * Add Attach functionality * Simplify code
This commit is contained in:
committed by
Brendan Burns
parent
14b59f6511
commit
2a54a8c370
130
src/IKubernetes.WebSocket.cs
Normal file
130
src/IKubernetes.WebSocket.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace k8s
|
||||||
|
{
|
||||||
|
public partial interface IKubernetes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a command in a pod.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name='name'>
|
||||||
|
/// name of the Pod
|
||||||
|
/// </param>
|
||||||
|
/// <param name='namespace'>
|
||||||
|
/// object name and auth scope, such as for teams and projects
|
||||||
|
/// </param>
|
||||||
|
/// <param name='command'>
|
||||||
|
/// Command is the remote command to execute. argv array. Not executed within a
|
||||||
|
/// shell.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='container'>
|
||||||
|
/// Container in which to execute the command. Defaults to only container if
|
||||||
|
/// there is only one container in the pod.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='stderr'>
|
||||||
|
/// Redirect the standard error stream of the pod for this call. Defaults to
|
||||||
|
/// <see langword="true"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='stdin'>
|
||||||
|
/// Redirect the standard input stream of the pod for this call. Defaults to
|
||||||
|
/// <see langword="true"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='stdout'>
|
||||||
|
/// Redirect the standard output stream of the pod for this call. Defaults to
|
||||||
|
/// <see langword="true"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='tty'>
|
||||||
|
/// TTY if true indicates that a tty will be allocated for the exec call.
|
||||||
|
/// Defaults to <see langword="true"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='customHeaders'>
|
||||||
|
/// Headers that will be added to request.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='cancellationToken'>
|
||||||
|
/// The cancellation token.
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Thrown when a required parameter is null
|
||||||
|
/// </exception>
|
||||||
|
/// <return>
|
||||||
|
/// A <see cref="ClientWebSocket"/> which can be used to communicate with the process running in the pod.
|
||||||
|
/// </return>
|
||||||
|
Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", string command = "/bin/bash", string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start port forwarding one or more ports of a pod.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name='name'>
|
||||||
|
/// The name of the Pod
|
||||||
|
/// </param>
|
||||||
|
/// <param name='namespace'>
|
||||||
|
/// The object name and auth scope, such as for teams and projects
|
||||||
|
/// </param>
|
||||||
|
/// <param name='ports'>
|
||||||
|
/// List of ports to forward.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='customHeaders'>
|
||||||
|
/// The headers that will be added to request.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='cancellationToken'>
|
||||||
|
/// The cancellation token.
|
||||||
|
/// </param>
|
||||||
|
Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable<int> ports, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// connect GET requests to attach of Pod
|
||||||
|
/// </summary>
|
||||||
|
/// <param name='name'>
|
||||||
|
/// name of the Pod
|
||||||
|
/// </param>
|
||||||
|
/// <param name='namespace'>
|
||||||
|
/// object name and auth scope, such as for teams and projects
|
||||||
|
/// </param>
|
||||||
|
/// <param name='container'>
|
||||||
|
/// The container in which to execute the command. Defaults to only container
|
||||||
|
/// if there is only one container in the pod.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='stderr'>
|
||||||
|
/// Stderr if true indicates that stderr is to be redirected for the attach
|
||||||
|
/// call. Defaults to true.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='stdin'>
|
||||||
|
/// Stdin if true, redirects the standard input stream of the pod for this
|
||||||
|
/// call. Defaults to false.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='stdout'>
|
||||||
|
/// Stdout if true indicates that stdout is to be redirected for the attach
|
||||||
|
/// call. Defaults to true.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='tty'>
|
||||||
|
/// TTY if true indicates that a tty will be allocated for the attach call.
|
||||||
|
/// This is passed through the container runtime so the tty is allocated on the
|
||||||
|
/// worker node by the container runtime. Defaults to false.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='customHeaders'>
|
||||||
|
/// Headers that will be added to request.
|
||||||
|
/// </param>
|
||||||
|
/// <param name='cancellationToken'>
|
||||||
|
/// The cancellation token.
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="HttpOperationException">
|
||||||
|
/// Thrown when the operation returned an invalid status code
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="SerializationException">
|
||||||
|
/// Thrown when unable to deserialize the response
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="ValidationException">
|
||||||
|
/// Thrown when a required parameter is null
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="System.ArgumentNullException">
|
||||||
|
/// Thrown when a required parameter is null
|
||||||
|
/// </exception>
|
||||||
|
/// <return>
|
||||||
|
/// A response object containing the response body and response headers.
|
||||||
|
/// </return>
|
||||||
|
Task<WebSocket> WebSocketNamespacedPodAttachAsync(string name, string @namespace, string container = default(string), bool stderr = true, bool stdin = false, bool stdout = true, bool tty = false, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/Kubernetes.WebSocket.cs
Normal file
246
src/Kubernetes.WebSocket.cs
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Rest;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace k8s
|
||||||
|
{
|
||||||
|
public partial class Kubernetes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a function which returns a <see cref="WebSocketBuilder"/> which <see cref="Kubernetes"/> will use to
|
||||||
|
/// create a new <see cref="WebSocket"/> connection to the Kubernetes cluster.
|
||||||
|
/// </summary>
|
||||||
|
public Func<WebSocketBuilder> CreateWebSocketBuilder { get; set; } = () => new WebSocketBuilder();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", string command = "/bin/sh", string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@namespace == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(@namespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracing
|
||||||
|
bool _shouldTrace = ServiceClientTracing.IsEnabled;
|
||||||
|
string _invocationId = null;
|
||||||
|
if (_shouldTrace)
|
||||||
|
{
|
||||||
|
_invocationId = ServiceClientTracing.NextInvocationId.ToString();
|
||||||
|
Dictionary<string, object> tracingParameters = new Dictionary<string, object>();
|
||||||
|
tracingParameters.Add("command", command);
|
||||||
|
tracingParameters.Add("container", container);
|
||||||
|
tracingParameters.Add("name", name);
|
||||||
|
tracingParameters.Add("namespace", @namespace);
|
||||||
|
tracingParameters.Add("stderr", stderr);
|
||||||
|
tracingParameters.Add("stdin", stdin);
|
||||||
|
tracingParameters.Add("stdout", stdout);
|
||||||
|
tracingParameters.Add("tty", tty);
|
||||||
|
tracingParameters.Add("cancellationToken", cancellationToken);
|
||||||
|
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodExecAsync), tracingParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct URL
|
||||||
|
var uriBuilder = new UriBuilder(BaseUri);
|
||||||
|
uriBuilder.Scheme = BaseUri.Scheme == "https" ? "wss" : "ws";
|
||||||
|
|
||||||
|
if (!uriBuilder.Path.EndsWith("/"))
|
||||||
|
{
|
||||||
|
uriBuilder.Path += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/exec";
|
||||||
|
|
||||||
|
|
||||||
|
uriBuilder.Query = QueryHelpers.AddQueryString(string.Empty, new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "command", command},
|
||||||
|
{ "container", container},
|
||||||
|
{ "stderr", stderr ? "1": "0"},
|
||||||
|
{ "stdin", stdin ? "1": "0"},
|
||||||
|
{ "stdout", stdout ? "1": "0"},
|
||||||
|
{ "tty", tty ? "1": "0"}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable<int> ports, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@namespace == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(@namespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ports == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(ports));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracing
|
||||||
|
bool _shouldTrace = ServiceClientTracing.IsEnabled;
|
||||||
|
string _invocationId = null;
|
||||||
|
if (_shouldTrace)
|
||||||
|
{
|
||||||
|
_invocationId = ServiceClientTracing.NextInvocationId.ToString();
|
||||||
|
Dictionary<string, object> tracingParameters = new Dictionary<string, object>();
|
||||||
|
tracingParameters.Add("name", name);
|
||||||
|
tracingParameters.Add("@namespace", @namespace);
|
||||||
|
tracingParameters.Add("ports", ports);
|
||||||
|
tracingParameters.Add("cancellationToken", cancellationToken);
|
||||||
|
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodPortForwardAsync), tracingParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct URL
|
||||||
|
var uriBuilder = new UriBuilder(this.BaseUri);
|
||||||
|
uriBuilder.Scheme = this.BaseUri.Scheme == "https" ? "wss" : "ws";
|
||||||
|
|
||||||
|
if (!uriBuilder.Path.EndsWith("/"))
|
||||||
|
{
|
||||||
|
uriBuilder.Path += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/portforward";
|
||||||
|
|
||||||
|
foreach (var port in ports)
|
||||||
|
{
|
||||||
|
uriBuilder.Query += $"ports={port}&";
|
||||||
|
}
|
||||||
|
|
||||||
|
return StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Task<WebSocket> WebSocketNamespacedPodAttachAsync(string name, string @namespace, string container = default(string), bool stderr = true, bool stdin = false, bool stdout = true, bool tty = false, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@namespace == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(@namespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracing
|
||||||
|
bool _shouldTrace = ServiceClientTracing.IsEnabled;
|
||||||
|
string _invocationId = null;
|
||||||
|
if (_shouldTrace)
|
||||||
|
{
|
||||||
|
_invocationId = ServiceClientTracing.NextInvocationId.ToString();
|
||||||
|
Dictionary<string, object> tracingParameters = new Dictionary<string, object>();
|
||||||
|
tracingParameters.Add("container", container);
|
||||||
|
tracingParameters.Add("name", name);
|
||||||
|
tracingParameters.Add("namespace", @namespace);
|
||||||
|
tracingParameters.Add("stderr", stderr);
|
||||||
|
tracingParameters.Add("stdin", stdin);
|
||||||
|
tracingParameters.Add("stdout", stdout);
|
||||||
|
tracingParameters.Add("tty", tty);
|
||||||
|
tracingParameters.Add("cancellationToken", cancellationToken);
|
||||||
|
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodAttachAsync), tracingParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct URL
|
||||||
|
var uriBuilder = new UriBuilder(this.BaseUri);
|
||||||
|
uriBuilder.Scheme = this.BaseUri.Scheme == "https" ? "wss" : "ws";
|
||||||
|
|
||||||
|
if (!uriBuilder.Path.EndsWith("/"))
|
||||||
|
{
|
||||||
|
uriBuilder.Path += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/portforward";
|
||||||
|
|
||||||
|
uriBuilder.Query = QueryHelpers.AddQueryString(string.Empty, new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "container", container},
|
||||||
|
{ "stderr", stderr ? "1": "0"},
|
||||||
|
{ "stdin", stdin ? "1": "0"},
|
||||||
|
{ "stdout", stdout ? "1": "0"},
|
||||||
|
{ "tty", tty ? "1": "0"}
|
||||||
|
});
|
||||||
|
|
||||||
|
return StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task<WebSocket> StreamConnectAsync(Uri uri, string invocationId = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
bool _shouldTrace = ServiceClientTracing.IsEnabled;
|
||||||
|
|
||||||
|
// Create WebSocket transport objects
|
||||||
|
WebSocketBuilder webSocketBuilder = this.CreateWebSocketBuilder();
|
||||||
|
|
||||||
|
// Set Headers
|
||||||
|
if (customHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (var _header in customHeaders)
|
||||||
|
{
|
||||||
|
webSocketBuilder.SetRequestHeader(_header.Key, string.Join(" ", _header.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Credentials
|
||||||
|
foreach (var cert in this.HttpClientHandler.ClientCertificates)
|
||||||
|
{
|
||||||
|
webSocketBuilder.AddClientCertificate(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequestMessage message = new HttpRequestMessage();
|
||||||
|
await this.Credentials.ProcessHttpRequestAsync(message, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var _header in message.Headers)
|
||||||
|
{
|
||||||
|
webSocketBuilder.SetRequestHeader(_header.Key, string.Join(" ", _header.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Request
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
WebSocket webSocket = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
webSocket = await webSocketBuilder.BuildAndConnectAsync(uri, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (_shouldTrace)
|
||||||
|
{
|
||||||
|
ServiceClientTracing.Error(invocationId, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_shouldTrace)
|
||||||
|
{
|
||||||
|
ServiceClientTracing.Exit(invocationId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return webSocket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<Authors>The Kubernetes Project Authors</Authors>
|
<Authors>The Kubernetes Project Authors</Authors>
|
||||||
<Copyright>2017 The Kubernetes Project Authors</Copyright>
|
<Copyright>2017 The Kubernetes Project Authors</Copyright>
|
||||||
<Description>Client library for the Kubernetes open source container orchestrator.</Description>
|
<Description>Client library for the Kubernetes open source container orchestrator.</Description>
|
||||||
|
|
||||||
<PackageLicenseUrl>https://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
|
<PackageLicenseUrl>https://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
|
||||||
<PackageProjectUrl>https://github.com/kubernetes-client/csharp</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/kubernetes-client/csharp</PackageProjectUrl>
|
||||||
<PackageTags>kubernetes;docker;containers;</PackageTags>
|
<PackageTags>kubernetes;docker;containers;</PackageTags>
|
||||||
@@ -23,5 +23,6 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||||
<PackageReference Include="YamlDotNet.NetCore" Version="1.0.0" />
|
<PackageReference Include="YamlDotNet.NetCore" Version="1.0.0" />
|
||||||
|
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
43
src/WebSocketBuilder.cs
Normal file
43
src/WebSocketBuilder.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace k8s
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="WebSocketBuilder"/> creates a new <see cref="WebSocket"/> object which connects to a remote WebSocket.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// By default, this uses the .NET <see cref="ClientWebSocket"/> class, but you can inherit from this class and change it to
|
||||||
|
/// use any class which inherits from <see cref="WebSocket"/>, should you want to use a third party framework or mock the requests.
|
||||||
|
/// </remarks>
|
||||||
|
public class WebSocketBuilder
|
||||||
|
{
|
||||||
|
protected ClientWebSocket WebSocket { get; private set; } = new ClientWebSocket();
|
||||||
|
|
||||||
|
public WebSocketBuilder()
|
||||||
|
{
|
||||||
|
this.WebSocket = new ClientWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual WebSocketBuilder SetRequestHeader(string headerName, string headerValue)
|
||||||
|
{
|
||||||
|
this.WebSocket.Options.SetRequestHeader(headerName, headerValue);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual WebSocketBuilder AddClientCertificate(X509Certificate certificate)
|
||||||
|
{
|
||||||
|
this.WebSocket.Options.ClientCertificates.Add(certificate);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<WebSocket> BuildAndConnectAsync(Uri uri, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await this.WebSocket.ConnectAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||||
|
return this.WebSocket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
140
tests/Kubernetes.WebSockets.Tests.cs
Normal file
140
tests/Kubernetes.WebSockets.Tests.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
using k8s.tests.Mock;
|
||||||
|
using Microsoft.Rest;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace k8s.tests
|
||||||
|
{
|
||||||
|
public class KubernetesExecTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tests the <see cref="Kubernetes.WebSocketNamespacedPodExecWithHttpMessagesAsync(string, string, string, string, bool, bool, bool, bool, Dictionary{string, List{string}}, CancellationToken)"/>
|
||||||
|
/// method. Changes the <see cref="WebSocketBuilder"/> used by the client with a mock builder, so this test never hits the network.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task"/> which represents the asynchronous test.
|
||||||
|
/// </returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task WebSocketNamespacedPodExecAsync()
|
||||||
|
{
|
||||||
|
var credentials = new BasicAuthenticationCredentials()
|
||||||
|
{
|
||||||
|
UserName = "my-user",
|
||||||
|
Password = "my-secret-password"
|
||||||
|
};
|
||||||
|
|
||||||
|
Kubernetes client = new Kubernetes(credentials);
|
||||||
|
client.BaseUri = new Uri("http://localhost");
|
||||||
|
|
||||||
|
MockWebSocketBuilder mockWebSocketBuilder = new MockWebSocketBuilder();
|
||||||
|
client.CreateWebSocketBuilder = () => mockWebSocketBuilder;
|
||||||
|
|
||||||
|
var webSocket = await client.WebSocketNamespacedPodExecAsync(
|
||||||
|
name: "mypod",
|
||||||
|
@namespace: "mynamespace",
|
||||||
|
command: "/bin/bash",
|
||||||
|
container: "mycontainer",
|
||||||
|
stderr: true,
|
||||||
|
stdin: true,
|
||||||
|
stdout: true,
|
||||||
|
tty: true,
|
||||||
|
customHeaders: new Dictionary<string, List<string>>()
|
||||||
|
{
|
||||||
|
{ "X-My-Header", new List<string>() { "myHeaderValue", "myHeaderValue2"} }
|
||||||
|
},
|
||||||
|
cancellationToken: CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var expectedHeaders = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "X-My-Header", "myHeaderValue myHeaderValue2" },
|
||||||
|
{ "Authorization", "Basic bXktdXNlcjpteS1zZWNyZXQtcGFzc3dvcmQ=" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(mockWebSocketBuilder.PublicWebSocket, webSocket); // Did the method return the correct web socket?
|
||||||
|
Assert.Equal(new Uri("ws://localhost:80/api/v1/namespaces/mynamespace/pods/mypod/exec?command=%2Fbin%2Fbash&container=mycontainer&stderr=1&stdin=1&stdout=1&tty=1"), mockWebSocketBuilder.Uri); // Did we connect to the correct URL?
|
||||||
|
Assert.Empty(mockWebSocketBuilder.Certificates); // No certificates were used in this test
|
||||||
|
Assert.Equal(expectedHeaders, mockWebSocketBuilder.RequestHeaders); // Did we use the expected headers
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WebSocketNamespacedPodPortForwardAsync()
|
||||||
|
{
|
||||||
|
var credentials = new BasicAuthenticationCredentials()
|
||||||
|
{
|
||||||
|
UserName = "my-user",
|
||||||
|
Password = "my-secret-password"
|
||||||
|
};
|
||||||
|
|
||||||
|
Kubernetes client = new Kubernetes(credentials);
|
||||||
|
client.BaseUri = new Uri("http://localhost");
|
||||||
|
|
||||||
|
MockWebSocketBuilder mockWebSocketBuilder = new MockWebSocketBuilder();
|
||||||
|
client.CreateWebSocketBuilder = () => mockWebSocketBuilder;
|
||||||
|
|
||||||
|
var webSocket = await client.WebSocketNamespacedPodPortForwardAsync(
|
||||||
|
name: "mypod",
|
||||||
|
@namespace: "mynamespace",
|
||||||
|
ports: new int[] { 80, 8080 },
|
||||||
|
customHeaders: new Dictionary<string, List<string>>()
|
||||||
|
{
|
||||||
|
{ "X-My-Header", new List<string>() { "myHeaderValue", "myHeaderValue2"} }
|
||||||
|
},
|
||||||
|
cancellationToken: CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var expectedHeaders = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "X-My-Header", "myHeaderValue myHeaderValue2" },
|
||||||
|
{ "Authorization", "Basic bXktdXNlcjpteS1zZWNyZXQtcGFzc3dvcmQ=" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(mockWebSocketBuilder.PublicWebSocket, webSocket); // Did the method return the correct web socket?
|
||||||
|
Assert.Equal(new Uri("ws://localhost/api/v1/namespaces/mynamespace/pods/mypod/portforward?ports=80&ports=8080&"), mockWebSocketBuilder.Uri); // Did we connect to the correct URL?
|
||||||
|
Assert.Empty(mockWebSocketBuilder.Certificates); // No certificates were used in this test
|
||||||
|
Assert.Equal(expectedHeaders, mockWebSocketBuilder.RequestHeaders); // Did we use the expected headers
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WebSocketNamespacedPodAttachAsync()
|
||||||
|
{
|
||||||
|
var credentials = new BasicAuthenticationCredentials()
|
||||||
|
{
|
||||||
|
UserName = "my-user",
|
||||||
|
Password = "my-secret-password"
|
||||||
|
};
|
||||||
|
|
||||||
|
Kubernetes client = new Kubernetes(credentials);
|
||||||
|
client.BaseUri = new Uri("http://localhost");
|
||||||
|
|
||||||
|
MockWebSocketBuilder mockWebSocketBuilder = new MockWebSocketBuilder();
|
||||||
|
client.CreateWebSocketBuilder = () => mockWebSocketBuilder;
|
||||||
|
|
||||||
|
var webSocket = await client.WebSocketNamespacedPodAttachAsync(
|
||||||
|
name: "mypod",
|
||||||
|
@namespace: "mynamespace",
|
||||||
|
container: "my-container",
|
||||||
|
stderr: true,
|
||||||
|
stdin: true,
|
||||||
|
stdout: true,
|
||||||
|
tty: true,
|
||||||
|
customHeaders: new Dictionary<string, List<string>>()
|
||||||
|
{
|
||||||
|
{ "X-My-Header", new List<string>() { "myHeaderValue", "myHeaderValue2"} }
|
||||||
|
},
|
||||||
|
cancellationToken: CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var expectedHeaders = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "X-My-Header", "myHeaderValue myHeaderValue2" },
|
||||||
|
{ "Authorization", "Basic bXktdXNlcjpteS1zZWNyZXQtcGFzc3dvcmQ=" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(mockWebSocketBuilder.PublicWebSocket, webSocket); // Did the method return the correct web socket?
|
||||||
|
Assert.Equal(new Uri("ws://localhost:80/api/v1/namespaces/mynamespace/pods/mypod/portforward?container=my-container&stderr=1&stdin=1&stdout=1&tty=1"), mockWebSocketBuilder.Uri); // Did we connect to the correct URL?
|
||||||
|
Assert.Empty(mockWebSocketBuilder.Certificates); // No certificates were used in this test
|
||||||
|
Assert.Equal(expectedHeaders, mockWebSocketBuilder.RequestHeaders); // Did we use the expected headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
tests/Mock/MockWebSocketBuilder.cs
Normal file
39
tests/Mock/MockWebSocketBuilder.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace k8s.tests.Mock
|
||||||
|
{
|
||||||
|
public class MockWebSocketBuilder : WebSocketBuilder
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> RequestHeaders { get; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
public Collection<X509Certificate> Certificates { get; } = new Collection<X509Certificate>();
|
||||||
|
|
||||||
|
public Uri Uri { get; private set; }
|
||||||
|
|
||||||
|
public WebSocket PublicWebSocket => this.WebSocket;
|
||||||
|
|
||||||
|
public override WebSocketBuilder AddClientCertificate(X509Certificate certificate)
|
||||||
|
{
|
||||||
|
this.Certificates.Add(certificate);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<WebSocket> BuildAndConnectAsync(Uri uri, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
this.Uri = uri;
|
||||||
|
return Task.FromResult<WebSocket>(this.WebSocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override WebSocketBuilder SetRequestHeader(string headerName, string headerValue)
|
||||||
|
{
|
||||||
|
this.RequestHeaders.Add(headerName, headerValue);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user