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>
|
||||
<Copyright>2017 The Kubernetes Project Authors</Copyright>
|
||||
<Description>Client library for the Kubernetes open source container orchestrator.</Description>
|
||||
|
||||
|
||||
<PackageLicenseUrl>https://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
|
||||
<PackageProjectUrl>https://github.com/kubernetes-client/csharp</PackageProjectUrl>
|
||||
<PackageTags>kubernetes;docker;containers;</PackageTags>
|
||||
@@ -23,5 +23,6 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="YamlDotNet.NetCore" Version="1.0.0" />
|
||||
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2"/>
|
||||
</ItemGroup>
|
||||
</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