// RunContainer creates and starts a container using the image specified in opts // with the ability to stream input and/or output. func (d *stiDocker) RunContainer(opts RunContainerOptions) error { createOpts := opts.asDockerCreateContainerOptions() // get info about the specified image image := createOpts.Config.Image inspect, err := d.InspectImage(image) imageMetadata := &api.Image{} if err == nil { updateImageWithInspect(imageMetadata, inspect) if opts.PullImage { _, err = d.CheckAndPullImage(image) } } if err != nil { glog.V(0).Infof("error: Unable to get image metadata for %s: %v", image, err) return err } entrypoint, err := d.GetImageEntrypoint(image) if err != nil { return fmt.Errorf("Couldn't get entrypoint of %q image: %v", image, err) } // If the image has an entrypoint already defined, // it will be overridden either by DefaultEntrypoint, // or by the value in opts.Entrypoint. // If the image does not have an entrypoint, but // opts.Entrypoint is supplied, opts.Entrypoint will // be respected. if len(entrypoint) != 0 && len(opts.Entrypoint) == 0 { opts.Entrypoint = DefaultEntrypoint } // tarDestination will be passed as location to PostExecute function // and will be used as the prefix for the CMD (scripts/run) var tarDestination string var cmd []string if !opts.TargetImage { if len(opts.CommandExplicit) != 0 { cmd = opts.CommandExplicit } else { tarDestination = determineTarDestinationDir(opts, imageMetadata) cmd = constructCommand(opts, imageMetadata, tarDestination) } glog.V(5).Infof("Setting %q command for container ...", strings.Join(cmd, " ")) } createOpts.Config.Cmd = cmd // Create a new container. glog.V(2).Infof("Creating container with options {Name:%q Config:%+v HostConfig:%+v} ...", createOpts.Name, createOpts.Config, createOpts.HostConfig) ctx, cancel := getDefaultContext() defer cancel() if createOpts.HostConfig != nil && createOpts.HostConfig.ShmSize <= 0 { createOpts.HostConfig.ShmSize = DefaultShmSize } container, err := d.client.ContainerCreate(ctx, createOpts.Config, createOpts.HostConfig, createOpts.NetworkingConfig, createOpts.Name) if err != nil { return err } // Container was created, so we defer its removal, and also remove it if we get a SIGINT/SIGTERM/SIGQUIT/SIGHUP. removeContainer := func() { glog.V(4).Infof("Removing container %q ...", container.ID) if removeErr := d.RemoveContainer(container.ID); removeErr != nil { glog.V(0).Infof("warning: Failed to remove container %q: %v", container.ID, removeErr) } else { glog.V(4).Infof("Removed container %q", container.ID) } } dumpStack := func(signal os.Signal) { if signal == syscall.SIGQUIT { buf := make([]byte, 1<<16) runtime.Stack(buf, true) fmt.Printf("%s", buf) } os.Exit(2) } return interrupt.New(dumpStack, removeContainer).Run(func() error { // Attach to the container on go thread (different than with go-dockerclient, since it provided a non-blocking attach which we don't seem to have with k8s/engine-api) // Attach to the container on go thread to mimic blocking behavior we had with go-dockerclient (k8s wrapper blocks); then use borrowed code // from k8s to dump logs via return // still preserve the flow of attaching before starting to handle various timing issues encountered in the past, as well as allow for --run option glog.V(2).Infof("Attaching to container %q ...", container.ID) errorChannel := make(chan error) timeoutTimer := time.NewTimer(DefaultDockerTimeout) var attachLoggingError error // unit tests found a DATA RACE on attachLoggingError; at first a simple mutex seemed sufficient, but a race condition in holdHijackedConnection manifested // where <-receiveStdout would block even after the container had exitted, blocking the return with attachLoggingError; rather than trying to discern if the // container exited in holdHijackedConnection, we'll using channel based signaling coupled with a time to avoid blocking forever attachExit := make(chan bool, 1) go func() { ctx, cancel := getDefaultContext() defer cancel() resp, attachErr := d.client.ContainerAttach(ctx, container.ID, opts.asDockerAttachToContainerOptions()) errorChannel <- attachErr if attachErr != nil { glog.V(0).Infof("error: Unable to attach to container %q: %v", container.ID, attachErr) return } defer resp.Close() attachLoggingError = d.holdHijackedConnection(false, opts.Stdin, opts.Stdout, opts.Stderr, resp) attachExit <- true }() // this error check should handle the result from the d.client.ContainerAttach call ... we progress to start when that occurs select { case err = <-errorChannel: // in non-error scenarios, temporary tracing confirmed that // unless the container starts, then exits, the attach blocks and // never returns either a nil for success or whatever err it might // return if the attach failed if err != nil { return err } break case <-timeoutTimer.C: return fmt.Errorf("timed out waiting to attach to container %s ", container.ID) } // Start the container glog.V(2).Infof("Starting container %q ...", container.ID) ctx, cancel := getDefaultContext() defer cancel() err = d.client.ContainerStart(ctx, container.ID, dockertypes.ContainerStartOptions{}) if err != nil { return err } // Run OnStart hook if defined. OnStart might block, so we run it in a // new goroutine, and wait for it to be done later on. onStartDone := make(chan error, 1) if opts.OnStart != nil { go func() { onStartDone <- opts.OnStart(container.ID) }() } if opts.TargetImage { // When TargetImage is true, we're dealing with an invocation of `s2i build ... --run` // so this will, e.g., run a web server and block until the user interrupts it (or // the container exits normally). dump port/etc information for the user. dumpContainerInfo(container, d, image) } // Return an error if the exit code of the container is // non-zero. glog.V(4).Infof("Waiting for container %q to stop ...", container.ID) exitCode, err := d.client.ContainerWait(context.Background(), container.ID) if err != nil { return fmt.Errorf("waiting for container %q to stop: %v", container.ID, err) } if exitCode != 0 { return errors.NewContainerError(container.ID, exitCode, "") } // FIXME: If Stdout or Stderr can be closed, close it to notify that // there won't be any more writes. This is a hack to close the write // half of a pipe so that the read half sees io.EOF. // In particular, this is needed to eventually terminate code that runs // on OnStart and blocks reading from the pipe. if c, ok := opts.Stdout.(io.Closer); ok { c.Close() } if c, ok := opts.Stderr.(io.Closer); ok { c.Close() } // OnStart must be done before we move on. if opts.OnStart != nil { if err = <-onStartDone; err != nil { return err } } // Run PostExec hook if defined. if opts.PostExec != nil { glog.V(2).Infof("Invoking PostExecute function") if err = opts.PostExec.PostExecute(container.ID, tarDestination); err != nil { return err } } select { case <-attachExit: return attachLoggingError case <-time.After(DefaultDockerTimeout): return nil } }) }
// RunContainer creates and starts a container using the image specified in opts // with the ability to stream input and/or output. func (d *stiDocker) RunContainer(opts RunContainerOptions) error { createOpts := opts.asDockerCreateContainerOptions() // get info about the specified image image := createOpts.Config.Image var ( imageMetadata *docker.Image err error ) if opts.PullImage { imageMetadata, err = d.CheckAndPullImage(image) } else { imageMetadata, err = d.client.InspectImage(image) } // if the original image has no entrypoint, and the run invocation // is trying to set an entrypoint, ignore it. We only want to // set the entrypoint if we need to override a default entrypoint // in the image. This allows us to still work with a minimal image // that does not contain "/usr/bin/env" since we don't attempt to override // the entrypoint. if len(opts.Entrypoint) != 0 { entrypoint, err := d.GetImageEntrypoint(image) if err != nil { return err } if len(entrypoint) == 0 { opts.Entrypoint = nil } } if err != nil { glog.V(0).Infof("error: Unable to get image metadata for %s: %v", image, err) return err } // tarDestination will be passed as location to PostExecute function // and will be used as the prefix for the CMD (scripts/run) var tarDestination string var cmd []string if !opts.TargetImage { if len(opts.CommandExplicit) != 0 { cmd = opts.CommandExplicit } else { tarDestination = determineTarDestinationDir(opts, imageMetadata) cmd = constructCommand(opts, imageMetadata, tarDestination) } glog.V(5).Infof("Setting %q command for container ...", strings.Join(cmd, " ")) } createOpts.Config.Cmd = cmd // Create a new container. glog.V(2).Infof("Creating container with options {Name:%q Config:%+v HostConfig:%+v} ...", createOpts.Name, createOpts.Config, createOpts.HostConfig) var container *docker.Container if err = util.TimeoutAfter(DefaultDockerTimeout, "timeout after waiting %v for Docker to create container", func() error { var createErr error container, createErr = d.client.CreateContainer(createOpts) return createErr }); err != nil { return err } containerName := containerNameOrID(container) // Container was created, so we defer its removal, and also remove it if we get a SIGINT/SIGTERM/SIGQUIT/SIGHUP. removeContainer := func() { glog.V(4).Infof("Removing container %q ...", containerName) if err := d.RemoveContainer(container.ID); err != nil { glog.V(0).Infof("warning: Failed to remove container %q: %v", containerName, err) } else { glog.V(4).Infof("Removed container %q", containerName) } } dumpStack := func(signal os.Signal) { if signal == syscall.SIGQUIT { buf := make([]byte, 1<<16) runtime.Stack(buf, true) fmt.Printf("%s", buf) } os.Exit(2) } return interrupt.New(dumpStack, removeContainer).Run(func() error { // Attach to the container. glog.V(2).Infof("Attaching to container %q ...", containerName) attachOpts := opts.asDockerAttachToContainerOptions() attachOpts.Container = container.ID if _, err = d.client.AttachToContainerNonBlocking(attachOpts); err != nil { glog.V(0).Infof("error: Unable to attach to container %q with options %+v: %v", containerName, attachOpts, err) return err } // Start the container. glog.V(2).Infof("Starting container %q ...", containerName) if err := util.TimeoutAfter(DefaultDockerTimeout, "timeout after waiting %v for Docker to start container", func() error { return d.client.StartContainer(container.ID, nil) }); err != nil { return err } // Run OnStart hook if defined. OnStart might block, so we run it in a // new goroutine, and wait for it to be done later on. onStartDone := make(chan error, 1) if opts.OnStart != nil { go func() { onStartDone <- opts.OnStart(container.ID) }() } if opts.TargetImage { // When TargetImage is true, we're dealing with an invocation of `s2i build ... --run` // so this will, e.g., run a web server and block until the user interrupts it (or // the container exits normally). dump port/etc information for the user. dumpContainerInfo(container, d, image) } // Return an error if the exit code of the container is // non-zero. glog.V(4).Infof("Waiting for container %q to stop ...", containerName) exitCode, err := d.client.WaitContainer(container.ID) if err != nil { return fmt.Errorf("waiting for container %q to stop: %v", containerName, err) } if exitCode != 0 { return errors.NewContainerError(container.Name, exitCode, "") } // FIXME: If Stdout or Stderr can be closed, close it to notify that // there won't be any more writes. This is a hack to close the write // half of a pipe so that the read half sees io.EOF. // In particular, this is needed to eventually terminate code that runs // on OnStart and blocks reading from the pipe. if c, ok := opts.Stdout.(io.Closer); ok { c.Close() } if c, ok := opts.Stderr.(io.Closer); ok { c.Close() } // OnStart must be done before we move on. if opts.OnStart != nil { if err = <-onStartDone; err != nil { return err } } // Run PostExec hook if defined. if opts.PostExec != nil { glog.V(2).Infof("Invoking PostExecute function") if err = opts.PostExec.PostExecute(container.ID, tarDestination); err != nil { return err } } return nil }) }
// RunContainer creates and starts a container using the image specified in opts // with the ability to stream input and/or output. func (d *stiDocker) RunContainer(opts RunContainerOptions) error { createOpts := opts.asDockerCreateContainerOptions() // get info about the specified image image := createOpts.Config.Image inspect, err := d.InspectImage(image) imageMetadata := &api.Image{} if err == nil { updateImageWithInspect(imageMetadata, inspect) if opts.PullImage { _, err = d.CheckAndPullImage(image) } } if err != nil { glog.V(0).Infof("error: Unable to get image metadata for %s: %v", image, err) return err } entrypoint, err := d.GetImageEntrypoint(image) if err != nil { return fmt.Errorf("could not get entrypoint of %q image: %v", image, err) } // If the image has an entrypoint already defined, // it will be overridden either by DefaultEntrypoint, // or by the value in opts.Entrypoint. // If the image does not have an entrypoint, but // opts.Entrypoint is supplied, opts.Entrypoint will // be respected. if len(entrypoint) != 0 && len(opts.Entrypoint) == 0 { opts.Entrypoint = DefaultEntrypoint } // tarDestination will be passed as location to PostExecute function // and will be used as the prefix for the CMD (scripts/run) var tarDestination string var cmd []string if !opts.TargetImage { if len(opts.CommandExplicit) != 0 { cmd = opts.CommandExplicit } else { tarDestination = determineTarDestinationDir(opts, imageMetadata) cmd = constructCommand(opts, imageMetadata, tarDestination) } glog.V(5).Infof("Setting %q command for container ...", strings.Join(cmd, " ")) } createOpts.Config.Cmd = cmd if createOpts.HostConfig != nil && createOpts.HostConfig.ShmSize <= 0 { createOpts.HostConfig.ShmSize = DefaultShmSize } // Create a new container. glog.V(2).Infof("Creating container with options {Name:%q Config:%+v HostConfig:%+v} ...", createOpts.Name, createOpts.Config, createOpts.HostConfig) ctx, cancel := getDefaultContext() defer cancel() container, err := d.client.ContainerCreate(ctx, createOpts.Config, createOpts.HostConfig, createOpts.NetworkingConfig, createOpts.Name) if err != nil { return err } // Container was created, so we defer its removal, and also remove it if we get a SIGINT/SIGTERM/SIGQUIT/SIGHUP. removeContainer := func() { glog.V(4).Infof("Removing container %q ...", container.ID) if removeErr := d.RemoveContainer(container.ID); removeErr != nil { glog.V(0).Infof("warning: Failed to remove container %q: %v", container.ID, removeErr) } else { glog.V(4).Infof("Removed container %q", container.ID) } } dumpStack := func(signal os.Signal) { if signal == syscall.SIGQUIT { buf := make([]byte, 1<<16) runtime.Stack(buf, true) fmt.Printf("%s", buf) } os.Exit(2) } return interrupt.New(dumpStack, removeContainer).Run(func() error { glog.V(2).Infof("Attaching to container %q ...", container.ID) ctx, cancel := getDefaultContext() defer cancel() resp, err := d.client.ContainerAttach(ctx, container.ID, opts.asDockerAttachToContainerOptions()) if err != nil { glog.V(0).Infof("error: Unable to attach to container %q: %v", container.ID, err) return err } defer resp.Close() // Start the container glog.V(2).Infof("Starting container %q ...", container.ID) ctx, cancel = getDefaultContext() defer cancel() err = d.client.ContainerStart(ctx, container.ID) if err != nil { return err } // Run OnStart hook if defined. OnStart might block, so we run it in a // new goroutine, and wait for it to be done later on. onStartDone := make(chan error, 1) if opts.OnStart != nil { go func() { onStartDone <- opts.OnStart(container.ID) }() } if opts.TargetImage { // When TargetImage is true, we're dealing with an invocation of `s2i build ... --run` // so this will, e.g., run a web server and block until the user interrupts it (or // the container exits normally). dump port/etc information for the user. dumpContainerInfo(container, d, image) } err = d.holdHijackedConnection(false, opts.Stdin, opts.Stdout, opts.Stderr, resp) if err != nil { return err } // Return an error if the exit code of the container is // non-zero. glog.V(4).Infof("Waiting for container %q to stop ...", container.ID) exitCode, err := d.client.ContainerWait(context.Background(), container.ID) if err != nil { return fmt.Errorf("waiting for container %q to stop: %v", container.ID, err) } if exitCode != 0 { return s2ierr.NewContainerError(container.ID, exitCode, "") } // OnStart must be done before we move on. if opts.OnStart != nil { if err = <-onStartDone; err != nil { return err } } // Run PostExec hook if defined. if opts.PostExec != nil { glog.V(2).Infof("Invoking PostExecute function") if err = opts.PostExec.PostExecute(container.ID, tarDestination); err != nil { return err } } return nil }) }