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