diff --git a/cmd/wodby/ci/build/build.go b/cmd/wodby/ci/build/build.go index 7aacad2..bc1452e 100644 --- a/cmd/wodby/ci/build/build.go +++ b/cmd/wodby/ci/build/build.go @@ -141,7 +141,9 @@ var Cmd = &cobra.Command{ for _, appServiceBuildConfig := range appServiceBuildConfigs { buildArgs := make(map[string]string) var redactValues []string - buildArgs["COPY_FROM"] = opts.from + // A service can narrow the build to a subdirectory of the context, so + // the flags give the roots and the service gives the path within them. + buildArgs["COPY_FROM"] = joinCopyPath(opts.from, appServiceBuildConfig.CopyFrom) buildArgs["WODBY_BASE_IMAGE"] = appServiceBuildConfig.Image buildFiles := newBuildFiles(context, appServiceBuildConfig.Name, opts.dockerfile) cacheFrom, cacheTo, err := resolveCacheOptions(config, appServiceBuildConfig.Name, opts) @@ -172,7 +174,7 @@ var Cmd = &cobra.Command{ return errors.WithStack(err) } dockerfile = string(d) - if err := addDockerfileBuildArgs(buildArgs, dockerfile, appServiceBuildConfig, opts.to, logger, &redactValues); err != nil { + if err := addDockerfileBuildArgs(buildArgs, dockerfile, appServiceBuildConfig, joinCopyPath(opts.to, appServiceBuildConfig.CopyTo), logger, &redactValues); err != nil { return errors.WithStack(err) } } else if fileExists(buildFiles.dockerfilePath) { @@ -183,7 +185,7 @@ var Cmd = &cobra.Command{ return errors.WithStack(err) } dockerfile = string(d) - if err := addDockerfileBuildArgs(buildArgs, dockerfile, appServiceBuildConfig, opts.to, logger, &redactValues); err != nil { + if err := addDockerfileBuildArgs(buildArgs, dockerfile, appServiceBuildConfig, joinCopyPath(opts.to, appServiceBuildConfig.CopyTo), logger, &redactValues); err != nil { return errors.WithStack(err) } } else { @@ -191,13 +193,13 @@ var Cmd = &cobra.Command{ dockerfileSource = dockerfileSourceService fmt.Println("Dockerfile provided by app service") dockerfile = *appServiceBuildConfig.Dockerfile - if err := addDockerfileBuildArgs(buildArgs, dockerfile, appServiceBuildConfig, opts.to, logger, &redactValues); err != nil { + if err := addDockerfileBuildArgs(buildArgs, dockerfile, appServiceBuildConfig, joinCopyPath(opts.to, appServiceBuildConfig.CopyTo), logger, &redactValues); err != nil { return errors.WithStack(err) } } else { dockerfileSource = dockerfileSourceDefault fmt.Println("No Dockerfile provided by app service, using the default") - buildArgs["COPY_TO"] = opts.to + buildArgs["COPY_TO"] = joinCopyPath(opts.to, appServiceBuildConfig.CopyTo) // Replace default image user in dockerfile template. defaultUser, err := dockerClient.GetImageDefaultUser(appServiceBuildConfig.Image) if err != nil { @@ -347,6 +349,24 @@ func dockerfileContentHash(dockerfile string) string { return "sha256:" + hex.EncodeToString(sum[:]) } +// joinCopyPath places a service's copy subdirectory under the root given by the +// --from/--to flags. An empty subdirectory leaves the root untouched, which is +// the default whole-context build. +func joinCopyPath(root string, subdir string) string { + if subdir == "" { + if root == "" { + return "." + } + + return root + } + if root == "" { + root = "." + } + + return path.Join(root, subdir) +} + func authoredDockerfile(source string) bool { return source == dockerfileSourceFlag || source == dockerfileSourceContext } diff --git a/cmd/wodby/ci/build/build_test.go b/cmd/wodby/ci/build/build_test.go index d225412..15c39b3 100644 --- a/cmd/wodby/ci/build/build_test.go +++ b/cmd/wodby/ci/build/build_test.go @@ -468,3 +468,27 @@ func TestDockerfileContentHash(t *testing.T) { t.Fatalf("dockerfileContentHash(\"\") = %q, want empty", dockerfileContentHash("")) } } + +func TestJoinCopyPath(t *testing.T) { + for _, tc := range []struct { + name string + root string + subdir string + want string + }{ + {name: "default root, no subdir", root: ".", subdir: "", want: "."}, + {name: "default root with subdir", root: ".", subdir: "web", want: "web"}, + {name: "explicit root with subdir", root: "static", subdir: "web", want: "static/web"}, + {name: "absolute destination with subdir", root: "/var/www/html", subdir: "web", want: "/var/www/html/web"}, + {name: "explicit root, no subdir", root: "static", subdir: "", want: "static"}, + {name: "empty root, no subdir", root: "", subdir: "", want: "."}, + {name: "empty root with subdir", root: "", subdir: "web", want: "web"}, + {name: "nested subdir", root: ".", subdir: "apps/web", want: "apps/web"}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := joinCopyPath(tc.root, tc.subdir); got != tc.want { + t.Fatalf("joinCopyPath(%q, %q) = %q, want %q", tc.root, tc.subdir, got, tc.want) + } + }) + } +} diff --git a/pkg/types/build.go b/pkg/types/build.go index c6061e5..20ffc1d 100644 --- a/pkg/types/build.go +++ b/pkg/types/build.go @@ -25,14 +25,18 @@ type ( Services []*AppServiceBuildConfig `json:"services"` } AppServiceBuildConfig struct { - Name string `json:"name"` - Title string `json:"title"` - Image string `json:"image"` - Managed bool `json:"managed"` - Main bool `json:"main"` - Dockerfile *string `json:"dockerfile"` - Dockerignore *string `json:"dockerignore"` - Args []*AppServiceBuildArg `json:"args"` + Name string `json:"name"` + Title string `json:"title"` + Image string `json:"image"` + Managed bool `json:"managed"` + Main bool `json:"main"` + Dockerfile *string `json:"dockerfile"` + Dockerignore *string `json:"dockerignore"` + // CopyFrom and CopyTo narrow the build to a subdirectory, relative to the + // --from and --to paths. Empty means copy the whole context. + CopyFrom string `json:"copyFrom"` + CopyTo string `json:"copyTo"` + Args []*AppServiceBuildArg `json:"args"` } AppServiceBuildArg struct { Name string `json:"name"`