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 }