github.com/containerd/nerdctl@v1.7.7/pkg/containerutil/containerutil.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package containerutil 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "path" 26 "path/filepath" 27 "strconv" 28 "strings" 29 "time" 30 31 "github.com/containerd/console" 32 "github.com/containerd/containerd" 33 "github.com/containerd/containerd/cio" 34 "github.com/containerd/containerd/containers" 35 "github.com/containerd/containerd/oci" 36 "github.com/containerd/containerd/runtime/restart" 37 "github.com/containerd/log" 38 "github.com/containerd/nerdctl/pkg/consoleutil" 39 "github.com/containerd/nerdctl/pkg/errutil" 40 "github.com/containerd/nerdctl/pkg/formatter" 41 "github.com/containerd/nerdctl/pkg/labels" 42 "github.com/containerd/nerdctl/pkg/nsutil" 43 "github.com/containerd/nerdctl/pkg/portutil" 44 "github.com/containerd/nerdctl/pkg/rootlessutil" 45 "github.com/containerd/nerdctl/pkg/signalutil" 46 "github.com/containerd/nerdctl/pkg/taskutil" 47 "github.com/moby/sys/signal" 48 "github.com/opencontainers/runtime-spec/specs-go" 49 ) 50 51 // PrintHostPort writes to `writer` the public (HostIP:HostPort) of a given `containerPort/protocol` in a container. 52 // if `containerPort < 0`, it writes all public ports of the container. 53 func PrintHostPort(ctx context.Context, writer io.Writer, container containerd.Container, containerPort int, proto string) error { 54 l, err := container.Labels(ctx) 55 if err != nil { 56 return err 57 } 58 ports, err := portutil.ParsePortsLabel(l) 59 if err != nil { 60 return err 61 } 62 63 if containerPort < 0 { 64 for _, p := range ports { 65 fmt.Fprintf(writer, "%d/%s -> %s:%d\n", p.ContainerPort, p.Protocol, p.HostIP, p.HostPort) 66 } 67 return nil 68 } 69 70 for _, p := range ports { 71 if p.ContainerPort == int32(containerPort) && strings.ToLower(p.Protocol) == proto { 72 fmt.Fprintf(writer, "%s:%d\n", p.HostIP, p.HostPort) 73 return nil 74 } 75 } 76 return fmt.Errorf("no public port %d/%s published for %q", containerPort, proto, container.ID()) 77 } 78 79 // ContainerStatus returns the container's status from its task. 80 func ContainerStatus(ctx context.Context, c containerd.Container) (containerd.Status, error) { 81 // Just in case, there is something wrong in server. 82 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 83 defer cancel() 84 85 task, err := c.Task(ctx, nil) 86 if err != nil { 87 return containerd.Status{}, err 88 } 89 90 return task.Status(ctx) 91 } 92 93 // ContainerNetNSPath returns the netns path of a container. 94 func ContainerNetNSPath(ctx context.Context, c containerd.Container) (string, error) { 95 task, err := c.Task(ctx, nil) 96 if err != nil { 97 return "", err 98 } 99 status, err := task.Status(ctx) 100 if err != nil { 101 return "", err 102 } 103 if status.Status != containerd.Running { 104 return "", fmt.Errorf("invalid target container: %s, should be running", c.ID()) 105 } 106 return fmt.Sprintf("/proc/%d/ns/net", task.Pid()), nil 107 } 108 109 // UpdateStatusLabel updates the "containerd.io/restart.status" 110 // label of the container according to the value of restart desired status. 111 func UpdateStatusLabel(ctx context.Context, container containerd.Container, status containerd.ProcessStatus) error { 112 opt := containerd.WithAdditionalContainerLabels(map[string]string{ 113 restart.StatusLabel: string(status), 114 }) 115 return container.Update(ctx, containerd.UpdateContainerOpts(opt)) 116 } 117 118 // UpdateExplicitlyStoppedLabel updates the "containerd.io/restart.explicitly-stopped" 119 // label of the container according to the value of explicitlyStopped. 120 func UpdateExplicitlyStoppedLabel(ctx context.Context, container containerd.Container, explicitlyStopped bool) error { 121 opt := containerd.WithAdditionalContainerLabels(map[string]string{ 122 restart.ExplicitlyStoppedLabel: strconv.FormatBool(explicitlyStopped), 123 }) 124 return container.Update(ctx, containerd.UpdateContainerOpts(opt)) 125 } 126 127 // UpdateErrorLabel updates the "nerdctl/error" 128 // label of the container according to the container error. 129 func UpdateErrorLabel(ctx context.Context, container containerd.Container, err error) error { 130 opt := containerd.WithAdditionalContainerLabels(map[string]string{ 131 labels.Error: err.Error(), 132 }) 133 return container.Update(ctx, containerd.UpdateContainerOpts(opt)) 134 } 135 136 // WithBindMountHostProcfs replaces procfs mount with rbind. 137 // Required for --pid=host on rootless. 138 // 139 // https://github.com/moby/moby/pull/41893/files 140 // https://github.com/containers/podman/blob/v3.0.0-rc1/pkg/specgen/generate/oci.go#L248-L257 141 func WithBindMountHostProcfs(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { 142 for i, m := range s.Mounts { 143 if path.Clean(m.Destination) == "/proc" { 144 newM := specs.Mount{ 145 Destination: "/proc", 146 Type: "bind", 147 Source: "/proc", 148 Options: []string{"rbind", "nosuid", "noexec", "nodev"}, 149 } 150 s.Mounts[i] = newM 151 } 152 } 153 154 // Remove ReadonlyPaths for /proc/* 155 newROP := s.Linux.ReadonlyPaths[:0] 156 for _, x := range s.Linux.ReadonlyPaths { 157 x = path.Clean(x) 158 if !strings.HasPrefix(x, "/proc/") { 159 newROP = append(newROP, x) 160 } 161 } 162 s.Linux.ReadonlyPaths = newROP 163 return nil 164 } 165 166 // GenerateSharingPIDOpts returns the oci.SpecOpts that shares the host linux namespace from `targetCon` 167 // If `targetCon` doesn't have a `PIDNamespace`, a new one is generated from its `Pid`. 168 func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) ([]oci.SpecOpts, error) { 169 opts := make([]oci.SpecOpts, 0) 170 171 task, err := targetCon.Task(ctx, nil) 172 if err != nil { 173 return nil, err 174 } 175 status, err := task.Status(ctx) 176 if err != nil { 177 return nil, err 178 } 179 180 if status.Status != containerd.Running { 181 return nil, fmt.Errorf("shared container is not running") 182 } 183 184 spec, err := targetCon.Spec(ctx) 185 if err != nil { 186 return nil, err 187 } 188 189 isHost := true 190 for _, n := range spec.Linux.Namespaces { 191 if n.Type == specs.PIDNamespace { 192 isHost = false 193 } 194 } 195 if isHost { 196 opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) 197 if rootlessutil.IsRootless() { 198 opts = append(opts, WithBindMountHostProcfs) 199 } 200 } else { 201 ns := specs.LinuxNamespace{ 202 Type: specs.PIDNamespace, 203 Path: fmt.Sprintf("/proc/%d/ns/pid", task.Pid()), 204 } 205 opts = append(opts, oci.WithLinuxNamespace(ns)) 206 } 207 return opts, nil 208 } 209 210 // Start starts `container` with `attach` flag. If `attach` is true, it will attach to the container's stdio. 211 func Start(ctx context.Context, container containerd.Container, flagA bool, client *containerd.Client, detachKeys string) (err error) { 212 // defer the storage of start error in the dedicated label 213 defer func() { 214 if err != nil { 215 UpdateErrorLabel(ctx, container, err) 216 } 217 }() 218 lab, err := container.Labels(ctx) 219 if err != nil { 220 return err 221 } 222 223 if err := ReconfigNetContainer(ctx, container, client, lab); err != nil { 224 return err 225 } 226 227 if err := ReconfigPIDContainer(ctx, container, client, lab); err != nil { 228 return err 229 } 230 231 process, err := container.Spec(ctx) 232 if err != nil { 233 return err 234 } 235 flagT := process.Process.Terminal 236 var con console.Console 237 if flagA && flagT { 238 con = console.Current() 239 defer con.Reset() 240 if err := con.SetRaw(); err != nil { 241 return err 242 } 243 } 244 245 logURI := lab[labels.LogURI] 246 namespace := lab[labels.Namespace] 247 cStatus := formatter.ContainerStatus(ctx, container) 248 if cStatus == "Up" { 249 log.G(ctx).Warnf("container %s is already running", container.ID()) 250 return nil 251 } 252 253 _, restartPolicyExist := lab[restart.PolicyLabel] 254 if restartPolicyExist { 255 if err := UpdateStatusLabel(ctx, container, containerd.Running); err != nil { 256 return err 257 } 258 } 259 260 if err := UpdateExplicitlyStoppedLabel(ctx, container, false); err != nil { 261 return err 262 } 263 if oldTask, err := container.Task(ctx, nil); err == nil { 264 if _, err := oldTask.Delete(ctx); err != nil { 265 log.G(ctx).WithError(err).Debug("failed to delete old task") 266 } 267 } 268 detachC := make(chan struct{}) 269 attachStreamOpt := []string{} 270 if flagA { 271 // In start, flagA attaches only STDOUT/STDERR 272 // source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start 273 attachStreamOpt = []string{"STDOUT", "STDERR"} 274 } 275 task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, false, flagT, true, con, logURI, detachKeys, namespace, detachC) 276 if err != nil { 277 return err 278 } 279 280 if err := task.Start(ctx); err != nil { 281 return err 282 } 283 if !flagA { 284 return nil 285 } 286 if flagA && flagT { 287 if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil { 288 log.G(ctx).WithError(err).Error("console resize") 289 } 290 } 291 sigc := signalutil.ForwardAllSignals(ctx, task) 292 defer signalutil.StopCatch(sigc) 293 294 statusC, err := task.Wait(ctx) 295 if err != nil { 296 return err 297 } 298 select { 299 // io.Wait() would return when either 1) the user detaches from the container OR 2) the container is about to exit. 300 // 301 // If we replace the `select` block with io.Wait() and 302 // directly use task.Status() to check the status of the container after io.Wait() returns, 303 // it can still be running even though the container is about to exit (somehow especially for Windows). 304 // 305 // As a result, we need a separate detachC to distinguish from the 2 cases mentioned above. 306 case <-detachC: 307 io := task.IO() 308 if io == nil { 309 return errors.New("got a nil IO from the task") 310 } 311 io.Wait() 312 case status := <-statusC: 313 code, _, err := status.Result() 314 if err != nil { 315 return err 316 } 317 if code != 0 { 318 return errutil.NewExitCoderErr(int(code)) 319 } 320 } 321 return nil 322 } 323 324 // Stop stops `container` by sending SIGTERM. If the container is not stopped after `timeout`, it sends a SIGKILL. 325 func Stop(ctx context.Context, container containerd.Container, timeout *time.Duration) (err error) { 326 // defer the storage of stop error in the dedicated label 327 defer func() { 328 if err != nil { 329 UpdateErrorLabel(ctx, container, err) 330 } 331 }() 332 if err := UpdateExplicitlyStoppedLabel(ctx, container, true); err != nil { 333 return err 334 } 335 336 l, err := container.Labels(ctx) 337 if err != nil { 338 return err 339 } 340 341 if timeout == nil { 342 t, ok := l[labels.StopTimeout] 343 if !ok { 344 // Default is 10 seconds. 345 t = "10" 346 } 347 td, err := time.ParseDuration(t + "s") 348 if err != nil { 349 return err 350 } 351 timeout = &td 352 } 353 354 task, err := container.Task(ctx, cio.Load) 355 if err != nil { 356 return err 357 } 358 359 status, err := task.Status(ctx) 360 if err != nil { 361 return err 362 } 363 364 paused := false 365 366 switch status.Status { 367 case containerd.Created, containerd.Stopped: 368 return nil 369 case containerd.Paused, containerd.Pausing: 370 paused = true 371 default: 372 } 373 374 // NOTE: ctx is main context so that it's ok to use for task.Wait(). 375 exitCh, err := task.Wait(ctx) 376 if err != nil { 377 return err 378 } 379 380 if *timeout > 0 { 381 sig, err := signal.ParseSignal("SIGTERM") 382 if err != nil { 383 return err 384 } 385 if stopSignal, ok := l[containerd.StopSignalLabel]; ok { 386 sig, err = signal.ParseSignal(stopSignal) 387 if err != nil { 388 return err 389 } 390 } 391 392 if err := task.Kill(ctx, sig); err != nil { 393 return err 394 } 395 396 // signal will be sent once resume is finished 397 if paused { 398 if err := task.Resume(ctx); err != nil { 399 log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err) 400 } else { 401 // no need to do it again when send sigkill signal 402 paused = false 403 } 404 } 405 406 sigtermCtx, sigtermCtxCancel := context.WithTimeout(ctx, *timeout) 407 defer sigtermCtxCancel() 408 409 err = waitContainerStop(sigtermCtx, exitCh, container.ID()) 410 if err == nil { 411 return nil 412 } 413 414 if ctx.Err() != nil { 415 return ctx.Err() 416 } 417 } 418 419 sig, err := signal.ParseSignal("SIGKILL") 420 if err != nil { 421 return err 422 } 423 424 if err := task.Kill(ctx, sig); err != nil { 425 return err 426 } 427 428 // signal will be sent once resume is finished 429 if paused { 430 if err := task.Resume(ctx); err != nil { 431 log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err) 432 } 433 } 434 return waitContainerStop(ctx, exitCh, container.ID()) 435 } 436 437 func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, id string) error { 438 select { 439 case <-ctx.Done(): 440 if err := ctx.Err(); err != nil { 441 return fmt.Errorf("wait container %v: %w", id, err) 442 } 443 return nil 444 case status := <-exitCh: 445 return status.Error() 446 } 447 } 448 449 // Pause pauses a container by its id. 450 func Pause(ctx context.Context, client *containerd.Client, id string) error { 451 container, err := client.LoadContainer(ctx, id) 452 if err != nil { 453 return err 454 } 455 456 task, err := container.Task(ctx, cio.Load) 457 if err != nil { 458 return err 459 } 460 461 status, err := task.Status(ctx) 462 if err != nil { 463 return err 464 } 465 466 switch status.Status { 467 case containerd.Paused: 468 return fmt.Errorf("container %s is already paused", id) 469 case containerd.Created, containerd.Stopped: 470 return fmt.Errorf("container %s is not running", id) 471 default: 472 return task.Pause(ctx) 473 } 474 } 475 476 // Unpause unpauses a container by its id. 477 func Unpause(ctx context.Context, client *containerd.Client, id string) error { 478 container, err := client.LoadContainer(ctx, id) 479 if err != nil { 480 return err 481 } 482 483 task, err := container.Task(ctx, cio.Load) 484 if err != nil { 485 return err 486 } 487 488 status, err := task.Status(ctx) 489 if err != nil { 490 return err 491 } 492 493 switch status.Status { 494 case containerd.Paused: 495 return task.Resume(ctx) 496 default: 497 return fmt.Errorf("container %s is not paused", id) 498 } 499 } 500 501 // ContainerStateDirPath returns the path to the Nerdctl-managed state directory for the container with the given ID. 502 func ContainerStateDirPath(ns, dataStore, id string) (string, error) { 503 if err := nsutil.ValidateNamespaceName(ns); err != nil { 504 return "", fmt.Errorf("invalid namespace name %q for determining state dir of container %q: %s", ns, id, err) 505 } 506 return filepath.Join(dataStore, "containers", ns, id), nil 507 } 508 509 // ContainerVolume is a struct representing a volume in a container. 510 type ContainerVolume struct { 511 Type string 512 Name string 513 Source string 514 Destination string 515 Mode string 516 RW bool 517 Propagation string 518 } 519 520 // GetContainerVolumes is a function that returns a slice of containerVolume pointers. 521 // It accepts a map of container labels as input, where key is the label name and value is its associated value. 522 // The function iterates over the predefined volume labels (AnonymousVolumes and Mounts) 523 // and for each, it checks if the labels exists in the provided container labels. 524 // If yes, it decodes the label value from JSON format and appends the volumes to the result. 525 // In case of error during decoding, it logs the error and continues to the next label. 526 func GetContainerVolumes(containerLabels map[string]string) []*ContainerVolume { 527 var vols []*ContainerVolume 528 volLabels := []string{labels.AnonymousVolumes, labels.Mounts} 529 for _, volLabel := range volLabels { 530 names, ok := containerLabels[volLabel] 531 if !ok { 532 continue 533 } 534 var ( 535 volumes []*ContainerVolume 536 err error 537 ) 538 if volLabel == labels.Mounts { 539 err = json.Unmarshal([]byte(names), &volumes) 540 } 541 if volLabel == labels.AnonymousVolumes { 542 var anonymous []string 543 err = json.Unmarshal([]byte(names), &anonymous) 544 for _, anony := range anonymous { 545 volumes = append(volumes, &ContainerVolume{Name: anony}) 546 } 547 548 } 549 if err != nil { 550 log.L.Warn(err) 551 } 552 vols = append(vols, volumes...) 553 } 554 return vols 555 }