package builder import ( "bytes" "fmt" "io" "io/ioutil" "os" "os/exec" "strings" "sync" "github.com/docker/docker/api" "github.com/docker/docker/builder/parser" "github.com/docker/docker/cliconfig" "github.com/docker/docker/daemon" "github.com/docker/docker/graph" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/httputils" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" ) // whitelist of commands allowed for a commit/import var validCommitCommands = map[string]bool{ "entrypoint": true, "cmd": true, "user": true, "workdir": true, "env": true, "volume": true, "expose": true, "onbuild": true, } type Config struct { DockerfileName string RemoteURL string RepoName string SuppressOutput bool NoCache bool Remove bool ForceRemove bool Pull bool JSONFormat bool Memory int64 MemorySwap int64 CpuShares int64 CpuSetCpus string CpuSetMems string AuthConfig *cliconfig.AuthConfig ConfigFile *cliconfig.ConfigFile Stdout io.Writer Context io.ReadCloser // When closed, the job has been cancelled. // Note: not all jobs implement cancellation. // See Job.Cancel() and Job.WaitCancelled() cancelled chan struct{} cancelOnce sync.Once } // When called, causes the Job.WaitCancelled channel to unblock. func (b *Config) Cancel() { b.cancelOnce.Do(func() { close(b.cancelled) }) } // Returns a channel which is closed ("never blocks") when the job is cancelled. func (b *Config) WaitCancelled() <-chan struct{} { return b.cancelled } func NewBuildConfig() *Config { return &Config{ AuthConfig: &cliconfig.AuthConfig{}, ConfigFile: &cliconfig.ConfigFile{}, cancelled: make(chan struct{}), } } func Build(d *daemon.Daemon, buildConfig *Config) error { var ( repoName string tag string context io.ReadCloser ) repoName, tag = parsers.ParseRepositoryTag(buildConfig.RepoName) if repoName != "" { if err := registry.ValidateRepositoryName(repoName); err != nil { return err } if len(tag) > 0 { if err := graph.ValidateTagName(tag); err != nil { return err } } } if buildConfig.RemoteURL == "" { context = ioutil.NopCloser(buildConfig.Context) } else if urlutil.IsGitURL(buildConfig.RemoteURL) { if !urlutil.IsGitTransport(buildConfig.RemoteURL) { buildConfig.RemoteURL = "https://" + buildConfig.RemoteURL } root, err := ioutil.TempDir("", "docker-build-git") if err != nil { return err } defer os.RemoveAll(root) if output, err := exec.Command("git", "clone", "--recursive", buildConfig.RemoteURL, root).CombinedOutput(); err != nil { return fmt.Errorf("Error trying to use git: %s (%s)", err, output) } c, err := archive.Tar(root, archive.Uncompressed) if err != nil { return err } context = c } else if urlutil.IsURL(buildConfig.RemoteURL) { f, err := httputils.Download(buildConfig.RemoteURL) if err != nil { return err } defer f.Body.Close() dockerFile, err := ioutil.ReadAll(f.Body) if err != nil { return err } // When we're downloading just a Dockerfile put it in // the default name - don't allow the client to move/specify it buildConfig.DockerfileName = api.DefaultDockerfileName c, err := archive.Generate(buildConfig.DockerfileName, string(dockerFile)) if err != nil { return err } context = c } defer context.Close() sf := streamformatter.NewStreamFormatter(buildConfig.JSONFormat) builder := &Builder{ Daemon: d, OutStream: &streamformatter.StdoutFormater{ Writer: buildConfig.Stdout, StreamFormatter: sf, }, ErrStream: &streamformatter.StderrFormater{ Writer: buildConfig.Stdout, StreamFormatter: sf, }, Verbose: !buildConfig.SuppressOutput, UtilizeCache: !buildConfig.NoCache, Remove: buildConfig.Remove, ForceRemove: buildConfig.ForceRemove, Pull: buildConfig.Pull, OutOld: buildConfig.Stdout, StreamFormatter: sf, AuthConfig: buildConfig.AuthConfig, ConfigFile: buildConfig.ConfigFile, dockerfileName: buildConfig.DockerfileName, cpuShares: buildConfig.CpuShares, cpuSetCpus: buildConfig.CpuSetCpus, cpuSetMems: buildConfig.CpuSetMems, memory: buildConfig.Memory, memorySwap: buildConfig.MemorySwap, cancelled: buildConfig.WaitCancelled(), } id, err := builder.Run(context) if err != nil { return err } if repoName != "" { return d.Repositories().Tag(repoName, tag, id, true) } return nil } func BuildFromConfig(d *daemon.Daemon, c *runconfig.Config, changes []string) (*runconfig.Config, error) { ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n"))) if err != nil { return nil, err } // ensure that the commands are valid for _, n := range ast.Children { if !validCommitCommands[n.Value] { return nil, fmt.Errorf("%s is not a valid change command", n.Value) } } builder := &Builder{ Daemon: d, Config: c, OutStream: ioutil.Discard, ErrStream: ioutil.Discard, disableCommit: true, } for i, n := range ast.Children { if err := builder.dispatch(i, n); err != nil { return nil, err } } return builder.Config, nil } func Commit(d *daemon.Daemon, name string, c *daemon.ContainerCommitConfig) (string, error) { container, err := d.Get(name) if err != nil { return "", err } newConfig, err := BuildFromConfig(d, c.Config, c.Changes) if err != nil { return "", err } if err := runconfig.Merge(newConfig, container.Config); err != nil { return "", err } img, err := d.Commit(container, c.Repo, c.Tag, c.Comment, c.Author, c.Pause, newConfig) if err != nil { return "", err } return img.ID, nil }