-
Notifications
You must be signed in to change notification settings - Fork 291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for executing commands within a container #63
Add support for executing commands within a container #63
Conversation
examples/exec/NuGet.config
Outdated
@@ -0,0 +1,5 @@ | |||
<configuration> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to add this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want the 'F5' experience to work with this example, then yes.
ClientWebSocket
only works properly with corefx 2.1 which has dotnet/corefx#25645; so very recent nightly builds. If you don't target netcoreapp2.1
the code will compile but you'll hit a bug in .NET Core so the sample won't work.
Once there's a public preview of .NET Core 2.1 this can be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, turns out Travis doesn't have .NET Core 2.1 (which is reasonable 😄 ), this caused the build to break so I reverted this change for now.
examples/exec/exec.csproj
Outdated
<ProjectReference Include="..\..\src\KubernetesClient.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do? Do we want this in here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if that file doesn't exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need it, so I removed the reference.
src/ExecClient.cs
Outdated
var commandLength = Encoding.UTF8.GetByteCount(command); | ||
var buffer = ArrayPool<byte>.Shared.Rent(commandLength + 4); | ||
|
||
try |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: may as well move this try down to around the send, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The try/finally protects buffer
which comes from the shared array pool. The code that initializes the first 4 bytes of the buffer won't fail, but Encoding.UTF8.GetBytes
may throw an exception. So if we move the try
down to SendAsync
, we may leak a buffer if GetBytes
fails.
(It's very unlikely it will since we already call Encoding.UTF8.GetByteCount
)
I can move it down; but because the try
is there make sure we return buffer
once we're done with it, I prefer to keep it close to buffer
.
I don't have a strong preference, though, so can always move it down if you prefer it that way.
src/ExecClient.cs
Outdated
|
||
while (!cancellationToken.IsCancellationRequested && this.socket.CloseStatus == null) | ||
{ | ||
// We always get data in this format: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ultimately we're going to want to pull this logic out so that we can use it for Attach and PortForward, but we can do that in a different PR...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I didn't have the time to look at the protocol for Attach yet (not sure whether it uses stream multiplexing or not).
I had a cursory look at PortForward
and I believe it's simpler - there is no multiplexing so you can just use ReceiveAsync
/SendAsync
to talk with the remote socket.
You may want to add code which wraps the ClientWebSocket
in Stream
-like class, though.
That said, one thing at a time 😄
src/ExecClient.cs
Outdated
|
||
switch (streamType) | ||
{ | ||
case 1: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make 1
and 2
constants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thx.
This looks great! Thanks for doing it (and the thorough investigation...) Some minor nits, and you need to sign the CLA, then I think this is good to merge! Thanks again! |
@brendanburns You're welcome, I updated the code/responded to your comments, the CLA status should also be OK now. Let me know if you need anything else! |
Cloud you please also attach some testcases and also have a look at my suggestion about channel length |
@tg123 I'll try to add some unit tests. I'm sorry - I may have missed your comment about channel length. Can you let me know where you left that comment (or repeat it here)? |
examples/exec/Program.cs
Outdated
var pod = args[0]; | ||
var container = args.Length > 1 ? args[1] : null; | ||
|
||
var k8sClientConfig = KubernetesClientConfiguration.BuildConfigFromConfigFile(kubeconfigPath: "kube-config.yml"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cloud you please use ctor() which use $HOME/.kube
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thx, I fixed this.
src/ExecClient.cs
Outdated
|
||
namespace k8s | ||
{ | ||
public class ExecClient : IDisposable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
attach/ exec / port-forward are share the same streaming protocol, it is better to have shared websocket handler like java client does
src/ExecClient.cs
Outdated
try | ||
{ | ||
// The first 4 bytes represent the stream index. For stdin this is 0 | ||
buffer[0] = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you sure channel is 4 bytes? not 1 byte?
channel define here
https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#websockets-and-spdy
java client impl:
https://github.com/kubernetes-client/java/blob/master/util/src/main/java/io/kubernetes/client/util/WebSocketStreamHandler.java#L175
python client impl:
https://github.com/kubernetes-client/python-base/blob/master/stream/ws_client.py#L114
src/ExecClient.cs
Outdated
@@ -9,6 +9,10 @@ namespace k8s | |||
{ | |||
public class ExecClient : IDisposable | |||
{ | |||
private const int StdInStreamIndex = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const from py client
https://github.com/kubernetes-client/python-base/blob/master/stream/ws_client.py#L24
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thx, I added the other channel indices as well.
@qmfrederik really sorry I did not click submit review posted yesterday but reviews were shown on my page. you can now see it. |
@tg123 Thanks for the feedback! I updated the PR. As for unit tests, I added unit tests for The easiest way I found to do this was by introducing a Let me see what I can do about the ExecClient (or whatever its future shape may be). |
@tg123 @brendandburns Thanks, I updated the PR. Based on your feedback, would it be better to rename Alternatively, we can split |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.gitignore
Outdated
@@ -2,6 +2,7 @@ | |||
.vs | |||
obj/ | |||
bin/ | |||
kube-config.yml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be removed
src/ExecClient.cs
Outdated
try | ||
{ | ||
// The first byte represent the stream index. For stdin this is 0 | ||
buffer[0] = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cloud be const
examples/exec/Program.cs
Outdated
var k8SClientConfig = KubernetesClientConfiguration.BuildConfigFromConfigFile(); | ||
var client = new Kubernetes(k8SClientConfig); | ||
|
||
var webSocket = await client.WebSocketNamespacedPodExecWithHttpMessagesAsync(pod, container: container, command: "/bin/bash").ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the API cloud return ExecClient
directly, make websocket with channel
for intern use only
src/ExecClient.cs
Outdated
try | ||
{ | ||
// The first byte represent the stream index. For stdin this is 0 | ||
buffer[0] = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to hide channel in the websocket
class which can be reuse in attach. but I think it is OK to impl them in next PR.
tests/ExecClientTests.cs
Outdated
public class ExecClientTests | ||
{ | ||
[Fact] | ||
public void ReadWriteTest() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cloud you please align with naming style only verb
like #51
src/ExecClient.cs
Outdated
Encoding.UTF8.GetBytes(command, 0, command.Length, buffer, 1); | ||
|
||
ArraySegment<byte> segment = new ArraySegment<byte>(buffer, 0, commandLength + 1); | ||
await this.socket.SendAsync(segment, WebSocketMessageType.Binary, true, cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
send/recy websocket in C# seems did not thread safe.
src/ExecClient.cs
Outdated
} | ||
|
||
public event EventHandler ConnectionClosed; | ||
public event EventHandler<string> StandardOutputReceived; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer java client style
ExecClient with 3 streams Stdin, Stdout, Stderr
event string does not seem to be friendly if you want to readline or something else
@tg123 Thanks! I split off the exec-related functionality for now; we can do this in a separate PR. I'd really prefer the Kubernetes C# client to expose a "normal" It would be fine for the C# client to add default 'wrappers' around WebSocket which allow you to interact with the data in a specific way (e.g. by demuxing the streams); but as a user I'd really like to have access to the raw WebSocket object, too. Let me know if the WebSocket-related changes look good to me. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be LGTM if file name changed
And next
Add an Ext class to wrap the StreamConnect
to ChannelSupporttedStream
and then another Ext class which
wrap ChannelSupporttedStream
to
ExecClient
/ AttachClient
/ PortForwardClient
would the be fine?
src/IKubernetes.Exec.cs
Outdated
/// <return> | ||
/// A <see cref="ClientWebSocket"/> which can be used to communicate with the process running in the pod. | ||
/// </return> | ||
Task<WebSocket> WebSocketNamespacedPodExecWithHttpMessagesAsync(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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FileName to Ixxx.Websocket.cs or Ixxx.Stream.cs
Cloud we name this StreamConnectAsync(path, ...) or something else, which just return plain websocket
since this PR has nothing to do with exec
@tg123 I tried to update the PR in line with your feedback:
|
src/Kubernetes.WebSocket.cs
Outdated
_queryParameters.Add(string.Format("stdout={0}", stdout ? 1 : 0)); | ||
_queryParameters.Add(string.Format("tty={0}", tty ? 1 : 0)); | ||
|
||
uriBuilder.Query = string.Join("&", _queryParameters); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
take look at QueryHelpers.AddQueryString
I already imported this.
it might help work encode strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like this:
string queryString = string.Empty;
queryString = QueryHelpers.AddQueryString(queryString, "command", command);
if (container != null)
{
queryString = QueryHelpers.AddQueryString(queryString, "container", container);
}
queryString = QueryHelpers.AddQueryString(queryString, "stderr", stderr ? "1" : "0");
queryString = QueryHelpers.AddQueryString(queryString, "stdin", stdin ? "1" : "0");
queryString = QueryHelpers.AddQueryString(queryString, "stdout", stdout ? "1" : "0");
queryString = QueryHelpers.AddQueryString(queryString, "tty", tty ? "1" : "0");
uriBuilder.Query = queryString;
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var queryString = QueryHelpers.AddQueryString(string.Empty, new Dictionary<string, string>
{
{ "command", "aaaa"},
{ "tty", "1"}
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks!
LGTM looking forward friendly api in #64 |
by the way |
@tg123 Sure, rebased & squashed. |
@tg123, @brendandburns Are we good to merge this one? Anything else you need? |
Ah, makes sense. Thanks for letting me know! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more small-ish comments...
/// <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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this in the WebSocket
class? I feel like this should be in the port forward class...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's in the IKubernetes
interface, this file just contains manual extensions to IKubernetes
for API calls which returns WebSockets.
I can move it to something like IKubernetes.PortForward
, if that's what you'd prefer - so one file per method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not also add attach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done ;-)
{ | ||
webSocket = await webSocketBuilder.BuildAndConnectAsync(uri, CancellationToken.None).ConfigureAwait(false); | ||
} | ||
catch (Exception ex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't catch bare Exception
only catch expected Exceptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The catch catch statement is there so we can trace errors; the exception itself is rethrown.
.NET doesn't have the concept of checked exceptions and the MSDN documentation doesn't list which exceptions ClientWebSocket.ConnectAsync
can throw.
/// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class doesn't seem to add much imho. Why not just inline the various calls in the class in the code above...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The calls are not inlined because the unit tests use a mock WebSocketBuilder
which traces all the parameters which have been passed and returns a dummy WebSocket
.
I used a Builder-pattern becuase the code also uses the UriBuilder
class.
I can change it a function/property like GetWebSocket(Dictionary<string,string> headers, Collection<X509Certificate2> clientCertificates, Uri uri)
, though, if that's what you'd prefer. As long as the unit tests can intercept the call it's fine with ime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Injectable for testing is fair...
@@ -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"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems netstandard can use websocket without this dependeny
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can on netstandard2.0 but it doesn't work on netstandard1.4
. Did you run dotnet restore
when you tested this?
…ent#63) * Add support for executing commands within a container * Add WebSocketNamespacedPodPortForwardAsync * Add Attach functionality * Simplify code
This PR adds support for the equivalent of
kubectl exec
and allows you to run commands within a container.I've added a
exec
sample application which is a very basic attempt to replicatekubectl exec
using the C# Kubernetes client.From a code point of view:
IKubernetes.WebSocketNamespacedPodExecWithHttpMessagesAsync
method which is very similar toIKubernetes.ConnectGetNamespacedPodExecWithHttpMessagesAsync
but returns aClientWebSocket
, allowing callers to interact with the process running on the containerExecClient
convenience class which wraps aroundClientWebSocket
and provides access to the stdin, stdout and stderr streams.There are important gotchas related to
ClientWebSocket
, the WebSocket client which ships as part of .NET Core:ClientWebSocket.Options
are ignored - this is where you set the authorization header or specify the client credentials. Long story short: you need the latest preview bits of .NET Core for this to work. See dotnet/corefx#5120ClientWebSocket
which prevented the client & server on agreeing on the WebSockets subprotocol. This was fixed recently in .NET Core - see Fix subprotocol check in WebSocketHandle.Managed dotnet/corefx#25645ClientWebSocket
implementation on Linux is fairly complete; this is not the case on Windows. The Linux implementation is a managed implementation which works on Windows, too. See dotnet/corefx#5120 for how you can use the managed implementation on Windows.I think it's still safe to merge this code as: