Skip to content

Add v5 WebSocket streaming protocol support (stdin close/EOF)#1725

Open
Copilot wants to merge 3 commits intomasterfrom
copilot/add-v5-kubernetes-streaming-support
Open

Add v5 WebSocket streaming protocol support (stdin close/EOF)#1725
Copilot wants to merge 3 commits intomasterfrom
copilot/add-v5-kubernetes-streaming-support

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 18, 2026

The C# client only supported the v4 streaming protocol, which has no mechanism to signal EOF on stdin. The v5 protocol adds a dedicated close message ([255, channelIndex]) to close individual channels, enabling proper stdin EOF when piping data to a container process.

Changes

  • WebSocketProtocol.cs — Added V5BinaryWebsocketProtocol ("v5.channel.k8s.io") and V5Base64WebsocketProtocol ("v5.base64.channel.k8s.io") constants.
  • StreamDemuxer.cs — Added supportsClose constructor parameter + SupportsClose property; CloseChannel(byte/ChannelIndex) method that sends [255, channelIndex]; RunLoop handles incoming v5 close messages under lock(buffers) and tracks closedChannels so late GetStream callers get immediate EOF; Write now sets endOfMessage: true on the final (or only) chunk so close frames are always standalone WebSocket messages.
  • IStreamDemuxer.cs — Added CloseChannel overloads to the interface.
  • MuxedStream.cs — Overrides Dispose(bool) to send the v5 close frame when SupportsClose is true; also implements IAsyncDisposable with DisposeAsync() to allow callers to await the channel-close without blocking.

Usage

var webSocket = await client.WebSocketNamespacedPodExecAsync(...,
    webSocketSubProtocol: WebSocketProtocol.V5BinaryWebsocketProtocol);

var demuxer = new StreamDemuxer(webSocket, supportsClose: true);
demuxer.Start();

var stdin = demuxer.GetStream(null, ChannelIndex.StdIn);
await stdin.WriteAsync(data, 0, data.Length);

// Preferred: awaits the close frame without blocking
await stdin.DisposeAsync();

// Alternative: synchronous dispose also sends the close frame
// stdin.Dispose();

// Or explicitly: await demuxer.CloseChannel(ChannelIndex.StdIn);

🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@k8s-ci-robot k8s-ci-robot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Mar 18, 2026
Copilot AI linked an issue Mar 18, 2026 that may be closed by this pull request
@k8s-ci-robot k8s-ci-robot added size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Mar 18, 2026
Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
@k8s-ci-robot k8s-ci-robot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Mar 18, 2026
Copilot AI changed the title [WIP] Add support for v5 of Kubernetes streaming protocol Add v5 WebSocket streaming protocol support (stdin close/EOF) Mar 18, 2026
Copilot AI requested a review from brendandburns March 18, 2026 15:12
@brendandburns brendandburns marked this pull request as ready for review March 19, 2026 18:30
Copilot AI review requested due to automatic review settings March 19, 2026 18:30
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Mar 19, 2026
@k8s-ci-robot k8s-ci-robot requested a review from tg123 March 19, 2026 18:30
@brendandburns
Copy link
Copy Markdown
Contributor

/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Mar 19, 2026
@k8s-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: brendandburns, Copilot

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Mar 19, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Kubernetes remotecommand v5 WebSocket streaming protocol support to allow per-channel close/EOF signaling (notably for stdin), extending the existing demux/mux stream implementation and adding test coverage.

Changes:

  • Added v5 WebSocket subprotocol constants (v5.channel.k8s.io, v5.base64.channel.k8s.io).
  • Extended StreamDemuxer/IStreamDemuxer with v5 channel-close support and inbound close-frame handling.
  • Updated MuxedStream.Dispose to automatically send the v5 close frame when enabled; added new unit tests for v5 close behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/KubernetesClient/WebSocketProtocol.cs Adds v5 subprotocol constants for selecting the protocol at connect time.
src/KubernetesClient/IStreamDemuxer.cs Extends the demuxer contract with CloseChannel overloads.
src/KubernetesClient/StreamDemuxer.cs Implements sending/receiving v5 close-channel frames and exposes SupportsClose.
src/KubernetesClient/MuxedStream.cs Sends close-channel on disposal of writable streams when v5 close is enabled.
tests/KubernetesClient.Tests/StreamDemuxerTests.cs Adds tests covering send/receive close-channel and dispose behavior for v5 vs v4.
Comments suppressed due to low confidence (1)

src/KubernetesClient/StreamDemuxer.cs:226

  • CloseChannel sends a v5 close frame unconditionally. When SupportsClose is false (v4 protocol), this will still transmit the [255, index] message and can confuse/violate the negotiated subprotocol. Consider guarding this method (e.g., throw InvalidOperationException or no-op) when SupportsClose is false, and document the behavior in the interface XML docs.
        public async Task CloseChannel(byte index, CancellationToken cancellationToken = default)
        {
            var closeBuffer = new byte[] { ChannelCloseIndex, index };
            await webSocket.SendAsync(new ArraySegment<byte>(closeBuffer), WebSocketMessageType.Binary, true, cancellationToken)
                .ConfigureAwait(false);
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +260 to +264
var channelToClose = buffer[1];
if (buffers.ContainsKey(channelToClose))
{
buffers[channelToClose].WriteEnd();
}
Comment on lines +106 to +113
protected override void Dispose(bool disposing)
{
if (disposing && !disposed && outputIndex.HasValue && muxer != null && muxer.SupportsClose)
{
try
{
muxer.CloseChannel(outputIndex.Value).GetAwaiter().GetResult();
}
Comment on lines +548 to +552
stream.Dispose();

// Wait a short time and confirm no message was sent.
await Task.Delay(50).ConfigureAwait(true);
Assert.Empty(sentMessages);
/// </returns>
public async Task CloseChannel(byte index, CancellationToken cancellationToken = default)
{
var closeBuffer = new byte[] { ChannelCloseIndex, index };
Comment on lines +256 to +268
if (SupportsClose && streamIndex == ChannelCloseIndex)
{
if (result.Count >= 2)
{
var channelToClose = buffer[1];
if (buffers.ContainsKey(channelToClose))
{
buffers[channelToClose].WriteEnd();
}
}

continue;
}
@brendandburns
Copy link
Copy Markdown
Contributor

@copilot please address comments and fix the ci/cd failure

…, test cleanup

Agent-Logs-Url: https://github.com/kubernetes-client/csharp/sessions/f7ecce7d-42eb-4bcb-a761-de8b9a69956c

Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
@k8s-ci-robot k8s-ci-robot removed the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Apr 2, 2026
@k8s-ci-robot
Copy link
Copy Markdown
Contributor

New changes are detected. LGTM label has been removed.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot please address comments and fix the ci/cd failure

Addressed all five review comments in commit 495e01d:

  1. Thread safety (StreamDemuxer.cs:260-264): Added lock(buffers) around the v5 close handler in RunLoop; also moved the inputBuffer lookup inside the existing lock in GetStream to prevent races.

  2. Late-subscribe EOF (StreamDemuxer.cs:256-268): Added a closedChannels HashSet<byte> (protected by the same lock). When a v5 close arrives, the channel is added to closedChannels. GetStream checks closedChannels when registering a new buffer and calls WriteEnd() immediately if the server already closed that channel.

  3. endOfMessage flag (StreamDemuxer.cs:223): Write now passes endOfMessage: true for the final (or only) chunk, ensuring each channel-data write is a complete, standalone WebSocket message and the close frame can never be treated as a continuation fragment.

  4. Async dispose (MuxedStream.cs:106-113): Added IAsyncDisposable with a proper override async ValueTask DisposeAsync() that awaits CloseChannel. Synchronous Dispose retains its existing blocking behavior for backward compatibility.

  5. Test cleanup (StreamDemuxerTests.cs:548-552): Removed Task.Delay(50) + the useless WaitForAsync(() => true); the V4 test is now a plain synchronous [Fact]. Also added tests for DisposeAsync and late-subscribe EOF.

The CI failure (MSBuild 17 not supporting .NET SDK 10 on the Windows runner) is a pre-existing infrastructure issue unrelated to these changes.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • crl.comodoca.com
    • Triggering command: /tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/.dotnet/dotnet /tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/.dotnet/dotnet restore --no-dependencies /home/REDACTED/work/csharp/csharp/src/KubernetesClient.Classic/KubernetesClient.Classic.csproj --packages /tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
  • ocsp.comodoca.com
    • Triggering command: /tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/.dotnet/dotnet /tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/.dotnet/dotnet restore --no-dependencies /home/REDACTED/work/csharp/csharp/src/KubernetesClient.Classic/KubernetesClient.Classic.csproj --packages /tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/tmp/codeql-scratch-a12d4d5b5555dafc/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing support (closing stdin) for websocket exec

4 participants