diff --git a/build/build.go b/build/build.go index 1783e087..0b83999b 100644 --- a/build/build.go +++ b/build/build.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "sync" + "syscall" "time" "github.com/containerd/containerd/images" @@ -79,6 +80,7 @@ type Inputs struct { InStream io.Reader ContextState *llb.State DockerfileInline string + NamedContexts map[string]string } type DriverInfo struct { @@ -1080,6 +1082,27 @@ func LoadInputs(ctx context.Context, d driver.Driver, inp Inputs, pw progress.Wr target.FrontendAttrs["filename"] = dockerfileName + for k, v := range inp.NamedContexts { + target.FrontendAttrs["frontend.caps"] = "moby.buildkit.frontend.contexts+forward" + if urlutil.IsGitURL(v) || urlutil.IsURL(v) || strings.HasPrefix(v, "docker-image://") { + target.FrontendAttrs["context:"+k] = v + continue + } + st, err := os.Stat(v) + if err != nil { + return nil, errors.Wrapf(err, "failed to get build context %v", k) + } + if !st.IsDir() { + return nil, errors.Wrapf(syscall.ENOTDIR, "failed to get build context path %v", v) + } + localName := k + if k == "context" || k == "dockerfile" { + localName = "_" + k // underscore to avoid collisions + } + target.LocalDirs[localName] = v + target.FrontendAttrs["context:"+k] = "local:" + localName + } + release := func() { for _, dir := range toRemove { os.RemoveAll(dir) diff --git a/commands/build.go b/commands/build.go index dae6d326..2861820c 100644 --- a/commands/build.go +++ b/commands/build.go @@ -20,17 +20,20 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" dockeropts "github.com/docker/cli/opts" + "github.com/docker/distribution/reference" "github.com/docker/docker/pkg/ioutils" "github.com/docker/go-units" "github.com/moby/buildkit/client" "github.com/moby/buildkit/session/auth/authprovider" "github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/util/appcontext" + "github.com/moby/buildkit/util/grpcerrors" "github.com/morikuni/aec" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" + "google.golang.org/grpc/codes" ) const defaultTargetName = "default" @@ -44,6 +47,7 @@ type buildOptions struct { cacheFrom []string cacheTo []string cgroupParent string + contexts []string extraHosts []string imageIDFile string labels []string @@ -100,11 +104,17 @@ func runBuild(dockerCli command.Cli, in buildOptions) (err error) { in.progress = "quiet" } + contexts, err := parseContextNames(in.contexts) + if err != nil { + return err + } + opts := build.Options{ Inputs: build.Inputs{ ContextPath: in.contextPath, DockerfilePath: in.dockerfileName, InStream: os.Stdin, + NamedContexts: contexts, }, BuildArgs: listToMap(in.buildArgs, true), ExtraHosts: in.extraHosts, @@ -209,6 +219,7 @@ func runBuild(dockerCli command.Cli, in buildOptions) (err error) { } imageID, err := buildTargets(ctx, dockerCli, map[string]build.Options{defaultTargetName: opts}, in.progress, contextPathHash, in.builder, in.metadataFile) + err = wrapBuildError(err) if err != nil { return err } @@ -339,6 +350,8 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") flags.SetAnnotation("cgroup-parent", annotation.ExternalURL, []string{"https://docs.docker.com/engine/reference/commandline/build/#use-a-custom-parent-cgroup---cgroup-parent"}) + flags.StringArrayVar(&options.contexts, "build-context", []string{}, "Additional build contexts (e.g., name=path)") + flags.StringVarP(&options.dockerfileName, "file", "f", "", `Name of the Dockerfile (default: "PATH/Dockerfile")`) flags.SetAnnotation("file", annotation.ExternalURL, []string{"https://docs.docker.com/engine/reference/commandline/build/#specify-a-dockerfile--f"}) @@ -462,3 +475,49 @@ func listToMap(values []string, defaultEnv bool) map[string]string { } return result } + +func parseContextNames(values []string) (map[string]string, error) { + if len(values) == 0 { + return nil, nil + } + result := make(map[string]string, len(values)) + for _, value := range values { + kv := strings.SplitN(value, "=", 2) + if len(kv) != 2 { + return nil, errors.Errorf("invalid context value: %s, expected key=value", value) + } + named, err := reference.ParseNormalizedNamed(kv[0]) + if err != nil { + return nil, errors.Wrapf(err, "invalid context name %s", kv[0]) + } + name := strings.TrimSuffix(reference.FamiliarString(named), ":latest") + result[name] = kv[1] + } + return result, nil +} + +func wrapBuildError(err error) error { + if err == nil { + return nil + } + st, ok := grpcerrors.AsGRPCStatus(err) + if ok { + if st.Code() == codes.Unimplemented && strings.Contains(st.Message(), "unsupported frontend capability moby.buildkit.frontend.contexts") { + return &wrapped{err, "current frontend does not support --build-context. Named contexts are supported since Dockerfile v1.4"} + } + } + return err +} + +type wrapped struct { + err error + msg string +} + +func (w *wrapped) Error() string { + return w.msg +} + +func (w *wrapped) Unwrap() error { + return w.err +} diff --git a/docs/reference/buildx_build.md b/docs/reference/buildx_build.md index 49b24d02..c391bd93 100644 --- a/docs/reference/buildx_build.md +++ b/docs/reference/buildx_build.md @@ -18,6 +18,7 @@ Start a build | [`--add-host stringSlice`](https://docs.docker.com/engine/reference/commandline/build/#add-entries-to-container-hosts-file---add-host) | Add a custom host-to-IP mapping (format: `host:ip`) | | [`--allow stringSlice`](#allow) | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`) | | [`--build-arg stringArray`](https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg) | Set build-time variables | +| [`--build-context stringArray`](#build-context) | Additional build contexts (e.g., name=path) | | [`--builder string`](#builder) | Override the configured builder instance | | [`--cache-from stringArray`](#cache-from) | External cache sources (e.g., `user/app:cache`, `type=local,src=path/to/dir`) | | [`--cache-to stringArray`](#cache-to) | Cache export destinations (e.g., `user/app:cache`, `type=local,dest=path/to/dir`) | @@ -303,6 +304,39 @@ $ docker buildx build --cache-to=type=gha . More info about cache exporters and available attributes: https://github.com/moby/buildkit#export-cache + +### Additional build contexts (--build-context) + +``` +--build-context=name=VALUE +``` + +Define additional build context with specified contents. In Dockerfile the context can be accessed when `FROM name` or `--from=name` is used. +When Dockerfile defines a stage with the same name it is overwritten. + +The value can be a local source directory, container image (with docker-image:// prefix), Git or HTTP URL. + +**Examples** + +Replace `alpine:latest` with a pinned one: + +```console +$ docker buildx build --build-context alpine=docker-image://alpine@sha256:0123456789 . +``` + +Expose a secondary local source directory: + +```console +$ docker buildx build --build-context project=path/to/project/source . +# docker buildx build --build-context project=https://github.com/myuser/project.git . +``` + +```Dockerfile +FROM alpine +COPY --from=project myfile / +``` + + ### Allow extra privileged entitlement (--allow) ``` diff --git a/go.mod b/go.mod index dc7e0e0d..d05fdca5 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( go.opentelemetry.io/otel v1.2.0 go.opentelemetry.io/otel/trace v1.2.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + google.golang.org/grpc v1.42.0 gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect diff --git a/vendor/modules.txt b/vendor/modules.txt index 2994568b..d90a129b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -566,6 +566,7 @@ google.golang.org/genproto/googleapis/rpc/errdetails google.golang.org/genproto/googleapis/rpc/status google.golang.org/genproto/protobuf/field_mask # google.golang.org/grpc v1.42.0 +## explicit google.golang.org/grpc google.golang.org/grpc/attributes google.golang.org/grpc/backoff