Skip to content

Commit 2a54a8c

Browse files
qmfrederikbrendandburns
authored andcommitted
Add support for executing commands within a container (#63)
* Add support for executing commands within a container * Add WebSocketNamespacedPodPortForwardAsync * Add Attach functionality * Simplify code
1 parent 14b59f6 commit 2a54a8c

6 files changed

+600
-1
lines changed

Diff for: src/IKubernetes.WebSocket.cs

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using System.Collections.Generic;
2+
using System.Net.WebSockets;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
namespace k8s
7+
{
8+
public partial interface IKubernetes
9+
{
10+
/// <summary>
11+
/// Executes a command in a pod.
12+
/// </summary>
13+
/// <param name='name'>
14+
/// name of the Pod
15+
/// </param>
16+
/// <param name='namespace'>
17+
/// object name and auth scope, such as for teams and projects
18+
/// </param>
19+
/// <param name='command'>
20+
/// Command is the remote command to execute. argv array. Not executed within a
21+
/// shell.
22+
/// </param>
23+
/// <param name='container'>
24+
/// Container in which to execute the command. Defaults to only container if
25+
/// there is only one container in the pod.
26+
/// </param>
27+
/// <param name='stderr'>
28+
/// Redirect the standard error stream of the pod for this call. Defaults to
29+
/// <see langword="true"/>.
30+
/// </param>
31+
/// <param name='stdin'>
32+
/// Redirect the standard input stream of the pod for this call. Defaults to
33+
/// <see langword="true"/>.
34+
/// </param>
35+
/// <param name='stdout'>
36+
/// Redirect the standard output stream of the pod for this call. Defaults to
37+
/// <see langword="true"/>.
38+
/// </param>
39+
/// <param name='tty'>
40+
/// TTY if true indicates that a tty will be allocated for the exec call.
41+
/// Defaults to <see langword="true"/>.
42+
/// </param>
43+
/// <param name='customHeaders'>
44+
/// Headers that will be added to request.
45+
/// </param>
46+
/// <param name='cancellationToken'>
47+
/// The cancellation token.
48+
/// </param>
49+
/// <exception cref="ArgumentNullException">
50+
/// Thrown when a required parameter is null
51+
/// </exception>
52+
/// <return>
53+
/// A <see cref="ClientWebSocket"/> which can be used to communicate with the process running in the pod.
54+
/// </return>
55+
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));
56+
57+
/// <summary>
58+
/// Start port forwarding one or more ports of a pod.
59+
/// </summary>
60+
/// <param name='name'>
61+
/// The name of the Pod
62+
/// </param>
63+
/// <param name='namespace'>
64+
/// The object name and auth scope, such as for teams and projects
65+
/// </param>
66+
/// <param name='ports'>
67+
/// List of ports to forward.
68+
/// </param>
69+
/// <param name='customHeaders'>
70+
/// The headers that will be added to request.
71+
/// </param>
72+
/// <param name='cancellationToken'>
73+
/// The cancellation token.
74+
/// </param>
75+
Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable<int> ports, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
76+
77+
/// <summary>
78+
/// connect GET requests to attach of Pod
79+
/// </summary>
80+
/// <param name='name'>
81+
/// name of the Pod
82+
/// </param>
83+
/// <param name='namespace'>
84+
/// object name and auth scope, such as for teams and projects
85+
/// </param>
86+
/// <param name='container'>
87+
/// The container in which to execute the command. Defaults to only container
88+
/// if there is only one container in the pod.
89+
/// </param>
90+
/// <param name='stderr'>
91+
/// Stderr if true indicates that stderr is to be redirected for the attach
92+
/// call. Defaults to true.
93+
/// </param>
94+
/// <param name='stdin'>
95+
/// Stdin if true, redirects the standard input stream of the pod for this
96+
/// call. Defaults to false.
97+
/// </param>
98+
/// <param name='stdout'>
99+
/// Stdout if true indicates that stdout is to be redirected for the attach
100+
/// call. Defaults to true.
101+
/// </param>
102+
/// <param name='tty'>
103+
/// TTY if true indicates that a tty will be allocated for the attach call.
104+
/// This is passed through the container runtime so the tty is allocated on the
105+
/// worker node by the container runtime. Defaults to false.
106+
/// </param>
107+
/// <param name='customHeaders'>
108+
/// Headers that will be added to request.
109+
/// </param>
110+
/// <param name='cancellationToken'>
111+
/// The cancellation token.
112+
/// </param>
113+
/// <exception cref="HttpOperationException">
114+
/// Thrown when the operation returned an invalid status code
115+
/// </exception>
116+
/// <exception cref="SerializationException">
117+
/// Thrown when unable to deserialize the response
118+
/// </exception>
119+
/// <exception cref="ValidationException">
120+
/// Thrown when a required parameter is null
121+
/// </exception>
122+
/// <exception cref="System.ArgumentNullException">
123+
/// Thrown when a required parameter is null
124+
/// </exception>
125+
/// <return>
126+
/// A response object containing the response body and response headers.
127+
/// </return>
128+
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));
129+
}
130+
}

Diff for: src/Kubernetes.WebSocket.cs

+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
using Microsoft.AspNetCore.WebUtilities;
2+
using Microsoft.Rest;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Net.Http;
6+
using System.Net.WebSockets;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
namespace k8s
11+
{
12+
public partial class Kubernetes
13+
{
14+
/// <summary>
15+
/// Gets a function which returns a <see cref="WebSocketBuilder"/> which <see cref="Kubernetes"/> will use to
16+
/// create a new <see cref="WebSocket"/> connection to the Kubernetes cluster.
17+
/// </summary>
18+
public Func<WebSocketBuilder> CreateWebSocketBuilder { get; set; } = () => new WebSocketBuilder();
19+
20+
/// <inheritdoc/>
21+
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))
22+
{
23+
if (name == null)
24+
{
25+
throw new ArgumentNullException(nameof(name));
26+
}
27+
28+
if (@namespace == null)
29+
{
30+
throw new ArgumentNullException(nameof(@namespace));
31+
}
32+
33+
if (command == null)
34+
{
35+
throw new ArgumentNullException(nameof(command));
36+
}
37+
38+
// Tracing
39+
bool _shouldTrace = ServiceClientTracing.IsEnabled;
40+
string _invocationId = null;
41+
if (_shouldTrace)
42+
{
43+
_invocationId = ServiceClientTracing.NextInvocationId.ToString();
44+
Dictionary<string, object> tracingParameters = new Dictionary<string, object>();
45+
tracingParameters.Add("command", command);
46+
tracingParameters.Add("container", container);
47+
tracingParameters.Add("name", name);
48+
tracingParameters.Add("namespace", @namespace);
49+
tracingParameters.Add("stderr", stderr);
50+
tracingParameters.Add("stdin", stdin);
51+
tracingParameters.Add("stdout", stdout);
52+
tracingParameters.Add("tty", tty);
53+
tracingParameters.Add("cancellationToken", cancellationToken);
54+
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodExecAsync), tracingParameters);
55+
}
56+
57+
// Construct URL
58+
var uriBuilder = new UriBuilder(BaseUri);
59+
uriBuilder.Scheme = BaseUri.Scheme == "https" ? "wss" : "ws";
60+
61+
if (!uriBuilder.Path.EndsWith("/"))
62+
{
63+
uriBuilder.Path += "/";
64+
}
65+
66+
uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/exec";
67+
68+
69+
uriBuilder.Query = QueryHelpers.AddQueryString(string.Empty, new Dictionary<string, string>
70+
{
71+
{ "command", command},
72+
{ "container", container},
73+
{ "stderr", stderr ? "1": "0"},
74+
{ "stdin", stdin ? "1": "0"},
75+
{ "stdout", stdout ? "1": "0"},
76+
{ "tty", tty ? "1": "0"}
77+
});
78+
79+
return this.StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
80+
}
81+
82+
/// <inheritdoc/>
83+
public Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable<int> ports, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
84+
{
85+
if (name == null)
86+
{
87+
throw new ArgumentNullException(nameof(name));
88+
}
89+
90+
if (@namespace == null)
91+
{
92+
throw new ArgumentNullException(nameof(@namespace));
93+
}
94+
95+
if (ports == null)
96+
{
97+
throw new ArgumentNullException(nameof(ports));
98+
}
99+
100+
// Tracing
101+
bool _shouldTrace = ServiceClientTracing.IsEnabled;
102+
string _invocationId = null;
103+
if (_shouldTrace)
104+
{
105+
_invocationId = ServiceClientTracing.NextInvocationId.ToString();
106+
Dictionary<string, object> tracingParameters = new Dictionary<string, object>();
107+
tracingParameters.Add("name", name);
108+
tracingParameters.Add("@namespace", @namespace);
109+
tracingParameters.Add("ports", ports);
110+
tracingParameters.Add("cancellationToken", cancellationToken);
111+
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodPortForwardAsync), tracingParameters);
112+
}
113+
114+
// Construct URL
115+
var uriBuilder = new UriBuilder(this.BaseUri);
116+
uriBuilder.Scheme = this.BaseUri.Scheme == "https" ? "wss" : "ws";
117+
118+
if (!uriBuilder.Path.EndsWith("/"))
119+
{
120+
uriBuilder.Path += "/";
121+
}
122+
123+
uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/portforward";
124+
125+
foreach (var port in ports)
126+
{
127+
uriBuilder.Query += $"ports={port}&";
128+
}
129+
130+
return StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
131+
}
132+
133+
/// <inheritdoc/>
134+
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))
135+
{
136+
if (name == null)
137+
{
138+
throw new ArgumentNullException(nameof(name));
139+
}
140+
141+
if (@namespace == null)
142+
{
143+
throw new ArgumentNullException(nameof(@namespace));
144+
}
145+
146+
// Tracing
147+
bool _shouldTrace = ServiceClientTracing.IsEnabled;
148+
string _invocationId = null;
149+
if (_shouldTrace)
150+
{
151+
_invocationId = ServiceClientTracing.NextInvocationId.ToString();
152+
Dictionary<string, object> tracingParameters = new Dictionary<string, object>();
153+
tracingParameters.Add("container", container);
154+
tracingParameters.Add("name", name);
155+
tracingParameters.Add("namespace", @namespace);
156+
tracingParameters.Add("stderr", stderr);
157+
tracingParameters.Add("stdin", stdin);
158+
tracingParameters.Add("stdout", stdout);
159+
tracingParameters.Add("tty", tty);
160+
tracingParameters.Add("cancellationToken", cancellationToken);
161+
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodAttachAsync), tracingParameters);
162+
}
163+
164+
// Construct URL
165+
var uriBuilder = new UriBuilder(this.BaseUri);
166+
uriBuilder.Scheme = this.BaseUri.Scheme == "https" ? "wss" : "ws";
167+
168+
if (!uriBuilder.Path.EndsWith("/"))
169+
{
170+
uriBuilder.Path += "/";
171+
}
172+
173+
uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/portforward";
174+
175+
uriBuilder.Query = QueryHelpers.AddQueryString(string.Empty, new Dictionary<string, string>
176+
{
177+
{ "container", container},
178+
{ "stderr", stderr ? "1": "0"},
179+
{ "stdin", stdin ? "1": "0"},
180+
{ "stdout", stdout ? "1": "0"},
181+
{ "tty", tty ? "1": "0"}
182+
});
183+
184+
return StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
185+
}
186+
187+
protected async Task<WebSocket> StreamConnectAsync(Uri uri, string invocationId = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
188+
{
189+
bool _shouldTrace = ServiceClientTracing.IsEnabled;
190+
191+
// Create WebSocket transport objects
192+
WebSocketBuilder webSocketBuilder = this.CreateWebSocketBuilder();
193+
194+
// Set Headers
195+
if (customHeaders != null)
196+
{
197+
foreach (var _header in customHeaders)
198+
{
199+
webSocketBuilder.SetRequestHeader(_header.Key, string.Join(" ", _header.Value));
200+
}
201+
}
202+
203+
// Set Credentials
204+
foreach (var cert in this.HttpClientHandler.ClientCertificates)
205+
{
206+
webSocketBuilder.AddClientCertificate(cert);
207+
}
208+
209+
HttpRequestMessage message = new HttpRequestMessage();
210+
await this.Credentials.ProcessHttpRequestAsync(message, cancellationToken);
211+
212+
foreach (var _header in message.Headers)
213+
{
214+
webSocketBuilder.SetRequestHeader(_header.Key, string.Join(" ", _header.Value));
215+
}
216+
217+
// Send Request
218+
cancellationToken.ThrowIfCancellationRequested();
219+
220+
WebSocket webSocket = null;
221+
222+
try
223+
{
224+
webSocket = await webSocketBuilder.BuildAndConnectAsync(uri, CancellationToken.None).ConfigureAwait(false);
225+
}
226+
catch (Exception ex)
227+
{
228+
if (_shouldTrace)
229+
{
230+
ServiceClientTracing.Error(invocationId, ex);
231+
}
232+
233+
throw;
234+
}
235+
finally
236+
{
237+
if (_shouldTrace)
238+
{
239+
ServiceClientTracing.Exit(invocationId, null);
240+
}
241+
}
242+
243+
return webSocket;
244+
}
245+
}
246+
}

Diff for: src/KubernetesClient.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Authors>The Kubernetes Project Authors</Authors>
55
<Copyright>2017 The Kubernetes Project Authors</Copyright>
66
<Description>Client library for the Kubernetes open source container orchestrator.</Description>
7-
7+
88
<PackageLicenseUrl>https://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
99
<PackageProjectUrl>https://github.com/kubernetes-client/csharp</PackageProjectUrl>
1010
<PackageTags>kubernetes;docker;containers;</PackageTags>
@@ -23,5 +23,6 @@
2323
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
2424
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
2525
<PackageReference Include="YamlDotNet.NetCore" Version="1.0.0" />
26+
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2"/>
2627
</ItemGroup>
2728
</Project>

0 commit comments

Comments
 (0)