mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 13:27:16 +00:00
k8s.io/utils/exec.CodeExitError was the only remaining direct use of k8s.io/utils. Replace it with a local error type implementing the same interface. The streaming server uses this interface to distinguish commands terminating with a non-zero exit status from errors executing the command, and to propagate the exit status to the client. Add a FIXME to define this error contract in k8s.io/cri-streaming, where it is consumed, instead of requiring runtime implementations to implicitly match k8s.io/utils/exec.ExitError. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
137 lines
4.4 KiB
Go
137 lines
4.4 KiB
Go
/*
|
|
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.
|
|
*/
|
|
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
|
|
ctrdutil "github.com/containerd/containerd/v2/internal/cri/util"
|
|
streaming "k8s.io/cri-streaming/pkg/streaming"
|
|
remotecommand "k8s.io/cri-streaming/pkg/streaming/remotecommand"
|
|
"k8s.io/streaming/pkg/runtime"
|
|
)
|
|
|
|
// exitError is an interface that presents an API similar to os.ProcessState, which is
|
|
// what exitError from os/exec is. This is designed to make testing a bit easier and
|
|
// probably loses some of the cross-platform properties of the underlying library.
|
|
//
|
|
// exitError matches [k8s.io/utils/exec.ExitError], which is used by
|
|
// [remotecommand.ServeExec] to distinguish a command exiting with a non-zero
|
|
// status from an error executing the command.
|
|
//
|
|
// FIXME(thaJeztah): define this contract in k8s.io/cri-streaming instead.
|
|
// ServeExec only requires Exited and ExitStatus, so the upstream interface
|
|
// could be reduced to those methods in addition to error.
|
|
//
|
|
// - https://github.com/kubernetes/cri-streaming/blob/v0.36.3/pkg/streaming/remotecommand/exec.go#L48-L64
|
|
// - https://github.com/kubernetes/utils/blob/28399d86e0b55e612b83ad94dc6bac8e5de2a5ac/exec/exec.go#L83-L91
|
|
type exitError interface {
|
|
error
|
|
fmt.Stringer
|
|
Exited() bool
|
|
ExitStatus() int
|
|
}
|
|
|
|
// codeExitError is an implementation of exitError consisting of an error object
|
|
// and an exit code (the upper bits of os.exec.ExitStatus).
|
|
type codeExitError struct {
|
|
error
|
|
code int
|
|
}
|
|
|
|
func (e codeExitError) String() string { return e.Error() }
|
|
func (e codeExitError) Exited() bool { return true } // Exited is to check if the process has finished
|
|
func (e codeExitError) ExitStatus() int { return e.code } // ExitStatus is for checking the error code
|
|
|
|
// FIXME(thaJeztah): assert against remotecommand.ExitError once k8s.io/cri-streaming defines the exit-error contract.
|
|
var _ exitError = (*codeExitError)(nil)
|
|
|
|
type streamRuntime struct {
|
|
c *criService
|
|
}
|
|
|
|
func newStreamRuntime(c *criService) streaming.Runtime {
|
|
return &streamRuntime{c: c}
|
|
}
|
|
|
|
// Exec executes a command inside the container. An exitError is returned if
|
|
// the command exits with a non-zero status.
|
|
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 {
|
|
exitCode, err := s.c.execInContainer(ctrdutil.WithNamespace(ctx), containerID, execOptions{
|
|
cmd: cmd,
|
|
stdin: stdin,
|
|
stdout: stdout,
|
|
stderr: stderr,
|
|
tty: tty,
|
|
resize: resize,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to exec in container: %w", err)
|
|
}
|
|
if *exitCode == 0 {
|
|
return nil
|
|
}
|
|
return &codeExitError{
|
|
error: fmt.Errorf("error executing command %v, exit code %d", cmd, *exitCode),
|
|
code: int(*exitCode),
|
|
}
|
|
}
|
|
|
|
func (s *streamRuntime) Attach(ctx context.Context, containerID string, in io.Reader, out, err io.WriteCloser, tty bool,
|
|
resize <-chan remotecommand.TerminalSize) error {
|
|
return s.c.attachContainer(ctrdutil.WithNamespace(ctx), containerID, in, out, err, tty, resize)
|
|
}
|
|
|
|
func (s *streamRuntime) PortForward(ctx context.Context, podSandboxID string, port int32, stream io.ReadWriteCloser) error {
|
|
if port <= 0 || port > math.MaxUint16 {
|
|
return fmt.Errorf("invalid port %d", port)
|
|
}
|
|
ctx = ctrdutil.WithNamespace(ctx)
|
|
return s.c.portForward(ctx, podSandboxID, port, stream)
|
|
}
|
|
|
|
// handleResizing spawns a goroutine that processes the resize channel, calling resizeFunc for each
|
|
// remotecommand.TerminalSize received from the channel.
|
|
func handleResizing(ctx context.Context, resize <-chan remotecommand.TerminalSize, resizeFunc func(size remotecommand.TerminalSize)) {
|
|
if resize == nil {
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
defer runtime.HandleCrash()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case size, ok := <-resize:
|
|
if !ok {
|
|
return
|
|
}
|
|
if size.Height < 1 || size.Width < 1 {
|
|
continue
|
|
}
|
|
resizeFunc(size)
|
|
}
|
|
}
|
|
}()
|
|
}
|