diff --git a/e2e/build_test.go b/e2e/build_test.go index a61e0f3ff..79cea17f7 100644 --- a/e2e/build_test.go +++ b/e2e/build_test.go @@ -68,6 +68,50 @@ func TestBuildWithoutTag(t *testing.T) { }) } +func TestBuildWithArgs(t *testing.T) { + runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { + cmd := info.configuredCmd + + testDir := path.Join("testdata", "build") + cmd.Command = dockerCli.Command("app", "build", path.Join(testDir, "single"), "--build-arg", "REPLACE_BY_BUILD_ARG=replaced") + icmd.RunCmd(cmd).Assert(t, icmd.Success) + + cfg := getDockerConfigDir(t, cmd) + + f := path.Join(cfg, "app", "bundles", "_ids") + infos, err := ioutil.ReadDir(f) + assert.NilError(t, err) + assert.Equal(t, len(infos), 1) + id := infos[0].Name() + + f = path.Join(cfg, "app", "bundles", "_ids", id, "bundle.json") + data, err := ioutil.ReadFile(f) + assert.NilError(t, err) + var bndl bundle.Bundle + err = json.Unmarshal(data, &bndl) + assert.NilError(t, err) + + cmd.Command = dockerCli.Command("inspect", bndl.Images["worker"].Digest) + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + ExitCode: 0, + Out: `"com.docker.labelled.arg": "replaced"`, + }) + }) +} + +func TestBuildWithArgsDefinedTwice(t *testing.T) { + runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { + cmd := info.configuredCmd + + testDir := path.Join("testdata", "build") + cmd.Command = dockerCli.Command("app", "build", path.Join(testDir, "single"), "--build-arg", "REPLACE_BY_BUILD_ARG=replaced", "--build-arg", "REPLACE_BY_BUILD_ARG=replaced_twice") + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + ExitCode: 1, + Err: `'--build-arg REPLACE_BY_BUILD_ARG' is defined twice`, + }) + }) +} + func getDockerConfigDir(t *testing.T, cmd icmd.Cmd) string { var cfg string for _, s := range cmd.Env { diff --git a/e2e/testdata/build/single.dockerapp/docker-compose.yml b/e2e/testdata/build/single.dockerapp/docker-compose.yml index 59c58173a..b5852455e 100644 --- a/e2e/testdata/build/single.dockerapp/docker-compose.yml +++ b/e2e/testdata/build/single.dockerapp/docker-compose.yml @@ -7,6 +7,9 @@ services: worker: build: context: ./worker + args: + - REPLACE_BY_BUILD_ARG=original + - STATIC_ARG=static dockerfile: Dockerfile.worker db: image: postgres:9.3 diff --git a/e2e/testdata/build/worker/Dockerfile.worker b/e2e/testdata/build/worker/Dockerfile.worker index 449fd7468..fb578cd2f 100644 --- a/e2e/testdata/build/worker/Dockerfile.worker +++ b/e2e/testdata/build/worker/Dockerfile.worker @@ -1 +1,6 @@ -FROM scratch \ No newline at end of file +FROM scratch +ARG REPLACE_BY_BUILD_ARG +ARG STATIC_ARG +LABEL com.docker.labelled.arg=$REPLACE_BY_BUILD_ARG +LABEL com.docker.labelled.optional=$STATIC_ARG + diff --git a/internal/commands/build/build.go b/internal/commands/build/build.go index 06f1e9991..72b15f545 100644 --- a/internal/commands/build/build.go +++ b/internal/commands/build/build.go @@ -34,6 +34,7 @@ type buildOptions struct { progress string pull bool tag string + args []string } func Cmd(dockerCli command.Cli) *cobra.Command { @@ -57,6 +58,7 @@ func Cmd(dockerCli command.Cli) *cobra.Command { flags.StringVar(&opts.progress, "progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output") flags.StringVarP(&opts.tag, "tag", "t", "", "Application image and optionally a tag in the 'image:tag' format") flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image") + flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables") return cmd } @@ -67,6 +69,11 @@ func runBuild(dockerCli command.Cli, application string, opt buildOptions) (refe return nil, err } + err = checkBuildArgsUniqueness(opt.args) + if err != nil { + return nil, err + } + var ref reference.Reference ref, err = packager.GetNamedTagged(opt.tag) if err != nil { @@ -211,3 +218,15 @@ func debugSolveResponses(resp map[string]*client.SolveResponse) { } } } + +func checkBuildArgsUniqueness(args []string) error { + set := make(map[string]bool) + for _, value := range args { + key := strings.Split(value, "=")[0] + if _, ok := set[key]; ok { + return fmt.Errorf("'--build-arg %s' is defined twice", key) + } + set[key] = true + } + return nil +} diff --git a/internal/commands/build/compose.go b/internal/commands/build/compose.go index 32a92be99..b35940910 100644 --- a/internal/commands/build/compose.go +++ b/internal/commands/build/compose.go @@ -18,7 +18,7 @@ func parseCompose(app *types.App, options buildOptions) (map[string]build.Option return nil, err } - services, err := load(parsed) + services, err := load(parsed, options.args) if err != nil { return nil, fmt.Errorf("Failed to parse compose file: %s", err) } diff --git a/internal/commands/build/types.go b/internal/commands/build/types.go index fc7da489a..664358d86 100644 --- a/internal/commands/build/types.go +++ b/internal/commands/build/types.go @@ -3,6 +3,7 @@ package build import ( "fmt" "reflect" + "strings" "github.com/docker/cli/cli/compose/loader" compose "github.com/docker/cli/cli/compose/types" @@ -24,7 +25,7 @@ type ImageBuildConfig struct { Args compose.MappingWithEquals `yaml:",omitempty" json:"args,omitempty"` } -func load(dict map[string]interface{}) ([]ServiceConfig, error) { +func load(dict map[string]interface{}, buildArgs []string) ([]ServiceConfig, error) { section, ok := dict["services"] if !ok { return nil, fmt.Errorf("compose file doesn't declare any service") @@ -33,14 +34,14 @@ func load(dict map[string]interface{}) ([]ServiceConfig, error) { if !ok { return nil, fmt.Errorf("Invalid compose file: 'services' should be a map") } - return loadServices(services) + return loadServices(services, buildArgs) } -func loadServices(servicesDict map[string]interface{}) ([]ServiceConfig, error) { +func loadServices(servicesDict map[string]interface{}, buildArgs []string) ([]ServiceConfig, error) { var services []ServiceConfig for name, serviceDef := range servicesDict { - serviceConfig, err := loadService(name, serviceDef.(map[string]interface{})) + serviceConfig, err := loadService(name, serviceDef.(map[string]interface{}), buildArgs) if err != nil { return nil, err } @@ -49,14 +50,19 @@ func loadServices(servicesDict map[string]interface{}) ([]ServiceConfig, error) return services, nil } -func loadService(name string, serviceDict map[string]interface{}) (*ServiceConfig, error) { +func loadService(name string, serviceDict map[string]interface{}, buildArgs []string) (*ServiceConfig, error) { serviceConfig := &ServiceConfig{Name: name} + args := transformBuildArgsArray(buildArgs, "=", true) + if err := loader.Transform(serviceDict, serviceConfig, loader.Transformer{ TypeOf: reflect.TypeOf(ImageBuildConfig{}), Func: transformBuildConfig, }); err != nil { return nil, err } + if serviceConfig.Build != nil { + serviceConfig.Build.mergeArgs(args) + } return serviceConfig, nil } @@ -70,3 +76,30 @@ func transformBuildConfig(data interface{}) (interface{}, error) { return data, errors.Errorf("invalid type %T for service build", value) } } + +func transformBuildArgsArray(array []string, sep string, allowNil bool) map[string]string { + result := make(map[string]string) + for _, value := range array { + parts := strings.SplitN(value, sep, 2) + key := parts[0] + switch { + case len(parts) == 1 && !allowNil: + result[key] = "" + default: + result[key] = parts[1] + } + } + return result +} + +func (m ImageBuildConfig) mergeArgs(mapToMerge map[string]string) { + for key := range m.Args { + if val, ok := mapToMerge[key]; ok { + if val == "" { + m.Args[key] = nil + } else { + m.Args[key] = &val + } + } + } +}