mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
502 lines
13 KiB
Go
502 lines
13 KiB
Go
// Copyright 2026 The containerd Authors.
|
|
// Copyright 2026 The gVisor 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
|
|
//
|
|
// https://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 shimutils provides utility functions for testing the shim. It is intended to be used in
|
|
// conjunction with the containerd testing framework.
|
|
package shimutils
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
task "github.com/containerd/containerd/api/runtime/task/v2"
|
|
ttrpc "github.com/containerd/ttrpc"
|
|
typeurl "github.com/containerd/typeurl/v2"
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"gvisor.dev/gvisor/pkg/shim/v1/runtimeoptions"
|
|
"gvisor.dev/gvisor/pkg/test/testutil"
|
|
"gvisor.dev/gvisor/runsc/specutils"
|
|
|
|
"context"
|
|
|
|
events "github.com/containerd/containerd/api/services/ttrpc/events/v1"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
)
|
|
|
|
const (
|
|
// SocketAddress is the name of the socket file used to communicate with the shim.
|
|
SocketAddress = "containerd.sock"
|
|
)
|
|
|
|
var (
|
|
shimContextMutex sync.Mutex
|
|
)
|
|
|
|
// GetRunscPath returns the path to the runsc binary.
|
|
func GetRunscPath() (string, error) {
|
|
return testutil.FindRunsc()
|
|
}
|
|
|
|
// GetShimPath returns the path to the containerd-shim-runsc-v1 binary.
|
|
func GetShimPath() (string, error) {
|
|
return testutil.FindFile("release/containerd-shim-runsc-v1")
|
|
}
|
|
|
|
// NewSandboxSpec returns a new sandbox spec.
|
|
func NewSandboxSpec() *specs.Spec {
|
|
spec := newSpec("sleep", "100000")
|
|
spec.Annotations[specutils.ContainerdContainerTypeAnnotation] = specutils.ContainerdContainerTypeSandbox
|
|
return spec
|
|
}
|
|
|
|
// NewContainerSpec returns a new container spec.
|
|
func NewContainerSpec(sandboxID string, args []string) *specs.Spec {
|
|
spec := newSpec(args...)
|
|
spec.Annotations[specutils.ContainerdContainerTypeAnnotation] = specutils.ContainerdContainerTypeContainer
|
|
spec.Annotations[specutils.ContainerdSandboxIDAnnotation] = sandboxID
|
|
return spec
|
|
}
|
|
|
|
func newSpec(args ...string) *specs.Spec {
|
|
spec := testutil.NewSpecWithArgs(args...)
|
|
if spec.Linux == nil {
|
|
spec.Linux = &specs.Linux{}
|
|
}
|
|
spec.Linux.Namespaces = []specs.LinuxNamespace{
|
|
{
|
|
Type: specs.PIDNamespace,
|
|
},
|
|
{
|
|
Type: specs.MountNamespace,
|
|
},
|
|
{
|
|
Type: specs.UserNamespace,
|
|
},
|
|
}
|
|
spec.Linux.UIDMappings = []specs.LinuxIDMapping{
|
|
{
|
|
ContainerID: 0,
|
|
HostID: uint32(os.Getuid()),
|
|
Size: 1,
|
|
},
|
|
}
|
|
spec.Linux.GIDMappings = []specs.LinuxIDMapping{
|
|
{
|
|
ContainerID: 0,
|
|
HostID: uint32(os.Getgid()),
|
|
Size: 1,
|
|
},
|
|
}
|
|
spec.Process.User = specs.User{
|
|
UID: uint32(os.Getuid()),
|
|
GID: uint32(os.Getgid()),
|
|
}
|
|
return spec
|
|
}
|
|
|
|
// Container is a test container.
|
|
type Container struct {
|
|
id string
|
|
bundle string
|
|
spec *specs.Spec
|
|
}
|
|
|
|
// NewContainer creates a new container with the given spec and returns it. It also creates the
|
|
// necessary directory structure and files for the container to run.
|
|
func NewContainer(spec *specs.Spec, containerd *MockContainerd) (*Container, error) {
|
|
c := &Container{
|
|
id: testutil.RandomContainerID(),
|
|
spec: spec,
|
|
}
|
|
|
|
if err := c.createDirectoryStructure(containerd.wd); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.writeSpec(spec); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
// ID returns the ID of the container.
|
|
func (c *Container) ID() string {
|
|
return c.id
|
|
}
|
|
|
|
// Bundle returns the path to the bundle directory of the container.
|
|
func (c *Container) Bundle() string {
|
|
return c.bundle
|
|
}
|
|
|
|
func (c *Container) createDirectoryStructure(wd string) error {
|
|
bundle := filepath.Join(wd, fmt.Sprintf("bundle-%s", c.id))
|
|
if err := os.MkdirAll(bundle, 0o777); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.bundle = bundle
|
|
return nil
|
|
}
|
|
|
|
func (c *Container) writeSpec(spec *specs.Spec) error {
|
|
specPath := filepath.Join(c.bundle, "config.json")
|
|
b, err := json.Marshal(spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(specPath, b, 0755); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// dummyEventsServer is a mock implementation of the events service.
|
|
type dummyEventsServer struct {
|
|
t *testing.T
|
|
ch chan any
|
|
|
|
mu sync.Mutex
|
|
events []any
|
|
}
|
|
|
|
// Forward forwards the event to the event channel.
|
|
func (s *dummyEventsServer) Forward(ctx context.Context, req *events.ForwardRequest) (*emptypb.Empty, error) {
|
|
if req.Envelope != nil && req.Envelope.Event != nil {
|
|
evt, err := typeurl.UnmarshalAny(req.Envelope.Event)
|
|
if err != nil {
|
|
s.t.Errorf("dummyEventsServer: failed to unmarshal event: %v", err)
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.events = append(s.events, evt)
|
|
s.mu.Unlock()
|
|
|
|
select {
|
|
case s.ch <- evt:
|
|
default:
|
|
s.t.Logf("dummyEventsServer: event channel full, dropping from channel (still in history): %v", evt)
|
|
}
|
|
} else {
|
|
s.t.Errorf("dummyEventsServer: received nil envelope or event: %+v", req)
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (s *dummyEventsServer) getEvents() []any {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
copied := make([]any, len(s.events))
|
|
copy(copied, s.events)
|
|
return copied
|
|
}
|
|
|
|
func (s *dummyEventsServer) clearEvents() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.events = nil
|
|
}
|
|
|
|
// MockContainerd plays the role of containerd to test the shim. It creates
|
|
// the necessary directory structure and files for the shim to run and sends requests to the shim.
|
|
type MockContainerd struct {
|
|
wd string
|
|
shim *exec.Cmd
|
|
eventServer *ttrpc.Server
|
|
eventListener net.Listener
|
|
EventChan chan any
|
|
eventWd string
|
|
eventSocket string
|
|
eventsImpl *dummyEventsServer
|
|
}
|
|
|
|
// We need a directory structure to save and other artifacts. The directory
|
|
// structure is as follows:
|
|
//
|
|
// working_dir/
|
|
// config.toml // runsc config
|
|
// bundle-%ID%/
|
|
// config.json
|
|
// bundle-%ID2%/
|
|
// config.json
|
|
// shim.%ID%.log
|
|
// shim.%ID2%.log
|
|
//
|
|
// We need this directory structure to be created before we can start the
|
|
// shim.
|
|
|
|
// NewMockContainerdWithSuffix creates a new MockContainerd with a custom suffix for the working directory to avoid conflicts.
|
|
func NewMockContainerdWithSuffix(t *testing.T, suffix string, shimArgs, runscArgs map[string]any) *MockContainerd {
|
|
s := &MockContainerd{}
|
|
// Create working directory.
|
|
name := t.Name()
|
|
if suffix != "" {
|
|
name = name + "-" + suffix
|
|
}
|
|
wd, err := newWorkingDir(name)
|
|
if err != nil {
|
|
t.Fatalf("failed to create working directory: %v", err)
|
|
}
|
|
s.wd = wd
|
|
|
|
if err := os.MkdirAll(s.root(), 0o777); err != nil {
|
|
t.Fatalf("failed to create containerd root directory: %v", err)
|
|
}
|
|
|
|
// Set the runsc config.
|
|
if err := newRunscConfig(s, shimArgs, runscArgs); err != nil {
|
|
t.Fatalf("failed to create runsc config: %v", err)
|
|
}
|
|
|
|
// Start TTRPC event server on a short socket path under /tmp to prevent
|
|
// "bind: invalid argument" unix socket length limit errors.
|
|
eventWd, err := os.MkdirTemp("/tmp", "containerd-events-")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir for events: %v", err)
|
|
}
|
|
s.eventWd = eventWd
|
|
s.eventSocket = filepath.Join(eventWd, "events.sock")
|
|
|
|
if s.eventListener, err = net.Listen("unix", s.eventSocket); err != nil {
|
|
os.RemoveAll(eventWd)
|
|
t.Fatalf("failed to listen on events socket: %v", err)
|
|
}
|
|
|
|
server, err := ttrpc.NewServer()
|
|
if err != nil {
|
|
s.eventListener.Close()
|
|
os.RemoveAll(eventWd)
|
|
t.Fatalf("failed to create ttrpc server: %v", err)
|
|
}
|
|
s.eventServer = server
|
|
|
|
s.EventChan = make(chan any, 128)
|
|
s.eventsImpl = &dummyEventsServer{t: t, ch: s.EventChan}
|
|
events.RegisterEventsService(server, s.eventsImpl)
|
|
|
|
go func() {
|
|
_ = server.Serve(t.Context(), s.eventListener)
|
|
}()
|
|
|
|
t.Cleanup(func() {
|
|
server.Close()
|
|
s.eventListener.Close()
|
|
os.RemoveAll(eventWd)
|
|
})
|
|
|
|
return s
|
|
}
|
|
|
|
// NewMockContainerd creates a new MockContainerd.
|
|
func NewMockContainerd(t *testing.T, shimArgs, runscArgs map[string]any) *MockContainerd {
|
|
return NewMockContainerdWithSuffix(t, "", shimArgs, runscArgs)
|
|
}
|
|
|
|
// WorkingDir returns the working directory of the mock containerd.
|
|
func (m *MockContainerd) WorkingDir() string {
|
|
return m.wd
|
|
}
|
|
|
|
// Events returns all events received by the mock containerd.
|
|
func (m *MockContainerd) Events() []any {
|
|
return m.eventsImpl.getEvents()
|
|
}
|
|
|
|
// ClearEvents clears the received events history.
|
|
func (m *MockContainerd) ClearEvents() {
|
|
m.eventsImpl.clearEvents()
|
|
}
|
|
|
|
// root returns the root directory for containers.
|
|
func (m *MockContainerd) root() string {
|
|
return filepath.Join(m.wd, "containers")
|
|
}
|
|
|
|
// StartShim starts the shim binary and waits for it to start.
|
|
func (m *MockContainerd) StartShim(t *testing.T, c *Container) error {
|
|
f, err := os.Create(filepath.Join(m.wd, "log"))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create shim log file: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
shimBinary, err := GetShimPath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find shim binary: %v", err)
|
|
}
|
|
args := []string{
|
|
"-namespace", "default",
|
|
"-debug",
|
|
"-id", c.id,
|
|
"-socket", SocketAddress,
|
|
"-bundle", c.bundle,
|
|
}
|
|
m.shim = exec.CommandContext(t.Context(), shimBinary, args...)
|
|
m.shim.Dir = m.wd
|
|
m.shim.Stdout = f
|
|
m.shim.Stderr = f
|
|
m.shim.Env = append(os.Environ(), "TTRPC_ADDRESS="+m.eventSocket)
|
|
t.Logf("starting shim binary: %s with args: %v", shimBinary, args)
|
|
if err := m.shim.Start(); err != nil {
|
|
return fmt.Errorf("failed to start shim: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if m.shim.Process != nil {
|
|
m.shim.Process.Kill()
|
|
}
|
|
})
|
|
waitErr := make(chan error, 1)
|
|
go func() {
|
|
waitErr <- m.shim.Wait()
|
|
}()
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
select {
|
|
case err := <-waitErr:
|
|
return fmt.Errorf("shim exited prematurely: %v", err)
|
|
default:
|
|
_, statErr := os.Stat(filepath.Join(m.wd, SocketAddress))
|
|
if statErr == nil {
|
|
return nil
|
|
}
|
|
if !os.IsNotExist(statErr) {
|
|
return fmt.Errorf("os.Stat failed: %v", statErr)
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}
|
|
return fmt.Errorf("shim did not start in time")
|
|
}
|
|
|
|
// GetClient returns a client to the shim socket which can be used to send requests to the shim.
|
|
func (m *MockContainerd) GetClient(t *testing.T) task.TaskService {
|
|
var client task.TaskService
|
|
m.withShimContext(t, func(t *testing.T) {
|
|
conn, err := net.DialTimeout("unix", SocketAddress, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("failed to dial shim socket: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
conn.Close()
|
|
})
|
|
client = task.NewTaskClient(ttrpc.NewClient(conn))
|
|
})
|
|
return client
|
|
}
|
|
|
|
func (m *MockContainerd) withShimContext(t *testing.T, f func(t *testing.T)) {
|
|
shimContextMutex.Lock()
|
|
defer shimContextMutex.Unlock()
|
|
|
|
pwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get current working directory: %v", err)
|
|
}
|
|
if err := os.Chdir(m.wd); err != nil {
|
|
t.Fatalf("failed to change working directory: %v", err)
|
|
}
|
|
f(t)
|
|
if err := os.Chdir(pwd); err != nil {
|
|
t.Fatalf("failed to back to the original working directory: %v", err)
|
|
}
|
|
}
|
|
|
|
func newRunscConfig(m *MockContainerd, shimArgs, runscArgs map[string]any) error {
|
|
runscDebugPath := filepath.Join(m.WorkingDir(), "%ID%") + "/"
|
|
runscPath, err := GetRunscPath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find runsc binary: %v", err)
|
|
}
|
|
|
|
runscConfig := map[string]any{
|
|
"debug": "true",
|
|
"debug-log": runscDebugPath,
|
|
"TESTONLY-unsafe-nonroot": "true",
|
|
"platform": "systrap",
|
|
"network": "none",
|
|
}
|
|
for k, v := range runscArgs {
|
|
runscConfig[k] = v
|
|
}
|
|
|
|
config := map[string]any{
|
|
"binary_name": runscPath,
|
|
"root": m.root(),
|
|
"runsc_config": runscConfig,
|
|
}
|
|
|
|
for k, v := range shimArgs {
|
|
config[k] = v
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := toml.NewEncoder(&buf).Encode(config); err != nil {
|
|
return fmt.Errorf("toml.Marshal failed: %v", err)
|
|
}
|
|
if err := os.WriteFile(RunscConfigPath(m.wd), buf.Bytes(), 0644); err != nil {
|
|
return fmt.Errorf("os.WriteFile failed: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetRuntimeOptions returns the runtime options for the shim.
|
|
func (m *MockContainerd) GetRuntimeOptions() (*anypb.Any, error) {
|
|
opts := &runtimeoptions.Options{
|
|
TypeUrl: "io.containerd.runsc.v1.options",
|
|
ConfigPath: RunscConfigPath(m.wd),
|
|
}
|
|
any, err := typeurl.MarshalAny(opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal runtime options: %v", err)
|
|
}
|
|
|
|
ret, err := typeurl.MarshalAnyToProto(any)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal runtime options to proto: %v", err)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// RunscConfigPath returns the path to the runsc config file.
|
|
func RunscConfigPath(wd string) string {
|
|
return filepath.Join(wd, "config.toml")
|
|
}
|
|
|
|
func newWorkingDir(name string) (string, error) {
|
|
prefix := os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR")
|
|
if prefix == "" {
|
|
prefix = os.Getenv("TEST_TMPDIR")
|
|
}
|
|
|
|
wd := filepath.Join(prefix, name)
|
|
if err := os.MkdirAll(wd, 0o777); err != nil {
|
|
return "", err
|
|
}
|
|
return wd, nil
|
|
}
|