diff --git a/devcontainer/devcontainer.go b/devcontainer/devcontainer.go index ea07bfcd..0d10405d 100644 --- a/devcontainer/devcontainer.go +++ b/devcontainer/devcontainer.go @@ -69,6 +69,7 @@ type Compiled struct { BuildContext string FeatureContexts map[string]string BuildArgs []string + Target string User string ContainerEnv map[string]string @@ -140,6 +141,7 @@ func (s Spec) HasDockerfile() bool { func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string, fallbackDockerfile, workspaceFolder string, useBuildContexts bool, lookupEnv func(string) (string, bool)) (*Compiled, error) { params := &Compiled{ User: s.ContainerUser, + Target: s.Build.Target, ContainerEnv: s.ContainerEnv, RemoteEnv: s.RemoteEnv, } diff --git a/devcontainer/devcontainer_test.go b/devcontainer/devcontainer_test.go index 5b7fe033..47403446 100644 --- a/devcontainer/devcontainer_test.go +++ b/devcontainer/devcontainer_test.go @@ -190,6 +190,27 @@ func TestCompileDevContainer(t *testing.T) { require.Equal(t, filepath.Join(dcDir, "Dockerfile"), params.DockerfilePath) require.Equal(t, dcDir, params.BuildContext) }) + t.Run("WithBuildTarget", func(t *testing.T) { + t.Parallel() + fs := memfs.New() + dc := &devcontainer.Spec{ + Build: devcontainer.BuildSpec{ + Dockerfile: "Dockerfile", + Target: "my-target", + }, + } + dcDir := "/workspaces/coder/.devcontainer" + err := fs.MkdirAll(dcDir, 0o755) + require.NoError(t, err) + file, err := fs.OpenFile(filepath.Join(dcDir, "Dockerfile"), os.O_CREATE|os.O_WRONLY, 0o644) + require.NoError(t, err) + _, err = io.WriteString(file, "FROM scratch AS my-target\nUSER testuser") + require.NoError(t, err) + _ = file.Close() + params, err := dc.Compile(fs, dcDir, workingDir, "", "", false, stubLookupEnv) + require.NoError(t, err) + require.Equal(t, "my-target", params.Target) + }) } func TestImageFromDockerfile(t *testing.T) { diff --git a/docs/env-variables.md b/docs/env-variables.md index e6fa7ca5..f78be936 100644 --- a/docs/env-variables.md +++ b/docs/env-variables.md @@ -24,6 +24,7 @@ | `--ignore-paths` | `ENVBUILDER_IGNORE_PATHS` | | The comma separated list of paths to ignore when building the workspace. | | `--build-secrets` | `ENVBUILDER_BUILD_SECRETS` | | The list of secret environment variables to use when building the image. | | `--skip-rebuild` | `ENVBUILDER_SKIP_REBUILD` | | Skip building if the MagicFile exists. This is used to skip building when a container is restarting. e.g. docker stop -> docker start This value can always be set to true - even if the container is being started for the first time. | +| `--skip-unused-stages` | `ENVBUILDER_SKIP_UNUSED_STAGES` | | Skip building all unused docker stages. Otherwise it builds by default all stages, even the unnecessary ones until it reaches the target stage / end of Dockerfile. | | `--git-url` | `ENVBUILDER_GIT_URL` | | The URL of a Git repository containing a Devcontainer or Docker image to clone. This is optional. | | `--git-clone-depth` | `ENVBUILDER_GIT_CLONE_DEPTH` | | The depth to use when cloning the Git repository. | | `--git-clone-single-branch` | `ENVBUILDER_GIT_CLONE_SINGLE_BRANCH` | | Clone only a single branch of the Git repository. | diff --git a/envbuilder.go b/envbuilder.go index d8cd8396..45a9d3ec 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -539,6 +539,8 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro CustomPlatform: platforms.Format(platforms.Normalize(platforms.DefaultSpec())), SnapshotMode: "redo", RunV2: true, + SkipUnusedStages: opts.SkipUnusedStages, + Target: buildParams.Target, RunStdout: stdoutWriter, RunStderr: stderrWriter, Destinations: destinations, diff --git a/options/options.go b/options/options.go index 8cdf723a..8186b357 100644 --- a/options/options.go +++ b/options/options.go @@ -100,6 +100,10 @@ type Options struct { // This value can always be set to true - even if the container is being // started for the first time. SkipRebuild bool + // SkipUnusedStages builds only used stages if defined to true. Otherwise, + // it builds by default all stages, even the unnecessary ones until it + // reaches the target stage / end of Dockerfile + SkipUnusedStages bool // GitURL is the URL of the Git repository to clone. This is optional. GitURL string // GitCloneDepth is the depth to use when cloning the Git repository. @@ -359,6 +363,14 @@ func (o *Options) CLI() serpent.OptionSet { "docker start This value can always be set to true - even if the " + "container is being started for the first time.", }, + { + Flag: "skip-unused-stages", + Env: WithEnvPrefix("SKIP_UNUSED_STAGES"), + Value: serpent.BoolOf(&o.SkipUnusedStages), + Description: "Skip building all unused docker stages. Otherwise it builds by " + + "default all stages, even the unnecessary ones until it reaches the " + + "target stage / end of Dockerfile.", + }, { Flag: "git-url", Env: WithEnvPrefix("GIT_URL"), diff --git a/options/options_test.go b/options/options_test.go index ed5dcd3c..2de597ff 100644 --- a/options/options_test.go +++ b/options/options_test.go @@ -37,30 +37,36 @@ func TestEnvOptionParsing(t *testing.T) { t.Run("bool", func(t *testing.T) { t.Run("lowercase", func(t *testing.T) { t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "true") + t.Setenv(options.WithEnvPrefix("SKIP_UNUSED_STAGES"), "true") t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "false") t.Setenv(options.WithEnvPrefix("GIT_CLONE_THINPACK"), "false") o := runCLI() require.True(t, o.SkipRebuild) + require.True(t, o.SkipUnusedStages) require.False(t, o.GitCloneSingleBranch) require.False(t, o.GitCloneThinPack) }) t.Run("uppercase", func(t *testing.T) { t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "TRUE") + t.Setenv(options.WithEnvPrefix("SKIP_UNUSED_STAGES"), "TRUE") t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "FALSE") t.Setenv(options.WithEnvPrefix("GIT_CLONE_THINPACK"), "FALSE") o := runCLI() require.True(t, o.SkipRebuild) + require.True(t, o.SkipUnusedStages) require.False(t, o.GitCloneSingleBranch) require.False(t, o.GitCloneThinPack) }) t.Run("numeric", func(t *testing.T) { t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "1") + t.Setenv(options.WithEnvPrefix("SKIP_UNUSED_STAGES"), "1") t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "0") t.Setenv(options.WithEnvPrefix("GIT_CLONE_THINPACK"), "0") o := runCLI() require.True(t, o.SkipRebuild) + require.True(t, o.SkipUnusedStages) require.False(t, o.GitCloneSingleBranch) require.False(t, o.GitCloneThinPack) }) diff --git a/options/testdata/options.golden b/options/testdata/options.golden index 92a85232..fb5955b5 100644 --- a/options/testdata/options.golden +++ b/options/testdata/options.golden @@ -179,6 +179,11 @@ OPTIONS: value can always be set to true - even if the container is being started for the first time. + --skip-unused-stages bool, $ENVBUILDER_SKIP_UNUSED_STAGES + Skip building all unused docker stages. Otherwise it builds by default + all stages, even the unnecessary ones until it reaches the target + stage / end of Dockerfile. + --ssl-cert-base64 string, $ENVBUILDER_SSL_CERT_BASE64 The content of an SSL cert file. This is useful for self-signed certificates.