github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/container-utils/testutils/containerd.go (about)

     1  // Copyright 2022 The Inspektor Gadget authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package testutils
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  	"syscall"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/containerd/containerd"
    26  	"github.com/containerd/containerd/cio"
    27  	"github.com/containerd/containerd/namespaces"
    28  	"github.com/containerd/containerd/oci"
    29  	"github.com/containerd/containerd/pkg/cri/constants"
    30  	"github.com/containerd/containerd/snapshots"
    31  	"github.com/opencontainers/runtime-spec/specs-go"
    32  )
    33  
    34  const (
    35  	taskKillTimeout = 3 * time.Second
    36  )
    37  
    38  func NewContainerdContainer(name, cmd string, options ...Option) Container {
    39  	c := &ContainerdContainer{
    40  		containerSpec: containerSpec{
    41  			name:    name,
    42  			cmd:     cmd,
    43  			options: defaultContainerOptions(),
    44  		},
    45  	}
    46  	for _, o := range options {
    47  		o(c.options)
    48  	}
    49  	return c
    50  }
    51  
    52  type ContainerdContainer struct {
    53  	containerSpec
    54  
    55  	client     *containerd.Client
    56  	nsCtx      context.Context
    57  	exitStatus <-chan containerd.ExitStatus
    58  }
    59  
    60  func (c *ContainerdContainer) initClientAndCtx() error {
    61  	var err error
    62  	c.client, err = containerd.New("/run/containerd/containerd.sock",
    63  		containerd.WithTimeout(3*time.Second),
    64  	)
    65  	if err != nil {
    66  		return fmt.Errorf("creating a client: %w", err)
    67  	}
    68  
    69  	namespace := constants.K8sContainerdNamespace
    70  	if c.options.namespace != "" {
    71  		namespace = c.options.namespace
    72  	}
    73  	c.nsCtx = namespaces.WithNamespace(c.options.ctx, namespace)
    74  	return nil
    75  }
    76  
    77  func (c *ContainerdContainer) Run(t *testing.T) {
    78  	if err := c.initClientAndCtx(); err != nil {
    79  		t.Fatalf("Failed to initialize client: %s", err)
    80  	}
    81  
    82  	// Download and unpack the image
    83  	fullImage := getFullImage(c.options)
    84  	image, err := c.client.Pull(c.nsCtx, fullImage)
    85  	if err != nil {
    86  		t.Fatalf("Failed to pull the image %q: %s", fullImage, err)
    87  	}
    88  
    89  	unpacked, err := image.IsUnpacked(c.nsCtx, "")
    90  	if err != nil {
    91  		t.Fatalf("image.IsUnpacked: %v", err)
    92  	}
    93  	if !unpacked {
    94  		if err := image.Unpack(c.nsCtx, ""); err != nil {
    95  			t.Fatalf("image.Unpack: %v", err)
    96  		}
    97  	}
    98  
    99  	// Create the container
   100  	var specOpts []oci.SpecOpts
   101  	specOpts = append(specOpts, oci.WithDefaultSpec())
   102  	specOpts = append(specOpts, oci.WithDefaultUnixDevices)
   103  	specOpts = append(specOpts, oci.WithImageConfig(image))
   104  	if len(c.cmd) != 0 {
   105  		specOpts = append(specOpts, oci.WithProcessArgs("/bin/sh", "-c", c.cmd))
   106  	}
   107  	if c.options.seccompProfile != "" {
   108  		t.Fatalf("testutils/containerd: seccomp profiles are not supported yet")
   109  	}
   110  	if c.options.portBindings != nil {
   111  		t.Fatalf("testutils/containerd: Port bindings are not supported yet")
   112  	}
   113  
   114  	var spec specs.Spec
   115  	container, err := c.client.NewContainer(c.nsCtx, c.name,
   116  		containerd.WithImage(image),
   117  		containerd.WithImageConfigLabels(image),
   118  		containerd.WithAdditionalContainerLabels(image.Labels()),
   119  		containerd.WithSnapshotter(""),
   120  		containerd.WithNewSnapshot(c.name, image, snapshots.WithLabels(map[string]string{})),
   121  		containerd.WithImageStopSignal(image, "SIGTERM"),
   122  		containerd.WithSpec(&spec, specOpts...),
   123  	)
   124  	if err != nil {
   125  		t.Fatalf("Failed to create container %q: %s", c.name, err)
   126  	}
   127  	c.id = container.ID()
   128  
   129  	containerIO := cio.NullIO
   130  	output := &strings.Builder{}
   131  	if c.options.logs {
   132  		containerIO = cio.NewCreator(cio.WithStreams(nil, output, output))
   133  	}
   134  	// Now create and start the task
   135  	task, err := container.NewTask(c.nsCtx, containerIO)
   136  	if err != nil {
   137  		container.Delete(c.nsCtx, containerd.WithSnapshotCleanup)
   138  		t.Fatalf("Failed to create task %q: %s", c.name, err)
   139  	}
   140  
   141  	err = task.Start(c.nsCtx)
   142  	if err != nil {
   143  		container.Delete(c.nsCtx, containerd.WithSnapshotCleanup)
   144  		t.Fatalf("Failed to start task %q: %s", c.name, err)
   145  	}
   146  
   147  	c.exitStatus, err = task.Wait(c.nsCtx)
   148  	if err != nil {
   149  		t.Fatalf("Failed to wait on task %q: %s", c.name, err)
   150  	}
   151  	c.pid = int(task.Pid())
   152  
   153  	if c.options.wait {
   154  		s := <-c.exitStatus
   155  		if s.ExitCode() != 0 {
   156  			t.Logf("Exitcode for task %q: %d", c.name, s.ExitCode())
   157  		}
   158  	}
   159  
   160  	if c.options.logs {
   161  		t.Logf("Container %q output:\n%s", c.name, output.String())
   162  	}
   163  
   164  	if c.options.removal {
   165  		err := c.deleteAndClose(t, task, container)
   166  		if err != nil {
   167  			t.Fatalf("Failed to delete container %q: %s", c.name, err)
   168  		}
   169  	}
   170  }
   171  
   172  func (c *ContainerdContainer) Start(t *testing.T) {
   173  	if c.started {
   174  		t.Logf("Warn(%s): trying to start already running container\n", c.name)
   175  		return
   176  	}
   177  	c.start(t)
   178  	c.started = true
   179  }
   180  
   181  func (c *ContainerdContainer) start(t *testing.T) {
   182  	for _, o := range []Option{WithoutWait(), withoutRemoval()} {
   183  		o(c.options)
   184  	}
   185  	c.Run(t)
   186  }
   187  
   188  func (c *ContainerdContainer) Stop(t *testing.T) {
   189  	if !c.started && !c.options.forceDelete {
   190  		t.Logf("Warn(%s): trying to stop already stopped container\n", c.name)
   191  		return
   192  	}
   193  	if c.client == nil {
   194  		if c.options.forceDelete {
   195  			t.Logf("Warn(%s): trying to stop container with nil client. Forcing deletion\n", c.name)
   196  			if err := c.initClientAndCtx(); err != nil {
   197  				t.Fatalf("Failed to initialize client: %s", err)
   198  			}
   199  		} else {
   200  			t.Fatalf("Client is not initialized")
   201  		}
   202  	}
   203  
   204  	c.stop(t)
   205  	c.started = false
   206  }
   207  
   208  // deleteAndClose kill the task, delete the container and close the client
   209  func (c *ContainerdContainer) deleteAndClose(t *testing.T, task containerd.Task, container containerd.Container) error {
   210  	task.Kill(c.nsCtx, syscall.SIGKILL)
   211  
   212  	// We need to wait until the task is killed before trying to delete it. But
   213  	// don't wait forever as the task might be already stopped.
   214  	select {
   215  	case <-c.exitStatus:
   216  	case <-time.After(taskKillTimeout):
   217  		t.Logf("Timeout %v waiting for container's task %q to be killed. Go ahead with deletion",
   218  			taskKillTimeout, c.name)
   219  	}
   220  
   221  	_, err := task.Delete(c.nsCtx)
   222  	if err != nil {
   223  		return fmt.Errorf("deleting task %q: %w", c.name, err)
   224  	}
   225  
   226  	err = container.Delete(c.nsCtx, containerd.WithSnapshotCleanup)
   227  	if err != nil {
   228  		return fmt.Errorf("deleting container %q: %w", c.name, err)
   229  	}
   230  
   231  	err = c.client.Close()
   232  	if err != nil {
   233  		return fmt.Errorf("closing client: %w", err)
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  func (c *ContainerdContainer) stop(t *testing.T) {
   240  	container, err := c.client.LoadContainer(c.nsCtx, c.name)
   241  	if err != nil {
   242  		t.Fatalf("Failed to get container %q: %s", c.name, err)
   243  	}
   244  
   245  	task, err := container.Task(c.nsCtx, nil)
   246  	if err != nil {
   247  		t.Fatalf("Failed to get task %q: %s", c.name, err)
   248  	}
   249  
   250  	err = c.deleteAndClose(t, task, container)
   251  	if err != nil {
   252  		t.Fatalf("Failed to delete container %q: %s", c.name, err)
   253  	}
   254  }
   255  
   256  func getFullImage(options *containerOptions) string {
   257  	if strings.Contains(options.image, ":") {
   258  		return options.image
   259  	}
   260  	return options.image + ":" + options.imageTag
   261  }
   262  
   263  func RunContainerdFailedContainer(ctx context.Context, t *testing.T) {
   264  	NewContainerdContainer("test-ig-failed-container", "/none", WithoutLogs(), WithoutWait(), WithContext(ctx)).Run(t)
   265  }