github.com/demonoid81/containerd@v1.3.4/task.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 containerd 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 goruntime "runtime" 26 "strings" 27 "syscall" 28 "time" 29 30 "github.com/containerd/containerd/api/services/tasks/v1" 31 "github.com/containerd/containerd/api/types" 32 "github.com/containerd/containerd/cio" 33 "github.com/containerd/containerd/content" 34 "github.com/containerd/containerd/diff" 35 "github.com/containerd/containerd/errdefs" 36 "github.com/containerd/containerd/images" 37 "github.com/containerd/containerd/mount" 38 "github.com/containerd/containerd/plugin" 39 "github.com/containerd/containerd/rootfs" 40 "github.com/containerd/containerd/runtime/linux/runctypes" 41 "github.com/containerd/containerd/runtime/v2/runc/options" 42 "github.com/containerd/typeurl" 43 google_protobuf "github.com/gogo/protobuf/types" 44 digest "github.com/opencontainers/go-digest" 45 is "github.com/opencontainers/image-spec/specs-go" 46 v1 "github.com/opencontainers/image-spec/specs-go/v1" 47 specs "github.com/opencontainers/runtime-spec/specs-go" 48 "github.com/pkg/errors" 49 ) 50 51 // UnknownExitStatus is returned when containerd is unable to 52 // determine the exit status of a process. This can happen if the process never starts 53 // or if an error was encountered when obtaining the exit status, it is set to 255. 54 const UnknownExitStatus = 255 55 56 const ( 57 checkpointDateFormat = "01-02-2006-15:04:05" 58 checkpointNameFormat = "containerd.io/checkpoint/%s:%s" 59 ) 60 61 // Status returns process status and exit information 62 type Status struct { 63 // Status of the process 64 Status ProcessStatus 65 // ExitStatus returned by the process 66 ExitStatus uint32 67 // ExitedTime is the time at which the process died 68 ExitTime time.Time 69 } 70 71 // ProcessInfo provides platform specific process information 72 type ProcessInfo struct { 73 // Pid is the process ID 74 Pid uint32 75 // Info includes additional process information 76 // Info varies by platform 77 Info *google_protobuf.Any 78 } 79 80 // ProcessStatus returns a human readable status for the Process representing its current status 81 type ProcessStatus string 82 83 const ( 84 // Running indicates the process is currently executing 85 Running ProcessStatus = "running" 86 // Created indicates the process has been created within containerd but the 87 // user's defined process has not started 88 Created ProcessStatus = "created" 89 // Stopped indicates that the process has ran and exited 90 Stopped ProcessStatus = "stopped" 91 // Paused indicates that the process is currently paused 92 Paused ProcessStatus = "paused" 93 // Pausing indicates that the process is currently switching from a 94 // running state into a paused state 95 Pausing ProcessStatus = "pausing" 96 // Unknown indicates that we could not determine the status from the runtime 97 Unknown ProcessStatus = "unknown" 98 ) 99 100 // IOCloseInfo allows specific io pipes to be closed on a process 101 type IOCloseInfo struct { 102 Stdin bool 103 } 104 105 // IOCloserOpts allows the caller to set specific pipes as closed on a process 106 type IOCloserOpts func(*IOCloseInfo) 107 108 // WithStdinCloser closes the stdin of a process 109 func WithStdinCloser(r *IOCloseInfo) { 110 r.Stdin = true 111 } 112 113 // CheckpointTaskInfo allows specific checkpoint information to be set for the task 114 type CheckpointTaskInfo struct { 115 Name string 116 // ParentCheckpoint is the digest of a parent checkpoint 117 ParentCheckpoint digest.Digest 118 // Options hold runtime specific settings for checkpointing a task 119 Options interface{} 120 121 runtime string 122 } 123 124 // Runtime name for the container 125 func (i *CheckpointTaskInfo) Runtime() string { 126 return i.runtime 127 } 128 129 // CheckpointTaskOpts allows the caller to set checkpoint options 130 type CheckpointTaskOpts func(*CheckpointTaskInfo) error 131 132 // TaskInfo sets options for task creation 133 type TaskInfo struct { 134 // Checkpoint is the Descriptor for an existing checkpoint that can be used 135 // to restore a task's runtime and memory state 136 Checkpoint *types.Descriptor 137 // RootFS is a list of mounts to use as the task's root filesystem 138 RootFS []mount.Mount 139 // Options hold runtime specific settings for task creation 140 Options interface{} 141 runtime string 142 } 143 144 // Runtime name for the container 145 func (i *TaskInfo) Runtime() string { 146 return i.runtime 147 } 148 149 // Task is the executable object within containerd 150 type Task interface { 151 Process 152 153 // Pause suspends the execution of the task 154 Pause(context.Context) error 155 // Resume the execution of the task 156 Resume(context.Context) error 157 // Exec creates a new process inside the task 158 Exec(context.Context, string, *specs.Process, cio.Creator) (Process, error) 159 // Pids returns a list of system specific process ids inside the task 160 Pids(context.Context) ([]ProcessInfo, error) 161 // Checkpoint serializes the runtime and memory information of a task into an 162 // OCI Index that can be push and pulled from a remote resource. 163 // 164 // Additional software like CRIU maybe required to checkpoint and restore tasks 165 // NOTE: Checkpoint supports to dump task information to a directory, in this way, 166 // an empty OCI Index will be returned. 167 Checkpoint(context.Context, ...CheckpointTaskOpts) (Image, error) 168 // Update modifies executing tasks with updated settings 169 Update(context.Context, ...UpdateTaskOpts) error 170 // LoadProcess loads a previously created exec'd process 171 LoadProcess(context.Context, string, cio.Attach) (Process, error) 172 // Metrics returns task metrics for runtime specific metrics 173 // 174 // The metric types are generic to containerd and change depending on the runtime 175 // For the built in Linux runtime, github.com/containerd/cgroups.Metrics 176 // are returned in protobuf format 177 Metrics(context.Context) (*types.Metric, error) 178 } 179 180 var _ = (Task)(&task{}) 181 182 type task struct { 183 client *Client 184 185 io cio.IO 186 id string 187 pid uint32 188 } 189 190 // ID of the task 191 func (t *task) ID() string { 192 return t.id 193 } 194 195 // Pid returns the pid or process id for the task 196 func (t *task) Pid() uint32 { 197 return t.pid 198 } 199 200 func (t *task) Start(ctx context.Context) error { 201 r, err := t.client.TaskService().Start(ctx, &tasks.StartRequest{ 202 ContainerID: t.id, 203 }) 204 if err != nil { 205 if t.io != nil { 206 t.io.Cancel() 207 t.io.Close() 208 } 209 return errdefs.FromGRPC(err) 210 } 211 t.pid = r.Pid 212 return nil 213 } 214 215 func (t *task) Kill(ctx context.Context, s syscall.Signal, opts ...KillOpts) error { 216 var i KillInfo 217 for _, o := range opts { 218 if err := o(ctx, &i); err != nil { 219 return err 220 } 221 } 222 _, err := t.client.TaskService().Kill(ctx, &tasks.KillRequest{ 223 Signal: uint32(s), 224 ContainerID: t.id, 225 ExecID: i.ExecID, 226 All: i.All, 227 }) 228 if err != nil { 229 return errdefs.FromGRPC(err) 230 } 231 return nil 232 } 233 234 func (t *task) Pause(ctx context.Context) error { 235 _, err := t.client.TaskService().Pause(ctx, &tasks.PauseTaskRequest{ 236 ContainerID: t.id, 237 }) 238 return errdefs.FromGRPC(err) 239 } 240 241 func (t *task) Resume(ctx context.Context) error { 242 _, err := t.client.TaskService().Resume(ctx, &tasks.ResumeTaskRequest{ 243 ContainerID: t.id, 244 }) 245 return errdefs.FromGRPC(err) 246 } 247 248 func (t *task) Status(ctx context.Context) (Status, error) { 249 r, err := t.client.TaskService().Get(ctx, &tasks.GetRequest{ 250 ContainerID: t.id, 251 }) 252 if err != nil { 253 return Status{}, errdefs.FromGRPC(err) 254 } 255 return Status{ 256 Status: ProcessStatus(strings.ToLower(r.Process.Status.String())), 257 ExitStatus: r.Process.ExitStatus, 258 ExitTime: r.Process.ExitedAt, 259 }, nil 260 } 261 262 func (t *task) Wait(ctx context.Context) (<-chan ExitStatus, error) { 263 c := make(chan ExitStatus, 1) 264 go func() { 265 defer close(c) 266 r, err := t.client.TaskService().Wait(ctx, &tasks.WaitRequest{ 267 ContainerID: t.id, 268 }) 269 if err != nil { 270 c <- ExitStatus{ 271 code: UnknownExitStatus, 272 err: err, 273 } 274 return 275 } 276 c <- ExitStatus{ 277 code: r.ExitStatus, 278 exitedAt: r.ExitedAt, 279 } 280 }() 281 return c, nil 282 } 283 284 // Delete deletes the task and its runtime state 285 // it returns the exit status of the task and any errors that were encountered 286 // during cleanup 287 func (t *task) Delete(ctx context.Context, opts ...ProcessDeleteOpts) (*ExitStatus, error) { 288 for _, o := range opts { 289 if err := o(ctx, t); err != nil { 290 return nil, err 291 } 292 } 293 status, err := t.Status(ctx) 294 if err != nil && errdefs.IsNotFound(err) { 295 return nil, err 296 } 297 switch status.Status { 298 case Stopped, Unknown, "": 299 case Created: 300 if t.client.runtime == fmt.Sprintf("%s.%s", plugin.RuntimePlugin, "windows") { 301 // On windows Created is akin to Stopped 302 break 303 } 304 fallthrough 305 default: 306 return nil, errors.Wrapf(errdefs.ErrFailedPrecondition, "task must be stopped before deletion: %s", status.Status) 307 } 308 if t.io != nil { 309 t.io.Cancel() 310 t.io.Wait() 311 } 312 r, err := t.client.TaskService().Delete(ctx, &tasks.DeleteTaskRequest{ 313 ContainerID: t.id, 314 }) 315 if err != nil { 316 return nil, errdefs.FromGRPC(err) 317 } 318 // Only cleanup the IO after a successful Delete 319 if t.io != nil { 320 t.io.Close() 321 } 322 return &ExitStatus{code: r.ExitStatus, exitedAt: r.ExitedAt}, nil 323 } 324 325 func (t *task) Exec(ctx context.Context, id string, spec *specs.Process, ioCreate cio.Creator) (_ Process, err error) { 326 if id == "" { 327 return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "exec id must not be empty") 328 } 329 i, err := ioCreate(id) 330 if err != nil { 331 return nil, err 332 } 333 defer func() { 334 if err != nil && i != nil { 335 i.Cancel() 336 i.Close() 337 } 338 }() 339 any, err := typeurl.MarshalAny(spec) 340 if err != nil { 341 return nil, err 342 } 343 cfg := i.Config() 344 request := &tasks.ExecProcessRequest{ 345 ContainerID: t.id, 346 ExecID: id, 347 Terminal: cfg.Terminal, 348 Stdin: cfg.Stdin, 349 Stdout: cfg.Stdout, 350 Stderr: cfg.Stderr, 351 Spec: any, 352 } 353 if _, err := t.client.TaskService().Exec(ctx, request); err != nil { 354 i.Cancel() 355 i.Wait() 356 i.Close() 357 return nil, errdefs.FromGRPC(err) 358 } 359 return &process{ 360 id: id, 361 task: t, 362 io: i, 363 }, nil 364 } 365 366 func (t *task) Pids(ctx context.Context) ([]ProcessInfo, error) { 367 response, err := t.client.TaskService().ListPids(ctx, &tasks.ListPidsRequest{ 368 ContainerID: t.id, 369 }) 370 if err != nil { 371 return nil, errdefs.FromGRPC(err) 372 } 373 var processList []ProcessInfo 374 for _, p := range response.Processes { 375 processList = append(processList, ProcessInfo{ 376 Pid: p.Pid, 377 Info: p.Info, 378 }) 379 } 380 return processList, nil 381 } 382 383 func (t *task) CloseIO(ctx context.Context, opts ...IOCloserOpts) error { 384 r := &tasks.CloseIORequest{ 385 ContainerID: t.id, 386 } 387 var i IOCloseInfo 388 for _, o := range opts { 389 o(&i) 390 } 391 r.Stdin = i.Stdin 392 _, err := t.client.TaskService().CloseIO(ctx, r) 393 return errdefs.FromGRPC(err) 394 } 395 396 func (t *task) IO() cio.IO { 397 return t.io 398 } 399 400 func (t *task) Resize(ctx context.Context, w, h uint32) error { 401 _, err := t.client.TaskService().ResizePty(ctx, &tasks.ResizePtyRequest{ 402 ContainerID: t.id, 403 Width: w, 404 Height: h, 405 }) 406 return errdefs.FromGRPC(err) 407 } 408 409 // NOTE: Checkpoint supports to dump task information to a directory, in this way, an empty 410 // OCI Index will be returned. 411 func (t *task) Checkpoint(ctx context.Context, opts ...CheckpointTaskOpts) (Image, error) { 412 ctx, done, err := t.client.WithLease(ctx) 413 if err != nil { 414 return nil, err 415 } 416 defer done(ctx) 417 cr, err := t.client.ContainerService().Get(ctx, t.id) 418 if err != nil { 419 return nil, err 420 } 421 422 request := &tasks.CheckpointTaskRequest{ 423 ContainerID: t.id, 424 } 425 i := CheckpointTaskInfo{ 426 runtime: cr.Runtime.Name, 427 } 428 for _, o := range opts { 429 if err := o(&i); err != nil { 430 return nil, err 431 } 432 } 433 // set a default name 434 if i.Name == "" { 435 i.Name = fmt.Sprintf(checkpointNameFormat, t.id, time.Now().Format(checkpointDateFormat)) 436 } 437 request.ParentCheckpoint = i.ParentCheckpoint 438 if i.Options != nil { 439 any, err := typeurl.MarshalAny(i.Options) 440 if err != nil { 441 return nil, err 442 } 443 request.Options = any 444 } 445 // make sure we pause it and resume after all other filesystem operations are completed 446 if err := t.Pause(ctx); err != nil { 447 return nil, err 448 } 449 defer t.Resume(ctx) 450 index := v1.Index{ 451 Versioned: is.Versioned{ 452 SchemaVersion: 2, 453 }, 454 Annotations: make(map[string]string), 455 } 456 if err := t.checkpointTask(ctx, &index, request); err != nil { 457 return nil, err 458 } 459 // if checkpoint image path passed, jump checkpoint image, 460 // return an empty image 461 if isCheckpointPathExist(cr.Runtime.Name, i.Options) { 462 return NewImage(t.client, images.Image{}), nil 463 } 464 465 if cr.Image != "" { 466 if err := t.checkpointImage(ctx, &index, cr.Image); err != nil { 467 return nil, err 468 } 469 index.Annotations["image.name"] = cr.Image 470 } 471 if cr.SnapshotKey != "" { 472 if err := t.checkpointRWSnapshot(ctx, &index, cr.Snapshotter, cr.SnapshotKey); err != nil { 473 return nil, err 474 } 475 } 476 desc, err := t.writeIndex(ctx, &index) 477 if err != nil { 478 return nil, err 479 } 480 im := images.Image{ 481 Name: i.Name, 482 Target: desc, 483 Labels: map[string]string{ 484 "containerd.io/checkpoint": "true", 485 }, 486 } 487 if im, err = t.client.ImageService().Create(ctx, im); err != nil { 488 return nil, err 489 } 490 return NewImage(t.client, im), nil 491 } 492 493 // UpdateTaskInfo allows updated specific settings to be changed on a task 494 type UpdateTaskInfo struct { 495 // Resources updates a tasks resource constraints 496 Resources interface{} 497 } 498 499 // UpdateTaskOpts allows a caller to update task settings 500 type UpdateTaskOpts func(context.Context, *Client, *UpdateTaskInfo) error 501 502 func (t *task) Update(ctx context.Context, opts ...UpdateTaskOpts) error { 503 request := &tasks.UpdateTaskRequest{ 504 ContainerID: t.id, 505 } 506 var i UpdateTaskInfo 507 for _, o := range opts { 508 if err := o(ctx, t.client, &i); err != nil { 509 return err 510 } 511 } 512 if i.Resources != nil { 513 any, err := typeurl.MarshalAny(i.Resources) 514 if err != nil { 515 return err 516 } 517 request.Resources = any 518 } 519 _, err := t.client.TaskService().Update(ctx, request) 520 return errdefs.FromGRPC(err) 521 } 522 523 func (t *task) LoadProcess(ctx context.Context, id string, ioAttach cio.Attach) (Process, error) { 524 if id == t.id && ioAttach == nil { 525 return t, nil 526 } 527 response, err := t.client.TaskService().Get(ctx, &tasks.GetRequest{ 528 ContainerID: t.id, 529 ExecID: id, 530 }) 531 if err != nil { 532 err = errdefs.FromGRPC(err) 533 if errdefs.IsNotFound(err) { 534 return nil, errors.Wrapf(err, "no running process found") 535 } 536 return nil, err 537 } 538 var i cio.IO 539 if ioAttach != nil { 540 if i, err = attachExistingIO(response, ioAttach); err != nil { 541 return nil, err 542 } 543 } 544 return &process{ 545 id: id, 546 task: t, 547 io: i, 548 }, nil 549 } 550 551 func (t *task) Metrics(ctx context.Context) (*types.Metric, error) { 552 response, err := t.client.TaskService().Metrics(ctx, &tasks.MetricsRequest{ 553 Filters: []string{ 554 "id==" + t.id, 555 }, 556 }) 557 if err != nil { 558 return nil, errdefs.FromGRPC(err) 559 } 560 561 if response.Metrics == nil { 562 _, err := t.Status(ctx) 563 if err != nil && errdefs.IsNotFound(err) { 564 return nil, err 565 } 566 return nil, errors.New("no metrics received") 567 } 568 569 return response.Metrics[0], nil 570 } 571 572 func (t *task) checkpointTask(ctx context.Context, index *v1.Index, request *tasks.CheckpointTaskRequest) error { 573 response, err := t.client.TaskService().Checkpoint(ctx, request) 574 if err != nil { 575 return errdefs.FromGRPC(err) 576 } 577 // NOTE: response.Descriptors can be an empty slice if checkpoint image is jumped 578 // add the checkpoint descriptors to the index 579 for _, d := range response.Descriptors { 580 index.Manifests = append(index.Manifests, v1.Descriptor{ 581 MediaType: d.MediaType, 582 Size: d.Size_, 583 Digest: d.Digest, 584 Platform: &v1.Platform{ 585 OS: goruntime.GOOS, 586 Architecture: goruntime.GOARCH, 587 }, 588 Annotations: d.Annotations, 589 }) 590 } 591 return nil 592 } 593 594 func (t *task) checkpointRWSnapshot(ctx context.Context, index *v1.Index, snapshotterName string, id string) error { 595 opts := []diff.Opt{ 596 diff.WithReference(fmt.Sprintf("checkpoint-rw-%s", id)), 597 } 598 rw, err := rootfs.CreateDiff(ctx, id, t.client.SnapshotService(snapshotterName), t.client.DiffService(), opts...) 599 if err != nil { 600 return err 601 } 602 rw.Platform = &v1.Platform{ 603 OS: goruntime.GOOS, 604 Architecture: goruntime.GOARCH, 605 } 606 index.Manifests = append(index.Manifests, rw) 607 return nil 608 } 609 610 func (t *task) checkpointImage(ctx context.Context, index *v1.Index, image string) error { 611 if image == "" { 612 return fmt.Errorf("cannot checkpoint image with empty name") 613 } 614 ir, err := t.client.ImageService().Get(ctx, image) 615 if err != nil { 616 return err 617 } 618 index.Manifests = append(index.Manifests, ir.Target) 619 return nil 620 } 621 622 func (t *task) writeIndex(ctx context.Context, index *v1.Index) (d v1.Descriptor, err error) { 623 labels := map[string]string{} 624 for i, m := range index.Manifests { 625 labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = m.Digest.String() 626 } 627 buf := bytes.NewBuffer(nil) 628 if err := json.NewEncoder(buf).Encode(index); err != nil { 629 return v1.Descriptor{}, err 630 } 631 return writeContent(ctx, t.client.ContentStore(), v1.MediaTypeImageIndex, t.id, buf, content.WithLabels(labels)) 632 } 633 634 func writeContent(ctx context.Context, store content.Ingester, mediaType, ref string, r io.Reader, opts ...content.Opt) (d v1.Descriptor, err error) { 635 writer, err := store.Writer(ctx, content.WithRef(ref)) 636 if err != nil { 637 return d, err 638 } 639 defer writer.Close() 640 size, err := io.Copy(writer, r) 641 if err != nil { 642 return d, err 643 } 644 645 if err := writer.Commit(ctx, size, "", opts...); err != nil { 646 if !errdefs.IsAlreadyExists(err) { 647 return d, err 648 } 649 } 650 return v1.Descriptor{ 651 MediaType: mediaType, 652 Digest: writer.Digest(), 653 Size: size, 654 }, nil 655 } 656 657 // isCheckpointPathExist only suitable for runc runtime now 658 func isCheckpointPathExist(runtime string, v interface{}) bool { 659 if v == nil { 660 return false 661 } 662 663 switch runtime { 664 case plugin.RuntimeRuncV1, plugin.RuntimeRuncV2: 665 if opts, ok := v.(*options.CheckpointOptions); ok && opts.ImagePath != "" { 666 return true 667 } 668 669 case plugin.RuntimeLinuxV1: 670 if opts, ok := v.(*runctypes.CheckpointOptions); ok && opts.ImagePath != "" { 671 return true 672 } 673 } 674 675 return false 676 }