Repository: containerd/containerd
Pull request #11795: clones k8s util exec used by streaming code removing k8s util dependencies
Author: login: mikebrow
Labels: name: area/cri
description: Container Runtime Interface (CRI)
name: dependencies
description: Pull requests that update a dependency file
name: kind/refactor
name: size/L
Created: 2025-05-05T23:33:23Z | Merged: 2025-05-15T19:14:10Z

one of N PRs to clean up / eliminate our k8s.io vendor impact to only external apis that carry no k8s.io internal baggage like the min golang version definition in go.mod

https://github.com/kubernetes/kubernetes/issues/130799

this one clones utils/exec which finishes out our direct use of k8s/utils/* 
this one is a part of the streaming exec/probe/portforword/... bucket of changes


Diff:
diff --git a/go.mod b/go.mod
index ecde30b217132..6590311fff78d 100644
--- a/go.mod
+++ b/go.mod
@@ -85,7 +85,6 @@ require (
 	k8s.io/client-go v0.32.3
 	k8s.io/cri-api v0.32.3
 	k8s.io/klog/v2 v2.130.1
-	k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
 	tags.cncf.io/container-device-interface v1.0.1
 )
 
@@ -145,6 +144,7 @@ require (
 	google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/api v0.32.3 // indirect
+	k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
 	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
diff --git a/integration/remote/remote_runtime.go b/integration/remote/remote_runtime.go
index 964d4f606f502..9ed6b06e45b4b 100644
--- a/integration/remote/remote_runtime.go
+++ b/integration/remote/remote_runtime.go
@@ -42,11 +42,11 @@ import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
 	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
-	utilexec "k8s.io/utils/exec"
 
 	internalapi "github.com/containerd/containerd/v2/integration/cri-api/pkg/apis"
 	"github.com/containerd/containerd/v2/integration/remote/logreduction"
 	"github.com/containerd/containerd/v2/integration/remote/util"
+	executil "github.com/containerd/containerd/v2/internal/cri/executil"
 	"github.com/containerd/log"
 )
 
@@ -413,7 +413,7 @@ func (r *RuntimeService) ExecSync(containerID string, cmd []string, timeout time
 	log.L.Infof("[RuntimeService] ExecSync Response (containerID=%v, ExitCode=%v)", containerID, resp.ExitCode)
 	err = nil
 	if resp.ExitCode != 0 {
-		err = utilexec.CodeExitError{
+		err = executil.CodeExitError{
 			Err:  fmt.Errorf("command '%s' exited with %d: %s", strings.Join(cmd, " "), resp.ExitCode, resp.Stderr),
 			Code: int(resp.ExitCode),
 		}
diff --git a/internal/cri/bandwidth/linux.go b/internal/cri/bandwidth/linux.go
index 15f1cb6d5c110..0850b9c75a637 100644
--- a/internal/cri/bandwidth/linux.go
+++ b/internal/cri/bandwidth/linux.go
@@ -42,8 +42,7 @@ import (
 	"net"
 	"strings"
 
-	"k8s.io/utils/exec"
-
+	executil "github.com/containerd/containerd/v2/internal/cri/executil"
 	resource "github.com/containerd/containerd/v2/internal/cri/resourcequantity"
 	"github.com/containerd/containerd/v2/internal/cri/setutils"
 	"github.com/containerd/containerd/v2/internal/lazyregexp"
@@ -62,14 +61,14 @@ var (
 // Uses the hierarchical token bucket queuing discipline (htb), this requires Linux 2.4.20 or newer
 // or a custom kernel with that queuing discipline backported.
 type tcShaper struct {
-	e     exec.Interface
+	e     executil.Interface
 	iface string
 }
 
 // NewTCShaper makes a new tcShaper for the given interface
 func NewTCShaper(iface string) Shaper {
 	shaper := &tcShaper{
-		e:     exec.New(),
+		e:     executil.New(),
 		iface: iface,
 	}
 	return shaper
diff --git a/internal/cri/executil/README.md b/internal/cri/executil/README.md
new file mode 100644
index 0000000000000..c2c41aeae4e14
--- /dev/null
+++ b/internal/cri/executil/README.md
@@ -0,0 +1,4 @@
+# Executil
+
+This package provides an interface for `os/exec`. It makes it easier to mock
+and replace in tests
\ No newline at end of file
diff --git a/vendor/k8s.io/utils/exec/doc.go b/internal/cri/executil/doc.go
similarity index 50%
rename from vendor/k8s.io/utils/exec/doc.go
rename to internal/cri/executil/doc.go
index cbb44bdb5d1f6..dde93f0011790 100644
--- a/vendor/k8s.io/utils/exec/doc.go
+++ b/internal/cri/executil/doc.go
@@ -1,3 +1,19 @@
+/*
+   Copyright The containerd 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.
+*/
+
 /*
 Copyright 2017 The Kubernetes Authors.
 
@@ -15,4 +31,4 @@ limitations under the License.
 */
 
 // Package exec provides an injectable interface and implementations for running commands.
-package exec // import "k8s.io/utils/exec"
+package executil // cloned from "k8s.io/utils/exec"
diff --git a/vendor/k8s.io/utils/exec/exec.go b/internal/cri/executil/exec.go
similarity index 86%
rename from vendor/k8s.io/utils/exec/exec.go
rename to internal/cri/executil/exec.go
index d9c91e3ca3c60..d8c9b11cea290 100644
--- a/vendor/k8s.io/utils/exec/exec.go
+++ b/internal/cri/executil/exec.go
@@ -1,3 +1,19 @@
+/*
+   Copyright The containerd 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.
+*/
+
 /*
 Copyright 2017 The Kubernetes Authors.
 
@@ -14,10 +30,11 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package exec
+package executil
 
 import (
 	"context"
+	"errors"
 	"io"
 	"io/fs"
 	osexec "os/exec"
@@ -254,3 +271,18 @@ func (e CodeExitError) Exited() bool {
 func (e CodeExitError) ExitStatus() int {
 	return e.Code
 }
+
+// maskErrDotCmd reverts the behavior of osexec.Cmd to what it was before go1.19
+// specifically set the Err field to nil (LookPath returns a new error when the file
+// is resolved to the current directory.
+func maskErrDotCmd(cmd *osexec.Cmd) *osexec.Cmd {
+	cmd.Err = maskErrDot(cmd.Err)
+	return cmd
+}
+
+func maskErrDot(err error) error {
+	if err != nil && errors.Is(err, osexec.ErrDot) {
+		return nil
+	}
+	return err
+}
diff --git a/internal/cri/executil/exec_test.go b/internal/cri/executil/exec_test.go
new file mode 100644
index 0000000000000..fe4a6ceffe4a1
--- /dev/null
+++ b/internal/cri/executil/exec_test.go
@@ -0,0 +1,284 @@
+/*
+   Copyright The containerd 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.
+*/
+
+/*
+Copyright 2017 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 executil
+
+import (
+	"context"
+	"io"
+	"os"
+	osexec "os/exec"
+	goruntime "runtime"
+	"testing"
+	"time"
+)
+
+func TestExecutorNoArgs(t *testing.T) {
+	ex := New()
+
+	cmd := ex.Command("true")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Errorf("expected success, got %v", err)
+	}
+	if len(out) != 0 {
+		t.Errorf("expected no output, got %q", string(out))
+	}
+
+	cmd = ex.Command("false")
+	out, err = cmd.CombinedOutput()
+	if err == nil {
+		t.Errorf("expected failure, got nil error")
+	}
+	if len(out) != 0 {
+		t.Errorf("expected no output, got %q", string(out))
+	}
+	ee, ok := err.(ExitError)
+	if !ok {
+		t.Errorf("expected an ExitError, got %+v", err)
+	}
+	if ee.Exited() {
+		if code := ee.ExitStatus(); code != 1 {
+			t.Errorf("expected exit status 1, got %d", code)
+		}
+	}
+
+	cmd = ex.Command("/does/not/exist")
+	_, err = cmd.CombinedOutput()
+	if err == nil {
+		t.Errorf("expected failure, got nil error")
+	}
+	if ee, ok := err.(ExitError); ok {
+		t.Errorf("expected non-ExitError, got %+v", ee)
+	}
+}
+
+func TestExecutorWithArgs(t *testing.T) {
+	if goruntime.GOOS == "windows" {
+		t.Skip("Skipped on Windows.")
+	}
+
+	ex := New()
+
+	cmd := ex.Command("echo", "stdout")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Errorf("expected success, got %+v", err)
+	}
+	if string(out) != "stdout\n" {
+		t.Errorf("unexpected output: %q", string(out))
+	}
+
+	cmd = ex.Command("/bin/sh", "-c", "echo stderr > /dev/stderr")
+	out, err = cmd.CombinedOutput()
+	if err != nil {
+		t.Errorf("expected success, got %+v", err)
+	}
+	if string(out) != "stderr\n" {
+		t.Errorf("unexpected output: %q", string(out))
+	}
+}
+
+func TestLookPath(t *testing.T) {
+	ex := New()
+
+	shExpected, _ := osexec.LookPath("sh")
+	sh, _ := ex.LookPath("sh")
+	if sh != shExpected {
+		t.Errorf("unexpected result for LookPath: got %s, expected %s", sh, shExpected)
+	}
+}
+
+func TestExecutableNotFound(t *testing.T) {
+	exec := New()
+
+	cmd := exec.Command("fake_executable_name")
+	_, err := cmd.CombinedOutput()
+	if err != ErrExecutableNotFound {
+		t.Errorf("cmd.CombinedOutput(): Expected error ErrExecutableNotFound but got %v", err)
+	}
+
+	cmd = exec.Command("fake_executable_name")
+	_, err = cmd.Output()
+	if err != ErrExecutableNotFound {
+		t.Errorf("cmd.Output(): Expected error ErrExecutableNotFound but got %v", err)
+	}
+
+	cmd = exec.Command("fake_executable_name")
+	err = cmd.Run()
+	if err != ErrExecutableNotFound {
+		t.Errorf("cmd.Run(): Expected error ErrExecutableNotFound but got %v", err)
+	}
+}
+
+func TestStopBeforeStart(t *testing.T) {
+	cmd := New().Command("echo", "hello")
+
+	// no panic calling Stop before calling Run
+	cmd.Stop()
+
+	cmd.Run()
+
+	// no panic calling Stop after command is done
+	cmd.Stop()
+}
+
+func TestTimeout(t *testing.T) {
+	exec := New()
+	ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
+	defer cancel()
+
+	err := exec.CommandContext(ctx, "sleep", "2").Run()
+	if err != context.DeadlineExceeded {
+		t.Errorf("expected %v but got %v", context.DeadlineExceeded, err)
+	}
+}
+
+func TestSetEnv(t *testing.T) {
+	if goruntime.GOOS == "windows" {
+		t.Skip("Skipped on Windows.")
+	}
+
+	ex := New()
+
+	out, err := ex.Command("/bin/sh", "-c", "echo $FOOBAR").CombinedOutput()
+	if err != nil {
+		t.Errorf("expected success, got %+v", err)
+	}
+	if string(out) != "\n" {
+		t.Errorf("unexpected output: %q", string(out))
+	}
+
+	cmd := ex.Command("/bin/sh", "-c", "echo $FOOBAR")
+	cmd.SetEnv([]string{"FOOBAR=baz"})
+	out, err = cmd.CombinedOutput()
+	if err != nil {
+		t.Errorf("expected success, got %+v", err)
+	}
+	if string(out) != "baz\n" {
+		t.Errorf("unexpected output: %q", string(out))
+	}
+}
+
+func TestStdIOPipes(t *testing.T) {
+	if goruntime.GOOS == "windows" {
+		t.Skip("Skipped on Windows.")
+	}
+
+	cmd := New().Command("/bin/sh", "-c", "echo 'OUT'>&1; echo 'ERR'>&2")
+
+	stdoutPipe, err := cmd.StdoutPipe()
+	if err != nil {
+		t.Fatalf("expected StdoutPipe() not to error, got: %v", err)
+	}
+	stderrPipe, err := cmd.StderrPipe()
+	if err != nil {
+		t.Fatalf("expected StderrPipe() not to error, got: %v", err)
+	}
+
+	stdout := make(chan string)
+	stderr := make(chan string)
+
+	go func() {
+		stdout <- readAll(t, stdoutPipe, "StdOut")
+	}()
+	go func() {
+		stderr <- readAll(t, stderrPipe, "StdErr")
+	}()
+
+	if err := cmd.Start(); err != nil {
+		t.Errorf("expected Start() not to error, got: %v", err)
+	}
+
+	if e, a := "OUT\n", <-stdout; e != a {
+		t.Errorf("expected StdOut to be '%s', got: '%v'", e, a)
+	}
+
+	if e, a := "ERR\n", <-stderr; e != a {
+		t.Errorf("expected StdErr to be '%s', got: '%v'", e, a)
+	}
+
+	if err := cmd.Wait(); err != nil {
+		t.Errorf("expected Wait() not to error, got: %v", err)
+	}
+}
+
+func readAll(t *testing.T, r io.Reader, n string) string {
+	t.Helper()
+
+	b, err := io.ReadAll(r)
+	if err != nil {
+		t.Fatalf("unexpected error when reading from %s: %v", n, err)
+	}
+
+	return string(b)
+}
+
+func TestExecutorGo119LookPath(t *testing.T) {
+	if goruntime.GOOS == "windows" {
+		t.Skip("Skipped on Windows.")
+	}
+
+	orig := os.Getenv("PATH")
+	defer func() { os.Setenv("PATH", orig) }()
+
+	t.Setenv("PATH", "./testdata")
+
+	ex := New()
+	path, err := ex.LookPath("hello.sh")
+	if err != nil {
+		t.Errorf("expected success, got %v", err)
+	}
+	if path != "testdata/hello.sh" {
+		t.Errorf("expected relative path to hello script, got %v", path)
+	}
+
+	cmd := ex.Command("hello.sh")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Errorf("expected success, got %v", err)
+	} else {
+		if len(out) != 5 {
+			t.Errorf("expected 'hello' output, got %q", string(out))
+		}
+	}
+
+	cmd = ex.CommandContext(context.Background(), "hello.sh")
+	out, err = cmd.CombinedOutput()
+	if err != nil {
+		t.Errorf("expected success, got %v", err)
+	} else {
+		if len(out) != 5 {
+			t.Errorf("expected 'hello' output, got %q", string(out))
+		}
+	}
+}
diff --git a/internal/cri/executil/testdata/hello.sh b/internal/cri/executil/testdata/hello.sh
new file mode 100755
index 0000000000000..676d7f7929629
--- /dev/null
+++ b/internal/cri/executil/testdata/hello.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+#   Copyright The containerd 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.
+
+echo -n "hello"
\ No newline at end of file
diff --git a/internal/cri/server/streaming.go b/internal/cri/server/streaming.go
index b37a8976befec..5d366ce5bdb6c 100644
--- a/internal/cri/server/streaming.go
+++ b/internal/cri/server/streaming.go
@@ -24,8 +24,8 @@ import (
 
 	"k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/client-go/tools/remotecommand"
-	"k8s.io/utils/exec"
 
+	executil "github.com/containerd/containerd/v2/internal/cri/executil"
 	streaming "github.com/containerd/containerd/v2/internal/cri/streamingserver"
 	ctrdutil "github.com/containerd/containerd/v2/internal/cri/util"
 )
@@ -38,7 +38,7 @@ func newStreamRuntime(c *criService) streaming.Runtime {
 	return &streamRuntime{c: c}
 }
 
-// Exec executes a command inside the container. exec.ExitError is returned if the command
+// Exec executes a command inside the container. executil.ExitError is returned if the command
 // returns non-zero exit code.
 func (s *streamRuntime) Exec(ctx context.Context, containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser,
 	tty bool, resize <-chan remotecommand.TerminalSize) error {
@@ -56,7 +56,7 @@ func (s *streamRuntime) Exec(ctx context.Context, containerID string, cmd []stri
 	if *exitCode == 0 {
 		return nil
 	}
-	return &exec.CodeExitError{
+	return &executil.CodeExitError{
 		Err:  fmt.Errorf("error executing command %v, exit code %d", cmd, *exitCode),
 		Code: int(*exitCode),
 	}
diff --git a/internal/cri/streamingserver/remotecommand/exec.go b/internal/cri/streamingserver/remotecommand/exec.go
index 339ef7fa439ed..b475dfbfcd9f2 100644
--- a/internal/cri/streamingserver/remotecommand/exec.go
+++ b/internal/cri/streamingserver/remotecommand/exec.go
@@ -39,13 +39,13 @@ import (
 	"net/http"
 	"time"
 
+	utilexec "github.com/containerd/containerd/v2/internal/cri/executil"
 	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"
-	utilexec "k8s.io/utils/exec"
 )
 
 // Executor knows how to execute a command in a container in a pod.
diff --git a/vendor/k8s.io/utils/exec/README.md b/vendor/k8s.io/utils/exec/README.md
deleted file mode 100644
index 7944e8dd3be4b..0000000000000
--- a/vendor/k8s.io/utils/exec/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Exec
-
-This package provides an interface for `os/exec`. It makes it easier to mock
-and replace in tests, especially with the [FakeExec](testing/fake_exec.go)
-struct.
diff --git a/vendor/k8s.io/utils/exec/fixup_go118.go b/vendor/k8s.io/utils/exec/fixup_go118.go
deleted file mode 100644
index acf45f1cd5b4a..0000000000000
--- a/vendor/k8s.io/utils/exec/fixup_go118.go
+++ /dev/null
@@ -1,32 +0,0 @@
-//go:build !go1.19
-// +build !go1.19
-
-/*
-Copyright 2022 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 exec
-
-import (
-	osexec "os/exec"
-)
-
-func maskErrDotCmd(cmd *osexec.Cmd) *osexec.Cmd {
-	return cmd
-}
-
-func maskErrDot(err error) error {
-	return err
-}
diff --git a/vendor/k8s.io/utils/exec/fixup_go119.go b/vendor/k8s.io/utils/exec/fixup_go119.go
deleted file mode 100644
index 55874c9297e38..0000000000000
--- a/vendor/k8s.io/utils/exec/fixup_go119.go
+++ /dev/null
@@ -1,40 +0,0 @@
-//go:build go1.19
-// +build go1.19
-
-/*
-Copyright 2022 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 exec
-
-import (
-	"errors"
-	osexec "os/exec"
-)
-
-// maskErrDotCmd reverts the behavior of osexec.Cmd to what it was before go1.19
-// specifically set the Err field to nil (LookPath returns a new error when the file
-// is resolved to the current directory.
-func maskErrDotCmd(cmd *osexec.Cmd) *osexec.Cmd {
-	cmd.Err = maskErrDot(cmd.Err)
-	return cmd
-}
-
-func maskErrDot(err error) error {
-	if err != nil && errors.Is(err, osexec.ErrDot) {
-		return nil
-	}
-	return err
-}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index af88d3b498511..943b0253c309b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -869,7 +869,6 @@ k8s.io/klog/v2/internal/sloghandler
 ## explicit; go 1.18
 k8s.io/utils/clock
 k8s.io/utils/clock/testing
-k8s.io/utils/exec
 k8s.io/utils/internal/third_party/forked/golang/net
 k8s.io/utils/net
 k8s.io/utils/ptr


Reviews:
user: login: samuelkarp
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T16:43:37Z
user: login: fuweid
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T18:10:06Z
user: login: mikebrow
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T18:34:19Z

Review comments:
diff_hunk: @@ -0,0 +1,17 @@
+#!/bin/bash
+
+#   Copyright The containerd 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.
+
+echo -n "hello"
path: internal/cri/executil/testdata/hello.sh
user: login: fuweid
body: unexpected end-of-file char
created_at: 2025-05-15T18:09:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+#!/bin/bash
+
+#   Copyright The containerd 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.
+
+echo -n "hello"
path: internal/cri/executil/testdata/hello.sh
user: login: mikebrow
body: thx will add the extra new line in the next one :-) 
created_at: 2025-05-15T18:34:18Z
author_association: MEMBER

PR comments:
user: login: mikebrow
created_at: 2025-05-06T15:45:03Z
body: Interesting... will take a look
```
  STEP: pod-Service(hostNetwork): http @ 05/06/25 14:28:34.889
  STEP: dialing(http) test-container-pod --> 10.108.118.99:80 (config.clusterIP) @ 05/06/25 14:28:34.889
  I0506 14:28:34.903007 38541 exec_util.go:63] ExecWithOptions {Command:[/bin/sh -c curl -g -q -s 'http://172.31.1.226:9080/dial?request=hostname&protocol=http&host=10.108.118.99&port=80&tries=1'] Namespace:nettest-7114 PodName:test-container-pod ContainerName:webserver Stdin: CaptureStdout:true CaptureStderr:true PreserveWhitespace:false Quiet:false}
  I0506 14:28:34.903023 38541 exec_util.go:68] ExecWithOptions: Clientset creation
  I0506 14:28:34.903067 38541 exec_util.go:84] ExecWithOptions: execute(https://3.221.161.47:6443/api/v1/namespaces/nettest-7114/pods/test-container-pod/exec?command=%2Fbin%2Fsh&command=-c&command=curl+-g+-q+-s+%27http%3A%2F%2F172.31.1.226%3A9080%2Fdial%3Frequest%3Dhostname%26protocol%3Dhttp%26host%3D10.108.118.99%26port%3D80%26tries%3D1%27&container=webserver&stderr=true&stdout=true)
  I0506 14:28:34.988755 38541 exec_util.go:201] unexpected error trying to use websockets for pod exec: <nil>
  I0506 14:28:34.988826 38541 utils.go:351] encountered error during dial (received unexpected responses... 
  Attempt 0
  Command curl -g -q -s 'http://172.31.1.226:9080/dial?request=hostname&protocol=http&host=10.108.118.99&port=80&tries=1'
  retrieved map[ip-172-31-3-81:{}]
  expected map[ip-172-31-3-81.ec2.internal:{} ip-172-31-6-221.ec2.internal:{}])
  [FAILED] in [It] - k8s.io/kubernetes/test/e2e/network/networking.go:479 @ 05/06/25 14:28:34.988
```

fixed..
author_association: MEMBER
user: login: mikebrow
created_at: 2025-05-07T17:18:12Z
body: /test pull-containerd-k8s-e2e-ec2
author_association: MEMBER
user: login: mikebrow
created_at: 2025-05-07T19:01:35Z
body: /test pull-containerd-k8s-e2e-ec2

author_association: MEMBER
user: login: mikebrow
created_at: 2025-05-15T16:49:03Z
body: rebased on top of https://github.com/containerd/containerd/pull/11840  ...

status update:  with this change and 11840 both k8s.io/kubelet/ and /utils/ are removed from direct import;  next up will be client-go and apimachinery - remote streaming commands and our wrappers to http spdy  

But this one should be good to merge async..
author_association: MEMBER