Merge pull request #137298 from dims/dsri/cri-streaming-option-a-hardcut

cri streaming option a hardcut - add new staging repositories `streaming` and `cri-streaming`
This commit is contained in:
Kubernetes Prow Robot 2026-03-13 17:23:36 +05:30 committed by GitHub
commit 2bd6c7fe3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
159 changed files with 3496 additions and 600 deletions

4
go.mod
View file

@ -96,6 +96,7 @@ require (
k8s.io/controller-manager v0.0.0
k8s.io/cri-api v0.0.0
k8s.io/cri-client v0.0.0
k8s.io/cri-streaming v0.0.0
k8s.io/csi-translation-lib v0.0.0
k8s.io/dynamic-resource-allocation v0.0.0
k8s.io/endpointslice v0.0.0
@ -113,6 +114,7 @@ require (
k8s.io/mount-utils v0.0.0
k8s.io/pod-security-admission v0.0.0
k8s.io/sample-apiserver v0.0.0
k8s.io/streaming v0.0.0
k8s.io/system-validators v1.12.1
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730
@ -231,6 +233,7 @@ replace (
k8s.io/controller-manager => ./staging/src/k8s.io/controller-manager
k8s.io/cri-api => ./staging/src/k8s.io/cri-api
k8s.io/cri-client => ./staging/src/k8s.io/cri-client
k8s.io/cri-streaming => ./staging/src/k8s.io/cri-streaming
k8s.io/csi-translation-lib => ./staging/src/k8s.io/csi-translation-lib
k8s.io/dynamic-resource-allocation => ./staging/src/k8s.io/dynamic-resource-allocation
k8s.io/endpointslice => ./staging/src/k8s.io/endpointslice
@ -248,4 +251,5 @@ replace (
k8s.io/sample-apiserver => ./staging/src/k8s.io/sample-apiserver
k8s.io/sample-cli-plugin => ./staging/src/k8s.io/sample-cli-plugin
k8s.io/sample-controller => ./staging/src/k8s.io/sample-controller
k8s.io/streaming => ./staging/src/k8s.io/streaming
)

View file

@ -20,6 +20,7 @@ use (
./staging/src/k8s.io/controller-manager
./staging/src/k8s.io/cri-api
./staging/src/k8s.io/cri-client
./staging/src/k8s.io/cri-streaming
./staging/src/k8s.io/csi-translation-lib
./staging/src/k8s.io/dynamic-resource-allocation
./staging/src/k8s.io/endpointslice
@ -37,4 +38,5 @@ use (
./staging/src/k8s.io/sample-apiserver
./staging/src/k8s.io/sample-cli-plugin
./staging/src/k8s.io/sample-controller
./staging/src/k8s.io/streaming
)

View file

@ -0,0 +1,286 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tests
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
restclient "k8s.io/client-go/rest"
remoteclient "k8s.io/client-go/tools/remotecommand"
clientspdy "k8s.io/client-go/transport/spdy"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
streaming "k8s.io/cri-streaming/pkg/streaming"
remotecommandserver "k8s.io/cri-streaming/pkg/streaming/remotecommand"
streamingspdy "k8s.io/streaming/pkg/httpstream/spdy"
)
const (
execContainerID = "cri-streaming-exec-container"
attachContainerID = "cri-streaming-attach-container"
execInput = "exec-stdin"
execOutput = "exec-stdout"
execErr = "exec-stderr"
attachInput = "attach-stdin"
attachOutput = "attach-stdout"
attachErr = "attach-stderr"
)
type fakeStreamingRuntime struct{}
func (*fakeStreamingRuntime) Exec(ctx context.Context, containerID string, cmd []string, in io.Reader, out, errStream io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
if containerID != execContainerID {
return fmt.Errorf("unexpected exec container ID: %q", containerID)
}
stdinData, err := io.ReadAll(in)
if err != nil {
return err
}
if string(stdinData) != execInput {
return fmt.Errorf("unexpected exec stdin: %q", string(stdinData))
}
if _, err := io.WriteString(out, execOutput); err != nil {
return err
}
if _, err := io.WriteString(errStream, execErr); err != nil {
return err
}
return nil
}
func (*fakeStreamingRuntime) Attach(ctx context.Context, containerID string, in io.Reader, out, errStream io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
if containerID != attachContainerID {
return fmt.Errorf("unexpected attach container ID: %q", containerID)
}
stdinData, err := io.ReadAll(in)
if err != nil {
return err
}
if string(stdinData) != attachInput {
return fmt.Errorf("unexpected attach stdin: %q", string(stdinData))
}
if _, err := io.WriteString(out, attachOutput); err != nil {
return err
}
if _, err := io.WriteString(errStream, attachErr); err != nil {
return err
}
return nil
}
func (*fakeStreamingRuntime) PortForward(ctx context.Context, podSandboxID string, port int32, stream io.ReadWriteCloser) error {
return errors.New("not implemented")
}
func newCRIStreamingTestServer(t *testing.T) (streaming.Server, *httptest.Server) {
t.Helper()
var server streaming.Server
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server.ServeHTTP(w, r)
}))
baseURL, err := url.Parse(httpServer.URL)
if err != nil {
httpServer.Close()
t.Fatalf("failed to parse test server URL: %v", err)
}
config := streaming.DefaultConfig
config.BaseURL = baseURL
server, err = streaming.NewServer(config, &fakeStreamingRuntime{})
if err != nil {
httpServer.Close()
t.Fatalf("failed to create cri streaming server: %v", err)
}
return server, httpServer
}
// Verifies that client-go's SPDY executor can successfully stream against
// the extracted cri-streaming server for both Exec and Attach endpoints.
func TestCRIStreamingSPDYExecAttachCompatibility(t *testing.T) {
server, httpServer := newCRIStreamingTestServer(t)
defer httpServer.Close()
t.Run("exec", func(t *testing.T) {
response, err := server.GetExec(&runtimeapi.ExecRequest{
ContainerId: execContainerID,
Cmd: []string{"echo", "exec"},
Stdin: true,
Stdout: true,
Stderr: true,
})
if err != nil {
t.Fatalf("failed to get exec URL: %v", err)
}
requestURL, err := url.Parse(response.Url)
if err != nil {
t.Fatalf("failed to parse exec URL: %v", err)
}
executor, err := remoteclient.NewSPDYExecutor(&restclient.Config{Host: requestURL.Host}, "POST", requestURL)
if err != nil {
t.Fatalf("failed to build exec executor: %v", err)
}
var stdout bytes.Buffer
var stderr bytes.Buffer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = executor.StreamWithContext(ctx, remoteclient.StreamOptions{
Stdin: strings.NewReader(execInput),
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
t.Fatalf("unexpected exec stream error: %v", err)
}
if got := stdout.String(); got != execOutput {
t.Fatalf("unexpected exec stdout: %q", got)
}
if got := stderr.String(); got != execErr {
t.Fatalf("unexpected exec stderr: %q", got)
}
})
t.Run("attach", func(t *testing.T) {
response, err := server.GetAttach(&runtimeapi.AttachRequest{
ContainerId: attachContainerID,
Stdin: true,
Stdout: true,
Stderr: true,
})
if err != nil {
t.Fatalf("failed to get attach URL: %v", err)
}
requestURL, err := url.Parse(response.Url)
if err != nil {
t.Fatalf("failed to parse attach URL: %v", err)
}
executor, err := remoteclient.NewSPDYExecutor(&restclient.Config{Host: requestURL.Host}, "POST", requestURL)
if err != nil {
t.Fatalf("failed to build attach executor: %v", err)
}
var stdout bytes.Buffer
var stderr bytes.Buffer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = executor.StreamWithContext(ctx, remoteclient.StreamOptions{
Stdin: strings.NewReader(attachInput),
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
t.Fatalf("unexpected attach stream error: %v", err)
}
if got := stdout.String(); got != attachOutput {
t.Fatalf("unexpected attach stdout: %q", got)
}
if got := stderr.String(); got != attachErr {
t.Fatalf("unexpected attach stderr: %q", got)
}
})
}
// Verifies client-go's compatibility upgrader adapter path by building a
// streaming SPDY roundtripper from k8s.io/streaming and adapting it into a
// client-go remotecommand executor.
func TestCRIStreamingSPDYExecCompatibilityWithStreamingUpgraderAdapter(t *testing.T) {
server, httpServer := newCRIStreamingTestServer(t)
defer httpServer.Close()
response, err := server.GetExec(&runtimeapi.ExecRequest{
ContainerId: execContainerID,
Cmd: []string{"echo", "exec"},
Stdin: true,
Stdout: true,
Stderr: true,
})
if err != nil {
t.Fatalf("failed to get exec URL: %v", err)
}
requestURL, err := url.Parse(response.Url)
if err != nil {
t.Fatalf("failed to parse exec URL: %v", err)
}
roundTripper, err := streamingspdy.NewRoundTripperWithConfig(streamingspdy.RoundTripperConfig{
PingPeriod: 5 * time.Second,
})
if err != nil {
t.Fatalf("failed to build streaming roundtripper: %v", err)
}
executor, err := remoteclient.NewSPDYExecutorForTransports(
roundTripper,
clientspdy.NewUpgraderForStreaming(roundTripper),
"POST",
requestURL,
)
if err != nil {
t.Fatalf("failed to build adapter-based executor: %v", err)
}
var stdout bytes.Buffer
var stderr bytes.Buffer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = executor.StreamWithContext(ctx, remoteclient.StreamOptions{
Stdin: strings.NewReader(execInput),
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
t.Fatalf("unexpected exec stream error: %v", err)
}
if got := stdout.String(); got != execOutput {
t.Fatalf("unexpected exec stdout: %q", got)
}
if got := stderr.String(); got != execErr {
t.Fatalf("unexpected exec stderr: %q", got)
}
}

View file

@ -19,6 +19,7 @@ package tests
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"net"
@ -31,11 +32,12 @@ import (
"testing"
"time"
"k8s.io/apimachinery/pkg/types"
"github.com/gorilla/websocket"
restclient "k8s.io/client-go/rest"
. "k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
"k8s.io/cri-streaming/pkg/streaming/portforward"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
// fakePortForwarder simulates port forwarding for testing. It implements
@ -52,7 +54,7 @@ type fakePortForwarder struct {
var _ portforward.PortForwarder = &fakePortForwarder{}
func (pf *fakePortForwarder) PortForward(_ context.Context, name string, uid types.UID, port int32, stream io.ReadWriteCloser) error {
func (pf *fakePortForwarder) PortForward(_ context.Context, name string, uid string, port int32, stream io.ReadWriteCloser) error {
defer stream.Close()
// read from the client
@ -249,3 +251,108 @@ func TestForwardPortsReturnsErrorWhenAllBindsFailed(t *testing.T) {
t.Fatal("expected non-nil error for pf2.ForwardPorts")
}
}
func TestForwardPortsWebSocketV4Framing(t *testing.T) {
const testPort int32 = 5001
const clientPayload = "abcd"
const serverPayload = "1234"
done := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer close(done)
opts, err := portforward.NewV4Options(req)
if err != nil {
t.Errorf("failed to build v4 options: %v", err)
return
}
pf := &fakePortForwarder{
expected: map[int32]string{testPort: clientPayload},
received: make(map[int32]string),
send: map[int32]string{testPort: serverPayload},
}
portforward.ServePortForward(w, req, pf, "pod", "uid", opts, 0, 10*time.Second, portforward.SupportedProtocols)
got, ok := pf.received[testPort]
if !ok {
t.Errorf("server did not receive data for port %d", testPort)
return
}
if got != clientPayload {
t.Errorf("server expected %q, got %q for port %d", clientPayload, got, testPort)
}
}))
defer server.Close()
wsURL := strings.Replace(server.URL, "http://", "ws://", 1) + fmt.Sprintf("?port=%d", testPort)
dialer := &websocket.Dialer{
Subprotocols: []string{"v4." + wsstream.ChannelWebSocketProtocol},
}
conn, _, err := dialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("websocket dial failed: %v", err)
}
defer conn.Close()
expectedPortBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(expectedPortBytes, uint16(testPort))
seenDataPreamble := false
seenErrorPreamble := false
for !(seenDataPreamble && seenErrorPreamble) {
_, frame, err := conn.ReadMessage()
if err != nil {
t.Fatalf("failed reading websocket preamble frame: %v", err)
}
if len(frame) == 0 {
continue
}
channel := frame[0]
payload := frame[1:]
switch channel {
case 0:
if !bytes.Equal(expectedPortBytes, payload) {
t.Fatalf("unexpected data-channel preamble payload: %q", payload)
}
seenDataPreamble = true
case 1:
if !bytes.Equal(expectedPortBytes, payload) {
t.Fatalf("unexpected error-channel preamble payload: %q", payload)
}
seenErrorPreamble = true
}
}
frame := append([]byte{0}, []byte(clientPayload)...)
if err := conn.WriteMessage(websocket.BinaryMessage, frame); err != nil {
t.Fatalf("failed writing data frame: %v", err)
}
gotServerPayload := false
for !gotServerPayload {
_, frame, err := conn.ReadMessage()
if err != nil {
t.Fatalf("failed reading websocket response frame: %v", err)
}
if len(frame) == 0 {
continue
}
if frame[0] != 0 {
continue
}
if bytes.Equal(frame[1:], []byte(serverPayload)) {
gotServerPayload = true
}
}
if err := conn.Close(); err != nil {
t.Fatalf("failed closing websocket connection: %v", err)
}
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("timeout waiting for websocket portforward handler to complete")
}
}

View file

@ -31,15 +31,14 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/httpstream"
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
restclient "k8s.io/client-go/rest"
remoteclient "k8s.io/client-go/tools/remotecommand"
"k8s.io/client-go/transport/spdy"
"k8s.io/kubelet/pkg/cri/streaming/remotecommand"
"k8s.io/cri-streaming/pkg/streaming/remotecommand"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/streaming/pkg/httpstream"
)
type fakeExecutor struct {
@ -56,22 +55,22 @@ type fakeExecutor struct {
exec bool
}
func (ex *fakeExecutor) ExecInContainer(_ context.Context, name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remoteclient.TerminalSize, timeout time.Duration) error {
func (ex *fakeExecutor) ExecInContainer(_ context.Context, name string, uid string, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error {
return ex.run(name, uid, container, cmd, in, out, err, tty)
}
func (ex *fakeExecutor) AttachContainer(_ context.Context, name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remoteclient.TerminalSize) error {
func (ex *fakeExecutor) AttachContainer(_ context.Context, name string, uid string, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
return ex.run(name, uid, container, nil, in, out, err, tty)
}
func (ex *fakeExecutor) run(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error {
func (ex *fakeExecutor) run(name string, uid string, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error {
ex.command = cmd
ex.tty = tty
if e, a := "pod", name; e != a {
ex.t.Errorf("%s: pod: expected %q, got %q", ex.testName, e, a)
}
if e, a := "uid", uid; e != string(a) {
if e, a := "uid", uid; e != a {
ex.t.Errorf("%s: uid: expected %q, got %q", ex.testName, e, a)
}
if ex.exec {
@ -339,7 +338,7 @@ func TestDial(t *testing.T) {
Body: io.NopCloser(&bytes.Buffer{}),
},
}
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: upgrader}, "POST", &url.URL{Host: "something.com", Scheme: "https"})
dialer := spdy.NewDialerForStreaming(spdy.NewUpgraderForStreaming(upgrader), &http.Client{Transport: upgrader}, "POST", &url.URL{Host: "something.com", Scheme: "https"})
conn, protocol, err := dialer.Dial("protocol1")
if err != nil {
t.Fatal(err)

View file

@ -46,9 +46,9 @@ import (
utilfeature "k8s.io/apiserver/pkg/util/feature"
resourcehelper "k8s.io/component-helpers/resource"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
"k8s.io/cri-streaming/pkg/streaming/portforward"
remotecommandserver "k8s.io/cri-streaming/pkg/streaming/remotecommand"
"k8s.io/klog/v2"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand"
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/api/v1/resource"
podshelper "k8s.io/kubernetes/pkg/apis/core/pods"

View file

@ -49,8 +49,8 @@ import (
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/component-base/metrics/testutil"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
"k8s.io/kubelet/pkg/cri/streaming/remotecommand"
"k8s.io/cri-streaming/pkg/streaming/portforward"
"k8s.io/cri-streaming/pkg/streaming/remotecommand"
_ "k8s.io/kubernetes/pkg/apis/core/install"
"k8s.io/kubernetes/pkg/features"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"

View file

@ -77,11 +77,11 @@ import (
zpagesfeatures "k8s.io/component-base/zpages/features"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
"k8s.io/cri-client/pkg/util"
"k8s.io/cri-streaming/pkg/streaming"
"k8s.io/cri-streaming/pkg/streaming/portforward"
remotecommandserver "k8s.io/cri-streaming/pkg/streaming/remotecommand"
podresourcesapi "k8s.io/kubelet/pkg/apis/podresources/v1"
podresourcesapiv1alpha1 "k8s.io/kubelet/pkg/apis/podresources/v1alpha1"
"k8s.io/kubelet/pkg/cri/streaming"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand"
kubelettypes "k8s.io/kubelet/pkg/types"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"

View file

@ -50,8 +50,6 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apimachinery/pkg/util/wait"
@ -60,11 +58,13 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/tools/remotecommand"
remotecommand "k8s.io/client-go/tools/remotecommand"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
statsapi "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/test/utils/ktesting"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/spdy"
// Do some initialization to decode the query parameters correctly.
"k8s.io/apiserver/pkg/server/dynamiccertificates"
@ -75,9 +75,9 @@ import (
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/kubelet/pkg/cri/streaming"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand"
"k8s.io/cri-streaming/pkg/streaming"
"k8s.io/cri-streaming/pkg/streaming/portforward"
remotecommandserver "k8s.io/cri-streaming/pkg/streaming/remotecommand"
_ "k8s.io/kubernetes/pkg/apis/core/install"
"k8s.io/kubernetes/pkg/features"
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
@ -180,16 +180,16 @@ func (fk *fakeKubelet) SyncLoopHealthCheck(req *http.Request) error {
}
type fakeRuntime struct {
execFunc func(string, []string, io.Reader, io.WriteCloser, io.WriteCloser, bool, <-chan remotecommand.TerminalSize) error
attachFunc func(string, io.Reader, io.WriteCloser, io.WriteCloser, bool, <-chan remotecommand.TerminalSize) error
execFunc func(string, []string, io.Reader, io.WriteCloser, io.WriteCloser, bool, <-chan remotecommandserver.TerminalSize) error
attachFunc func(string, io.Reader, io.WriteCloser, io.WriteCloser, bool, <-chan remotecommandserver.TerminalSize) error
portForwardFunc func(string, int32, io.ReadWriteCloser) error
}
func (f *fakeRuntime) Exec(_ context.Context, containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
func (f *fakeRuntime) Exec(_ context.Context, containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
return f.execFunc(containerID, cmd, stdin, stdout, stderr, tty, resize)
}
func (f *fakeRuntime) Attach(_ context.Context, containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
func (f *fakeRuntime) Attach(_ context.Context, containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
return f.attachFunc(containerID, stdin, stdout, stderr, tty, resize)
}
@ -1544,12 +1544,12 @@ func testExecAttach(t *testing.T, verb string) {
return nil
}
ss.fakeRuntime.execFunc = func(containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
ss.fakeRuntime.execFunc = func(containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
assert.Equal(t, expectedCommand, strings.Join(cmd, " "), "cmd")
return testStream(containerID, stdin, stdout, stderr, tty, done)
}
ss.fakeRuntime.attachFunc = func(containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
ss.fakeRuntime.attachFunc = func(containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
return testStream(containerID, stdin, stdout, stderr, tty, done)
}
@ -1686,7 +1686,7 @@ func TestWebsocketExecAttach(t *testing.T) {
attachInvoked = true
}
ss.fakeRuntime.attachFunc = func(containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
ss.fakeRuntime.attachFunc = func(containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
defer close(done)
defer stdout.Close() //nolint:errcheck
_, err := io.Copy(stdout, stdin)
@ -2271,7 +2271,7 @@ func TestGetExecWebSocketHandlerSelection(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExtendWebSocketsToKubelet, tt.enableExtendWebSockets)
ss.fakeRuntime.execFunc = func(_ string, _ []string, _ io.Reader, stdout, _ io.WriteCloser, _ bool, _ <-chan remotecommand.TerminalSize) error {
ss.fakeRuntime.execFunc = func(_ string, _ []string, _ io.Reader, stdout, _ io.WriteCloser, _ bool, _ <-chan remotecommandserver.TerminalSize) error {
stdout.Close() //nolint:errcheck
return nil
}
@ -2361,7 +2361,7 @@ func TestGetAttachWebSocketHandlerSelection(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExtendWebSocketsToKubelet, tt.enableExtendWebSockets)
ss.fakeRuntime.attachFunc = func(_ string, _ io.Reader, stdout, _ io.WriteCloser, _ bool, _ <-chan remotecommand.TerminalSize) error {
ss.fakeRuntime.attachFunc = func(_ string, _ io.Reader, stdout, _ io.WriteCloser, _ bool, _ <-chan remotecommandserver.TerminalSize) error {
stdout.Close() //nolint:errcheck
return nil
}

View file

@ -29,7 +29,7 @@ import (
"golang.org/x/net/websocket"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
"k8s.io/cri-streaming/pkg/streaming/portforward"
"k8s.io/kubernetes/test/utils/ktesting"
)

View file

@ -24,7 +24,6 @@ import (
"slices"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/proxy"
"k8s.io/apiserver/pkg/authorization/authorizer"
@ -38,6 +37,7 @@ import (
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/kubelet/client"
"k8s.io/kubernetes/pkg/registry/core/pod"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
// ProxyREST implements the proxy subresource for a Pod

View file

@ -20,6 +20,7 @@ Repositories currently staged here:
- [`k8s.io/controller-manager`](https://github.com/kubernetes/controller-manager)
- [`k8s.io/cri-api`](https://github.com/kubernetes/cri-api)
- [`k8s.io/cri-client`](https://github.com/kubernetes/cri-client)
- [`k8s.io/cri-streaming`](https://github.com/kubernetes/cri-streaming)
- [`k8s.io/csi-translation-lib`](https://github.com/kubernetes/csi-translation-lib)
- [`k8s.io/dynamic-resource-allocation`](https://github.com/kubernetes/dynamic-resource-allocation)
- [`k8s.io/endpointslice`](https://github.com/kubernetes/endpointslice)

View file

@ -38,6 +38,7 @@
allowedImports:
- k8s.io/apimachinery
- k8s.io/kube-openapi
- k8s.io/streaming
- k8s.io/utils/clock
- k8s.io/utils/dump
- k8s.io/utils/net
@ -80,6 +81,7 @@
- k8s.io/client-go
- k8s.io/klog
- k8s.io/kube-openapi
- k8s.io/streaming
- k8s.io/utils
# prevent core machinery from taking explicit v1 references unless
@ -116,6 +118,7 @@
- k8s.io/client-go
- k8s.io/component-base
- k8s.io/kube-openapi
- k8s.io/streaming
- k8s.io/utils
- k8s.io/klog
- k8s.io/kms
@ -139,6 +142,7 @@
- k8s.io/component-base
- k8s.io/kube-aggregator
- k8s.io/kube-openapi
- k8s.io/streaming
- k8s.io/klog
- k8s.io/utils
@ -153,6 +157,7 @@
- k8s.io/kubectl
- k8s.io/kube-openapi
- k8s.io/metrics
- k8s.io/streaming
- k8s.io/utils
- k8s.io/klog
@ -327,6 +332,20 @@
- k8s.io/klog/v2
- k8s.io/utils
- baseImportPath: "./staging/src/k8s.io/cri-streaming"
allowedImports:
- k8s.io/cri-api
- k8s.io/cri-streaming
- k8s.io/klog/v2
- k8s.io/streaming
- k8s.io/utils
- baseImportPath: "./staging/src/k8s.io/streaming"
allowedImports:
- k8s.io/klog/v2
- k8s.io/streaming
- k8s.io/utils
- baseImportPath: "./staging/src/k8s.io/externaljwt"
allowedImports:
- k8s.io/externaljwt

View file

@ -1,7 +1,18 @@
rules:
- destination: streaming
branches:
- name: master
source:
branch: master
dirs:
- staging/src/k8s.io/streaming
library: true
- destination: apimachinery
branches:
- name: master
dependencies:
- repository: streaming
branch: master
source:
branch: master
dirs:
@ -33,6 +44,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
source:
branch: master
dirs:
@ -76,6 +89,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
source:
@ -149,6 +164,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
source:
branch: master
dirs:
@ -191,6 +208,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
@ -254,6 +273,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
@ -317,6 +338,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
source:
branch: master
dirs:
@ -360,6 +383,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
@ -443,6 +468,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
@ -545,6 +572,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
@ -672,6 +701,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
@ -769,6 +800,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
@ -881,6 +914,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
@ -956,6 +991,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: client-go
branch: master
source:
@ -1019,6 +1056,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: cli-runtime
branch: master
- repository: client-go
@ -1089,6 +1128,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: component-base
branch: master
- repository: api
@ -1192,6 +1233,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: client-go
branch: master
- repository: component-base
@ -1267,24 +1310,33 @@ rules:
dirs:
- staging/src/k8s.io/cri-client
library: true
- destination: cri-streaming
branches:
- name: master
dependencies:
- repository: cri-api
branch: master
- repository: streaming
branch: master
source:
branch: master
dirs:
- staging/src/k8s.io/cri-streaming
library: true
- destination: kubelet
branches:
- name: master
dependencies:
- repository: apimachinery
branch: master
- repository: apiserver
- repository: streaming
branch: master
- repository: api
branch: master
- repository: client-go
branch: master
- repository: cri-api
branch: master
- repository: component-base
branch: master
- repository: kms
branch: master
source:
branch: master
dirs:
@ -1378,6 +1430,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: client-go
branch: master
- repository: component-base
@ -1471,6 +1525,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: apiserver
branch: master
- repository: client-go
@ -1582,6 +1638,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: apiserver
branch: master
- repository: component-base
@ -1705,6 +1763,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: api
branch: master
source:
@ -1760,6 +1820,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
source:
branch: master
dirs:
@ -1841,6 +1903,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: cli-runtime
branch: master
- repository: client-go
@ -1954,6 +2018,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: apiserver
branch: master
- repository: client-go
@ -2045,14 +2111,14 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: apiserver
branch: master
- repository: api
branch: master
- repository: client-go
branch: master
- repository: cri-api
branch: master
- repository: component-base
branch: master
- repository: component-helpers
@ -2167,6 +2233,8 @@ rules:
dependencies:
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: component-base
branch: master
- repository: api
@ -2272,6 +2340,8 @@ rules:
branch: master
- repository: apimachinery
branch: master
- repository: streaming
branch: master
- repository: client-go
branch: master
- repository: component-base

View file

@ -35,4 +35,7 @@ require (
sigs.k8s.io/yaml v1.6.0 // indirect
)
replace k8s.io/apimachinery => ../apimachinery
replace (
k8s.io/apimachinery => ../apimachinery
k8s.io/streaming => ../streaming
)

View file

@ -1,5 +1,4 @@
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -122,6 +122,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect
k8s.io/kms v0.0.0 // indirect
k8s.io/streaming v0.0.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
)
@ -133,4 +134,5 @@ replace (
k8s.io/code-generator => ../code-generator
k8s.io/component-base => ../component-base
k8s.io/kms => ../kms
k8s.io/streaming => ../streaming
)

View file

@ -7,12 +7,10 @@ go 1.26.0
godebug default=go1.26
require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/fxamacker/cbor/v2 v2.9.0
github.com/google/gnostic-models v0.7.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/moby/spdystream v0.5.0
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/spf13/pflag v1.0.9
@ -23,6 +21,7 @@ require (
gopkg.in/inf.v0 v0.9.1
k8s.io/klog/v2 v2.140.0
k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf
k8s.io/streaming v0.0.0
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730
sigs.k8s.io/randfill v1.0.0
@ -39,6 +38,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/onsi/ginkgo/v2 v2.28.1 // indirect
@ -51,3 +51,5 @@ require (
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace k8s.io/streaming => ../streaming

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package httpstream adds multiplexed streaming support to HTTP requests and
// responses via connection upgrades.
// Package httpstream contains compatibility wrappers for streaming transport APIs.
//
// Deprecated: use k8s.io/streaming/pkg/httpstream directly.
package httpstream

View file

@ -0,0 +1,20 @@
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package spdy contains compatibility wrappers for the SPDY transport stack.
//
// Deprecated: use k8s.io/streaming/pkg/httpstream/spdy directly.
package spdy

View file

@ -0,0 +1,236 @@
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package spdy
import (
"crypto/tls"
"net"
"net/http"
"net/url"
"time"
apihttpstream "k8s.io/apimachinery/pkg/util/httpstream"
streamhttp "k8s.io/streaming/pkg/httpstream"
streamspdy "k8s.io/streaming/pkg/httpstream/spdy"
)
const HeaderSpdy31 = streamspdy.HeaderSpdy31
// SpdyRoundTripper is a compatibility wrapper around the streaming module's
// SPDY round tripper.
type SpdyRoundTripper struct {
delegate *streamspdy.SpdyRoundTripper
}
func NewRoundTripper(tlsConfig *tls.Config) (*SpdyRoundTripper, error) {
delegate, err := streamspdy.NewRoundTripper(tlsConfig)
if err != nil {
return nil, err
}
return &SpdyRoundTripper{delegate: delegate}, nil
}
func NewRoundTripperWithProxy(tlsConfig *tls.Config, proxier func(*http.Request) (*url.URL, error)) (*SpdyRoundTripper, error) {
delegate, err := streamspdy.NewRoundTripperWithProxy(tlsConfig, proxier)
if err != nil {
return nil, err
}
return &SpdyRoundTripper{delegate: delegate}, nil
}
// RoundTripperConfig is a set of options for an SpdyRoundTripper.
type RoundTripperConfig struct {
// TLS configuration used by the round tripper if UpgradeTransport not present.
TLS *tls.Config
// Proxier is a proxy function invoked on each request. Optional.
Proxier func(*http.Request) (*url.URL, error)
// PingPeriod is a period for sending SPDY Pings on the connection.
// Optional.
PingPeriod time.Duration
// UpgradeTransport is a subtitute transport used for dialing. If set,
// this field will be used instead of "TLS" and "Proxier" for connection creation.
// Optional.
UpgradeTransport http.RoundTripper
}
func NewRoundTripperWithConfig(cfg RoundTripperConfig) (*SpdyRoundTripper, error) {
delegate, err := streamspdy.NewRoundTripperWithConfig(streamspdy.RoundTripperConfig{
TLS: cfg.TLS,
Proxier: cfg.Proxier,
PingPeriod: cfg.PingPeriod,
UpgradeTransport: cfg.UpgradeTransport,
})
if err != nil {
return nil, err
}
return &SpdyRoundTripper{delegate: delegate}, nil
}
// TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during
// proxying with a spdy roundtripper.
func (s *SpdyRoundTripper) TLSClientConfig() *tls.Config {
return s.delegate.TLSClientConfig()
}
// Dial opens a network connection for an upgrade request.
func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) {
return s.delegate.Dial(req)
}
// RoundTrip executes a request and upgrades the connection.
func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return s.delegate.RoundTrip(req)
}
// NewConnection validates a server upgrade response and prepares the transport.
func (s *SpdyRoundTripper) NewConnection(resp *http.Response) (apihttpstream.Connection, error) {
conn, err := s.delegate.NewConnection(resp)
if err != nil {
return nil, err
}
return wrapConnection(conn), nil
}
type responseUpgraderAdapter struct {
delegate streamhttp.ResponseUpgrader
}
func (r *responseUpgraderAdapter) UpgradeResponse(w http.ResponseWriter, req *http.Request, newStreamHandler apihttpstream.NewStreamHandler) apihttpstream.Connection {
conn := r.delegate.UpgradeResponse(w, req, wrapNewStreamHandler(newStreamHandler))
return wrapConnection(conn)
}
func NewResponseUpgrader() apihttpstream.ResponseUpgrader {
return &responseUpgraderAdapter{delegate: streamspdy.NewResponseUpgrader()}
}
func NewResponseUpgraderWithPings(pingPeriod time.Duration) apihttpstream.ResponseUpgrader {
return &responseUpgraderAdapter{delegate: streamspdy.NewResponseUpgraderWithPings(pingPeriod)}
}
func NewClientConnection(conn net.Conn) (apihttpstream.Connection, error) {
c, err := streamspdy.NewClientConnection(conn)
if err != nil {
return nil, err
}
return wrapConnection(c), nil
}
func NewClientConnectionWithPings(conn net.Conn, pingPeriod time.Duration) (apihttpstream.Connection, error) {
c, err := streamspdy.NewClientConnectionWithPings(conn, pingPeriod)
if err != nil {
return nil, err
}
return wrapConnection(c), nil
}
func NewServerConnection(conn net.Conn, newStreamHandler apihttpstream.NewStreamHandler) (apihttpstream.Connection, error) {
c, err := streamspdy.NewServerConnection(conn, wrapNewStreamHandler(newStreamHandler))
if err != nil {
return nil, err
}
return wrapConnection(c), nil
}
func NewServerConnectionWithPings(conn net.Conn, newStreamHandler apihttpstream.NewStreamHandler, pingPeriod time.Duration) (apihttpstream.Connection, error) {
c, err := streamspdy.NewServerConnectionWithPings(conn, wrapNewStreamHandler(newStreamHandler), pingPeriod)
if err != nil {
return nil, err
}
return wrapConnection(c), nil
}
type streamAdapter struct {
delegate streamhttp.Stream
}
func (s *streamAdapter) Read(p []byte) (int, error) {
return s.delegate.Read(p)
}
func (s *streamAdapter) Write(p []byte) (int, error) {
return s.delegate.Write(p)
}
func (s *streamAdapter) Close() error {
return s.delegate.Close()
}
func (s *streamAdapter) Reset() error {
return s.delegate.Reset()
}
func (s *streamAdapter) Headers() http.Header {
return s.delegate.Headers()
}
func (s *streamAdapter) Identifier() uint32 {
return s.delegate.Identifier()
}
type connectionAdapter struct {
delegate streamhttp.Connection
}
func (c *connectionAdapter) CreateStream(headers http.Header) (apihttpstream.Stream, error) {
stream, err := c.delegate.CreateStream(headers)
if err != nil {
return nil, err
}
return &streamAdapter{delegate: stream}, nil
}
func (c *connectionAdapter) Close() error {
return c.delegate.Close()
}
func (c *connectionAdapter) CloseChan() <-chan bool {
return c.delegate.CloseChan()
}
func (c *connectionAdapter) SetIdleTimeout(timeout time.Duration) {
c.delegate.SetIdleTimeout(timeout)
}
func (c *connectionAdapter) RemoveStreams(streams ...apihttpstream.Stream) {
streamingStreams := make([]streamhttp.Stream, 0, len(streams))
for _, stream := range streams {
if stream == nil {
continue
}
if s, ok := stream.(streamhttp.Stream); ok {
streamingStreams = append(streamingStreams, s)
}
}
c.delegate.RemoveStreams(streamingStreams...)
}
func wrapConnection(conn streamhttp.Connection) apihttpstream.Connection {
if conn == nil {
return nil
}
return &connectionAdapter{delegate: conn}
}
func wrapNewStreamHandler(newStreamHandler apihttpstream.NewStreamHandler) streamhttp.NewStreamHandler {
if newStreamHandler == nil {
return nil
}
return func(stream streamhttp.Stream, replySent <-chan struct{}) error {
return newStreamHandler(&streamAdapter{delegate: stream}, replySent)
}
}

View file

@ -14,56 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package wsstream contains utilities for streaming content over WebSockets.
// The Conn type allows callers to multiplex multiple read/write channels over
// a single websocket.
// Package wsstream contains compatibility wrappers for websocket streaming.
//
// "channel.k8s.io"
//
// The Websocket RemoteCommand subprotocol "channel.k8s.io" prepends each binary message with a
// byte indicating the channel number (zero indexed) the message was sent on. Messages in both
// directions should prefix their messages with this channel byte. Used for remote execution,
// the channel numbers are by convention defined to match the POSIX file-descriptors assigned
// to STDIN, STDOUT, and STDERR (0, 1, and 2). No other conversion is performed on the raw
// subprotocol - writes are sent as they are received by the server.
//
// Example client session:
//
// CONNECT http://server.com with subprotocol "channel.k8s.io"
// WRITE []byte{0, 102, 111, 111, 10} # send "foo\n" on channel 0 (STDIN)
// READ []byte{1, 10} # receive "\n" on channel 1 (STDOUT)
// CLOSE
//
// "v2.channel.k8s.io"
//
// The second Websocket subprotocol version "v2.channel.k8s.io" is the same as version 1,
// but it is the first "versioned" subprotocol.
//
// "v3.channel.k8s.io"
//
// The third version of the Websocket RemoteCommand subprotocol adds another channel
// for terminal resizing events. This channel is prepended with the byte '3', and it
// transmits two window sizes (encoding TerminalSize struct) with integers in the range
// (0,65536].
//
// "v4.channel.k8s.io"
//
// The fourth version of the Websocket RemoteCommand subprotocol adds a channel for
// errors. This channel returns structured errors containing process exit codes. The
// error is "apierrors.StatusError{}".
//
// "v5.channel.k8s.io"
//
// The fifth version of the Websocket RemoteCommand subprotocol adds a CLOSE signal,
// which is sent as the first byte of the message. The second byte is the channel
// id. This CLOSE signal is handled by the websocket server by closing the stream,
// allowing the other streams to complete transmission if necessary, and gracefully
// shutdown the connection.
//
// Example client session:
//
// CONNECT http://server.com with subprotocol "v5.channel.k8s.io"
// WRITE []byte{0, 102, 111, 111, 10} # send "foo\n" on channel 0 (STDIN)
// WRITE []byte{255, 0} # send CLOSE signal (STDIN)
// CLOSE
// Deprecated: use k8s.io/streaming/pkg/httpstream/wsstream directly.
package wsstream

View file

@ -0,0 +1,91 @@
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wsstream
import (
"io"
"net/http"
"time"
"golang.org/x/net/websocket"
"k8s.io/klog/v2"
streamws "k8s.io/streaming/pkg/httpstream/wsstream"
)
const (
WebSocketProtocolHeader = streamws.WebSocketProtocolHeader
ChannelWebSocketProtocol = streamws.ChannelWebSocketProtocol
Base64ChannelWebSocketProtocol = streamws.Base64ChannelWebSocketProtocol
)
type ChannelType = streamws.ChannelType
const (
IgnoreChannel = streamws.IgnoreChannel
ReadChannel = streamws.ReadChannel
WriteChannel = streamws.WriteChannel
ReadWriteChannel = streamws.ReadWriteChannel
)
func IsWebSocketRequest(req *http.Request) bool {
return streamws.IsWebSocketRequest(req)
}
func IsWebSocketRequestWithStreamCloseProtocol(req *http.Request) bool {
return streamws.IsWebSocketRequestWithStreamCloseProtocol(req)
}
func IsWebSocketRequestWithTunnelingProtocol(req *http.Request) bool {
return streamws.IsWebSocketRequestWithTunnelingProtocol(req)
}
func IgnoreReceives(ws *websocket.Conn, timeout time.Duration) {
streamws.IgnoreReceives(ws, timeout)
}
func IgnoreReceivesWithLogger(logger klog.Logger, ws *websocket.Conn, timeout time.Duration) {
streamws.IgnoreReceivesWithLogger(logger, ws, timeout)
}
type ChannelProtocolConfig = streamws.ChannelProtocolConfig
func NewDefaultChannelProtocols(channels []ChannelType) map[string]ChannelProtocolConfig {
return streamws.NewDefaultChannelProtocols(channels)
}
type Conn = streamws.Conn
func NewConn(protocols map[string]ChannelProtocolConfig) *Conn {
return streamws.NewConn(protocols)
}
type ReaderProtocolConfig = streamws.ReaderProtocolConfig
func NewDefaultReaderProtocols() map[string]ReaderProtocolConfig {
return streamws.NewDefaultReaderProtocols()
}
type Reader = streamws.Reader
func NewReader(r io.Reader, ping bool, protocols map[string]ReaderProtocolConfig) *Reader {
return streamws.NewReader(r, ping, protocols)
}
func NewReaderWithLogger(logger klog.Logger, r io.Reader, ping bool, protocols map[string]ReaderProtocolConfig) *Reader {
return streamws.NewReaderWithLogger(logger, r, ping, protocols)
}

View file

@ -31,9 +31,9 @@ import (
"time"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/httpstream"
utilnet "k8s.io/apimachinery/pkg/util/net"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/streaming/pkg/httpstream"
"github.com/mxk/go-flowrate/flowrate"

View file

@ -42,8 +42,8 @@ import (
"golang.org/x/net/websocket"
"k8s.io/apimachinery/pkg/util/httpstream"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/streaming/pkg/httpstream"
)
const fakeStatusCode = 567

View file

@ -55,6 +55,7 @@ require (
k8s.io/klog/v2 v2.140.0
k8s.io/kms v0.0.0
k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf
k8s.io/streaming v0.0.0
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730
@ -129,4 +130,5 @@ replace (
k8s.io/client-go => ../client-go
k8s.io/component-base => ../component-base
k8s.io/kms => ../kms
k8s.io/streaming => ../streaming
)

View file

@ -24,8 +24,8 @@ import (
"strings"
"unicode/utf8"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
const bearerProtocolPrefix = "base64url.bearer.authorization.k8s.io."

View file

@ -34,7 +34,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
@ -44,6 +43,7 @@ import (
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/flushwriter"
"k8s.io/component-base/tracing"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
// StreamObject performs input stream negotiation from a ResourceStreamer and writes that to the response.

View file

@ -30,7 +30,6 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/audit"
@ -44,6 +43,7 @@ import (
compbasemetrics "k8s.io/component-base/metrics"
"k8s.io/component-base/tracing"
"k8s.io/klog/v2"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
// timeoutFactory abstracts watch timeout logic for testing

View file

@ -26,11 +26,12 @@ import (
"github.com/mxk/go-flowrate/flowrate"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
constants "k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apiserver/pkg/util/proxy/metrics"
"k8s.io/client-go/tools/remotecommand"
clientspdy "k8s.io/client-go/transport/spdy"
"k8s.io/client-go/util/exec"
"k8s.io/streaming/pkg/httpstream/spdy"
)
// StreamTranslatorHandler is a handler which translates WebSocket stream data
@ -77,7 +78,12 @@ func (h *StreamTranslatorHandler) ServeHTTP(w http.ResponseWriter, req *http.Req
websocketStreams.writeStatus(apierrors.NewInternalError(err)) //nolint:errcheck
return
}
spdyExecutor, err := remotecommand.NewSPDYExecutorRejectRedirects(spdyRoundTripper, spdyRoundTripper, "POST", h.Location)
spdyExecutor, err := remotecommand.NewSPDYExecutorRejectRedirects(
spdyRoundTripper,
clientspdy.NewUpgraderForStreaming(spdyRoundTripper),
"POST",
h.Location,
)
if err != nil {
metrics.IncStreamTranslatorRequest(req.Context(), strconv.Itoa(http.StatusInternalServerError))
websocketStreams.writeStatus(apierrors.NewInternalError(err)) //nolint:errcheck

View file

@ -38,8 +38,6 @@ import (
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
rcconstants "k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/util/proxy/metrics"
@ -48,6 +46,8 @@ import (
"k8s.io/client-go/transport"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/spdy"
)
// TestStreamTranslator_LoopbackStdinToStdout returns random data sent on the client's

View file

@ -31,14 +31,14 @@ import (
gwebsocket "github.com/gorilla/websocket"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
utilnet "k8s.io/apimachinery/pkg/util/net"
constants "k8s.io/apimachinery/pkg/util/portforward"
"k8s.io/apiserver/pkg/util/proxy/metrics"
"k8s.io/client-go/tools/portforward"
"k8s.io/klog/v2"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/spdy"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
// TunnelingHandler is a handler which tunnels SPDY through WebSockets.

View file

@ -33,8 +33,6 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
constants "k8s.io/apimachinery/pkg/util/portforward"
"k8s.io/apimachinery/pkg/util/proxy"
"k8s.io/apimachinery/pkg/util/wait"
@ -44,6 +42,8 @@ import (
"k8s.io/client-go/tools/portforward"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/spdy"
)
func TestTunnelingHandler_UpgradeStreamingAndTunneling(t *testing.T) {

View file

@ -21,7 +21,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
// fakeHandler implements http.Handler interface

View file

@ -25,10 +25,10 @@ import (
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
constants "k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
const (

View file

@ -1,59 +0,0 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Deprecated: This WebSockets package under apiserver is no longer in use.
// Please use the apimachinery version of the package at:
//
// k8s.io/apimachinery/pkg/util/httpstream/wsstream
package wsstream
import apimachinerywsstream "k8s.io/apimachinery/pkg/util/httpstream/wsstream"
// Aliases for all exported symbols previously in "conn.go"
const (
ChannelWebSocketProtocol = apimachinerywsstream.ChannelWebSocketProtocol
Base64ChannelWebSocketProtocol = apimachinerywsstream.Base64ChannelWebSocketProtocol
)
type ChannelType = apimachinerywsstream.ChannelType
const (
IgnoreChannel = apimachinerywsstream.IgnoreChannel
ReadChannel = apimachinerywsstream.ReadChannel
WriteChannel = apimachinerywsstream.WriteChannel
ReadWriteChannel = apimachinerywsstream.ReadWriteChannel
)
type ChannelProtocolConfig = apimachinerywsstream.ChannelProtocolConfig
var (
IsWebSocketRequest = apimachinerywsstream.IsWebSocketRequest
IgnoreReceives = apimachinerywsstream.IgnoreReceives
NewDefaultChannelProtocols = apimachinerywsstream.NewDefaultChannelProtocols
)
type Conn = apimachinerywsstream.Conn
var NewConn = apimachinerywsstream.NewConn
// Aliases for all exported symbols previously in "stream.go"
type ReaderProtocolConfig = apimachinerywsstream.ReaderProtocolConfig
var NewDefaultReaderProtocols = apimachinerywsstream.NewDefaultReaderProtocols
type Reader = apimachinerywsstream.Reader
var NewReader = apimachinerywsstream.NewReader

View file

@ -73,4 +73,5 @@ replace (
k8s.io/api => ../api
k8s.io/apimachinery => ../apimachinery
k8s.io/client-go => ../client-go
k8s.io/streaming => ../streaming
)

View file

@ -4,7 +4,6 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=

View file

@ -27,6 +27,7 @@ require (
k8s.io/apimachinery v0.0.0
k8s.io/klog/v2 v2.140.0
k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf
k8s.io/streaming v0.0.0
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730
sigs.k8s.io/randfill v1.0.0
@ -50,7 +51,6 @@ require (
github.com/moby/spdystream v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
@ -66,4 +66,5 @@ require (
replace (
k8s.io/api => ../api
k8s.io/apimachinery => ../apimachinery
k8s.io/streaming => ../streaming
)

View file

@ -64,7 +64,6 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=

View file

@ -19,9 +19,11 @@ package portforward
import (
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/klog/v2"
streamhttp "k8s.io/streaming/pkg/httpstream"
)
var _ httpstream.Dialer = &FallbackDialer{}
var _ streamhttp.Dialer = &StreamingFallbackDialer{}
// FallbackDialer encapsulates a primary and secondary dialer, including
// the boolean function to determine if the primary dialer failed. Implements
@ -42,6 +44,24 @@ func NewFallbackDialer(primary, secondary httpstream.Dialer, shouldFallback func
}
}
// StreamingFallbackDialer encapsulates a primary and secondary streaming dialer
// with fallback behavior.
type StreamingFallbackDialer struct {
primary streamhttp.Dialer
secondary streamhttp.Dialer
shouldFallback func(error) bool
}
// NewFallbackDialerForStreaming creates a fallback dialer for in-tree callers
// that use k8s.io/streaming/pkg/httpstream types.
func NewFallbackDialerForStreaming(primary, secondary streamhttp.Dialer, shouldFallback func(error) bool) streamhttp.Dialer {
return &StreamingFallbackDialer{
primary: primary,
secondary: secondary,
shouldFallback: shouldFallback,
}
}
// Dial is the single function necessary to implement the "httpstream.Dialer" interface.
// It takes the protocol version strings to request, returning an the upgraded
// httstream.Connection and the negotiated protocol version accepted. If the initial
@ -55,3 +75,14 @@ func (f *FallbackDialer) Dial(protocols ...string) (httpstream.Connection, strin
}
return conn, version, err
}
// Dial is the single function necessary to implement the
// "k8s.io/streaming/pkg/httpstream.Dialer" interface.
func (f *StreamingFallbackDialer) Dial(protocols ...string) (streamhttp.Connection, string, error) {
conn, version, err := f.primary.Dial(protocols...)
if err != nil && f.shouldFallback(err) {
klog.V(4).Infof("fallback to secondary dialer from primary dialer err: %v", err)
return f.secondary.Dial(protocols...)
}
return conn, version, err
}

View file

@ -26,10 +26,13 @@ import (
"strconv"
"strings"
"sync"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2"
streamhttp "k8s.io/streaming/pkg/httpstream"
netutils "k8s.io/utils/net"
)
@ -164,6 +167,12 @@ func New(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, rea
return NewOnAddresses(dialer, []string{"localhost"}, ports, stopChan, readyChan, out, errOut)
}
// NewForStreaming creates a new PortForwarder with localhost listen addresses
// for in-tree callers that use k8s.io/streaming/pkg/httpstream types.
func NewForStreaming(dialer streamhttp.Dialer, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (*PortForwarder, error) {
return NewOnAddressesForStreaming(dialer, []string{"localhost"}, ports, stopChan, readyChan, out, errOut)
}
// NewOnAddresses creates a new PortForwarder with custom listen addresses.
func NewOnAddresses(dialer httpstream.Dialer, addresses []string, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (*PortForwarder, error) {
if len(addresses) == 0 {
@ -191,6 +200,95 @@ func NewOnAddresses(dialer httpstream.Dialer, addresses []string, ports []string
}, nil
}
// NewOnAddressesForStreaming creates a new PortForwarder with custom listen
// addresses for in-tree callers that use k8s.io/streaming/pkg/httpstream types.
func NewOnAddressesForStreaming(dialer streamhttp.Dialer, addresses []string, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (*PortForwarder, error) {
return NewOnAddresses(&compatDialerAdapter{delegate: dialer}, addresses, ports, stopChan, readyChan, out, errOut)
}
type compatDialerAdapter struct {
delegate streamhttp.Dialer
}
func (d *compatDialerAdapter) Dial(protocols ...string) (httpstream.Connection, string, error) {
conn, protocol, err := d.delegate.Dial(protocols...)
if err != nil {
return nil, "", err
}
return &compatConnectionAdapter{delegate: conn}, protocol, nil
}
type compatConnectionAdapter struct {
delegate streamhttp.Connection
}
func (c *compatConnectionAdapter) CreateStream(headers http.Header) (httpstream.Stream, error) {
stream, err := c.delegate.CreateStream(headers)
if err != nil {
return nil, err
}
return &compatStreamAdapter{delegate: stream}, nil
}
func (c *compatConnectionAdapter) Close() error {
return c.delegate.Close()
}
func (c *compatConnectionAdapter) CloseChan() <-chan bool {
return c.delegate.CloseChan()
}
func (c *compatConnectionAdapter) SetIdleTimeout(timeout time.Duration) {
c.delegate.SetIdleTimeout(timeout)
}
func (c *compatConnectionAdapter) RemoveStreams(streams ...httpstream.Stream) {
streamingStreams := make([]streamhttp.Stream, 0, len(streams))
for _, stream := range streams {
if stream == nil {
continue
}
if s, ok := stream.(*compatStreamAdapter); ok {
streamingStreams = append(streamingStreams, s.delegate)
continue
}
if s, ok := stream.(streamhttp.Stream); ok {
streamingStreams = append(streamingStreams, s)
continue
}
klog.V(5).Infof("dropping unadaptable stream %T in portforward RemoveStreams", stream)
}
c.delegate.RemoveStreams(streamingStreams...)
}
type compatStreamAdapter struct {
delegate streamhttp.Stream
}
func (s *compatStreamAdapter) Read(p []byte) (int, error) {
return s.delegate.Read(p)
}
func (s *compatStreamAdapter) Write(p []byte) (int, error) {
return s.delegate.Write(p)
}
func (s *compatStreamAdapter) Close() error {
return s.delegate.Close()
}
func (s *compatStreamAdapter) Reset() error {
return s.delegate.Reset()
}
func (s *compatStreamAdapter) Headers() http.Header {
return s.delegate.Headers()
}
func (s *compatStreamAdapter) Identifier() uint32 {
return s.delegate.Identifier()
}
// ForwardPorts formats and executes a port forwarding request. The connection will remain
// open until stopChan is closed.
func (pf *PortForwarder) ForwardPorts() error {

View file

@ -24,11 +24,13 @@ import (
"time"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
httpstreamspdy "k8s.io/apimachinery/pkg/util/httpstream/spdy"
constants "k8s.io/apimachinery/pkg/util/portforward"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/transport/websocket"
"k8s.io/klog/v2"
streamhttp "k8s.io/streaming/pkg/httpstream"
streamspdy "k8s.io/streaming/pkg/httpstream/spdy"
)
const PingPeriod = 10 * time.Second
@ -40,6 +42,14 @@ type tunnelingDialer struct {
holder websocket.ConnectionHolder
}
// streamingTunnelingDialer implements "k8s.io/streaming/pkg/httpstream.Dialer"
// for in-tree callers.
type streamingTunnelingDialer struct {
url *url.URL
transport http.RoundTripper
holder websocket.ConnectionHolder
}
// NewTunnelingDialer creates and returns the tunnelingDialer structure which implemements the "httpstream.Dialer"
// interface. The dialer can upgrade a websocket request, creating a websocket connection. This function
// returns an error if one occurs.
@ -55,25 +65,36 @@ func NewSPDYOverWebsocketDialer(url *url.URL, config *restclient.Config) (httpst
}, nil
}
// Dial upgrades to a tunneling streaming connection, returning a SPDY connection
// containing a WebSockets connection (which implements "net.Conn"). Also
// returns the protocol negotiated, or an error.
func (d *tunnelingDialer) Dial(protocols ...string) (httpstream.Connection, string, error) {
// NewSPDYOverWebsocketDialerForStreaming creates a SPDY-over-websocket dialer
// for in-tree callers that use k8s.io/streaming/pkg/httpstream types.
func NewSPDYOverWebsocketDialerForStreaming(url *url.URL, config *restclient.Config) (streamhttp.Dialer, error) {
transport, holder, err := websocket.RoundTripperFor(config)
if err != nil {
return nil, err
}
return &streamingTunnelingDialer{
url: url,
transport: transport,
holder: holder,
}, nil
}
func negotiateSPDYOverWebsocket(url *url.URL, transport http.RoundTripper, holder websocket.ConnectionHolder, protocols ...string) (*TunnelingConnection, string, error) {
// There is no passed context, so skip the context when creating request for now.
// Websockets requires "GET" method: RFC 6455 Sec. 4.1 (page 17).
req, err := http.NewRequest("GET", d.url.String(), nil)
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, "", err
}
// Add the spdy tunneling prefix to the requested protocols. The tunneling
// handler will know how to negotiate these protocols.
tunnelingProtocols := []string{}
tunnelingProtocols := make([]string, 0, len(protocols))
for _, protocol := range protocols {
tunnelingProtocol := constants.WebsocketsSPDYTunnelingPrefix + protocol
tunnelingProtocols = append(tunnelingProtocols, tunnelingProtocol)
}
klog.V(4).Infoln("Before WebSocket Upgrade Connection...")
conn, err := websocket.Negotiate(d.transport, d.holder, req, tunnelingProtocols...)
conn, err := websocket.Negotiate(transport, holder, req, tunnelingProtocols...)
if err != nil {
return nil, "", err
}
@ -84,10 +105,32 @@ func (d *tunnelingDialer) Dial(protocols ...string) (httpstream.Connection, stri
protocol = strings.TrimPrefix(protocol, constants.WebsocketsSPDYTunnelingPrefix)
klog.V(4).Infof("negotiated protocol: %s", protocol)
// Wrap the websocket connection which implements "net.Conn".
tConn := NewTunnelingConnection("client", conn)
return NewTunnelingConnection("client", conn), protocol, nil
}
// Dial upgrades to a tunneling streaming connection, returning a SPDY connection
// containing a WebSockets connection (which implements "net.Conn"). Also
// returns the protocol negotiated, or an error.
func (d *tunnelingDialer) Dial(protocols ...string) (httpstream.Connection, string, error) {
tConn, protocol, err := negotiateSPDYOverWebsocket(d.url, d.transport, d.holder, protocols...)
if err != nil {
return nil, "", err
}
// Create SPDY connection injecting the previously created tunneling connection.
spdyConn, err := spdy.NewClientConnectionWithPings(tConn, PingPeriod)
spdyConn, err := httpstreamspdy.NewClientConnectionWithPings(tConn, PingPeriod)
return spdyConn, protocol, err
}
// Dial upgrades to a tunneling streaming connection for callers using
// k8s.io/streaming/pkg/httpstream types.
func (d *streamingTunnelingDialer) Dial(protocols ...string) (streamhttp.Connection, string, error) {
tConn, protocol, err := negotiateSPDYOverWebsocket(d.url, d.transport, d.holder, protocols...)
if err != nil {
return nil, "", err
}
// Create SPDY connection injecting the previously created tunneling connection.
spdyConn, err := streamspdy.NewClientConnectionWithPings(tConn, PingPeriod)
return spdyConn, protocol, err
}

View file

@ -25,17 +25,21 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/httpstream"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilnettesting "k8s.io/apimachinery/pkg/util/net/testing"
"k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
utilexec "k8s.io/client-go/util/exec"
"k8s.io/streaming/pkg/httpstream"
)
func TestFallbackClient_WebSocketPrimarySucceeds(t *testing.T) {
@ -234,6 +238,74 @@ func TestFallbackClient_PrimaryAndSecondaryFail(t *testing.T) {
}
}
func TestFallbackClient_SPDYSecondaryNonZeroExitCode(t *testing.T) {
const expectedExitCode = 23
const expectedStdout = "stdout-before-exit"
// Create fake SPDY server that writes stdout followed by a v4 status error.
spdyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx, err := createHTTPStreams(w, req, &StreamOptions{
Stdin: strings.NewReader("input"),
Stdout: &bytes.Buffer{},
})
if err != nil {
w.WriteHeader(http.StatusForbidden)
return
}
defer ctx.conn.Close()
if _, err := io.WriteString(ctx.stdoutStream, expectedStdout); err != nil {
t.Fatalf("error writing stdout stream: %v", err)
}
statusErr := &apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Reason: remotecommand.NonZeroExitCodeReason,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
{
Type: remotecommand.ExitCodeCauseType,
Message: "23",
},
},
},
Message: "command terminated with non-zero exit code: 23",
}}
if err := ctx.writeStatus(statusErr); err != nil {
t.Fatalf("error writing status stream: %v", err)
}
}))
defer spdyServer.Close()
spdyLocation, err := url.Parse(spdyServer.URL)
require.NoError(t, err)
// Primary websocket executor points at a SPDY-only endpoint and should fail.
websocketExecutor, err := NewWebSocketExecutor(&rest.Config{Host: spdyLocation.Host}, "GET", spdyServer.URL+"?stdin=true&stdout=true")
require.NoError(t, err)
spdyExecutor, err := NewSPDYExecutor(&rest.Config{Host: spdyLocation.Host}, "POST", spdyLocation)
require.NoError(t, err)
var sawPrimaryError atomic.Bool
exec, err := NewFallbackExecutor(websocketExecutor, spdyExecutor, func(err error) bool {
sawPrimaryError.Store(true)
return true
})
require.NoError(t, err)
var stdout bytes.Buffer
err = exec.StreamWithContext(context.Background(), StreamOptions{
Stdin: strings.NewReader("input"),
Stdout: &stdout,
})
require.Error(t, err)
require.True(t, sawPrimaryError.Load(), "expected primary websocket path to fail and trigger fallback")
var exitErr utilexec.ExitError
require.ErrorAs(t, err, &exitErr, "expected ExitError from secondary SPDY path, got: %T: %v", err, err)
require.Equal(t, expectedExitCode, exitErr.ExitStatus())
require.Equal(t, expectedStdout, stdout.String())
}
// localhostCert was generated from crypto/tls/generate_cert.go with the following command:
//
// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h

View file

@ -21,8 +21,8 @@ import (
"io"
"net/http"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/klog/v2"
"k8s.io/streaming/pkg/httpstream"
)
// StreamOptions holds information pertaining to the current streaming session:

View file

@ -22,11 +22,11 @@ import (
"net/http"
"net/url"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/remotecommand"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/transport/spdy"
"k8s.io/klog/v2"
"k8s.io/streaming/pkg/httpstream"
)
// spdyStreamExecutor handles transporting standard shell streams over an httpstream connection.
@ -109,7 +109,7 @@ func (e *spdyStreamExecutor) newConnectionAndStream(ctx context.Context, options
return fmt.Errorf("redirect not allowed")
}
}
conn, protocol, err := spdy.Negotiate(
conn, protocol, err := spdy.NegotiateStreaming(
e.upgrader,
&client,
req,

View file

@ -33,12 +33,13 @@ import (
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
utilexec "k8s.io/client-go/util/exec"
"k8s.io/klog/v2/ktesting"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/spdy"
)
type AttachFunc func(in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan TerminalSize) error
@ -183,6 +184,71 @@ func TestSPDYExecutorStream(t *testing.T) {
}
}
// TestSPDYExecutorNonZeroExitCode verifies SPDY v4 status-stream non-zero exit
// code handling remains compatible with CRI streaming servers.
func TestSPDYExecutorNonZeroExitCode(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
ctx, err := createHTTPStreams(writer, request, &StreamOptions{
Stdin: strings.NewReader("input"),
Stdout: &bytes.Buffer{},
})
if err != nil {
t.Errorf("unexpected stream setup error: %v", err)
return
}
defer ctx.conn.Close()
if _, err := io.WriteString(ctx.stdoutStream, "stdout-before-exit"); err != nil {
t.Errorf("unexpected stdout write error: %v", err)
return
}
err = ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Reason: remotecommandconsts.NonZeroExitCodeReason,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
{
Type: remotecommandconsts.ExitCodeCauseType,
Message: "17",
},
},
},
Message: "command terminated with non-zero exit code: 17",
}})
if err != nil {
t.Errorf("unexpected status write error: %v", err)
}
}))
defer server.Close()
uri, _ := url.Parse(server.URL)
exec, err := NewSPDYExecutor(&rest.Config{Host: uri.Host}, "POST", uri)
if err != nil {
t.Fatalf("unexpected executor error: %v", err)
}
var stdout bytes.Buffer
err = exec.StreamWithContext(context.Background(), StreamOptions{
Stdin: strings.NewReader("input"),
Stdout: &stdout,
})
if err == nil {
t.Fatal("expected non-zero exit error, got nil")
}
var exitErr utilexec.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("expected ExitError, got: %T: %v", err, err)
}
if exitErr.ExitStatus() != 17 {
t.Fatalf("expected exit code 17, got %d", exitErr.ExitStatus())
}
if got := stdout.String(); got != "stdout-before-exit" {
t.Fatalf("unexpected stdout: %q", got)
}
}
func newTestHTTPServer(f AttachFunc, options *StreamOptions) *httptest.Server {
//nolint:errcheck
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {

View file

@ -22,8 +22,8 @@ import (
"net/http"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/klog/v2"
"k8s.io/streaming/pkg/httpstream"
)
// streamProtocolV1 implements the first version of the streaming exec & attach

View file

@ -26,9 +26,9 @@ import (
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/klog/v2/ktesting"
"k8s.io/streaming/pkg/httpstream"
)
type fakeReader struct {

View file

@ -29,11 +29,11 @@ import (
gwebsocket "github.com/gorilla/websocket"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/remotecommand"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/transport/websocket"
"k8s.io/klog/v2"
"k8s.io/streaming/pkg/httpstream"
)
// writeDeadline defines the time that a client-side write to the websocket

View file

@ -42,13 +42,13 @@ import (
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
utilnettesting "k8s.io/apimachinery/pkg/util/net/testing"
"k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/klog/v2/ktesting"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
// TestWebSocketClient_LoopbackStdinToStdout returns random data sent on the STDIN channel

View file

@ -23,8 +23,10 @@ import (
"time"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
httpstreamspdy "k8s.io/apimachinery/pkg/util/httpstream/spdy"
restclient "k8s.io/client-go/rest"
"k8s.io/klog/v2"
streamhttp "k8s.io/streaming/pkg/httpstream"
)
// Upgrader validates a response from the server after a SPDY upgrade.
@ -43,7 +45,7 @@ func RoundTripperFor(config *restclient.Config) (http.RoundTripper, Upgrader, er
if config.Proxy != nil {
proxy = config.Proxy
}
upgradeRoundTripper, err := spdy.NewRoundTripperWithConfig(spdy.RoundTripperConfig{
upgradeRoundTripper, err := httpstreamspdy.NewRoundTripperWithConfig(httpstreamspdy.RoundTripperConfig{
TLS: tlsConfig,
Proxier: proxy,
PingPeriod: time.Second * 5,
@ -79,6 +81,18 @@ func NewDialer(upgrader Upgrader, client *http.Client, method string, url *url.U
}
}
// NewDialerForStreaming creates a SPDY dialer for in-tree callers that use
// k8s.io/streaming/pkg/httpstream types.
func NewDialerForStreaming(upgrader Upgrader, client *http.Client, method string, url *url.URL) streamhttp.Dialer {
return &streamingDialerAdapter{delegate: NewDialer(upgrader, client, method, url)}
}
// NewUpgraderForStreaming adapts a streaming upgrader for callers that need
// the compatibility Upgrader interface.
func NewUpgraderForStreaming(upgrader streamhttp.UpgradeRoundTripper) Upgrader {
return &compatUpgraderAdapter{delegate: upgrader}
}
func (d *dialer) Dial(protocols ...string) (httpstream.Connection, string, error) {
req, err := http.NewRequest(d.method, d.url.String(), nil)
if err != nil {
@ -105,3 +119,199 @@ func Negotiate(upgrader Upgrader, client *http.Client, req *http.Request, protoc
}
return conn, resp.Header.Get(httpstream.HeaderProtocolVersion), nil
}
// NegotiateStreaming is for in-tree callers that still operate on
// k8s.io/streaming/pkg/httpstream types.
func NegotiateStreaming(upgrader Upgrader, client *http.Client, req *http.Request, protocols ...string) (streamhttp.Connection, string, error) {
conn, protocol, err := Negotiate(upgrader, client, req, protocols...)
if err != nil {
return nil, "", err
}
return wrapStreamingConnection(conn), protocol, nil
}
type streamingDialerAdapter struct {
delegate httpstream.Dialer
}
func (d *streamingDialerAdapter) Dial(protocols ...string) (streamhttp.Connection, string, error) {
conn, protocol, err := d.delegate.Dial(protocols...)
if err != nil {
return nil, "", err
}
return wrapStreamingConnection(conn), protocol, nil
}
type compatUpgraderAdapter struct {
delegate streamhttp.UpgradeRoundTripper
}
func (u *compatUpgraderAdapter) NewConnection(resp *http.Response) (httpstream.Connection, error) {
conn, err := u.delegate.NewConnection(resp)
if err != nil {
return nil, err
}
return wrapCompatConnection(conn), nil
}
type streamingStreamAdapter struct {
delegate httpstream.Stream
}
func (s *streamingStreamAdapter) Read(p []byte) (int, error) {
return s.delegate.Read(p)
}
func (s *streamingStreamAdapter) Write(p []byte) (int, error) {
return s.delegate.Write(p)
}
func (s *streamingStreamAdapter) Close() error {
return s.delegate.Close()
}
func (s *streamingStreamAdapter) Reset() error {
return s.delegate.Reset()
}
func (s *streamingStreamAdapter) Headers() http.Header {
return s.delegate.Headers()
}
func (s *streamingStreamAdapter) Identifier() uint32 {
return s.delegate.Identifier()
}
type streamingConnectionAdapter struct {
delegate httpstream.Connection
}
func (c *streamingConnectionAdapter) CreateStream(headers http.Header) (streamhttp.Stream, error) {
stream, err := c.delegate.CreateStream(headers)
if err != nil {
return nil, err
}
return &streamingStreamAdapter{delegate: stream}, nil
}
func (c *streamingConnectionAdapter) Close() error {
return c.delegate.Close()
}
func (c *streamingConnectionAdapter) CloseChan() <-chan bool {
return c.delegate.CloseChan()
}
func (c *streamingConnectionAdapter) SetIdleTimeout(timeout time.Duration) {
c.delegate.SetIdleTimeout(timeout)
}
func (c *streamingConnectionAdapter) RemoveStreams(streams ...streamhttp.Stream) {
compatStreams := make([]httpstream.Stream, 0, len(streams))
for _, stream := range streams {
if stream == nil {
continue
}
if s, ok := stream.(*streamingStreamAdapter); ok {
compatStreams = append(compatStreams, s.delegate)
continue
}
if s, ok := stream.(httpstream.Stream); ok {
compatStreams = append(compatStreams, s)
continue
}
klog.V(5).Infof("dropping unadaptable streaming stream %T in RemoveStreams", stream)
}
c.delegate.RemoveStreams(compatStreams...)
}
func wrapStreamingConnection(conn httpstream.Connection) streamhttp.Connection {
if conn == nil {
return nil
}
if wrapped, ok := conn.(*compatConnectionAdapter); ok {
return wrapped.delegate
}
return &streamingConnectionAdapter{delegate: conn}
}
type compatStreamAdapter struct {
delegate streamhttp.Stream
}
func (s *compatStreamAdapter) Read(p []byte) (int, error) {
return s.delegate.Read(p)
}
func (s *compatStreamAdapter) Write(p []byte) (int, error) {
return s.delegate.Write(p)
}
func (s *compatStreamAdapter) Close() error {
return s.delegate.Close()
}
func (s *compatStreamAdapter) Reset() error {
return s.delegate.Reset()
}
func (s *compatStreamAdapter) Headers() http.Header {
return s.delegate.Headers()
}
func (s *compatStreamAdapter) Identifier() uint32 {
return s.delegate.Identifier()
}
type compatConnectionAdapter struct {
delegate streamhttp.Connection
}
func (c *compatConnectionAdapter) CreateStream(headers http.Header) (httpstream.Stream, error) {
stream, err := c.delegate.CreateStream(headers)
if err != nil {
return nil, err
}
return &compatStreamAdapter{delegate: stream}, nil
}
func (c *compatConnectionAdapter) Close() error {
return c.delegate.Close()
}
func (c *compatConnectionAdapter) CloseChan() <-chan bool {
return c.delegate.CloseChan()
}
func (c *compatConnectionAdapter) SetIdleTimeout(timeout time.Duration) {
c.delegate.SetIdleTimeout(timeout)
}
func (c *compatConnectionAdapter) RemoveStreams(streams ...httpstream.Stream) {
streamingStreams := make([]streamhttp.Stream, 0, len(streams))
for _, stream := range streams {
if stream == nil {
continue
}
if s, ok := stream.(*compatStreamAdapter); ok {
streamingStreams = append(streamingStreams, s.delegate)
continue
}
if s, ok := stream.(streamhttp.Stream); ok {
streamingStreams = append(streamingStreams, s)
continue
}
klog.V(5).Infof("dropping unadaptable compat stream %T in RemoveStreams", stream)
}
c.delegate.RemoveStreams(streamingStreams...)
}
func wrapCompatConnection(conn streamhttp.Connection) httpstream.Connection {
if conn == nil {
return nil
}
if wrapped, ok := conn.(*streamingConnectionAdapter); ok {
return wrapped.delegate
}
return &compatConnectionAdapter{delegate: conn}
}

View file

@ -31,11 +31,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
utilnet "k8s.io/apimachinery/pkg/util/net"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/transport"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
var (

View file

@ -32,10 +32,10 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
"k8s.io/apimachinery/pkg/util/remotecommand"
restclient "k8s.io/client-go/rest"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/wsstream"
)
func TestWebSocketRoundTripper_RoundTripperSucceeds(t *testing.T) {

View file

@ -104,6 +104,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/kms v0.0.0 // indirect
k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect
k8s.io/streaming v0.0.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
@ -120,4 +121,5 @@ replace (
k8s.io/component-helpers => ../component-helpers
k8s.io/controller-manager => ../controller-manager
k8s.io/kms => ../kms
k8s.io/streaming => ../streaming
)

View file

@ -40,4 +40,5 @@ require (
replace (
k8s.io/api => ../api
k8s.io/apimachinery => ../apimachinery
k8s.io/streaming => ../streaming
)

View file

@ -1,5 +1,4 @@
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -50,4 +50,7 @@ require (
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
)
replace k8s.io/apimachinery => ../apimachinery
replace (
k8s.io/apimachinery => ../apimachinery
k8s.io/streaming => ../streaming
)

View file

@ -1,7 +1,6 @@
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -90,4 +90,5 @@ replace (
k8s.io/api => ../api
k8s.io/apimachinery => ../apimachinery
k8s.io/client-go => ../client-go
k8s.io/streaming => ../streaming
)

View file

@ -9,7 +9,6 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=

View file

@ -56,4 +56,5 @@ replace (
k8s.io/api => ../api
k8s.io/apimachinery => ../apimachinery
k8s.io/client-go => ../client-go
k8s.io/streaming => ../streaming
)

View file

@ -2,7 +2,6 @@ cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -96,6 +96,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect
k8s.io/streaming v0.0.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
@ -110,4 +111,5 @@ replace (
k8s.io/client-go => ../client-go
k8s.io/component-base => ../component-base
k8s.io/kms => ../kms
k8s.io/streaming => ../streaming
)

View file

@ -84,4 +84,5 @@ replace (
k8s.io/client-go => ../client-go
k8s.io/component-base => ../component-base
k8s.io/cri-api => ../cri-api
k8s.io/streaming => ../streaming
)

View file

@ -9,7 +9,6 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=

View file

@ -0,0 +1,2 @@
Sorry, we do not accept changes directly against this repository. Please see
CONTRIBUTING.md for information on where and how to contribute instead.

View file

@ -0,0 +1,7 @@
# Contributing guidelines
Do not open pull requests directly against this repository, they will be ignored. Instead, please open pull requests against [kubernetes/kubernetes](https://git.k8s.io/kubernetes/). Please follow the same [contributing guide](https://git.k8s.io/kubernetes/CONTRIBUTING.md) you would follow for any other pull request made to kubernetes/kubernetes.
This repository is published from [kubernetes/kubernetes/staging/src/k8s.io/cri-streaming](https://git.k8s.io/kubernetes/staging/src/k8s.io/cri-streaming) by the [kubernetes publishing-bot](https://git.k8s.io/publishing-bot).
Please see [Staging Directory and Publishing](https://git.k8s.io/community/contributors/devel/sig-architecture/staging.md) for more information

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,18 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- dims
- mikebrow
- saschagrunert
- aojea
- seans3
- liggitt
reviewers:
- dims
- mikebrow
- saschagrunert
- aojea
- seans3
- liggitt
labels:
- sig/node

View file

@ -0,0 +1,35 @@
> ⚠️ **This is an automatically published [staged repository](https://git.k8s.io/kubernetes/staging#external-repository-staging-area) for Kubernetes**.
> Contributions, including issues and pull requests, should be made to the main Kubernetes repository: [https://github.com/kubernetes/kubernetes](https://github.com/kubernetes/kubernetes).
> This repository is read-only for importing, and not used for direct contributions.
> See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
# cri-streaming
This repository contains the Kubernetes CRI streaming server implementation used for:
- Exec
- Attach
- PortForward
The goal of this module is to provide a dedicated, runtime-focused import target for CRI streaming functionality without requiring consumers to depend on the full `k8s.io/kubelet` module surface.
## Migration notes
- The legacy package path `k8s.io/kubelet/pkg/cri/streaming` has moved to `k8s.io/cri-streaming/pkg/streaming`.
- Shared transport dependencies now come from `k8s.io/streaming/pkg/httpstream` and subpackages.
- This extraction does not provide compatibility shims at the old kubelet/apimachinery paths.
## Community, discussion, contribution, and support
cri-streaming is planned as a sub-project of [SIG Node](https://github.com/kubernetes/community/tree/master/sig-node).
You can reach maintainers of this project at:
- Slack: [#sig-node](https://kubernetes.slack.com/messages/sig-node)
- Mailing List: [kubernetes-sig-node](https://groups.google.com/forum/#!forum/kubernetes-sig-node)
Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/).
### Code of conduct
Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md).

View file

@ -0,0 +1,17 @@
# Defined below are the security contacts for this repo.
#
# They are the contact point for the Product Security Committee to reach out
# to for triaging and handling of incoming issues.
#
# The below names agree to abide by the
# [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy)
# and will be removed and replaced if they violate that agreement.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/
cjcullen
joelsmith
liggitt
philips
tallclair

View file

@ -0,0 +1,3 @@
# Kubernetes Community Code of Conduct
Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)

View file

@ -0,0 +1,18 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package cristreaming contains the staged module root for Kubernetes CRI streaming.
package cristreaming

View file

@ -0,0 +1,41 @@
// This is a generated file. Do not edit directly.
module k8s.io/cri-streaming
go 1.26.0
godebug default=go1.26
require (
github.com/emicklei/go-restful/v3 v3.13.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/stretchr/testify v1.11.1
go.uber.org/goleak v1.3.0
google.golang.org/grpc v1.78.0
k8s.io/cri-api v0.0.0
k8s.io/klog/v2 v2.140.0
k8s.io/streaming v0.0.0
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
k8s.io/cri-api => ../cri-api
k8s.io/streaming => ../streaming
)

91
staging/src/k8s.io/cri-streaming/go.sum generated Normal file
View file

@ -0,0 +1,91 @@
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=

View file

@ -0,0 +1,5 @@
rules:
# prevent import of k8s.io/kubernetes
- selectorRegexp: k8s[.]io/kubernetes
forbiddenPrefixes:
- ''

View file

@ -0,0 +1,18 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package streaming contains CRI server-side streaming interfaces and implementations.
package streaming

View file

@ -0,0 +1,38 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package httpresponse
import "net/http"
type wrapper interface {
Unwrap() http.ResponseWriter
}
// GetOriginal walks any response writer wrapper chain and returns the first writer.
func GetOriginal(w http.ResponseWriter) http.ResponseWriter {
for {
decorator, ok := w.(wrapper)
if !ok {
return w
}
inner := decorator.Unwrap()
if inner == w {
panic("http.ResponseWriter decorator chain has a cycle")
}
w = inner
}
}

View file

@ -22,3 +22,17 @@ const ProtocolV1Name = "portforward.k8s.io"
// SupportedProtocols are the supported port forwarding protocols.
var SupportedProtocols = []string{ProtocolV1Name}
const (
PortForwardV1Name = ProtocolV1Name
WebsocketsSPDYTunnelingPrefix = "SPDY/3.1+"
KubernetesSuffix = ".k8s.io"
WebsocketsSPDYTunnelingPortForwardV1 = WebsocketsSPDYTunnelingPrefix + PortForwardV1Name
PortHeader = "port"
PortForwardRequestIDHeader = "requestID"
StreamType = "streamType"
StreamTypeData = "data"
StreamTypeError = "error"
)

View file

@ -25,16 +25,14 @@ import (
"sync"
"time"
api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/spdy"
utilruntime "k8s.io/streaming/pkg/runtime"
"k8s.io/klog/v2"
)
func handleHTTPStreams(req *http.Request, w http.ResponseWriter, portForwarder PortForwarder, podName string, uid types.UID, supportedPortForwardProtocols []string, idleTimeout, streamCreationTimeout time.Duration) error {
func handleHTTPStreams(req *http.Request, w http.ResponseWriter, portForwarder PortForwarder, podName string, uid string, supportedPortForwardProtocols []string, idleTimeout, streamCreationTimeout time.Duration) error {
_, err := httpstream.Handshake(req, w, supportedPortForwardProtocols)
// negotiated protocol isn't currently used server side, but could be in the future
if err != nil {
@ -75,9 +73,9 @@ func handleHTTPStreams(req *http.Request, w http.ResponseWriter, portForwarder P
func httpStreamReceived(streams chan httpstream.Stream) func(httpstream.Stream, <-chan struct{}) error {
return func(stream httpstream.Stream, replySent <-chan struct{}) error {
// make sure it has a valid port header
portString := stream.Headers().Get(api.PortHeader)
portString := stream.Headers().Get(PortHeader)
if len(portString) == 0 {
return fmt.Errorf("%q header is required", api.PortHeader)
return fmt.Errorf("%q header is required", PortHeader)
}
port, err := strconv.ParseUint(portString, 10, 16)
if err != nil {
@ -88,11 +86,11 @@ func httpStreamReceived(streams chan httpstream.Stream) func(httpstream.Stream,
}
// make sure it has a valid stream type header
streamType := stream.Headers().Get(api.StreamType)
streamType := stream.Headers().Get(StreamType)
if len(streamType) == 0 {
return fmt.Errorf("%q header is required", api.StreamType)
return fmt.Errorf("%q header is required", StreamType)
}
if streamType != api.StreamTypeError && streamType != api.StreamTypeData {
if streamType != StreamTypeError && streamType != StreamTypeData {
return fmt.Errorf("invalid stream type %q", streamType)
}
@ -110,7 +108,7 @@ type httpStreamHandler struct {
streamPairs map[string]*httpStreamPair
streamCreationTimeout time.Duration
pod string
uid types.UID
uid string
forwarder PortForwarder
}
@ -173,7 +171,7 @@ func (h *httpStreamHandler) removeStreamPair(requestID string) {
// requestID returns the request id for stream.
func (h *httpStreamHandler) requestID(stream httpstream.Stream) string {
requestID := stream.Headers().Get(api.PortForwardRequestIDHeader)
requestID := stream.Headers().Get(PortForwardRequestIDHeader)
if len(requestID) == 0 {
klog.V(5).InfoS("Connection stream received without requestID header", "connection", h.conn)
// If we get here, it's because the connection came from an older client
@ -194,11 +192,11 @@ func (h *httpStreamHandler) requestID(stream httpstream.Stream) string {
// old clients that don't generate request ids. If there are concurrent
// new connections, it's possible that 1 connection gets streams whose IDs
// are not consecutive (e.g. 5 and 9 instead of 5 and 7).
streamType := stream.Headers().Get(api.StreamType)
streamType := stream.Headers().Get(StreamType)
switch streamType {
case api.StreamTypeError:
case StreamTypeError:
requestID = strconv.Itoa(int(stream.Identifier()))
case api.StreamTypeData:
case StreamTypeData:
requestID = strconv.Itoa(int(stream.Identifier()) - 2)
}
@ -220,7 +218,7 @@ Loop:
break Loop
case stream := <-h.streamChan:
requestID := h.requestID(stream)
streamType := stream.Headers().Get(api.StreamType)
streamType := stream.Headers().Get(StreamType)
klog.V(5).InfoS("Connection request received new type of stream", "connection", h.conn, "request", requestID, "streamType", streamType)
p, created := h.getStreamPair(requestID)
@ -245,7 +243,7 @@ func (h *httpStreamHandler) portForward(p *httpStreamPair) {
defer p.dataStream.Close()
defer p.errorStream.Close()
portString := p.dataStream.Headers().Get(api.PortHeader)
portString := p.dataStream.Headers().Get(PortHeader)
port, _ := strconv.ParseInt(portString, 10, 32)
klog.V(5).InfoS("Connection request invoking forwarder.PortForward for port", "connection", h.conn, "request", p.requestID, "port", portString)
@ -291,13 +289,13 @@ func (p *httpStreamPair) add(stream httpstream.Stream) (bool, error) {
p.lock.Lock()
defer p.lock.Unlock()
switch stream.Headers().Get(api.StreamType) {
case api.StreamTypeError:
switch stream.Headers().Get(StreamType) {
case StreamTypeError:
if p.errorStream != nil {
return false, errors.New("error stream already assigned")
}
p.errorStream = stream
case api.StreamTypeData:
case StreamTypeData:
if p.dataStream != nil {
return false, errors.New("data stream already assigned")
}

View file

@ -21,8 +21,7 @@ import (
"testing"
"time"
api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/streaming/pkg/httpstream"
)
func TestHTTPStreamReceived(t *testing.T) {
@ -148,7 +147,7 @@ func TestGetStreamPair(t *testing.T) {
// removed via complete
dataStream := newFakeHTTPStream()
dataStream.headers.Set(api.StreamType, api.StreamTypeData)
dataStream.headers.Set(StreamType, StreamTypeData)
complete, err := p.add(dataStream)
if err != nil {
t.Fatalf("unexpected error adding data stream to pair: %v", err)
@ -158,7 +157,7 @@ func TestGetStreamPair(t *testing.T) {
}
errorStream := newFakeHTTPStream()
errorStream.headers.Set(api.StreamType, api.StreamTypeError)
errorStream.headers.Set(StreamType, StreamTypeError)
complete, err = p.add(errorStream)
if err != nil {
t.Fatalf("unexpected error adding error stream to pair: %v", err)
@ -210,20 +209,20 @@ func TestRequestID(t *testing.T) {
h := &httpStreamHandler{}
s := newFakeHTTPStream()
s.headers.Set(api.StreamType, api.StreamTypeError)
s.headers.Set(StreamType, StreamTypeError)
s.id = 1
if e, a := "1", h.requestID(s); e != a {
t.Errorf("expected %q, got %q", e, a)
}
s.headers.Set(api.StreamType, api.StreamTypeData)
s.headers.Set(StreamType, StreamTypeData)
s.id = 3
if e, a := "1", h.requestID(s); e != a {
t.Errorf("expected %q, got %q", e, a)
}
s.id = 7
s.headers.Set(api.PortForwardRequestIDHeader, "2")
s.headers.Set(PortForwardRequestIDHeader, "2")
if e, a := "2", h.requestID(s); e != a {
t.Errorf("expected %q, got %q", e, a)
}

View file

@ -22,16 +22,15 @@ import (
"net/http"
"time"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/streaming/pkg/httpstream/wsstream"
"k8s.io/streaming/pkg/runtime"
)
// PortForwarder knows how to forward content from a data stream to/from a port
// in a pod.
type PortForwarder interface {
// PortForwarder copies data between a data stream and a port in a pod.
PortForward(ctx context.Context, name string, uid types.UID, port int32, stream io.ReadWriteCloser) error
PortForward(ctx context.Context, name string, uid string, port int32, stream io.ReadWriteCloser) error
}
// ServePortForward handles a port forwarding request. A single request is
@ -39,7 +38,7 @@ type PortForwarder interface {
// been timed out due to idleness. This function handles multiple forwarded
// connections; i.e., multiple `curl http://localhost:8888/` requests will be
// handled by a single invocation of ServePortForward.
func ServePortForward(w http.ResponseWriter, req *http.Request, portForwarder PortForwarder, podName string, uid types.UID, portForwardOptions *V4Options, idleTimeout time.Duration, streamCreationTimeout time.Duration, supportedProtocols []string) {
func ServePortForward(w http.ResponseWriter, req *http.Request, portForwarder PortForwarder, podName string, uid string, portForwardOptions *V4Options, idleTimeout time.Duration, streamCreationTimeout time.Duration, supportedProtocols []string) {
var err error
if wsstream.IsWebSocketRequest(req) {
err = handleWebSocketStreams(req, w, portForwarder, podName, uid, portForwardOptions, supportedProtocols, idleTimeout, streamCreationTimeout)

View file

@ -29,11 +29,9 @@ import (
"k8s.io/klog/v2"
api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
"k8s.io/cri-streaming/pkg/streaming/internal/httpresponse"
"k8s.io/streaming/pkg/httpstream/wsstream"
"k8s.io/streaming/pkg/runtime"
)
const (
@ -58,15 +56,15 @@ func NewV4Options(req *http.Request) (*V4Options, error) {
return &V4Options{}, nil
}
portStrings := req.URL.Query()[api.PortHeader]
portStrings := req.URL.Query()[PortHeader]
if len(portStrings) == 0 {
return nil, fmt.Errorf("query parameter %q is required", api.PortHeader)
return nil, fmt.Errorf("query parameter %q is required", PortHeader)
}
ports := make([]int32, 0, len(portStrings))
for _, portString := range portStrings {
if len(portString) == 0 {
return nil, fmt.Errorf("query parameter %q cannot be empty", api.PortHeader)
return nil, fmt.Errorf("query parameter %q cannot be empty", PortHeader)
}
for _, p := range strings.Split(portString, ",") {
port, err := strconv.ParseUint(p, 10, 16)
@ -94,7 +92,7 @@ func BuildV4Options(ports []int32) (*V4Options, error) {
// a PortForwarder. A pair of streams are created per port (DATA n,
// ERROR n+1). The associated port is written to each stream as a unsigned 16
// bit integer in little endian format.
func handleWebSocketStreams(req *http.Request, w http.ResponseWriter, portForwarder PortForwarder, podName string, uid types.UID, opts *V4Options, supportedPortForwardProtocols []string, idleTimeout, streamCreationTimeout time.Duration) error {
func handleWebSocketStreams(req *http.Request, w http.ResponseWriter, portForwarder PortForwarder, podName string, uid string, opts *V4Options, supportedPortForwardProtocols []string, idleTimeout, streamCreationTimeout time.Duration) error {
channels := make([]wsstream.ChannelType, 0, len(opts.Ports)*2)
for i := 0; i < len(opts.Ports); i++ {
channels = append(channels, wsstream.ReadWriteChannel, wsstream.WriteChannel)
@ -114,7 +112,7 @@ func handleWebSocketStreams(req *http.Request, w http.ResponseWriter, portForwar
},
})
conn.SetIdleTimeout(idleTimeout)
_, streams, err := conn.Open(responsewriter.GetOriginal(w), req)
_, streams, err := conn.Open(httpresponse.GetOriginal(w), req)
if err != nil {
err = fmt.Errorf("unable to upgrade websocket connection: %v", err)
return err
@ -161,7 +159,7 @@ type websocketStreamHandler struct {
conn *wsstream.Conn
streamPairs []*websocketStreamPair
pod string
uid types.UID
uid string
forwarder PortForwarder
}

View file

@ -23,23 +23,19 @@ import (
"net/http"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/streaming/pkg/runtime"
)
// Attacher knows how to attach to a running container in a pod.
type Attacher interface {
// AttachContainer attaches to the running container in the pod, copying data between in/out/err
// and the container's stdin/stdout/stderr.
AttachContainer(ctx context.Context, name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
AttachContainer(ctx context.Context, name string, uid string, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan TerminalSize) error
}
// ServeAttach handles requests to attach to a container. After creating/receiving the required
// streams, it delegates the actual attaching to attacher.
func ServeAttach(w http.ResponseWriter, req *http.Request, attacher Attacher, podName string, uid types.UID, container string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) {
func ServeAttach(w http.ResponseWriter, req *http.Request, attacher Attacher, podName string, uid string, container string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) {
ctx, ok := createStreams(req, w, streamOpts, supportedProtocols, idleTimeout, streamCreationTimeout)
if !ok {
// error is handled by createStreams
@ -51,10 +47,10 @@ func ServeAttach(w http.ResponseWriter, req *http.Request, attacher Attacher, po
if err != nil {
err = fmt.Errorf("error attaching to container: %v", err)
runtime.HandleError(err)
ctx.writeStatus(apierrors.NewInternalError(err))
ctx.writeStatus(newInternalError(err))
} else {
ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusSuccess,
ctx.writeStatus(&streamStatusError{ErrStatus: streamStatus{
Status: statusSuccess,
}})
}
}

View file

@ -0,0 +1,75 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
import "time"
const (
DefaultStreamCreationTimeout = 30 * time.Second
ExecStdinParam = "input"
ExecStdoutParam = "output"
ExecStderrParam = "error"
ExecTTYParam = "tty"
StreamType = "streamType"
StreamTypeStdin = "stdin"
StreamTypeStdout = "stdout"
StreamTypeStderr = "stderr"
StreamTypeError = "error"
StreamTypeResize = "resize"
// The SPDY subprotocol "channel.k8s.io" is used for remote command
// attachment/execution. This represents the initial unversioned subprotocol,
// which has the known bugs https://issues.k8s.io/13394 and
// https://issues.k8s.io/13395.
StreamProtocolV1Name = "channel.k8s.io"
// The SPDY subprotocol "v2.channel.k8s.io" is used for remote command
// attachment/execution. It is the second version of the subprotocol and
// resolves the issues present in the first version.
StreamProtocolV2Name = "v2.channel.k8s.io"
// The SPDY subprotocol "v3.channel.k8s.io" is used for remote command
// attachment/execution. It is the third version of the subprotocol and
// adds support for resizing container terminals.
StreamProtocolV3Name = "v3.channel.k8s.io"
// The SPDY subprotocol "v4.channel.k8s.io" is used for remote command
// attachment/execution. It is the 4th version of the subprotocol and
// adds support for exit codes.
StreamProtocolV4Name = "v4.channel.k8s.io"
// The subprotocol "v5.channel.k8s.io" is used for remote command
// attachment/execution. It is the 5th version of the subprotocol and
// adds support for a CLOSE signal.
StreamProtocolV5Name = "v5.channel.k8s.io"
NonZeroExitCodeReason = "NonZeroExitCode"
ExitCodeCauseType = "ExitCode"
// RemoteCommand stream identifiers. The first three identifiers (for STDIN,
// STDOUT, STDERR) are the same as their file descriptors.
StreamStdIn = 0
StreamStdOut = 1
StreamStdErr = 2
StreamErr = 3
StreamResize = 4
StreamClose = 255
)
var SupportedStreamingProtocols = []string{StreamProtocolV4Name, StreamProtocolV3Name, StreamProtocolV2Name, StreamProtocolV1Name}

View file

@ -23,12 +23,7 @@ import (
"net/http"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/streaming/pkg/runtime"
utilexec "k8s.io/utils/exec"
)
@ -36,13 +31,13 @@ import (
type Executor interface {
// ExecInContainer executes a command in a container in the pod, copying data
// between in/out/err and the container's stdin/stdout/stderr.
ExecInContainer(ctx context.Context, name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error
ExecInContainer(ctx context.Context, name string, uid string, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan TerminalSize, timeout time.Duration) error
}
// ServeExec handles requests to execute a command in a container. After
// creating/receiving the required streams, it delegates the actual execution
// to the executor.
func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podName string, uid types.UID, container string, cmd []string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) {
func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podName string, uid string, container string, cmd []string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) {
ctx, ok := createStreams(req, w, streamOpts, supportedProtocols, idleTimeout, streamCreationTimeout)
if !ok {
// error is handled by createStreams
@ -54,13 +49,13 @@ func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podN
if err != nil {
if exitErr, ok := err.(utilexec.ExitError); ok && exitErr.Exited() {
rc := exitErr.ExitStatus()
ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Reason: remotecommandconsts.NonZeroExitCodeReason,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
ctx.writeStatus(&streamStatusError{ErrStatus: streamStatus{
Status: statusFailure,
Reason: NonZeroExitCodeReason,
Details: &streamStatusDetails{
Causes: []streamStatusCause{
{
Type: remotecommandconsts.ExitCodeCauseType,
Type: ExitCodeCauseType,
Message: fmt.Sprintf("%d", rc),
},
},
@ -70,11 +65,11 @@ func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podN
} else {
err = fmt.Errorf("error executing command in container: %v", err)
runtime.HandleError(err)
ctx.writeStatus(apierrors.NewInternalError(err))
ctx.writeStatus(newInternalError(err))
}
} else {
ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusSuccess,
ctx.writeStatus(&streamStatusError{ErrStatus: streamStatus{
Status: statusSuccess,
}})
}
}

View file

@ -25,15 +25,10 @@ import (
"net/http"
"time"
api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/streaming/pkg/httpstream"
"k8s.io/streaming/pkg/httpstream/spdy"
"k8s.io/streaming/pkg/httpstream/wsstream"
"k8s.io/streaming/pkg/runtime"
"k8s.io/klog/v2"
)
@ -49,10 +44,10 @@ type Options struct {
// NewOptions creates a new Options from the Request.
func NewOptions(req *http.Request) (*Options, error) {
tty := req.FormValue(api.ExecTTYParam) == "1"
stdin := req.FormValue(api.ExecStdinParam) == "1"
stdout := req.FormValue(api.ExecStdoutParam) == "1"
stderr := req.FormValue(api.ExecStderrParam) == "1"
tty := req.FormValue(ExecTTYParam) == "1"
stdin := req.FormValue(ExecStdinParam) == "1"
stdout := req.FormValue(ExecStdoutParam) == "1"
stderr := req.FormValue(ExecStderrParam) == "1"
if tty && stderr {
// TODO: make this an error before we reach this method
klog.V(4).InfoS("Access to exec with tty and stderr is not supported, bypassing stderr")
@ -78,9 +73,9 @@ type connectionContext struct {
stdinStream io.ReadCloser
stdoutStream io.WriteCloser
stderrStream io.WriteCloser
writeStatus func(status *apierrors.StatusError) error
writeStatus func(status *streamStatusError) error
resizeStream io.ReadCloser
resizeChan chan remotecommand.TerminalSize
resizeChan chan TerminalSize
tty bool
}
@ -116,7 +111,7 @@ func createStreams(req *http.Request, w http.ResponseWriter, opts *Options, supp
}
if ctx.resizeStream != nil {
ctx.resizeChan = make(chan remotecommand.TerminalSize)
ctx.resizeChan = make(chan TerminalSize)
go handleResizeEvents(req.Context(), ctx.resizeStream, ctx.resizeChan)
}
@ -149,16 +144,16 @@ func createHTTPStreamStreams(req *http.Request, w http.ResponseWriter, opts *Opt
var handler protocolHandler
switch protocol {
case remotecommandconsts.StreamProtocolV4Name:
case StreamProtocolV4Name:
handler = &v4ProtocolHandler{}
case remotecommandconsts.StreamProtocolV3Name:
case StreamProtocolV3Name:
handler = &v3ProtocolHandler{}
case remotecommandconsts.StreamProtocolV2Name:
case StreamProtocolV2Name:
handler = &v2ProtocolHandler{}
case "":
klog.V(4).InfoS("Client did not request protocol negotiation. Falling back", "protocol", remotecommandconsts.StreamProtocolV1Name)
klog.V(4).InfoS("Client did not request protocol negotiation. Falling back", "protocol", StreamProtocolV1Name)
fallthrough
case remotecommandconsts.StreamProtocolV1Name:
case StreamProtocolV1Name:
handler = &v1ProtocolHandler{}
}
@ -201,7 +196,7 @@ type protocolHandler interface {
}
// v4ProtocolHandler implements the V4 protocol version for streaming command execution. It only differs
// in from v3 in the error stream format using an json-marshaled metav1.Status which carries
// in from v3 in the error stream format using a json-marshaled status object which carries
// the process' exit code.
type v4ProtocolHandler struct{}
@ -215,21 +210,21 @@ WaitForStreams:
for {
select {
case stream := <-streams:
streamType := stream.Headers().Get(api.StreamType)
streamType := stream.Headers().Get(StreamType)
switch streamType {
case api.StreamTypeError:
case StreamTypeError:
ctx.writeStatus = v4WriteStatusFunc(stream) // write json errors
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdin:
case StreamTypeStdin:
ctx.stdinStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdout:
case StreamTypeStdout:
ctx.stdoutStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStderr:
case StreamTypeStderr:
ctx.stderrStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeResize:
case StreamTypeResize:
ctx.resizeStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
default:
@ -266,21 +261,21 @@ WaitForStreams:
for {
select {
case stream := <-streams:
streamType := stream.Headers().Get(api.StreamType)
streamType := stream.Headers().Get(StreamType)
switch streamType {
case api.StreamTypeError:
case StreamTypeError:
ctx.writeStatus = v1WriteStatusFunc(stream)
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdin:
case StreamTypeStdin:
ctx.stdinStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdout:
case StreamTypeStdout:
ctx.stdoutStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStderr:
case StreamTypeStderr:
ctx.stderrStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeResize:
case StreamTypeResize:
ctx.resizeStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
default:
@ -317,18 +312,18 @@ WaitForStreams:
for {
select {
case stream := <-streams:
streamType := stream.Headers().Get(api.StreamType)
streamType := stream.Headers().Get(StreamType)
switch streamType {
case api.StreamTypeError:
case StreamTypeError:
ctx.writeStatus = v1WriteStatusFunc(stream)
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdin:
case StreamTypeStdin:
ctx.stdinStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdout:
case StreamTypeStdout:
ctx.stdoutStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStderr:
case StreamTypeStderr:
ctx.stderrStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
default:
@ -365,9 +360,9 @@ WaitForStreams:
for {
select {
case stream := <-streams:
streamType := stream.Headers().Get(api.StreamType)
streamType := stream.Headers().Get(StreamType)
switch streamType {
case api.StreamTypeError:
case StreamTypeError:
ctx.writeStatus = v1WriteStatusFunc(stream)
// This defer statement shouldn't be here, but due to previous refactoring, it ended up in
@ -376,13 +371,13 @@ WaitForStreams:
defer stream.Reset()
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdin:
case StreamTypeStdin:
ctx.stdinStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStdout:
case StreamTypeStdout:
ctx.stdoutStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
case api.StreamTypeStderr:
case StreamTypeStderr:
ctx.stderrStream = stream
go waitStreamReply(stream.replySent, replyChan, stop)
default:
@ -410,13 +405,13 @@ WaitForStreams:
// supportsTerminalResizing returns false because v1ProtocolHandler doesn't support it.
func (*v1ProtocolHandler) supportsTerminalResizing() bool { return false }
func handleResizeEvents(reqctx context.Context, stream io.Reader, channel chan<- remotecommand.TerminalSize) {
func handleResizeEvents(reqctx context.Context, stream io.Reader, channel chan<- TerminalSize) {
defer runtime.HandleCrash()
defer close(channel)
decoder := json.NewDecoder(stream)
for {
size := remotecommand.TerminalSize{}
size := TerminalSize{}
if err := decoder.Decode(&size); err != nil {
break
}
@ -429,9 +424,9 @@ func handleResizeEvents(reqctx context.Context, stream io.Reader, channel chan<-
}
}
func v1WriteStatusFunc(stream io.Writer) func(status *apierrors.StatusError) error {
return func(status *apierrors.StatusError) error {
if status.Status().Status == metav1.StatusSuccess {
func v1WriteStatusFunc(stream io.Writer) func(status *streamStatusError) error {
return func(status *streamStatusError) error {
if status.status().Status == statusSuccess {
return nil // send error messages
}
_, err := stream.Write([]byte(status.Error()))
@ -439,11 +434,11 @@ func v1WriteStatusFunc(stream io.Writer) func(status *apierrors.StatusError) err
}
}
// v4WriteStatusFunc returns a WriteStatusFunc that marshals a given api Status
// v4WriteStatusFunc returns a WriteStatusFunc that marshals a status object
// as json in the error channel.
func v4WriteStatusFunc(stream io.Writer) func(status *apierrors.StatusError) error {
return func(status *apierrors.StatusError) error {
bs, err := json.Marshal(status.Status())
func v4WriteStatusFunc(stream io.Writer) func(status *streamStatusError) error {
return func(status *streamStatusError) error {
bs, err := json.Marshal(status.status())
if err != nil {
return err
}

View file

@ -25,12 +25,10 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"k8s.io/client-go/tools/remotecommand"
)
func TestHandleResizeEvents(t *testing.T) {
var testTerminalSize remotecommand.TerminalSize
var testTerminalSize TerminalSize
rawTerminalSize, err := json.Marshal(&testTerminalSize)
require.NoError(t, err)
@ -63,7 +61,7 @@ func TestHandleResizeEvents(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
connCtx := connectionContext{
resizeStream: io.NopCloser(bytes.NewReader(testCase.resizeStreamData)),
resizeChan: make(chan remotecommand.TerminalSize),
resizeChan: make(chan TerminalSize),
}
go handleResizeEvents(ctx, connCtx.resizeStream, connCtx.resizeChan)

View file

@ -0,0 +1,23 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
// TerminalSize represents the width and height of a terminal.
type TerminalSize struct {
Width uint16
Height uint16
}

View file

@ -0,0 +1,71 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
import (
"fmt"
"net/http"
)
const (
statusSuccess = "Success"
statusFailure = "Failure"
statusReasonInternalError = "InternalError"
)
// streamStatusError carries status details written to the error stream.
type streamStatusError struct {
ErrStatus streamStatus
}
func (e *streamStatusError) Error() string {
return e.ErrStatus.Message
}
func (e *streamStatusError) status() streamStatus {
return e.ErrStatus
}
func newInternalError(err error) *streamStatusError {
return &streamStatusError{
ErrStatus: streamStatus{
Status: statusFailure,
Reason: statusReasonInternalError,
Message: fmt.Sprintf("Internal error occurred: %v", err),
Code: http.StatusInternalServerError,
},
}
}
type streamStatus struct {
Status string `json:"status,omitempty"`
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
Details *streamStatusDetails `json:"details,omitempty"`
Code int32 `json:"code,omitempty"`
}
type streamStatusDetails struct {
Causes []streamStatusCause `json:"causes,omitempty"`
}
// streamStatusCause uses "reason" on the wire to match metav1.StatusCause.
type streamStatusCause struct {
Type string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
}

View file

@ -21,9 +21,9 @@ import (
"net/http"
"time"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
"k8s.io/cri-streaming/pkg/streaming/internal/httpresponse"
"k8s.io/streaming/pkg/httpstream/wsstream"
"k8s.io/streaming/pkg/runtime"
)
const (
@ -95,7 +95,7 @@ func createWebSocketStreams(req *http.Request, w http.ResponseWriter, opts *Opti
},
})
conn.SetIdleTimeout(idleTimeout)
negotiatedProtocol, streams, err := conn.Open(responsewriter.GetOriginal(w), req)
negotiatedProtocol, streams, err := conn.Open(httpresponse.GetOriginal(w), req)
if err != nil {
runtime.HandleError(fmt.Errorf("unable to upgrade websocket connection: %v", err))
return nil, false

View file

@ -32,12 +32,9 @@ import (
restful "github.com/emicklei/go-restful/v3"
"k8s.io/apimachinery/pkg/types"
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/client-go/tools/remotecommand"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand"
"k8s.io/cri-streaming/pkg/streaming/portforward"
remotecommandserver "k8s.io/cri-streaming/pkg/streaming/remotecommand"
)
// Server is the library interface to serve the stream requests.
@ -62,8 +59,8 @@ type Server interface {
// Runtime is the interface to execute the commands and provide the streams.
type Runtime interface {
Exec(ctx context.Context, containerID string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
Attach(ctx context.Context, containerID string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
Exec(ctx context.Context, containerID string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error
Attach(ctx context.Context, containerID string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error
PortForward(ctx context.Context, podSandboxID string, port int32, stream io.ReadWriteCloser) error
}
@ -99,8 +96,8 @@ type Config struct {
// some fields like Addr must still be provided.
var DefaultConfig = Config{
StreamIdleTimeout: 4 * time.Hour,
StreamCreationTimeout: remotecommandconsts.DefaultStreamCreationTimeout,
SupportedRemoteCommandProtocols: remotecommandconsts.SupportedStreamingProtocols,
StreamCreationTimeout: remotecommandserver.DefaultStreamCreationTimeout,
SupportedRemoteCommandProtocols: remotecommandserver.SupportedStreamingProtocols,
SupportedPortForwardProtocols: portforward.SupportedProtocols,
}
@ -370,14 +367,14 @@ var _ remotecommandserver.Executor = &criAdapter{}
var _ remotecommandserver.Attacher = &criAdapter{}
var _ portforward.PortForwarder = &criAdapter{}
func (a *criAdapter) ExecInContainer(ctx context.Context, podName string, podUID types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error {
func (a *criAdapter) ExecInContainer(ctx context.Context, podName string, podUID string, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize, timeout time.Duration) error {
return a.Runtime.Exec(ctx, container, cmd, in, out, err, tty, resize)
}
func (a *criAdapter) AttachContainer(ctx context.Context, podName string, podUID types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
func (a *criAdapter) AttachContainer(ctx context.Context, podName string, podUID string, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
return a.Runtime.Attach(ctx, container, in, out, err, tty, resize)
}
func (a *criAdapter) PortForward(ctx context.Context, podName string, podUID types.UID, port int32, stream io.ReadWriteCloser) error {
func (a *criAdapter) PortForward(ctx context.Context, podName string, podUID string, port int32, stream io.ReadWriteCloser) error {
return a.Runtime.PortForward(ctx, podName, port, stream)
}

View file

@ -19,24 +19,20 @@ package streaming
import (
"context"
"crypto/tls"
"encoding/binary"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"sync"
"testing"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
api "k8s.io/api/core/v1"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/client-go/transport/spdy"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
kubeletportforward "k8s.io/kubelet/pkg/cri/streaming/portforward"
remotecommandserver "k8s.io/cri-streaming/pkg/streaming/remotecommand"
)
const (
@ -277,32 +273,22 @@ func TestServePortForward(t *testing.T) {
resp, err := s.GetPortForward(&runtimeapi.PortForwardRequest{
PodSandboxId: testPodSandboxID,
Port: []int32{testPort},
})
require.NoError(t, err)
reqURL, err := url.Parse(resp.Url)
require.NoError(t, err)
transport, upgrader, err := spdy.RoundTripperFor(&restclient.Config{})
require.NoError(t, err)
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", reqURL)
streamConn, _, err := dialer.Dial(kubeletportforward.ProtocolV1Name)
require.NoError(t, err)
defer streamConn.Close()
wsConn := dialWebSocket(t, reqURL)
defer wsConn.Close()
// Create the streams.
headers := http.Header{}
// Error stream is required, but unused in this test.
headers.Set(api.StreamType, api.StreamTypeError)
headers.Set(api.PortHeader, strconv.Itoa(testPort))
_, err = streamConn.CreateStream(headers)
require.NoError(t, err)
// Setup the data stream.
headers.Set(api.StreamType, api.StreamTypeData)
headers.Set(api.PortHeader, strconv.Itoa(testPort))
stream, err := streamConn.CreateStream(headers)
require.NoError(t, err)
expectedPortBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(expectedPortBytes, uint16(testPort))
requireWebSocketPayload(t, wsConn, 0, expectedPortBytes, false)
requireWebSocketPayload(t, wsConn, 1, expectedPortBytes, false)
doClientStreams(t, "portforward", stream, stream, nil)
writeWebSocketPayload(t, wsConn, 0, []byte("portforward"+testInput))
requireWebSocketPayload(t, wsConn, 0, []byte("portforward"+testOutput), false)
}
// Run the remote command test.
@ -338,38 +324,12 @@ func runRemoteCommandTest(t *testing.T, commandType string) {
require.NoError(t, err)
}
wg := sync.WaitGroup{}
wg.Add(2)
wsConn := dialWebSocket(t, reqURL)
defer wsConn.Close()
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
stderrR, stderrW := io.Pipe()
go func() {
defer wg.Done()
exec, err := remotecommand.NewSPDYExecutor(&restclient.Config{}, "POST", reqURL)
if err != nil {
t.Errorf("unexpected error %v", err)
return
}
opts := remotecommand.StreamOptions{
Stdin: stdinR,
Stdout: stdoutW,
Stderr: stderrW,
Tty: false,
}
if err = exec.StreamWithContext(context.Background(), opts); err != nil {
t.Errorf("unexpected error %v", err)
}
}()
go func() {
defer wg.Done()
doClientStreams(t, commandType, stdinW, stdoutR, stderrR)
}()
wg.Wait()
requireWebSocketPayload(t, wsConn, 2, []byte(commandType+testErr), false)
writeWebSocketPayload(t, wsConn, 0, []byte(commandType+testInput))
requireWebSocketPayload(t, wsConn, 1, []byte(commandType+testOutput), true)
// Repeat request with the same URL should be a 404.
resp, err := http.Get(reqURL.String())
@ -419,13 +379,13 @@ type fakeRuntime struct {
t *testing.T
}
func (f *fakeRuntime) Exec(_ context.Context, containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
func (f *fakeRuntime) Exec(_ context.Context, containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
assert.Equal(f.t, testContainerID, containerID)
doServerStreams(f.t, "exec", stdin, stdout, stderr)
return nil
}
func (f *fakeRuntime) Attach(_ context.Context, containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
func (f *fakeRuntime) Attach(_ context.Context, containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommandserver.TerminalSize) error {
assert.Equal(f.t, testContainerID, containerID)
doServerStreams(f.t, "attach", stdin, stdout, stderr)
return nil
@ -448,16 +408,6 @@ func doServerStreams(t *testing.T, prefix string, stdin io.Reader, stdout, stder
writeExpected(t, "server stdout", stdout, prefix+testOutput)
}
// Send & receive expected input/output. Must be the inverse of doServerStreams.
// Function will block until the expected i/o is finished.
func doClientStreams(t *testing.T, prefix string, stdin io.Writer, stdout, stderr io.Reader) {
if stderr != nil {
readExpected(t, "client stderr", stderr, prefix+testErr)
}
writeExpected(t, "client stdin", stdin, prefix+testInput)
readExpected(t, "client stdout", stdout, prefix+testOutput)
}
// Read and verify the expected string from the stream.
func readExpected(t *testing.T, streamName string, r io.Reader, expected string) {
result := make([]byte, len(expected))
@ -472,3 +422,52 @@ func writeExpected(t *testing.T, streamName string, w io.Writer, data string) {
assert.NoError(t, err, "stream %s", streamName)
assert.Equal(t, len(data), n, "stream %s", streamName)
}
func dialWebSocket(t *testing.T, reqURL *url.URL) *websocket.Conn {
t.Helper()
wsURL := *reqURL
switch wsURL.Scheme {
case "http":
wsURL.Scheme = "ws"
case "https":
wsURL.Scheme = "wss"
default:
t.Fatalf("unsupported URL scheme %q", wsURL.Scheme)
}
dialer := websocket.Dialer{
Subprotocols: []string{"v4.channel.k8s.io"},
}
conn, _, err := dialer.Dial(wsURL.String(), nil)
require.NoError(t, err)
return conn
}
func writeWebSocketPayload(t *testing.T, conn *websocket.Conn, channel byte, payload []byte) {
t.Helper()
frame := make([]byte, len(payload)+1)
frame[0] = channel
copy(frame[1:], payload)
require.NoError(t, conn.WriteMessage(websocket.BinaryMessage, frame))
}
func requireWebSocketPayload(t *testing.T, conn *websocket.Conn, channel byte, expected []byte, skipEmpty bool) {
t.Helper()
for {
_, frame, err := conn.ReadMessage()
require.NoError(t, err)
if len(frame) == 0 {
continue
}
if frame[0] != channel {
continue
}
payload := frame[1:]
if skipEmpty && len(payload) == 0 {
continue
}
assert.Equal(t, expected, payload)
return
}
}

View file

@ -39,4 +39,5 @@ require (
replace (
k8s.io/api => ../api
k8s.io/apimachinery => ../apimachinery
k8s.io/streaming => ../streaming
)

Some files were not shown because too many files have changed in this diff Show more