github.com/zhouyu0/docker-note@v0.0.0-20190722021225-b8d3825084db/container/container.go (about) 1 package container // import "github.com/docker/docker/container" 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "runtime" 12 "strings" 13 "sync" 14 "syscall" 15 "time" 16 17 "github.com/containerd/containerd/cio" 18 containertypes "github.com/docker/docker/api/types/container" 19 mounttypes "github.com/docker/docker/api/types/mount" 20 swarmtypes "github.com/docker/docker/api/types/swarm" 21 "github.com/docker/docker/container/stream" 22 "github.com/docker/docker/daemon/exec" 23 "github.com/docker/docker/daemon/logger" 24 "github.com/docker/docker/daemon/logger/jsonfilelog" 25 "github.com/docker/docker/daemon/logger/local" 26 "github.com/docker/docker/daemon/network" 27 "github.com/docker/docker/errdefs" 28 "github.com/docker/docker/image" 29 "github.com/docker/docker/layer" 30 "github.com/docker/docker/pkg/containerfs" 31 "github.com/docker/docker/pkg/idtools" 32 "github.com/docker/docker/pkg/ioutils" 33 "github.com/docker/docker/pkg/signal" 34 "github.com/docker/docker/pkg/symlink" 35 "github.com/docker/docker/pkg/system" 36 "github.com/docker/docker/restartmanager" 37 "github.com/docker/docker/volume" 38 volumemounts "github.com/docker/docker/volume/mounts" 39 "github.com/docker/go-units" 40 agentexec "github.com/docker/swarmkit/agent/exec" 41 "github.com/pkg/errors" 42 "github.com/sirupsen/logrus" 43 ) 44 45 const configFileName = "config.v2.json" 46 47 // ExitStatus provides exit reasons for a container. 48 type ExitStatus struct { 49 // The exit code with which the container exited. 50 ExitCode int 51 52 // Whether the container encountered an OOM. 53 OOMKilled bool 54 55 // Time at which the container died 56 ExitedAt time.Time 57 } 58 59 // Container holds the structure defining a container object. 60 type Container struct { 61 StreamConfig *stream.Config 62 // embed for Container to support states directly. 63 *State `json:"State"` // Needed for Engine API version <= 1.11 64 Root string `json:"-"` // Path to the "home" of the container, including metadata. 65 BaseFS containerfs.ContainerFS `json:"-"` // interface containing graphdriver mount 66 RWLayer layer.RWLayer `json:"-"` 67 ID string 68 Created time.Time 69 Managed bool 70 Path string 71 Args []string 72 Config *containertypes.Config 73 ImageID image.ID `json:"Image"` 74 NetworkSettings *network.Settings 75 LogPath string 76 Name string 77 Driver string 78 OS string 79 // MountLabel contains the options for the 'mount' command 80 MountLabel string 81 ProcessLabel string 82 RestartCount int 83 HasBeenStartedBefore bool 84 HasBeenManuallyStopped bool // used for unless-stopped restart policy 85 MountPoints map[string]*volumemounts.MountPoint 86 HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable 87 ExecCommands *exec.Store `json:"-"` 88 DependencyStore agentexec.DependencyGetter `json:"-"` 89 SecretReferences []*swarmtypes.SecretReference 90 ConfigReferences []*swarmtypes.ConfigReference 91 // logDriver for closing 92 LogDriver logger.Logger `json:"-"` 93 LogCopier *logger.Copier `json:"-"` 94 restartManager restartmanager.RestartManager 95 attachContext *attachContext 96 97 // Fields here are specific to Unix platforms 98 AppArmorProfile string 99 HostnamePath string 100 HostsPath string 101 ShmPath string 102 ResolvConfPath string 103 SeccompProfile string 104 NoNewPrivileges bool 105 106 // Fields here are specific to Windows 107 NetworkSharedContainerID string `json:"-"` 108 SharedEndpointList []string `json:"-"` 109 } 110 111 // NewBaseContainer creates a new container with its 112 // basic configuration. 113 func NewBaseContainer(id, root string) *Container { 114 return &Container{ 115 ID: id, 116 State: NewState(), 117 ExecCommands: exec.NewStore(), 118 Root: root, 119 MountPoints: make(map[string]*volumemounts.MountPoint), 120 StreamConfig: stream.NewConfig(), 121 attachContext: &attachContext{}, 122 } 123 } 124 125 // FromDisk loads the container configuration stored in the host. 126 func (container *Container) FromDisk() error { 127 pth, err := container.ConfigPath() 128 if err != nil { 129 return err 130 } 131 132 jsonSource, err := os.Open(pth) 133 if err != nil { 134 return err 135 } 136 defer jsonSource.Close() 137 138 dec := json.NewDecoder(jsonSource) 139 140 // Load container settings 141 if err := dec.Decode(container); err != nil { 142 return err 143 } 144 145 // Ensure the operating system is set if blank. Assume it is the OS of the 146 // host OS if not, to ensure containers created before multiple-OS 147 // support are migrated 148 if container.OS == "" { 149 container.OS = runtime.GOOS 150 } 151 152 return container.readHostConfig() 153 } 154 155 // toDisk saves the container configuration on disk and returns a deep copy. 156 func (container *Container) toDisk() (*Container, error) { 157 var ( 158 buf bytes.Buffer 159 deepCopy Container 160 ) 161 pth, err := container.ConfigPath() 162 if err != nil { 163 return nil, err 164 } 165 166 // Save container settings 167 f, err := ioutils.NewAtomicFileWriter(pth, 0600) 168 if err != nil { 169 return nil, err 170 } 171 defer f.Close() 172 173 w := io.MultiWriter(&buf, f) 174 if err := json.NewEncoder(w).Encode(container); err != nil { 175 return nil, err 176 } 177 178 if err := json.NewDecoder(&buf).Decode(&deepCopy); err != nil { 179 return nil, err 180 } 181 deepCopy.HostConfig, err = container.WriteHostConfig() 182 if err != nil { 183 return nil, err 184 } 185 186 return &deepCopy, nil 187 } 188 189 // CheckpointTo makes the Container's current state visible to queries, and persists state. 190 // Callers must hold a Container lock. 191 func (container *Container) CheckpointTo(store ViewDB) error { 192 deepCopy, err := container.toDisk() 193 if err != nil { 194 return err 195 } 196 return store.Save(deepCopy) 197 } 198 199 // readHostConfig reads the host configuration from disk for the container. 200 func (container *Container) readHostConfig() error { 201 container.HostConfig = &containertypes.HostConfig{} 202 // If the hostconfig file does not exist, do not read it. 203 // (We still have to initialize container.HostConfig, 204 // but that's OK, since we just did that above.) 205 pth, err := container.HostConfigPath() 206 if err != nil { 207 return err 208 } 209 210 f, err := os.Open(pth) 211 if err != nil { 212 if os.IsNotExist(err) { 213 return nil 214 } 215 return err 216 } 217 defer f.Close() 218 219 if err := json.NewDecoder(f).Decode(&container.HostConfig); err != nil { 220 return err 221 } 222 223 container.InitDNSHostConfig() 224 225 return nil 226 } 227 228 // WriteHostConfig saves the host configuration on disk for the container, 229 // and returns a deep copy of the saved object. Callers must hold a Container lock. 230 func (container *Container) WriteHostConfig() (*containertypes.HostConfig, error) { 231 var ( 232 buf bytes.Buffer 233 deepCopy containertypes.HostConfig 234 ) 235 236 pth, err := container.HostConfigPath() 237 if err != nil { 238 return nil, err 239 } 240 241 f, err := ioutils.NewAtomicFileWriter(pth, 0644) 242 if err != nil { 243 return nil, err 244 } 245 defer f.Close() 246 247 w := io.MultiWriter(&buf, f) 248 if err := json.NewEncoder(w).Encode(&container.HostConfig); err != nil { 249 return nil, err 250 } 251 252 if err := json.NewDecoder(&buf).Decode(&deepCopy); err != nil { 253 return nil, err 254 } 255 return &deepCopy, nil 256 } 257 258 // SetupWorkingDirectory sets up the container's working directory as set in container.Config.WorkingDir 259 func (container *Container) SetupWorkingDirectory(rootIdentity idtools.Identity) error { 260 // TODO @jhowardmsft, @gupta-ak LCOW Support. This will need revisiting. 261 // We will need to do remote filesystem operations here. 262 if container.OS != runtime.GOOS { 263 return nil 264 } 265 266 if container.Config.WorkingDir == "" { 267 return nil 268 } 269 270 container.Config.WorkingDir = filepath.Clean(container.Config.WorkingDir) 271 pth, err := container.GetResourcePath(container.Config.WorkingDir) 272 if err != nil { 273 return err 274 } 275 276 if err := idtools.MkdirAllAndChownNew(pth, 0755, rootIdentity); err != nil { 277 pthInfo, err2 := os.Stat(pth) 278 if err2 == nil && pthInfo != nil && !pthInfo.IsDir() { 279 return errors.Errorf("Cannot mkdir: %s is not a directory", container.Config.WorkingDir) 280 } 281 282 return err 283 } 284 285 return nil 286 } 287 288 // GetResourcePath evaluates `path` in the scope of the container's BaseFS, with proper path 289 // sanitisation. Symlinks are all scoped to the BaseFS of the container, as 290 // though the container's BaseFS was `/`. 291 // 292 // The BaseFS of a container is the host-facing path which is bind-mounted as 293 // `/` inside the container. This method is essentially used to access a 294 // particular path inside the container as though you were a process in that 295 // container. 296 // 297 // NOTE: The returned path is *only* safely scoped inside the container's BaseFS 298 // if no component of the returned path changes (such as a component 299 // symlinking to a different path) between using this method and using the 300 // path. See symlink.FollowSymlinkInScope for more details. 301 func (container *Container) GetResourcePath(path string) (string, error) { 302 if container.BaseFS == nil { 303 return "", errors.New("GetResourcePath: BaseFS of container " + container.ID + " is unexpectedly nil") 304 } 305 // IMPORTANT - These are paths on the OS where the daemon is running, hence 306 // any filepath operations must be done in an OS agnostic way. 307 r, e := container.BaseFS.ResolveScopedPath(path, false) 308 309 // Log this here on the daemon side as there's otherwise no indication apart 310 // from the error being propagated all the way back to the client. This makes 311 // debugging significantly easier and clearly indicates the error comes from the daemon. 312 if e != nil { 313 logrus.Errorf("Failed to ResolveScopedPath BaseFS %s path %s %s\n", container.BaseFS.Path(), path, e) 314 } 315 return r, e 316 } 317 318 // GetRootResourcePath evaluates `path` in the scope of the container's root, with proper path 319 // sanitisation. Symlinks are all scoped to the root of the container, as 320 // though the container's root was `/`. 321 // 322 // The root of a container is the host-facing configuration metadata directory. 323 // Only use this method to safely access the container's `container.json` or 324 // other metadata files. If in doubt, use container.GetResourcePath. 325 // 326 // NOTE: The returned path is *only* safely scoped inside the container's root 327 // if no component of the returned path changes (such as a component 328 // symlinking to a different path) between using this method and using the 329 // path. See symlink.FollowSymlinkInScope for more details. 330 func (container *Container) GetRootResourcePath(path string) (string, error) { 331 // IMPORTANT - These are paths on the OS where the daemon is running, hence 332 // any filepath operations must be done in an OS agnostic way. 333 cleanPath := filepath.Join(string(os.PathSeparator), path) 334 return symlink.FollowSymlinkInScope(filepath.Join(container.Root, cleanPath), container.Root) 335 } 336 337 // ExitOnNext signals to the monitor that it should not restart the container 338 // after we send the kill signal. 339 func (container *Container) ExitOnNext() { 340 container.RestartManager().Cancel() 341 } 342 343 // HostConfigPath returns the path to the container's JSON hostconfig 344 func (container *Container) HostConfigPath() (string, error) { 345 return container.GetRootResourcePath("hostconfig.json") 346 } 347 348 // ConfigPath returns the path to the container's JSON config 349 func (container *Container) ConfigPath() (string, error) { 350 return container.GetRootResourcePath(configFileName) 351 } 352 353 // CheckpointDir returns the directory checkpoints are stored in 354 func (container *Container) CheckpointDir() string { 355 return filepath.Join(container.Root, "checkpoints") 356 } 357 358 // StartLogger starts a new logger driver for the container. 359 func (container *Container) StartLogger() (logger.Logger, error) { 360 cfg := container.HostConfig.LogConfig 361 initDriver, err := logger.GetLogDriver(cfg.Type) 362 if err != nil { 363 return nil, errors.Wrap(err, "failed to get logging factory") 364 } 365 info := logger.Info{ 366 Config: cfg.Config, 367 ContainerID: container.ID, 368 ContainerName: container.Name, 369 ContainerEntrypoint: container.Path, 370 ContainerArgs: container.Args, 371 ContainerImageID: container.ImageID.String(), 372 ContainerImageName: container.Config.Image, 373 ContainerCreated: container.Created, 374 ContainerEnv: container.Config.Env, 375 ContainerLabels: container.Config.Labels, 376 DaemonName: "docker", 377 } 378 379 // Set logging file for "json-logger" 380 // TODO(@cpuguy83): Setup here based on log driver is a little weird. 381 switch cfg.Type { 382 case jsonfilelog.Name: 383 info.LogPath, err = container.GetRootResourcePath(fmt.Sprintf("%s-json.log", container.ID)) 384 if err != nil { 385 return nil, err 386 } 387 388 container.LogPath = info.LogPath 389 case local.Name: 390 // Do not set container.LogPath for the local driver 391 // This would expose the value to the API, which should not be done as it means 392 // that the log file implementation would become a stable API that cannot change. 393 logDir, err := container.GetRootResourcePath("local-logs") 394 if err != nil { 395 return nil, err 396 } 397 if err := os.MkdirAll(logDir, 0700); err != nil { 398 return nil, errdefs.System(errors.Wrap(err, "error creating local logs dir")) 399 } 400 info.LogPath = filepath.Join(logDir, "container.log") 401 } 402 403 l, err := initDriver(info) 404 if err != nil { 405 return nil, err 406 } 407 408 if containertypes.LogMode(cfg.Config["mode"]) == containertypes.LogModeNonBlock { 409 bufferSize := int64(-1) 410 if s, exists := cfg.Config["max-buffer-size"]; exists { 411 bufferSize, err = units.RAMInBytes(s) 412 if err != nil { 413 return nil, err 414 } 415 } 416 l = logger.NewRingLogger(l, info, bufferSize) 417 } 418 return l, nil 419 } 420 421 // GetProcessLabel returns the process label for the container. 422 func (container *Container) GetProcessLabel() string { 423 // even if we have a process label return "" if we are running 424 // in privileged mode 425 if container.HostConfig.Privileged { 426 return "" 427 } 428 return container.ProcessLabel 429 } 430 431 // GetMountLabel returns the mounting label for the container. 432 // This label is empty if the container is privileged. 433 func (container *Container) GetMountLabel() string { 434 return container.MountLabel 435 } 436 437 // GetExecIDs returns the list of exec commands running on the container. 438 func (container *Container) GetExecIDs() []string { 439 return container.ExecCommands.List() 440 } 441 442 // ShouldRestart decides whether the daemon should restart the container or not. 443 // This is based on the container's restart policy. 444 func (container *Container) ShouldRestart() bool { 445 shouldRestart, _, _ := container.RestartManager().ShouldRestart(uint32(container.ExitCode()), container.HasBeenManuallyStopped, container.FinishedAt.Sub(container.StartedAt)) 446 return shouldRestart 447 } 448 449 // AddMountPointWithVolume adds a new mount point configured with a volume to the container. 450 func (container *Container) AddMountPointWithVolume(destination string, vol volume.Volume, rw bool) { 451 operatingSystem := container.OS 452 if operatingSystem == "" { 453 operatingSystem = runtime.GOOS 454 } 455 volumeParser := volumemounts.NewParser(operatingSystem) 456 container.MountPoints[destination] = &volumemounts.MountPoint{ 457 Type: mounttypes.TypeVolume, 458 Name: vol.Name(), 459 Driver: vol.DriverName(), 460 Destination: destination, 461 RW: rw, 462 Volume: vol, 463 CopyData: volumeParser.DefaultCopyMode(), 464 } 465 } 466 467 // UnmountVolumes unmounts all volumes 468 func (container *Container) UnmountVolumes(volumeEventLog func(name, action string, attributes map[string]string)) error { 469 var errors []string 470 for _, volumeMount := range container.MountPoints { 471 if volumeMount.Volume == nil { 472 continue 473 } 474 475 if err := volumeMount.Cleanup(); err != nil { 476 errors = append(errors, err.Error()) 477 continue 478 } 479 480 attributes := map[string]string{ 481 "driver": volumeMount.Volume.DriverName(), 482 "container": container.ID, 483 } 484 volumeEventLog(volumeMount.Volume.Name(), "unmount", attributes) 485 } 486 if len(errors) > 0 { 487 return fmt.Errorf("error while unmounting volumes for container %s: %s", container.ID, strings.Join(errors, "; ")) 488 } 489 return nil 490 } 491 492 // IsDestinationMounted checks whether a path is mounted on the container or not. 493 func (container *Container) IsDestinationMounted(destination string) bool { 494 return container.MountPoints[destination] != nil 495 } 496 497 // StopSignal returns the signal used to stop the container. 498 func (container *Container) StopSignal() int { 499 var stopSignal syscall.Signal 500 if container.Config.StopSignal != "" { 501 stopSignal, _ = signal.ParseSignal(container.Config.StopSignal) 502 } 503 504 if int(stopSignal) == 0 { 505 stopSignal, _ = signal.ParseSignal(signal.DefaultStopSignal) 506 } 507 return int(stopSignal) 508 } 509 510 // StopTimeout returns the timeout (in seconds) used to stop the container. 511 func (container *Container) StopTimeout() int { 512 if container.Config.StopTimeout != nil { 513 return *container.Config.StopTimeout 514 } 515 return DefaultStopTimeout 516 } 517 518 // InitDNSHostConfig ensures that the dns fields are never nil. 519 // New containers don't ever have those fields nil, 520 // but pre created containers can still have those nil values. 521 // The non-recommended host configuration in the start api can 522 // make these fields nil again, this corrects that issue until 523 // we remove that behavior for good. 524 // See https://github.com/docker/docker/pull/17779 525 // for a more detailed explanation on why we don't want that. 526 func (container *Container) InitDNSHostConfig() { 527 container.Lock() 528 defer container.Unlock() 529 if container.HostConfig.DNS == nil { 530 container.HostConfig.DNS = make([]string, 0) 531 } 532 533 if container.HostConfig.DNSSearch == nil { 534 container.HostConfig.DNSSearch = make([]string, 0) 535 } 536 537 if container.HostConfig.DNSOptions == nil { 538 container.HostConfig.DNSOptions = make([]string, 0) 539 } 540 } 541 542 // UpdateMonitor updates monitor configure for running container 543 func (container *Container) UpdateMonitor(restartPolicy containertypes.RestartPolicy) { 544 type policySetter interface { 545 SetPolicy(containertypes.RestartPolicy) 546 } 547 548 if rm, ok := container.RestartManager().(policySetter); ok { 549 rm.SetPolicy(restartPolicy) 550 } 551 } 552 553 // FullHostname returns hostname and optional domain appended to it. 554 func (container *Container) FullHostname() string { 555 fullHostname := container.Config.Hostname 556 if container.Config.Domainname != "" { 557 fullHostname = fmt.Sprintf("%s.%s", fullHostname, container.Config.Domainname) 558 } 559 return fullHostname 560 } 561 562 // RestartManager returns the current restartmanager instance connected to container. 563 func (container *Container) RestartManager() restartmanager.RestartManager { 564 if container.restartManager == nil { 565 container.restartManager = restartmanager.New(container.HostConfig.RestartPolicy, container.RestartCount) 566 } 567 return container.restartManager 568 } 569 570 // ResetRestartManager initializes new restartmanager based on container config 571 func (container *Container) ResetRestartManager(resetCount bool) { 572 if container.restartManager != nil { 573 container.restartManager.Cancel() 574 } 575 if resetCount { 576 container.RestartCount = 0 577 } 578 container.restartManager = nil 579 } 580 581 type attachContext struct { 582 ctx context.Context 583 cancel context.CancelFunc 584 mu sync.Mutex 585 } 586 587 // InitAttachContext initializes or returns existing context for attach calls to 588 // track container liveness. 589 func (container *Container) InitAttachContext() context.Context { 590 container.attachContext.mu.Lock() 591 defer container.attachContext.mu.Unlock() 592 if container.attachContext.ctx == nil { 593 container.attachContext.ctx, container.attachContext.cancel = context.WithCancel(context.Background()) 594 } 595 return container.attachContext.ctx 596 } 597 598 // CancelAttachContext cancels attach context. All attach calls should detach 599 // after this call. 600 func (container *Container) CancelAttachContext() { 601 container.attachContext.mu.Lock() 602 if container.attachContext.ctx != nil { 603 container.attachContext.cancel() 604 container.attachContext.ctx = nil 605 } 606 container.attachContext.mu.Unlock() 607 } 608 609 func (container *Container) startLogging() error { 610 if container.HostConfig.LogConfig.Type == "none" { 611 return nil // do not start logging routines 612 } 613 614 l, err := container.StartLogger() 615 if err != nil { 616 return fmt.Errorf("failed to initialize logging driver: %v", err) 617 } 618 619 copier := logger.NewCopier(map[string]io.Reader{"stdout": container.StdoutPipe(), "stderr": container.StderrPipe()}, l) 620 container.LogCopier = copier 621 copier.Run() 622 container.LogDriver = l 623 624 return nil 625 } 626 627 // StdinPipe gets the stdin stream of the container 628 func (container *Container) StdinPipe() io.WriteCloser { 629 return container.StreamConfig.StdinPipe() 630 } 631 632 // StdoutPipe gets the stdout stream of the container 633 func (container *Container) StdoutPipe() io.ReadCloser { 634 return container.StreamConfig.StdoutPipe() 635 } 636 637 // StderrPipe gets the stderr stream of the container 638 func (container *Container) StderrPipe() io.ReadCloser { 639 return container.StreamConfig.StderrPipe() 640 } 641 642 // CloseStreams closes the container's stdio streams 643 func (container *Container) CloseStreams() error { 644 return container.StreamConfig.CloseStreams() 645 } 646 647 // InitializeStdio is called by libcontainerd to connect the stdio. 648 func (container *Container) InitializeStdio(iop *cio.DirectIO) (cio.IO, error) { 649 if err := container.startLogging(); err != nil { 650 container.Reset(false) 651 return nil, err 652 } 653 654 container.StreamConfig.CopyToPipe(iop) 655 656 if container.StreamConfig.Stdin() == nil && !container.Config.Tty { 657 if iop.Stdin != nil { 658 if err := iop.Stdin.Close(); err != nil { 659 logrus.Warnf("error closing stdin: %+v", err) 660 } 661 } 662 } 663 664 return &rio{IO: iop, sc: container.StreamConfig}, nil 665 } 666 667 // MountsResourcePath returns the path where mounts are stored for the given mount 668 func (container *Container) MountsResourcePath(mount string) (string, error) { 669 return container.GetRootResourcePath(filepath.Join("mounts", mount)) 670 } 671 672 // SecretMountPath returns the path of the secret mount for the container 673 func (container *Container) SecretMountPath() (string, error) { 674 return container.MountsResourcePath("secrets") 675 } 676 677 // SecretFilePath returns the path to the location of a secret on the host. 678 func (container *Container) SecretFilePath(secretRef swarmtypes.SecretReference) (string, error) { 679 secrets, err := container.SecretMountPath() 680 if err != nil { 681 return "", err 682 } 683 return filepath.Join(secrets, secretRef.SecretID), nil 684 } 685 686 func getSecretTargetPath(r *swarmtypes.SecretReference) string { 687 if filepath.IsAbs(r.File.Name) { 688 return r.File.Name 689 } 690 691 return filepath.Join(containerSecretMountPath, r.File.Name) 692 } 693 694 // CreateDaemonEnvironment creates a new environment variable slice for this container. 695 func (container *Container) CreateDaemonEnvironment(tty bool, linkedEnv []string) []string { 696 // Setup environment 697 os := container.OS 698 if os == "" { 699 os = runtime.GOOS 700 } 701 env := []string{} 702 if runtime.GOOS != "windows" || (runtime.GOOS == "windows" && os == "linux") { 703 env = []string{ 704 "PATH=" + system.DefaultPathEnv(os), 705 "HOSTNAME=" + container.Config.Hostname, 706 } 707 if tty { 708 env = append(env, "TERM=xterm") 709 } 710 env = append(env, linkedEnv...) 711 } 712 713 // because the env on the container can override certain default values 714 // we need to replace the 'env' keys where they match and append anything 715 // else. 716 env = ReplaceOrAppendEnvValues(env, container.Config.Env) 717 return env 718 } 719 720 type rio struct { 721 cio.IO 722 723 sc *stream.Config 724 } 725 726 func (i *rio) Close() error { 727 i.IO.Close() 728 729 return i.sc.CloseStreams() 730 } 731 732 func (i *rio) Wait() { 733 i.sc.Wait() 734 735 i.IO.Wait() 736 }