Skip to content

Commit 0f2832b

Browse files
qmfrederikbrendandburns
authored andcommitted
Allow callers to specify which WebSocket subprotocol to use (#154)
* Allow callers to specify which WebSocket subprotocol to use. * Update unit tests * Update unit test * Fix ordering * Trace CancellationToken
1 parent c608820 commit 0f2832b

File tree

7 files changed

+116
-63
lines changed

7 files changed

+116
-63
lines changed

src/KubernetesClient/IKubernetes.WebSocket.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public partial interface IKubernetes
4040
/// TTY if true indicates that a tty will be allocated for the exec call.
4141
/// Defaults to <see langword="true"/>.
4242
/// </param>
43+
/// <param name="webSocketSubProtocol">
44+
/// The Kubernetes-specific WebSocket sub protocol to use. See <see cref="WebSocketProtocol"/> for a list of available
45+
/// protocols.
46+
/// </param>
4347
/// <param name='customHeaders'>
4448
/// Headers that will be added to request.
4549
/// </param>
@@ -52,7 +56,7 @@ public partial interface IKubernetes
5256
/// <return>
5357
/// A <see cref="ClientWebSocket"/> which can be used to communicate with the process running in the pod.
5458
/// </return>
55-
Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", string command = null, 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));
59+
Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", 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));
5660

5761
/// <summary>
5862
/// Executes a command in a pod.
@@ -87,6 +91,10 @@ public partial interface IKubernetes
8791
/// TTY if true indicates that a tty will be allocated for the exec call.
8892
/// Defaults to <see langword="true"/>.
8993
/// </param>
94+
/// <param name="webSocketSubProtocol">
95+
/// The Kubernetes-specific WebSocket sub protocol to use. See <see cref="WebSocketProtocol"/> for a list of available
96+
/// protocols.
97+
/// </param>
9098
/// <param name='customHeaders'>
9199
/// Headers that will be added to request.
92100
/// </param>
@@ -99,7 +107,7 @@ public partial interface IKubernetes
99107
/// <return>
100108
/// A <see cref="ClientWebSocket"/> which can be used to communicate with the process running in the pod.
101109
/// </return>
102-
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, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
110+
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));
103111

104112
/// <summary>
105113
/// Start port forwarding one or more ports of a pod.
@@ -113,13 +121,17 @@ public partial interface IKubernetes
113121
/// <param name='ports'>
114122
/// List of ports to forward.
115123
/// </param>
124+
/// <param name="webSocketSubProtocol">
125+
/// The Kubernetes-specific WebSocket sub protocol to use. See <see cref="WebSocketProtocol"/> for a list of available
126+
/// protocols.
127+
/// </param>
116128
/// <param name='customHeaders'>
117129
/// The headers that will be added to request.
118130
/// </param>
119131
/// <param name='cancellationToken'>
120132
/// The cancellation token.
121133
/// </param>
122-
Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable<int> ports, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
134+
Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable<int> ports, string webSocketSubProtocol = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
123135

124136
/// <summary>
125137
/// connect GET requests to attach of Pod
@@ -151,6 +163,10 @@ public partial interface IKubernetes
151163
/// This is passed through the container runtime so the tty is allocated on the
152164
/// worker node by the container runtime. Defaults to false.
153165
/// </param>
166+
/// <param name="webSocketSubProtocol">
167+
/// The Kubernetes-specific WebSocket sub protocol to use. See <see cref="WebSocketProtocol"/> for a list of available
168+
/// protocols.
169+
/// </param>
154170
/// <param name='customHeaders'>
155171
/// Headers that will be added to request.
156172
/// </param>
@@ -172,6 +188,6 @@ public partial interface IKubernetes
172188
/// <return>
173189
/// A response object containing the response body and response headers.
174190
/// </return>
175-
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));
191+
Task<WebSocket> WebSocketNamespacedPodAttachAsync(string name, string @namespace, string container = default(string), bool stderr = true, bool stdin = false, bool stdout = true, bool tty = false, string webSocketSubProtol = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
176192
}
177193
}

src/KubernetesClient/K8sProtocol.cs

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/KubernetesClient/Kubernetes.WebSocket.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ public partial class Kubernetes
2323
public Func<WebSocketBuilder> CreateWebSocketBuilder { get; set; } = () => new WebSocketBuilder();
2424

2525
/// <inheritdoc/>
26-
public Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", string command = null, 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))
26+
public Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", 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))
2727
{
28-
return WebSocketNamespacedPodExecAsync(name, @namespace, new string[] { command }, container, stderr, stdin, stdout, tty, customHeaders, cancellationToken);
28+
return WebSocketNamespacedPodExecAsync(name, @namespace, new string[] { command }, container, stderr, stdin, stdout, tty, webSocketSubProtol, customHeaders, cancellationToken);
2929
}
3030

3131
/// <inheritdoc/>
32-
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, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
32+
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))
3333
{
3434
if (name == null)
3535
{
@@ -66,6 +66,7 @@ public partial class Kubernetes
6666
tracingParameters.Add("stdin", stdin);
6767
tracingParameters.Add("stdout", stdout);
6868
tracingParameters.Add("tty", tty);
69+
tracingParameters.Add("webSocketSubProtol", webSocketSubProtol);
6970
tracingParameters.Add("cancellationToken", cancellationToken);
7071
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodExecAsync), tracingParameters);
7172
}
@@ -103,11 +104,11 @@ public partial class Kubernetes
103104

104105
uriBuilder.Query = query;
105106

106-
return this.StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
107+
return this.StreamConnectAsync(uriBuilder.Uri, _invocationId, webSocketSubProtol, customHeaders, cancellationToken);
107108
}
108109

109110
/// <inheritdoc/>
110-
public Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable<int> ports, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
111+
public Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable<int> ports, string webSocketSubProtocol = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
111112
{
112113
if (name == null)
113114
{
@@ -134,6 +135,7 @@ public partial class Kubernetes
134135
tracingParameters.Add("name", name);
135136
tracingParameters.Add("@namespace", @namespace);
136137
tracingParameters.Add("ports", ports);
138+
tracingParameters.Add("webSocketSubProtocol", webSocketSubProtocol);
137139
tracingParameters.Add("cancellationToken", cancellationToken);
138140
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodPortForwardAsync), tracingParameters);
139141
}
@@ -158,11 +160,11 @@ public partial class Kubernetes
158160

159161

160162

161-
return StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
163+
return StreamConnectAsync(uriBuilder.Uri, _invocationId, webSocketSubProtocol, customHeaders, cancellationToken);
162164
}
163165

164166
/// <inheritdoc/>
165-
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))
167+
public Task<WebSocket> WebSocketNamespacedPodAttachAsync(string name, string @namespace, string container = default(string), bool stderr = true, bool stdin = false, bool stdout = true, bool tty = false, string webSocketSubProtol = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
166168
{
167169
if (name == null)
168170
{
@@ -188,6 +190,7 @@ public partial class Kubernetes
188190
tracingParameters.Add("stdin", stdin);
189191
tracingParameters.Add("stdout", stdout);
190192
tracingParameters.Add("tty", tty);
193+
tracingParameters.Add("webSocketSubProtol", webSocketSubProtol);
191194
tracingParameters.Add("cancellationToken", cancellationToken);
192195
ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodAttachAsync), tracingParameters);
193196
}
@@ -212,10 +215,10 @@ public partial class Kubernetes
212215
{ "tty", tty ? "1": "0"}
213216
}).TrimStart('?');
214217

215-
return StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
218+
return StreamConnectAsync(uriBuilder.Uri, _invocationId, webSocketSubProtol, customHeaders, cancellationToken);
216219
}
217220

218-
protected async Task<WebSocket> StreamConnectAsync(Uri uri, string invocationId = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
221+
protected async Task<WebSocket> StreamConnectAsync(Uri uri, string invocationId = null, string webSocketSubProtocol = null, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
219222
{
220223
bool _shouldTrace = ServiceClientTracing.IsEnabled;
221224

@@ -258,11 +261,16 @@ public partial class Kubernetes
258261
{
259262
webSocketBuilder.ExpectServerCertificate(this.CaCert);
260263
}
264+
261265
if (this.SkipTlsVerify)
262266
{
263267
webSocketBuilder.SkipServerCertificateValidation();
264268
}
265-
webSocketBuilder.Options.RequestedSubProtocols.Add(K8sProtocol.ChannelV1);
269+
270+
if (webSocketSubProtocol != null)
271+
{
272+
webSocketBuilder.Options.RequestedSubProtocols.Add(webSocketSubProtocol);
273+
}
266274
#endif // NETCOREAPP2_1
267275

268276
// Send Request
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
namespace k8s
2+
{
3+
/// <summary>
4+
/// Well-known WebSocket sub-protocols used by the Kubernetes API.
5+
/// </summary>
6+
public static class WebSocketProtocol
7+
{
8+
// The protocols are defined here:
9+
// https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/server/remotecommand/websocket.go#L36
10+
// and here:
11+
// https://github.com/kubernetes/kubernetes/blob/714f97d7baf4975ad3aa47735a868a81a984d1f0/staging/src/k8s.io/apiserver/pkg/util/wsstream/conn.go
12+
//
13+
// For a description of what's different in v4:
14+
// https://github.com/kubernetes/kubernetes/blob/317853c90c674920bfbbdac54fe66092ddc9f15f/pkg/kubelet/server/remotecommand/httpstream.go#L203
15+
16+
/// <summary>
17+
/// <para>
18+
/// The Websocket subprotocol "channel.k8s.io" prepends each binary message with a byte indicating
19+
/// the channel number (zero indexed) the message was sent on. Messages in both directions should
20+
/// prefix their messages with this channel byte. When used for remote execution, the channel numbers
21+
/// are by convention defined to match the POSIX file-descriptors assigned to STDIN, STDOUT, and STDERR
22+
/// (0, 1, and 2). No other conversion is performed on the raw subprotocol - writes are sent as they
23+
/// are received by the server.
24+
/// </para>
25+
///
26+
/// <para>
27+
/// Example client session:
28+
/// <code>
29+
/// CONNECT http://server.com with subprotocol "channel.k8s.io"
30+
/// WRITE []byte{0, 102, 111, 111, 10} # send "foo\n" on channel 0 (STDIN)
31+
/// READ []byte{1, 10} # receive "\n" on channel 1 (STDOUT)
32+
/// CLOSE
33+
/// </code>
34+
/// </para>
35+
/// </summary>
36+
public const string ChannelWebSocketProtocol = "channel.k8s.io";
37+
38+
/// <summary>
39+
/// <para>
40+
/// The Websocket subprotocol "base64.channel.k8s.io" base64 encodes each message with a character
41+
/// indicating the channel number (zero indexed) the message was sent on. Messages in both directions
42+
/// should prefix their messages with this channel char. When used for remote execution, the channel
43+
/// numbers are by convention defined to match the POSIX file-descriptors assigned to STDIN, STDOUT,
44+
/// and STDERR ('0', '1', and '2'). The data received on the server is base64 decoded (and must be
45+
/// be valid) and data written by the server to the client is base64 encoded.
46+
/// </para>
47+
///
48+
/// <para>
49+
/// Example client session:
50+
/// <code>
51+
/// CONNECT http://server.com with subprotocol "base64.channel.k8s.io"
52+
/// WRITE []byte{48, 90, 109, 57, 118, 67, 103, 111, 61} # send "foo\n" (base64: "Zm9vCgo=") on channel '0' (STDIN)
53+
/// READ []byte{49, 67, 103, 61, 61} # receive "\n" (base64: "Cg==") on channel '1' (STDOUT)
54+
/// CLOSE
55+
/// </code>
56+
/// </para>
57+
/// </summary>
58+
public const string Base64ChannelWebSocketProtocol = "base64.channel.k8s.io";
59+
60+
/// <summary>
61+
/// v4ProtocolHandler implements the V4 protocol version for streaming command execution. It only differs
62+
/// in from v3 in the error stream format using an json-marshaled metav1.Status which carries
63+
/// the process' exit code.
64+
/// </summary>
65+
public const string V4BinaryWebsocketProtocol = "v4." + ChannelWebSocketProtocol;
66+
67+
/// <summary>
68+
/// v4ProtocolHandler implements the V4 protocol version for streaming command execution. It only differs
69+
/// in from v3 in the error stream format using an json-marshaled metav1.Status which carries
70+
/// the process' exit code.
71+
/// </summary>
72+
public const string V4Base64WebsocketProtocol = "v4." + Base64ChannelWebSocketProtocol;
73+
}
74+
}

0 commit comments

Comments
 (0)