| package dockerfile |
| |
| // internals for handling commands. Covers many areas and a lot of |
| // non-contiguous functionality. Please read the comments. |
| |
| import ( |
| "crypto/sha256" |
| "encoding/hex" |
| "fmt" |
| "strings" |
| |
| "github.com/Sirupsen/logrus" |
| "github.com/docker/docker/api/types" |
| "github.com/docker/docker/api/types/backend" |
| "github.com/docker/docker/api/types/container" |
| containerpkg "github.com/docker/docker/container" |
| "github.com/docker/docker/pkg/jsonmessage" |
| "github.com/docker/docker/pkg/stringid" |
| "github.com/pkg/errors" |
| ) |
| |
| func (b *Builder) commit(dispatchState *dispatchState, comment string) error { |
| if b.disableCommit { |
| return nil |
| } |
| if !dispatchState.hasFromImage() { |
| return errors.New("Please provide a source image with `from` prior to commit") |
| } |
| |
| runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment)) |
| hit, err := b.probeCache(dispatchState, runConfigWithCommentCmd) |
| if err != nil || hit { |
| return err |
| } |
| id, err := b.create(runConfigWithCommentCmd) |
| if err != nil { |
| return err |
| } |
| |
| return b.commitContainer(dispatchState, id, runConfigWithCommentCmd) |
| } |
| |
| // TODO: see if any args can be dropped |
| func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error { |
| if b.disableCommit { |
| return nil |
| } |
| |
| commitCfg := &backend.ContainerCommitConfig{ |
| ContainerCommitConfig: types.ContainerCommitConfig{ |
| Author: dispatchState.maintainer, |
| Pause: true, |
| // TODO: this should be done by Commit() |
| Config: copyRunConfig(dispatchState.runConfig), |
| }, |
| ContainerConfig: containerConfig, |
| } |
| |
| // Commit the container |
| imageID, err := b.docker.Commit(id, commitCfg) |
| if err != nil { |
| return err |
| } |
| |
| dispatchState.imageID = imageID |
| b.buildStages.update(imageID, dispatchState.runConfig) |
| return nil |
| } |
| |
| func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error { |
| srcHash := getSourceHashFromInfos(inst.infos) |
| |
| // TODO: should this have been using origPaths instead of srcHash in the comment? |
| runConfigWithCommentCmd := copyRunConfig( |
| state.runConfig, |
| withCmdCommentString(fmt.Sprintf("%s %s in %s ", inst.cmdName, srcHash, inst.dest))) |
| if hit, err := b.probeCache(state, runConfigWithCommentCmd); err != nil || hit { |
| return err |
| } |
| |
| container, err := b.docker.ContainerCreate(types.ContainerCreateConfig{ |
| Config: runConfigWithCommentCmd, |
| // Set a log config to override any default value set on the daemon |
| HostConfig: &container.HostConfig{LogConfig: defaultLogConfig}, |
| }) |
| if err != nil { |
| return err |
| } |
| b.tmpContainers[container.ID] = struct{}{} |
| |
| // Twiddle the destination when it's a relative path - meaning, make it |
| // relative to the WORKINGDIR |
| dest, err := normaliseDest(inst.cmdName, state.runConfig.WorkingDir, inst.dest) |
| if err != nil { |
| return err |
| } |
| |
| for _, info := range inst.infos { |
| if err := b.docker.CopyOnBuild(container.ID, dest, info.root, info.path, inst.allowLocalDecompression); err != nil { |
| return err |
| } |
| } |
| return b.commitContainer(state, container.ID, runConfigWithCommentCmd) |
| } |
| |
| // For backwards compat, if there's just one info then use it as the |
| // cache look-up string, otherwise hash 'em all into one |
| func getSourceHashFromInfos(infos []copyInfo) string { |
| if len(infos) == 1 { |
| return infos[0].hash |
| } |
| var hashs []string |
| for _, info := range infos { |
| hashs = append(hashs, info.hash) |
| } |
| return hashStringSlice("multi", hashs) |
| } |
| |
| func hashStringSlice(prefix string, slice []string) string { |
| hasher := sha256.New() |
| hasher.Write([]byte(strings.Join(slice, ","))) |
| return prefix + ":" + hex.EncodeToString(hasher.Sum(nil)) |
| } |
| |
| type runConfigModifier func(*container.Config) |
| |
| func copyRunConfig(runConfig *container.Config, modifiers ...runConfigModifier) *container.Config { |
| copy := *runConfig |
| for _, modifier := range modifiers { |
| modifier(©) |
| } |
| return © |
| } |
| |
| func withCmd(cmd []string) runConfigModifier { |
| return func(runConfig *container.Config) { |
| runConfig.Cmd = cmd |
| } |
| } |
| |
| // withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for |
| // why there are two almost identical versions of this. |
| func withCmdComment(comment string) runConfigModifier { |
| return func(runConfig *container.Config) { |
| runConfig.Cmd = append(getShell(runConfig), "#(nop) ", comment) |
| } |
| } |
| |
| // withCmdCommentString exists to maintain compatibility with older versions. |
| // A few instructions (workdir, copy, add) used a nop comment that is a single arg |
| // where as all the other instructions used a two arg comment string. This |
| // function implements the single arg version. |
| func withCmdCommentString(comment string) runConfigModifier { |
| return func(runConfig *container.Config) { |
| runConfig.Cmd = append(getShell(runConfig), "#(nop) "+comment) |
| } |
| } |
| |
| func withEnv(env []string) runConfigModifier { |
| return func(runConfig *container.Config) { |
| runConfig.Env = env |
| } |
| } |
| |
| // withEntrypointOverride sets an entrypoint on runConfig if the command is |
| // not empty. The entrypoint is left unmodified if command is empty. |
| // |
| // The dockerfile RUN instruction expect to run without an entrypoint |
| // so the runConfig entrypoint needs to be modified accordingly. ContainerCreate |
| // will change a []string{""} entrypoint to nil, so we probe the cache with the |
| // nil entrypoint. |
| func withEntrypointOverride(cmd []string, entrypoint []string) runConfigModifier { |
| return func(runConfig *container.Config) { |
| if len(cmd) > 0 { |
| runConfig.Entrypoint = entrypoint |
| } |
| } |
| } |
| |
| // getShell is a helper function which gets the right shell for prefixing the |
| // shell-form of RUN, ENTRYPOINT and CMD instructions |
| func getShell(c *container.Config) []string { |
| if 0 == len(c.Shell) { |
| return append([]string{}, defaultShell[:]...) |
| } |
| return append([]string{}, c.Shell[:]...) |
| } |
| |
| // probeCache checks if cache match can be found for current build instruction. |
| // If an image is found, probeCache returns `(true, nil)`. |
| // If no image is found, it returns `(false, nil)`. |
| // If there is any error, it returns `(false, err)`. |
| func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) { |
| c := b.imageCache |
| if c == nil || b.options.NoCache || b.cacheBusted { |
| return false, nil |
| } |
| cache, err := c.GetCache(dispatchState.imageID, runConfig) |
| if err != nil { |
| return false, err |
| } |
| if len(cache) == 0 { |
| logrus.Debugf("[BUILDER] Cache miss: %s", runConfig.Cmd) |
| b.cacheBusted = true |
| return false, nil |
| } |
| |
| fmt.Fprint(b.Stdout, " ---> Using cache\n") |
| logrus.Debugf("[BUILDER] Use cached version: %s", runConfig.Cmd) |
| dispatchState.imageID = string(cache) |
| b.buildStages.update(dispatchState.imageID, runConfig) |
| |
| return true, nil |
| } |
| |
| func (b *Builder) create(runConfig *container.Config) (string, error) { |
| resources := container.Resources{ |
| CgroupParent: b.options.CgroupParent, |
| CPUShares: b.options.CPUShares, |
| CPUPeriod: b.options.CPUPeriod, |
| CPUQuota: b.options.CPUQuota, |
| CpusetCpus: b.options.CPUSetCPUs, |
| CpusetMems: b.options.CPUSetMems, |
| Memory: b.options.Memory, |
| MemorySwap: b.options.MemorySwap, |
| Ulimits: b.options.Ulimits, |
| } |
| |
| // TODO: why not embed a hostconfig in builder? |
| hostConfig := &container.HostConfig{ |
| SecurityOpt: b.options.SecurityOpt, |
| Isolation: b.options.Isolation, |
| ShmSize: b.options.ShmSize, |
| Resources: resources, |
| NetworkMode: container.NetworkMode(b.options.NetworkMode), |
| // Set a log config to override any default value set on the daemon |
| LogConfig: defaultLogConfig, |
| ExtraHosts: b.options.ExtraHosts, |
| } |
| |
| // Create the container |
| c, err := b.docker.ContainerCreate(types.ContainerCreateConfig{ |
| Config: runConfig, |
| HostConfig: hostConfig, |
| }) |
| if err != nil { |
| return "", err |
| } |
| for _, warning := range c.Warnings { |
| fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning) |
| } |
| |
| b.tmpContainers[c.ID] = struct{}{} |
| fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(c.ID)) |
| return c.ID, nil |
| } |
| |
| var errCancelled = errors.New("build cancelled") |
| |
| func (b *Builder) run(cID string, cmd []string) (err error) { |
| attached := make(chan struct{}) |
| errCh := make(chan error) |
| go func() { |
| errCh <- b.docker.ContainerAttachRaw(cID, nil, b.Stdout, b.Stderr, true, attached) |
| }() |
| |
| select { |
| case err := <-errCh: |
| return err |
| case <-attached: |
| } |
| |
| finished := make(chan struct{}) |
| cancelErrCh := make(chan error, 1) |
| go func() { |
| select { |
| case <-b.clientCtx.Done(): |
| logrus.Debugln("Build cancelled, killing and removing container:", cID) |
| b.docker.ContainerKill(cID, 0) |
| b.removeContainer(cID) |
| cancelErrCh <- errCancelled |
| case <-finished: |
| cancelErrCh <- nil |
| } |
| }() |
| |
| if err := b.docker.ContainerStart(cID, nil, "", ""); err != nil { |
| close(finished) |
| if cancelErr := <-cancelErrCh; cancelErr != nil { |
| logrus.Debugf("Build cancelled (%v) and got an error from ContainerStart: %v", |
| cancelErr, err) |
| } |
| return err |
| } |
| |
| // Block on reading output from container, stop on err or chan closed |
| if err := <-errCh; err != nil { |
| close(finished) |
| if cancelErr := <-cancelErrCh; cancelErr != nil { |
| logrus.Debugf("Build cancelled (%v) and got an error from errCh: %v", |
| cancelErr, err) |
| } |
| return err |
| } |
| |
| waitC, err := b.docker.ContainerWait(b.clientCtx, cID, containerpkg.WaitConditionNotRunning) |
| if err != nil { |
| // Unable to begin waiting for container. |
| close(finished) |
| if cancelErr := <-cancelErrCh; cancelErr != nil { |
| logrus.Debugf("Build cancelled (%v) and unable to begin ContainerWait: %d", cancelErr, err) |
| } |
| return err |
| } |
| |
| if status := <-waitC; status.ExitCode() != 0 { |
| close(finished) |
| if cancelErr := <-cancelErrCh; cancelErr != nil { |
| logrus.Debugf("Build cancelled (%v) and got a non-zero code from ContainerWait: %d", cancelErr, status.ExitCode()) |
| } |
| // TODO: change error type, because jsonmessage.JSONError assumes HTTP |
| return &jsonmessage.JSONError{ |
| Message: fmt.Sprintf("The command '%s' returned a non-zero code: %d", strings.Join(cmd, " "), status.ExitCode()), |
| Code: status.ExitCode(), |
| } |
| } |
| close(finished) |
| return <-cancelErrCh |
| } |
| |
| func (b *Builder) removeContainer(c string) error { |
| rmConfig := &types.ContainerRmConfig{ |
| ForceRemove: true, |
| RemoveVolume: true, |
| } |
| if err := b.docker.ContainerRm(c, rmConfig); err != nil { |
| fmt.Fprintf(b.Stdout, "Error removing intermediate container %s: %v\n", stringid.TruncateID(c), err) |
| return err |
| } |
| return nil |
| } |
| |
| func (b *Builder) clearTmp() { |
| for c := range b.tmpContainers { |
| if err := b.removeContainer(c); err != nil { |
| return |
| } |
| delete(b.tmpContainers, c) |
| fmt.Fprintf(b.Stdout, "Removing intermediate container %s\n", stringid.TruncateID(c)) |
| } |
| } |