|
|
@@ -0,0 +1,269 @@
|
|
|
+/*
|
|
|
+ Copyright 2020 Docker Compose CLI authors
|
|
|
+
|
|
|
+ Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+ you may not use this file except in compliance with the License.
|
|
|
+ You may obtain a copy of the License at
|
|
|
+
|
|
|
+ http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+
|
|
|
+ Unless required by applicable law or agreed to in writing, software
|
|
|
+ distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+ See the License for the specific language governing permissions and
|
|
|
+ limitations under the License.
|
|
|
+*/
|
|
|
+
|
|
|
+package compose
|
|
|
+
|
|
|
+import (
|
|
|
+ "context"
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "io/ioutil"
|
|
|
+ "os"
|
|
|
+ "path/filepath"
|
|
|
+ "runtime"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ buildx "github.com/docker/buildx/build"
|
|
|
+ "github.com/docker/cli/cli/command"
|
|
|
+ "github.com/docker/cli/cli/command/image/build"
|
|
|
+ dockertypes "github.com/docker/docker/api/types"
|
|
|
+ "github.com/docker/docker/cli"
|
|
|
+ "github.com/docker/docker/pkg/archive"
|
|
|
+ "github.com/docker/docker/pkg/idtools"
|
|
|
+ "github.com/docker/docker/pkg/jsonmessage"
|
|
|
+ "github.com/docker/docker/pkg/progress"
|
|
|
+ "github.com/docker/docker/pkg/streamformatter"
|
|
|
+ "github.com/docker/docker/pkg/urlutil"
|
|
|
+ "github.com/hashicorp/go-multierror"
|
|
|
+ "github.com/pkg/errors"
|
|
|
+)
|
|
|
+
|
|
|
+func (s *composeService) doBuildClassic(ctx context.Context, dockerCli *command.DockerCli, opts map[string]buildx.Options) (map[string]string, error) {
|
|
|
+ var nameDigests = make(map[string]string)
|
|
|
+ var errs error
|
|
|
+ for name, o := range opts {
|
|
|
+ digest, err := doBuildClassicSimpleImage(ctx, dockerCli, o)
|
|
|
+ if err != nil {
|
|
|
+ errs = multierror.Append(errs, err).ErrorOrNil()
|
|
|
+ }
|
|
|
+ nameDigests[name] = digest
|
|
|
+ }
|
|
|
+
|
|
|
+ return nameDigests, errs
|
|
|
+}
|
|
|
+
|
|
|
+// nolint: gocyclo
|
|
|
+func doBuildClassicSimpleImage(ctx context.Context, dockerCli *command.DockerCli, options buildx.Options) (string, error) {
|
|
|
+ var (
|
|
|
+ buildCtx io.ReadCloser
|
|
|
+ dockerfileCtx io.ReadCloser
|
|
|
+ contextDir string
|
|
|
+ tempDir string
|
|
|
+ relDockerfile string
|
|
|
+
|
|
|
+ err error
|
|
|
+ )
|
|
|
+
|
|
|
+ dockerfileName := options.Inputs.DockerfilePath
|
|
|
+ specifiedContext := options.Inputs.ContextPath
|
|
|
+ progBuff := dockerCli.Out()
|
|
|
+ buildBuff := dockerCli.Out()
|
|
|
+ if options.ImageIDFile != "" {
|
|
|
+ // Avoid leaving a stale file if we eventually fail
|
|
|
+ if err := os.Remove(options.ImageIDFile); err != nil && !os.IsNotExist(err) {
|
|
|
+ return "", errors.Wrap(err, "removing image ID file")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ switch {
|
|
|
+ case isLocalDir(specifiedContext):
|
|
|
+ contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, dockerfileName)
|
|
|
+ if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) {
|
|
|
+ // Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx
|
|
|
+ dockerfileCtx, err = os.Open(dockerfileName)
|
|
|
+ if err != nil {
|
|
|
+ return "", errors.Errorf("unable to open Dockerfile: %v", err)
|
|
|
+ }
|
|
|
+ defer dockerfileCtx.Close() // nolint:errcheck
|
|
|
+ }
|
|
|
+ case urlutil.IsGitURL(specifiedContext):
|
|
|
+ tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, dockerfileName)
|
|
|
+ case urlutil.IsURL(specifiedContext):
|
|
|
+ buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, dockerfileName)
|
|
|
+ default:
|
|
|
+ return "", errors.Errorf("unable to prepare context: path %q not found", specifiedContext)
|
|
|
+ }
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return "", errors.Errorf("unable to prepare context: %s", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if tempDir != "" {
|
|
|
+ defer os.RemoveAll(tempDir) // nolint:errcheck
|
|
|
+ contextDir = tempDir
|
|
|
+ }
|
|
|
+
|
|
|
+ // read from a directory into tar archive
|
|
|
+ if buildCtx == nil {
|
|
|
+ excludes, err := build.ReadDockerignore(contextDir)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := build.ValidateContextDirectory(contextDir, excludes); err != nil {
|
|
|
+ return "", errors.Errorf("error checking context: '%s'.", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // And canonicalize dockerfile name to a platform-independent one
|
|
|
+ relDockerfile = archive.CanonicalTarNameForPath(relDockerfile)
|
|
|
+
|
|
|
+ excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, false)
|
|
|
+ buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
|
|
|
+ ExcludePatterns: excludes,
|
|
|
+ ChownOpts: &idtools.Identity{UID: 0, GID: 0},
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // replace Dockerfile if it was added from stdin or a file outside the build-context, and there is archive context
|
|
|
+ if dockerfileCtx != nil && buildCtx != nil {
|
|
|
+ buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ buildCtx, err = build.Compress(buildCtx)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Setup an upload progress bar
|
|
|
+ progressOutput := streamformatter.NewProgressOutput(progBuff)
|
|
|
+ if !dockerCli.Out().IsTerminal() {
|
|
|
+ progressOutput = &lastProgressOutput{output: progressOutput}
|
|
|
+ }
|
|
|
+
|
|
|
+ // if up to this point nothing has set the context then we must have another
|
|
|
+ // way for sending it(streaming) and set the context to the Dockerfile
|
|
|
+ if dockerfileCtx != nil && buildCtx == nil {
|
|
|
+ buildCtx = dockerfileCtx
|
|
|
+ }
|
|
|
+
|
|
|
+ var body io.Reader
|
|
|
+ if buildCtx != nil {
|
|
|
+ body = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
|
|
|
+ }
|
|
|
+
|
|
|
+ configFile := dockerCli.ConfigFile()
|
|
|
+ creds, _ := configFile.GetAllCredentials()
|
|
|
+ authConfigs := make(map[string]dockertypes.AuthConfig, len(creds))
|
|
|
+ for k, auth := range creds {
|
|
|
+ authConfigs[k] = dockertypes.AuthConfig(auth)
|
|
|
+ }
|
|
|
+ buildOptions := imageBuildOptions(options)
|
|
|
+ buildOptions.Version = dockertypes.BuilderV1
|
|
|
+ buildOptions.Dockerfile = relDockerfile
|
|
|
+ buildOptions.AuthConfigs = authConfigs
|
|
|
+
|
|
|
+ ctx, cancel := context.WithCancel(ctx)
|
|
|
+ defer cancel()
|
|
|
+ response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ defer response.Body.Close() // nolint:errcheck
|
|
|
+
|
|
|
+ imageID := ""
|
|
|
+ aux := func(msg jsonmessage.JSONMessage) {
|
|
|
+ var result dockertypes.BuildResult
|
|
|
+ if err := json.Unmarshal(*msg.Aux, &result); err != nil {
|
|
|
+ fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err)
|
|
|
+ } else {
|
|
|
+ imageID = result.ID
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), aux)
|
|
|
+ if err != nil {
|
|
|
+ if jerr, ok := err.(*jsonmessage.JSONError); ok {
|
|
|
+ // If no error code is set, default to 1
|
|
|
+ if jerr.Code == 0 {
|
|
|
+ jerr.Code = 1
|
|
|
+ }
|
|
|
+ return "", cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
|
|
|
+ }
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Windows: show error message about modified file permissions if the
|
|
|
+ // daemon isn't running Windows.
|
|
|
+ if response.OSType != "windows" && runtime.GOOS == "windows" {
|
|
|
+ // if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
|
|
|
+ fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+
|
|
|
+ "image from Windows against a non-Windows Docker host. All files and "+
|
|
|
+ "directories added to build context will have '-rwxr-xr-x' permissions. "+
|
|
|
+ "It is recommended to double check and reset permissions for sensitive "+
|
|
|
+ "files and directories.")
|
|
|
+ }
|
|
|
+
|
|
|
+ if options.ImageIDFile != "" {
|
|
|
+ if imageID == "" {
|
|
|
+ return "", errors.Errorf("Server did not provide an image ID. Cannot write %s", options.ImageIDFile)
|
|
|
+ }
|
|
|
+ if err := ioutil.WriteFile(options.ImageIDFile, []byte(imageID), 0666); err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return imageID, nil
|
|
|
+}
|
|
|
+
|
|
|
+func isLocalDir(c string) bool {
|
|
|
+ _, err := os.Stat(c)
|
|
|
+ return err == nil
|
|
|
+}
|
|
|
+
|
|
|
+func imageBuildOptions(options buildx.Options) dockertypes.ImageBuildOptions {
|
|
|
+ return dockertypes.ImageBuildOptions{
|
|
|
+ Tags: options.Tags,
|
|
|
+ NoCache: options.NoCache,
|
|
|
+ PullParent: options.Pull,
|
|
|
+ BuildArgs: toMapStringStringPtr(options.BuildArgs),
|
|
|
+ Labels: options.Labels,
|
|
|
+ NetworkMode: options.NetworkMode,
|
|
|
+ ExtraHosts: options.ExtraHosts,
|
|
|
+ Target: options.Target,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func toMapStringStringPtr(source map[string]string) map[string]*string {
|
|
|
+ dest := make(map[string]*string)
|
|
|
+ for k, v := range source {
|
|
|
+ v := v
|
|
|
+ dest[k] = &v
|
|
|
+ }
|
|
|
+ return dest
|
|
|
+}
|
|
|
+
|
|
|
+// lastProgressOutput is the same as progress.Output except
|
|
|
+// that it only output with the last update. It is used in
|
|
|
+// non terminal scenarios to suppress verbose messages
|
|
|
+type lastProgressOutput struct {
|
|
|
+ output progress.Output
|
|
|
+}
|
|
|
+
|
|
|
+// WriteProgress formats progress information from a ProgressReader.
|
|
|
+func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
|
|
|
+ if !prog.LastUpdate {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return out.output.WriteProgress(prog)
|
|
|
+}
|