Prototype Exec API (#271)
This commit is contained in:
committed by
Kubernetes Prow Robot
parent
644cf76009
commit
466e33995d
24
src/KubernetesClient/ExecAsyncCallback.cs
Normal file
24
src/KubernetesClient/ExecAsyncCallback.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace k8s
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A prototype for a callback which asynchronously processes the standard input, standard output and standard error of a command executing in
|
||||||
|
/// a container.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stdIn">
|
||||||
|
/// The standard intput stream of the process.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="stdOut">
|
||||||
|
/// The standard output stream of the process.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="stdErr">
|
||||||
|
/// The standard error stream of the remote process.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task"/> which represents the asynchronous processing of the process input, output and error streams. This task
|
||||||
|
/// should complete once you're done interacting with the remote process.
|
||||||
|
/// </returns>
|
||||||
|
public delegate Task ExecAsyncCallback(Stream stdIn, Stream stdOut, Stream stdErr);
|
||||||
|
}
|
||||||
35
src/KubernetesClient/IKubernetes.Exec.cs
Normal file
35
src/KubernetesClient/IKubernetes.Exec.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace k8s
|
||||||
|
{
|
||||||
|
public partial interface IKubernetes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a command in a container in a pod.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">
|
||||||
|
/// The name of the pod which contains the container in which to execute the ocmmand.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="namespace">
|
||||||
|
/// The namespace of the container.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="container">
|
||||||
|
/// The container in which to run the command.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="command">
|
||||||
|
/// The command to execute.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="action">
|
||||||
|
/// A callback which processes the standard input, standard output and standard error.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="cancellationToken">
|
||||||
|
/// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task"/> which represents the asynchronous operation.
|
||||||
|
/// </returns>
|
||||||
|
Task<int> NamespacedPodExecAsync(string name, string @namespace, string container, IEnumerable<string> command, bool tty, ExecAsyncCallback action, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,6 +109,57 @@ namespace k8s
|
|||||||
/// </return>
|
/// </return>
|
||||||
Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", IEnumerable<string> command = null, string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, string webSocketSubProtol = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", IEnumerable<string> command = null, string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, string webSocketSubProtol = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||||
|
|
||||||
|
/// <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="webSocketSubProtocol">
|
||||||
|
/// The Kubernetes-specific WebSocket sub protocol to use. See <see cref="WebSocketProtocol"/> for a list of available
|
||||||
|
/// protocols.
|
||||||
|
/// </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="IStreamDemuxer"/> which can be used to communicate with the process running in the pod.
|
||||||
|
/// </return>
|
||||||
|
Task<IStreamDemuxer> MuxedStreamNamespacedPodExecAsync(string name, string @namespace = "default", IEnumerable<string> command = null, string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, string webSocketSubProtol = WebSocketProtocol.V4BinaryWebsocketProtocol, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start port forwarding one or more ports of a pod.
|
/// Start port forwarding one or more ports of a pod.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
102
src/KubernetesClient/IStreamDemuxer.cs
Normal file
102
src/KubernetesClient/IStreamDemuxer.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace k8s
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// The <see cref="IStreamDemuxer"/> interface allows you to interact with processes running in a container in a Kubernetes pod. You can start an exec or attach command
|
||||||
|
/// by calling <see cref="Kubernetes.WebSocketNamespacedPodExecAsync(string, string, IEnumerable{string}, string, bool, bool, bool, bool, Dictionary{string, List{string}}, CancellationToken)"/>
|
||||||
|
/// or <see cref="Kubernetes.WebSocketNamespacedPodAttachAsync(string, string, string, bool, bool, bool, bool, Dictionary{string, List{string}}, CancellationToken)"/>. These methods
|
||||||
|
/// will return you a <see cref="WebSocket"/> connection.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Kubernetes 'multiplexes' multiple channels over this <see cref="WebSocket"/> connection, such as standard input, standard output and standard error. The <see cref="StreamDemuxer"/>
|
||||||
|
/// allows you to extract individual <see cref="Stream"/>s from this <see cref="WebSocket"/> class. You can then use these streams to send/receive data from that process.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public interface IStreamDemuxer : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Starts reading the data sent by the server.
|
||||||
|
/// </summary>
|
||||||
|
void Start();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <see cref="Stream"/> which allows you to read to and/or write from a remote channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inputIndex">
|
||||||
|
/// The index of the channel from which to read.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="outputIndex">
|
||||||
|
/// The index of the channel to which to write.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Stream"/> which allows you to read/write to the requested channels.
|
||||||
|
/// </returns>
|
||||||
|
Stream GetStream(ChannelIndex? inputIndex, ChannelIndex? outputIndex);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <see cref="Stream"/> which allows you to read to and/or write from a remote channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inputIndex">
|
||||||
|
/// The index of the channel from which to read.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="outputIndex">
|
||||||
|
/// The index of the channel to which to write.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Stream"/> which allows you to read/write to the requested channels.
|
||||||
|
/// </returns>
|
||||||
|
Stream GetStream(byte? inputIndex, byte? outputIndex);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directly writes data to a channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">
|
||||||
|
/// The index of the channel to which to write.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="buffer">
|
||||||
|
/// The buffer from which to read data.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="offset">
|
||||||
|
/// The offset at which to start reading.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="count">
|
||||||
|
/// The number of bytes to read.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="cancellationToken">
|
||||||
|
/// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task"/> which represents the asynchronous operation.
|
||||||
|
/// </returns>
|
||||||
|
Task Write(ChannelIndex index, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default(CancellationToken));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directly writes data to a channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">
|
||||||
|
/// The index of the channel to which to write.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="buffer">
|
||||||
|
/// The buffer from which to read data.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="offset">
|
||||||
|
/// The offset at which to start reading.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="count">
|
||||||
|
/// The number of bytes to read.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="cancellationToken">
|
||||||
|
/// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task"/> which represents the asynchronous operation.
|
||||||
|
/// </returns>
|
||||||
|
Task Write(byte index, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default(CancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/KubernetesClient/Kubernetes.Exec.cs
Normal file
94
src/KubernetesClient/Kubernetes.Exec.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using k8s.Models;
|
||||||
|
using Microsoft.Rest;
|
||||||
|
using Microsoft.Rest.Serialization;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace k8s
|
||||||
|
{
|
||||||
|
public partial class Kubernetes
|
||||||
|
{
|
||||||
|
public async Task<int> NamespacedPodExecAsync(string name, string @namespace, string container, IEnumerable<string> command, bool tty, ExecAsyncCallback action, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// All other parameters are being validated by MuxedStreamNamespacedPodExecAsync
|
||||||
|
if (action == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var muxedStream = await this.MuxedStreamNamespacedPodExecAsync(name: name, @namespace: @namespace, command: command, container: container, tty: tty, cancellationToken: cancellationToken).ConfigureAwait(false))
|
||||||
|
using (Stream stdIn = muxedStream.GetStream(null, ChannelIndex.StdIn))
|
||||||
|
using (Stream stdOut= muxedStream.GetStream(ChannelIndex.StdOut, null))
|
||||||
|
using (Stream stdErr = muxedStream.GetStream(ChannelIndex.StdErr, null))
|
||||||
|
using (Stream error = muxedStream.GetStream(ChannelIndex.Error, null))
|
||||||
|
using (StreamReader errorReader = new StreamReader(error))
|
||||||
|
{
|
||||||
|
muxedStream.Start();
|
||||||
|
|
||||||
|
await action(stdIn, stdOut, stdErr).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var errors = await errorReader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// StatusError is defined here:
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/068e1642f63a1a8c48c16c18510e8854a4f4e7c5/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go#L37
|
||||||
|
var returnMessage = SafeJsonConvert.DeserializeObject<V1Status>(errors);
|
||||||
|
return GetExitCodeOrThrow(returnMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpOperationException httpEx) when (httpEx.Body is V1Status)
|
||||||
|
{
|
||||||
|
throw new KubernetesException((V1Status)httpEx.Body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the process' exit code based on a <see cref="V1Status"/> message.
|
||||||
|
///
|
||||||
|
/// This will:
|
||||||
|
/// - return 0 if the process completed successfully
|
||||||
|
/// - return the exit code if the process completed with a non-zero exit code
|
||||||
|
/// - throw a <see cref="KubernetesException"/> in all other cases.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="status">
|
||||||
|
/// A <see cref="V1Status"/> object.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// The process exit code.
|
||||||
|
/// </returns>
|
||||||
|
public static int GetExitCodeOrThrow(V1Status status)
|
||||||
|
{
|
||||||
|
if (status == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.Status == "Success")
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else if (status.Status == "Failure" && status.Reason == "NonZeroExitCode")
|
||||||
|
{
|
||||||
|
var exitCodeString = status.Details.Causes.FirstOrDefault(c => c.Reason == "ExitCode")?.Message;
|
||||||
|
|
||||||
|
if (int.TryParse(exitCodeString, out int exitCode))
|
||||||
|
{
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new KubernetesException(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new KubernetesException(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,15 @@ namespace k8s
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", IEnumerable<string> command = null, string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, string webSocketSubProtol = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
|
public virtual async Task<IStreamDemuxer> MuxedStreamNamespacedPodExecAsync(string name, string @namespace = "default", IEnumerable<string> command = null, string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, string webSocketSubProtol = WebSocketProtocol.V4BinaryWebsocketProtocol, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
WebSocket webSocket = await this.WebSocketNamespacedPodExecAsync(name: name, @namespace: @namespace, command: command, container: container, tty: tty, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
StreamDemuxer muxer = new StreamDemuxer(webSocket);
|
||||||
|
return muxer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public virtual Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", IEnumerable<string> command = null, string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, string webSocketSubProtol = WebSocketProtocol.V4BinaryWebsocketProtocol, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
if (name == null)
|
if (name == null)
|
||||||
{
|
{
|
||||||
@@ -54,6 +62,20 @@ namespace k8s
|
|||||||
throw new ArgumentOutOfRangeException(nameof(command));
|
throw new ArgumentOutOfRangeException(nameof(command));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandArray = command.ToArray();
|
||||||
|
foreach (var c in commandArray)
|
||||||
|
{
|
||||||
|
if (c.Length > 0 && c[0] == 0xfeff)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Detected an attempt to execute a command which starts with a Unicode byte order mark (BOM). This is probably incorrect. The command was {c}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tracing
|
// Tracing
|
||||||
bool _shouldTrace = ServiceClientTracing.IsEnabled;
|
bool _shouldTrace = ServiceClientTracing.IsEnabled;
|
||||||
string _invocationId = null;
|
string _invocationId = null;
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ namespace k8s
|
|||||||
throw new ArgumentException("You must specify at least inputBuffer or outputIndex");
|
throw new ArgumentException("You must specify at least inputBuffer or outputIndex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outputIndex != null)
|
||||||
|
{
|
||||||
this.muxer = muxer ?? throw new ArgumentNullException(nameof(muxer));
|
this.muxer = muxer ?? throw new ArgumentNullException(nameof(muxer));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override bool CanRead => this.inputBuffer != null;
|
public override bool CanRead => this.inputBuffer != null;
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ namespace k8s
|
|||||||
/// allows you to extract individual <see cref="Stream"/>s from this <see cref="WebSocket"/> class. You can then use these streams to send/receive data from that process.
|
/// allows you to extract individual <see cref="Stream"/>s from this <see cref="WebSocket"/> class. You can then use these streams to send/receive data from that process.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class StreamDemuxer : IDisposable
|
public class StreamDemuxer : IStreamDemuxer
|
||||||
{
|
{
|
||||||
private readonly WebSocket webSocket;
|
private readonly WebSocket webSocket;
|
||||||
private readonly Dictionary<byte, ByteBuffer> buffers = new Dictionary<byte, ByteBuffer>();
|
private readonly Dictionary<byte, ByteBuffer> buffers = new Dictionary<byte, ByteBuffer>();
|
||||||
private readonly CancellationTokenSource cts = new CancellationTokenSource();
|
private readonly CancellationTokenSource cts = new CancellationTokenSource();
|
||||||
private readonly StreamType streamType;
|
private readonly StreamType streamType;
|
||||||
|
private readonly bool ownsSocket;
|
||||||
private Task runLoop;
|
private Task runLoop;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -38,10 +39,15 @@ namespace k8s
|
|||||||
/// <param name="streamType">
|
/// <param name="streamType">
|
||||||
/// A <see cref="StreamType"/> specifies the type of the stream.
|
/// A <see cref="StreamType"/> specifies the type of the stream.
|
||||||
/// </param>
|
/// </param>
|
||||||
public StreamDemuxer(WebSocket webSocket, StreamType streamType = StreamType.RemoteCommand)
|
/// <param name="ownsSocket">
|
||||||
|
/// A value indicating whether this instance of the <see cref="StreamDemuxer"/> owns the underlying <see cref="WebSocket"/>,
|
||||||
|
/// and should dispose of it when this instance is disposed of.
|
||||||
|
/// </param>
|
||||||
|
public StreamDemuxer(WebSocket webSocket, StreamType streamType = StreamType.RemoteCommand, bool ownsSocket = false)
|
||||||
{
|
{
|
||||||
this.streamType = streamType;
|
this.streamType = streamType;
|
||||||
this.webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket));
|
this.webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket));
|
||||||
|
this.ownsSocket = ownsSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler ConnectionClosed;
|
public event EventHandler ConnectionClosed;
|
||||||
@@ -70,6 +76,11 @@ namespace k8s
|
|||||||
// Dispose methods can never throw.
|
// Dispose methods can never throw.
|
||||||
Debug.Write(ex);
|
Debug.Write(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.ownsSocket)
|
||||||
|
{
|
||||||
|
this.webSocket.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -2,20 +2,23 @@
|
|||||||
* These tests are for the netcoreapp2.1 version of the client (there are separate tests for netstandard that don't actually connect to a server).
|
* These tests are for the netcoreapp2.1 version of the client (there are separate tests for netstandard that don't actually connect to a server).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
using k8s.Models;
|
||||||
|
using Microsoft.Rest;
|
||||||
|
using Microsoft.Rest.Serialization;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Rest;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace k8s.Tests
|
namespace k8s.Tests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests for <see cref="KubeApiClient"/>'s exec-in-pod functionality.
|
/// Tests for <see cref="KubeApiClient"/>'s exec-in-pod functionality.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -92,5 +95,241 @@ namespace k8s.Tests
|
|||||||
WebSocketTestAdapter.CompleteTest();
|
WebSocketTestAdapter.CompleteTest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetExitCodeOrThrow_Success()
|
||||||
|
{
|
||||||
|
var status = new V1Status()
|
||||||
|
{
|
||||||
|
Metadata = null,
|
||||||
|
Status = "Success",
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(0, Kubernetes.GetExitCodeOrThrow(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetExitCodeOrThrow_NonZeroExitCode()
|
||||||
|
{
|
||||||
|
var status = new V1Status()
|
||||||
|
{
|
||||||
|
Metadata = null,
|
||||||
|
Status = "Failure",
|
||||||
|
Message = "command terminated with non-zero exit code: Error executing in Docker Container: 1",
|
||||||
|
Reason = "NonZeroExitCode",
|
||||||
|
Details = new V1StatusDetails()
|
||||||
|
{
|
||||||
|
Causes = new List<V1StatusCause>()
|
||||||
|
{
|
||||||
|
new V1StatusCause()
|
||||||
|
{
|
||||||
|
Reason = "ExitCode",
|
||||||
|
Message = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(1, Kubernetes.GetExitCodeOrThrow(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetExitCodeOrThrow_InvalidExitCode()
|
||||||
|
{
|
||||||
|
var status = new V1Status()
|
||||||
|
{
|
||||||
|
Metadata = null,
|
||||||
|
Status = "Failure",
|
||||||
|
Message = "command terminated with non-zero exit code: Error executing in Docker Container: 1",
|
||||||
|
Reason = "NonZeroExitCode",
|
||||||
|
Details = new V1StatusDetails()
|
||||||
|
{
|
||||||
|
Causes = new List<V1StatusCause>()
|
||||||
|
{
|
||||||
|
new V1StatusCause()
|
||||||
|
{
|
||||||
|
Reason = "ExitCode",
|
||||||
|
Message = "abc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var ex = Assert.Throws<KubernetesException>(() => Kubernetes.GetExitCodeOrThrow(status));
|
||||||
|
Assert.Equal(status, ex.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetExitCodeOrThrow_NoExitCode()
|
||||||
|
{
|
||||||
|
var status = new V1Status()
|
||||||
|
{
|
||||||
|
Metadata = null,
|
||||||
|
Status = "Failure",
|
||||||
|
Message = "command terminated with non-zero exit code: Error executing in Docker Container: 1",
|
||||||
|
Reason = "NonZeroExitCode",
|
||||||
|
Details = new V1StatusDetails()
|
||||||
|
{
|
||||||
|
Causes = new List<V1StatusCause>()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var ex = Assert.Throws<KubernetesException>(() => Kubernetes.GetExitCodeOrThrow(status));
|
||||||
|
Assert.Equal(status, ex.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetExitCodeOrThrow_OtherError()
|
||||||
|
{
|
||||||
|
var status = new V1Status()
|
||||||
|
{
|
||||||
|
Metadata = null,
|
||||||
|
Status = "Failure",
|
||||||
|
Reason = "SomethingElse"
|
||||||
|
};
|
||||||
|
|
||||||
|
var ex = Assert.Throws<KubernetesException>(() => Kubernetes.GetExitCodeOrThrow(status));
|
||||||
|
Assert.Equal(status, ex.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NamespacedPodExecAsync_ActionNull()
|
||||||
|
{
|
||||||
|
using (MemoryStream stdIn = new MemoryStream())
|
||||||
|
using (MemoryStream stdOut = new MemoryStream())
|
||||||
|
using (MemoryStream stdErr = new MemoryStream())
|
||||||
|
using (MemoryStream errorStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
var muxedStream = new Moq.Mock<IStreamDemuxer>();
|
||||||
|
muxedStream.Setup(m => m.GetStream(null, ChannelIndex.StdIn)).Returns(stdIn);
|
||||||
|
muxedStream.Setup(m => m.GetStream(ChannelIndex.StdOut, null)).Returns(stdOut);
|
||||||
|
muxedStream.Setup(m => m.GetStream(ChannelIndex.StdErr, null)).Returns(stdErr);
|
||||||
|
muxedStream.Setup(m => m.GetStream(ChannelIndex.Error, null)).Returns(errorStream);
|
||||||
|
|
||||||
|
var kubernetesMock = new Moq.Mock<Kubernetes>(
|
||||||
|
new object[] { Moq.Mock.Of<ServiceClientCredentials>(), new DelegatingHandler[] { } });
|
||||||
|
var command = new string[] { "/bin/bash", "-c", "echo Hello, World!" };
|
||||||
|
|
||||||
|
kubernetesMock.Setup(m => m.MuxedStreamNamespacedPodExecAsync("pod-name", "pod-namespace", command, "my-container", true, true, true, false, WebSocketProtocol.V4BinaryWebsocketProtocol, null, CancellationToken.None))
|
||||||
|
.Returns(Task.FromResult(muxedStream.Object));
|
||||||
|
|
||||||
|
using (Kubernetes client = kubernetesMock.Object)
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => client.NamespacedPodExecAsync("pod-name", "pod-namespace", "my-container", command, false, null, CancellationToken.None)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NamespacedPodExecAsync_HttpException_WithStatus()
|
||||||
|
{
|
||||||
|
var kubernetesMock = new Moq.Mock<Kubernetes>(
|
||||||
|
new object[] { Moq.Mock.Of<ServiceClientCredentials>(), new DelegatingHandler[] { } });
|
||||||
|
var command = new string[] { "/bin/bash", "-c", "echo Hello, World!" };
|
||||||
|
var handler = new ExecAsyncCallback((stdIn, stdOut, stdError) => Task.CompletedTask);
|
||||||
|
|
||||||
|
var status = new V1Status();
|
||||||
|
kubernetesMock.Setup(m => m.MuxedStreamNamespacedPodExecAsync("pod-name", "pod-namespace", command, "my-container", true, true, true, false, WebSocketProtocol.V4BinaryWebsocketProtocol, null, CancellationToken.None))
|
||||||
|
.Throws(new HttpOperationException() { Body = status });
|
||||||
|
|
||||||
|
using (Kubernetes client = kubernetesMock.Object)
|
||||||
|
{
|
||||||
|
var ex = await Assert.ThrowsAsync<KubernetesException>(() => client.NamespacedPodExecAsync("pod-name", "pod-namespace", "my-container", command, false, handler, CancellationToken.None)).ConfigureAwait(false);
|
||||||
|
Assert.Same(status, ex.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NamespacedPodExecAsync_HttpException_NoStatus()
|
||||||
|
{
|
||||||
|
var kubernetesMock = new Moq.Mock<Kubernetes>(
|
||||||
|
new object[] { Moq.Mock.Of<ServiceClientCredentials>(), new DelegatingHandler[] { } });
|
||||||
|
var command = new string[] { "/bin/bash", "-c", "echo Hello, World!" };
|
||||||
|
var handler = new ExecAsyncCallback((stdIn, stdOut, stdError) => Task.CompletedTask);
|
||||||
|
|
||||||
|
var exception = new HttpOperationException();
|
||||||
|
kubernetesMock.Setup(m => m.MuxedStreamNamespacedPodExecAsync("pod-name", "pod-namespace", command, "my-container", true, true, true, false, WebSocketProtocol.V4BinaryWebsocketProtocol, null, CancellationToken.None))
|
||||||
|
.Throws(exception);
|
||||||
|
|
||||||
|
using (Kubernetes client = kubernetesMock.Object)
|
||||||
|
{
|
||||||
|
var ex = await Assert.ThrowsAsync<HttpOperationException>(() => client.NamespacedPodExecAsync("pod-name", "pod-namespace", "my-container", command, false, handler, CancellationToken.None)).ConfigureAwait(false);
|
||||||
|
Assert.Same(exception, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NamespacedPodExecAsync_GenericException()
|
||||||
|
{
|
||||||
|
var kubernetesMock = new Moq.Mock<Kubernetes>(
|
||||||
|
new object[] { Moq.Mock.Of<ServiceClientCredentials>(), new DelegatingHandler[] { } });
|
||||||
|
var command = new string[] { "/bin/bash", "-c", "echo Hello, World!" };
|
||||||
|
var handler = new ExecAsyncCallback((stdIn, stdOut, stdError) => Task.CompletedTask);
|
||||||
|
|
||||||
|
var exception = new Exception();
|
||||||
|
kubernetesMock.Setup(m => m.MuxedStreamNamespacedPodExecAsync("pod-name", "pod-namespace", command, "my-container", true, true, true, false, WebSocketProtocol.V4BinaryWebsocketProtocol, null, CancellationToken.None))
|
||||||
|
.Throws(exception);
|
||||||
|
|
||||||
|
using (Kubernetes client = kubernetesMock.Object)
|
||||||
|
{
|
||||||
|
var ex = await Assert.ThrowsAsync<Exception>(() => client.NamespacedPodExecAsync("pod-name", "pod-namespace", "my-container", command, false, handler, CancellationToken.None)).ConfigureAwait(false);
|
||||||
|
Assert.Same(exception, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NamespacedPodExecAsync_ExitCode_NonZero()
|
||||||
|
{
|
||||||
|
var processStatus = new V1Status()
|
||||||
|
{
|
||||||
|
Metadata = null,
|
||||||
|
Status = "Failure",
|
||||||
|
Message = "command terminated with non-zero exit code: Error executing in Docker Container: 1",
|
||||||
|
Reason = "NonZeroExitCode",
|
||||||
|
Details = new V1StatusDetails()
|
||||||
|
{
|
||||||
|
Causes = new List<V1StatusCause>()
|
||||||
|
{
|
||||||
|
new V1StatusCause()
|
||||||
|
{
|
||||||
|
Reason = "ExitCode",
|
||||||
|
Message = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var processStatusJson = Encoding.UTF8.GetBytes(SafeJsonConvert.SerializeObject(processStatus));
|
||||||
|
var handler = new ExecAsyncCallback((stdIn, stdOut, stdError) => Task.CompletedTask);
|
||||||
|
|
||||||
|
using (MemoryStream stdIn = new MemoryStream())
|
||||||
|
using (MemoryStream stdOut = new MemoryStream())
|
||||||
|
using (MemoryStream stdErr = new MemoryStream())
|
||||||
|
using (MemoryStream errorStream = new MemoryStream(processStatusJson))
|
||||||
|
{
|
||||||
|
var muxedStream = new Moq.Mock<IStreamDemuxer>();
|
||||||
|
muxedStream.Setup(m => m.GetStream(null, ChannelIndex.StdIn)).Returns(stdIn);
|
||||||
|
muxedStream.Setup(m => m.GetStream(ChannelIndex.StdOut, null)).Returns(stdOut);
|
||||||
|
muxedStream.Setup(m => m.GetStream(ChannelIndex.StdErr, null)).Returns(stdErr);
|
||||||
|
muxedStream.Setup(m => m.GetStream(ChannelIndex.Error, null)).Returns(errorStream);
|
||||||
|
|
||||||
|
var kubernetesMock = new Moq.Mock<Kubernetes>(
|
||||||
|
new object[] { Moq.Mock.Of<ServiceClientCredentials>(), new DelegatingHandler[] { } });
|
||||||
|
var command = new string[] { "/bin/bash", "-c", "echo Hello, World!" };
|
||||||
|
|
||||||
|
var exception = new Exception();
|
||||||
|
kubernetesMock.Setup(m => m.MuxedStreamNamespacedPodExecAsync("pod-name", "pod-namespace", command, "my-container", true, true, true, false, WebSocketProtocol.V4BinaryWebsocketProtocol, null, CancellationToken.None))
|
||||||
|
.Returns(Task.FromResult(muxedStream.Object));
|
||||||
|
|
||||||
|
using (Kubernetes client = kubernetesMock.Object)
|
||||||
|
{
|
||||||
|
var exitCode = await client.NamespacedPodExecAsync("pod-name", "pod-namespace", "my-container", command, false, handler, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
Assert.Equal(1, exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<RootNamespace>k8s.tests</RootNamespace>
|
<RootNamespace>k8s.tests</RootNamespace>
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" NoWarn="NU1701" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" NoWarn="NU1701" />
|
||||||
|
<PackageReference Include="Moq" Version="4.10.1" />
|
||||||
|
|
||||||
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
|
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user