package builder import ( "fmt" "io" "os" "strings" "time" docker "github.com/fsouza/go-dockerclient" "k8s.io/kubernetes/pkg/util/interrupt" utilruntime "k8s.io/kubernetes/pkg/util/runtime" "github.com/openshift/source-to-image/pkg/tar" "github.com/openshift/imagebuilder/imageprogress" ) var ( // DefaultPushRetryCount is the number of retries of pushing the built Docker image // into a configured repository DefaultPushRetryCount = 6 // DefaultPushRetryDelay is the time to wait before triggering a push retry DefaultPushRetryDelay = 5 * time.Second // RetriableErrors is a set of strings that indicate that an retriable error occurred. RetriableErrors = []string{ "ping attempt failed with error", "is already in progress", "connection reset by peer", "transport closed before response was received", } ) // DockerClient is an interface to the Docker client that contains // the methods used by the common builder type DockerClient interface { BuildImage(opts docker.BuildImageOptions) error PushImage(opts docker.PushImageOptions, auth docker.AuthConfiguration) error RemoveImage(name string) error CreateContainer(opts docker.CreateContainerOptions) (*docker.Container, error) DownloadFromContainer(id string, opts docker.DownloadFromContainerOptions) error PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error RemoveContainer(opts docker.RemoveContainerOptions) error InspectImage(name string) (*docker.Image, error) StartContainer(id string, hostConfig *docker.HostConfig) error WaitContainer(id string) (int, error) Logs(opts docker.LogsOptions) error TagImage(name string, opts docker.TagImageOptions) error } func pullImage(client DockerClient, name string, authConfig docker.AuthConfiguration) error { logProgress := func(s string) { glog.V(0).Infof("%s", s) } opts := docker.PullImageOptions{ Repository: name, OutputStream: imageprogress.NewPullWriter(logProgress), RawJSONStream: true, } if glog.Is(5) { opts.OutputStream = os.Stderr opts.RawJSONStream = false } err := client.PullImage(opts, authConfig) if err == nil { return nil } return err } // pushImage pushes a docker image to the registry specified in its tag. // The method will retry to push the image when following scenarios occur: // - Docker registry is down temporarily or permanently // - other image is being pushed to the registry // If any other scenario the push will fail, without retries. func pushImage(client DockerClient, name string, authConfig docker.AuthConfiguration) error { repository, tag := docker.ParseRepositoryTag(name) logProgress := func(s string) { glog.V(0).Infof("%s", s) } opts := docker.PushImageOptions{ Name: repository, Tag: tag, OutputStream: imageprogress.NewPushWriter(logProgress), RawJSONStream: true, } if glog.Is(5) { opts.OutputStream = os.Stderr opts.RawJSONStream = false } var err error var retriableError = false for retries := 0; retries <= DefaultPushRetryCount; retries++ { err = client.PushImage(opts, authConfig) if err == nil { return nil } errMsg := fmt.Sprintf("%s", err) for _, errorString := range RetriableErrors { if strings.Contains(errMsg, errorString) { retriableError = true break } } if !retriableError { return err } utilruntime.HandleError(fmt.Errorf("push for image %s failed, will retry in %s ...", name, DefaultPushRetryDelay)) time.Sleep(DefaultPushRetryDelay) } return err } func removeImage(client DockerClient, name string) error { return client.RemoveImage(name) } // buildImage invokes a docker build on a particular directory func buildImage(client DockerClient, dir string, tar tar.Tar, opts *docker.BuildImageOptions) error { // TODO: be able to pass a stream directly to the Docker build to avoid the double temp hit if opts == nil { return fmt.Errorf("%s", "build image options nil") } r, w := io.Pipe() go func() { defer utilruntime.HandleCrash() defer w.Close() if err := tar.CreateTarStream(dir, false, w); err != nil { w.CloseWithError(err) } }() defer w.Close() opts.InputStream = r glog.V(5).Infof("Invoking Docker build to create %q", opts.Name) return client.BuildImage(*opts) } // tagImage uses the dockerClient to tag a Docker image with name. It is a // helper to facilitate the usage of dockerClient.TagImage, because the former // requires the name to be split into more explicit parts. func tagImage(dockerClient DockerClient, image, name string) error { repo, tag := docker.ParseRepositoryTag(name) return dockerClient.TagImage(image, docker.TagImageOptions{ Repo: repo, Tag: tag, // We need to set Force to true to update the tag even if it // already exists. This is the same behavior as `docker build -t // tag .`. Force: true, }) } // dockerRun mimics the 'docker run --rm' CLI command. It uses the Docker Remote // API to create and start a container and stream its logs. The container is // removed after it terminates. func dockerRun(client DockerClient, createOpts docker.CreateContainerOptions, logsOpts docker.LogsOptions) error { // Create a new container. glog.V(4).Infof("Creating container with options {Name:%q Config:%+v HostConfig:%+v} ...", createOpts.Name, createOpts.Config, createOpts.HostConfig) c, err := client.CreateContainer(createOpts) if err != nil { return fmt.Errorf("create container %q: %v", createOpts.Name, err) } containerName := getContainerNameOrID(c) removeContainer := func() { glog.V(4).Infof("Removing container %q ...", containerName) if err := client.RemoveContainer(docker.RemoveContainerOptions{ID: c.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) } } startWaitContainer := func() error { // Start the container. glog.V(4).Infof("Starting container %q ...", containerName) if err := client.StartContainer(c.ID, nil); err != nil { return fmt.Errorf("start container %q: %v", containerName, err) } // Stream container logs. logsOpts.Container = c.ID glog.V(4).Infof("Streaming logs of container %q with options %+v ...", containerName, logsOpts) if err := client.Logs(logsOpts); err != nil { return fmt.Errorf("streaming logs of %q: %v", containerName, err) } // 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 := client.WaitContainer(c.ID) if err != nil { return fmt.Errorf("waiting for container %q to stop: %v", containerName, err) } if exitCode != 0 { return fmt.Errorf("container %q returned non-zero exit code: %d", containerName, exitCode) } return nil } // the interrupt handler acts as a super-defer which will guarantee removeContainer is executed // either when startWaitContainer finishes, or when a SIGQUIT/SIGINT/SIGTERM is received. return interrupt.New(nil, removeContainer).Run(startWaitContainer) } func getContainerNameOrID(c *docker.Container) string { if c.Name != "" { return c.Name } return c.ID }