diff --git a/constants/constants.go b/constants/constants.go deleted file mode 100644 index fefa1394..00000000 --- a/constants/constants.go +++ /dev/null @@ -1,64 +0,0 @@ -package constants - -import ( - "errors" - "fmt" - "path/filepath" -) - -const ( - // WorkspacesDir is the path to the directory where - // all workspaces are stored by default. - WorkspacesDir = "/workspaces" - - // EmptyWorkspaceDir is the path to a workspace that has - // nothing going on... it's empty! - EmptyWorkspaceDir = WorkspacesDir + "/empty" - - // MagicDir is where all envbuilder related files are stored. - // This is a special directory that must not be modified - // by the user or images. - MagicDir = "/.envbuilder" -) - -var ( - ErrNoFallbackImage = errors.New("no fallback image has been specified") - - // MagicFile is a file that is created in the workspace - // when envbuilder has already been run. This is used - // to skip building when a container is restarting. - // e.g. docker stop -> docker start - MagicFile = filepath.Join(MagicDir, "built") - - // MagicFile is the location of the build context when - // using remote build mode. - MagicRemoteRepoDir = filepath.Join(MagicDir, "repo") - - // MagicBinaryLocation is the expected location of the envbuilder binary - // inside a builder image. - MagicBinaryLocation = filepath.Join(MagicDir, "bin", "envbuilder") - - // MagicImage is a file that is created in the image when - // envbuilder has already been run. This is used to skip - // the destructive initial build step when 'resuming' envbuilder - // from a previously built image. - MagicImage = filepath.Join(MagicDir, "image") - - // MagicTempDir is a directory inside the build context inside which - // we place files referenced by MagicDirectives. - MagicTempDir = ".envbuilder.tmp" - - // MagicDirectives are directives automatically appended to Dockerfiles - // when pushing the image. These directives allow the built image to be - // 're-used'. - MagicDirectives = fmt.Sprintf(` -COPY --chmod=0755 %[1]s %[2]s -COPY --chmod=0644 %[3]s %[4]s -USER root -WORKDIR / -ENTRYPOINT [%[2]q] -`, - ".envbuilder.tmp/envbuilder", MagicBinaryLocation, - ".envbuilder.tmp/image", MagicImage, - ) -) diff --git a/envbuilder.go b/envbuilder.go index 7f3c983a..ac64beea 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -25,7 +25,6 @@ import ( "time" "github.com/coder/envbuilder/buildinfo" - "github.com/coder/envbuilder/constants" "github.com/coder/envbuilder/git" "github.com/coder/envbuilder/options" "github.com/go-git/go-billy/v5" @@ -36,6 +35,7 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/coder/envbuilder/devcontainer" "github.com/coder/envbuilder/internal/ebutil" + "github.com/coder/envbuilder/internal/magicdir" "github.com/coder/envbuilder/log" "github.com/containerd/platforms" "github.com/distribution/distribution/v3/configuration" @@ -52,6 +52,9 @@ import ( "golang.org/x/xerrors" ) +// ErrNoFallbackImage is returned when no fallback image has been specified. +var ErrNoFallbackImage = errors.New("no fallback image has been specified") + // DockerConfig represents the Docker configuration file. type DockerConfig configfile.ConfigFile @@ -68,6 +71,9 @@ func Run(ctx context.Context, opts options.Options) error { if opts.CacheRepo == "" && opts.PushImage { return fmt.Errorf("--cache-repo must be set when using --push-image") } + + magicDir := magicdir.At(opts.MagicDirBase) + // Default to the shell! initArgs := []string{"-c", opts.InitScript} if opts.InitArgs != "" { @@ -92,7 +98,7 @@ func Run(ctx context.Context, opts options.Options) error { opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"), buildinfo.Version()) - cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64) + cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.Logger, magicDir, opts.DockerConfigBase64) if err != nil { return err } @@ -168,7 +174,7 @@ func Run(ctx context.Context, opts options.Options) error { } defaultBuildParams := func() (*devcontainer.Compiled, error) { - dockerfile := filepath.Join(constants.MagicDir, "Dockerfile") + dockerfile := magicDir.Join("Dockerfile") file, err := opts.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return nil, err @@ -176,11 +182,11 @@ func Run(ctx context.Context, opts options.Options) error { defer file.Close() if opts.FallbackImage == "" { if fallbackErr != nil { - return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), constants.ErrNoFallbackImage) + return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage) } // We can't use errors.Join here because our tests // don't support parsing a multiline error. - return nil, constants.ErrNoFallbackImage + return nil, ErrNoFallbackImage } content := "FROM " + opts.FallbackImage _, err = file.Write([]byte(content)) @@ -190,7 +196,7 @@ func Run(ctx context.Context, opts options.Options) error { return &devcontainer.Compiled{ DockerfilePath: dockerfile, DockerfileContent: content, - BuildContext: constants.MagicDir, + BuildContext: magicDir.Path(), }, nil } @@ -232,7 +238,7 @@ func Run(ctx context.Context, opts options.Options) error { opts.Logger(log.LevelInfo, "No Dockerfile or image specified; falling back to the default image...") fallbackDockerfile = defaultParams.DockerfilePath } - buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, constants.MagicDir, fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv) + buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, magicDir.Path(), fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv) if err != nil { return fmt.Errorf("compile devcontainer.json: %w", err) } @@ -304,7 +310,7 @@ func Run(ctx context.Context, opts options.Options) error { // So we add them to the default ignore list. See: // https://github.com/GoogleContainerTools/kaniko/blob/63be4990ca5a60bdf06ddc4d10aa4eca0c0bc714/cmd/executor/cmd/root.go#L136 ignorePaths := append([]string{ - constants.MagicDir, + magicDir.Path(), opts.WorkspaceFolder, // See: https://github.com/coder/envbuilder/issues/37 "/etc/resolv.conf", @@ -332,31 +338,25 @@ func Run(ctx context.Context, opts options.Options) error { if err := util.AddAllowedPathToDefaultIgnoreList(opts.BinaryPath); err != nil { return fmt.Errorf("add envbuilder binary to ignore list: %w", err) } - if err := util.AddAllowedPathToDefaultIgnoreList(constants.MagicImage); err != nil { + if err := util.AddAllowedPathToDefaultIgnoreList(magicDir.Image()); err != nil { return fmt.Errorf("add magic image file to ignore list: %w", err) } - magicTempDir := filepath.Join(buildParams.BuildContext, constants.MagicTempDir) - if err := opts.Filesystem.MkdirAll(magicTempDir, 0o755); err != nil { + magicTempDir := magicdir.At(buildParams.BuildContext, magicdir.TempDir) + if err := opts.Filesystem.MkdirAll(magicTempDir.Path(), 0o755); err != nil { return fmt.Errorf("create magic temp dir in build context: %w", err) } // Add the magic directives that embed the binary into the built image. - buildParams.DockerfileContent += constants.MagicDirectives + buildParams.DockerfileContent += magicdir.Directives // Copy the envbuilder binary into the build context. // External callers will need to specify the path to the desired envbuilder binary. - envbuilderBinDest := filepath.Join( - magicTempDir, - filepath.Base(constants.MagicBinaryLocation), - ) + envbuilderBinDest := filepath.Join(magicTempDir.Path(), "envbuilder") // Also touch the magic file that signifies the image has been built! - magicImageDest := filepath.Join( - magicTempDir, - filepath.Base(constants.MagicImage), - ) + magicImageDest := magicTempDir.Image() // Clean up after build! var cleanupOnce sync.Once cleanupBuildContext = func() { cleanupOnce.Do(func() { - for _, path := range []string{magicImageDest, envbuilderBinDest, magicTempDir} { + for _, path := range []string{magicImageDest, envbuilderBinDest, magicTempDir.Path()} { if err := opts.Filesystem.Remove(path); err != nil { opts.Logger(log.LevelWarn, "failed to clean up magic temp dir from build context: %w", err) } @@ -370,15 +370,14 @@ func Run(ctx context.Context, opts options.Options) error { return fmt.Errorf("copy envbuilder binary to build context: %w", err) } - opts.Logger(log.LevelDebug, "touching magic image file at %q in build context %q", magicImageDest, buildParams.BuildContext) + opts.Logger(log.LevelDebug, "touching magic image file at %q in build context %q", magicImageDest, magicTempDir) if err := touchFile(opts.Filesystem, magicImageDest, 0o755); err != nil { return fmt.Errorf("touch magic image file in build context: %w", err) } - } // temp move of all ro mounts - tempRemountDest := filepath.Join("/", constants.MagicDir, "mnt") + tempRemountDest := magicDir.Join("mnt") // ignorePrefixes is a superset of ignorePaths that we pass to kaniko's // IgnoreList. ignorePrefixes := append([]string{"/dev", "/proc", "/sys"}, ignorePaths...) @@ -399,8 +398,8 @@ func Run(ctx context.Context, opts options.Options) error { defer closeStderr() build := func() (v1.Image, error) { defer cleanupBuildContext() - _, alreadyBuiltErr := opts.Filesystem.Stat(constants.MagicFile) - _, isImageErr := opts.Filesystem.Stat(constants.MagicImage) + _, alreadyBuiltErr := opts.Filesystem.Stat(magicDir.Built()) + _, isImageErr := opts.Filesystem.Stat(magicDir.Image()) if (alreadyBuiltErr == nil && opts.SkipRebuild) || isImageErr == nil { endStage := startStage("🏗️ Skipping build because of cache...") imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent) @@ -545,7 +544,7 @@ func Run(ctx context.Context, opts options.Options) error { // Create the magic file to indicate that this build // has already been ran before! - file, err := opts.Filesystem.Create(constants.MagicFile) + file, err := opts.Filesystem.Create(magicDir.Built()) if err != nil { return fmt.Errorf("create magic file: %w", err) } @@ -752,7 +751,7 @@ func Run(ctx context.Context, opts options.Options) error { opts.Logger(log.LevelInfo, "=== Running the setup command %q as the root user...", opts.SetupScript) envKey := "ENVBUILDER_ENV" - envFile := filepath.Join("/", constants.MagicDir, "environ") + envFile := magicDir.Join("environ") file, err := os.Create(envFile) if err != nil { return fmt.Errorf("create environ file: %w", err) @@ -862,6 +861,8 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) return nil, fmt.Errorf("--cache-repo must be set when using --get-cached-image") } + magicDir := magicdir.At(opts.MagicDirBase) + stageNumber := 0 startStage := func(format string, args ...any) func(format string, args ...any) { now := time.Now() @@ -876,7 +877,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"), buildinfo.Version()) - cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64) + cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.Logger, magicDir, opts.DockerConfigBase64) if err != nil { return nil, err } @@ -960,11 +961,11 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) defer file.Close() if opts.FallbackImage == "" { if fallbackErr != nil { - return nil, fmt.Errorf("%s: %w", fallbackErr.Error(), constants.ErrNoFallbackImage) + return nil, fmt.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage) } // We can't use errors.Join here because our tests // don't support parsing a multiline error. - return nil, constants.ErrNoFallbackImage + return nil, ErrNoFallbackImage } content := "FROM " + opts.FallbackImage _, err = file.Write([]byte(content)) @@ -1080,7 +1081,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) // So we add them to the default ignore list. See: // https://github.com/GoogleContainerTools/kaniko/blob/63be4990ca5a60bdf06ddc4d10aa4eca0c0bc714/cmd/executor/cmd/root.go#L136 ignorePaths := append([]string{ - constants.MagicDir, + magicDir.Path(), opts.WorkspaceFolder, // See: https://github.com/coder/envbuilder/issues/37 "/etc/resolv.conf", @@ -1103,29 +1104,25 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) // build via executor.RunCacheProbe we need to have the *exact* copy of the // envbuilder binary available used to build the image and we also need to // add the magic directives to the Dockerfile content. - buildParams.DockerfileContent += constants.MagicDirectives - magicTempDir := filepath.Join(buildParams.BuildContext, constants.MagicTempDir) + // MAGICDIR + buildParams.DockerfileContent += magicdir.Directives + magicTempDir := filepath.Join(buildParams.BuildContext, magicdir.TempDir) if err := opts.Filesystem.MkdirAll(magicTempDir, 0o755); err != nil { return nil, fmt.Errorf("create magic temp dir in build context: %w", err) } - envbuilderBinDest := filepath.Join( - magicTempDir, - filepath.Base(constants.MagicBinaryLocation), - ) + envbuilderBinDest := filepath.Join(magicTempDir, "envbuilder") // Copy the envbuilder binary into the build context. - opts.Logger(log.LevelDebug, "copying envbuilder binary at %q to build context %q", opts.BinaryPath, buildParams.BuildContext) + opts.Logger(log.LevelDebug, "copying envbuilder binary at %q to build context %q", opts.BinaryPath, envbuilderBinDest) if err := copyFile(opts.Filesystem, opts.BinaryPath, envbuilderBinDest, 0o755); err != nil { return nil, xerrors.Errorf("copy envbuilder binary to build context: %w", err) } - // Also touch the magic file that signifies the image has been built! - magicImageDest := filepath.Join( - magicTempDir, - filepath.Base(constants.MagicImage), - ) + // Also touch the magic file that signifies the image has been built!A + magicImageDest := filepath.Join(magicTempDir, "image") + opts.Logger(log.LevelDebug, "touching magic image file at %q in build context %q", magicImageDest, magicTempDir) if err := touchFile(opts.Filesystem, magicImageDest, 0o755); err != nil { - return nil, fmt.Errorf("touch magic image file in build context: %w", err) + return nil, fmt.Errorf("touch magic image file at %q: %w", magicImageDest, err) } defer func() { // Clean up after we're done! @@ -1417,21 +1414,24 @@ func findDevcontainerJSON(workspaceFolder string, options options.Options) (stri // maybeDeleteFilesystem wraps util.DeleteFilesystem with a guard to hopefully stop // folks from unwittingly deleting their entire root directory. func maybeDeleteFilesystem(logger log.Func, force bool) error { + // We always expect the magic directory to be set to the default, signifying that + // the user is running envbuilder in a container. + // If this is set to anything else we should bail out to prevent accidental data loss. + // defaultMagicDir := magicdir.MagicDir("") kanikoDir, ok := os.LookupEnv("KANIKO_DIR") - if !ok || strings.TrimSpace(kanikoDir) != constants.MagicDir { - if force { - bailoutSecs := 10 - logger(log.LevelWarn, "WARNING! BYPASSING SAFETY CHECK! THIS WILL DELETE YOUR ROOT FILESYSTEM!") - logger(log.LevelWarn, "You have %d seconds to bail out!", bailoutSecs) - for i := bailoutSecs; i > 0; i-- { - logger(log.LevelWarn, "%d...", i) - <-time.After(time.Second) - } - } else { - logger(log.LevelError, "KANIKO_DIR is not set to %s. Bailing!\n", constants.MagicDir) + if !ok || strings.TrimSpace(kanikoDir) != magicdir.Default.Path() { + if !force { + logger(log.LevelError, "KANIKO_DIR is not set to %s. Bailing!\n", magicdir.Default.Path()) logger(log.LevelError, "To bypass this check, set FORCE_SAFE=true.") return errors.New("safety check failed") } + bailoutSecs := 10 + logger(log.LevelWarn, "WARNING! BYPASSING SAFETY CHECK! THIS WILL DELETE YOUR ROOT FILESYSTEM!") + logger(log.LevelWarn, "You have %d seconds to bail out!", bailoutSecs) + for i := bailoutSecs; i > 0; i-- { + logger(log.LevelWarn, "%d...", i) + <-time.After(time.Second) + } } return util.DeleteFilesystem() @@ -1469,13 +1469,13 @@ func touchFile(fs billy.Filesystem, dst string, mode fs.FileMode) error { return f.Close() } -func initDockerConfigJSON(dockerConfigBase64 string) (func() error, error) { +func initDockerConfigJSON(logf log.Func, magicDir magicdir.MagicDir, dockerConfigBase64 string) (func() error, error) { var cleanupOnce sync.Once noop := func() error { return nil } if dockerConfigBase64 == "" { return noop, nil } - cfgPath := filepath.Join(constants.MagicDir, "config.json") + cfgPath := filepath.Join(magicDir.Path(), "config.json") decoded, err := base64.StdEncoding.DecodeString(dockerConfigBase64) if err != nil { return noop, fmt.Errorf("decode docker config: %w", err) @@ -1489,10 +1489,14 @@ func initDockerConfigJSON(dockerConfigBase64 string) (func() error, error) { if err != nil { return noop, fmt.Errorf("parse docker config: %w", err) } + for k := range configFile.AuthConfigs { + logf(log.LevelInfo, "Docker config contains auth for registry %q", k) + } err = os.WriteFile(cfgPath, decoded, 0o644) if err != nil { return noop, fmt.Errorf("write docker config: %w", err) } + logf(log.LevelInfo, "Wrote Docker config JSON to %s", cfgPath) cleanup := func() error { var cleanupErr error cleanupOnce.Do(func() { @@ -1501,7 +1505,7 @@ func initDockerConfigJSON(dockerConfigBase64 string) (func() error, error) { if !errors.Is(err, fs.ErrNotExist) { cleanupErr = fmt.Errorf("remove docker config: %w", cleanupErr) } - _, _ = fmt.Fprintf(os.Stderr, "failed to remove the Docker config secret file: %s\n", cleanupErr) + logf(log.LevelError, "Failed to remove the Docker config secret file: %s", cleanupErr) } }) return cleanupErr diff --git a/integration/integration_test.go b/integration/integration_test.go index 43d728c2..e0e012ba 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -22,8 +22,8 @@ import ( "time" "github.com/coder/envbuilder" - "github.com/coder/envbuilder/constants" "github.com/coder/envbuilder/devcontainer/features" + "github.com/coder/envbuilder/internal/magicdir" "github.com/coder/envbuilder/options" "github.com/coder/envbuilder/testutil/gittest" "github.com/coder/envbuilder/testutil/mwtest" @@ -365,7 +365,8 @@ func TestBuildFromDockerfile(t *testing.T) { require.Equal(t, "hello", strings.TrimSpace(output)) // Verify that the Docker configuration secret file is removed - output = execContainer(t, ctr, "stat "+filepath.Join(constants.MagicDir, "config.json")) + configJSONContainerPath := magicdir.Default.Join("config.json") + output = execContainer(t, ctr, "stat "+configJSONContainerPath) require.Contains(t, output, "No such file or directory") } @@ -591,7 +592,7 @@ func TestCloneFailsFallback(t *testing.T) { _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", "bad-value"), }}) - require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) }) } @@ -609,7 +610,7 @@ func TestBuildFailsFallback(t *testing.T) { envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) - require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) require.ErrorContains(t, err, "dockerfile parse error") }) t.Run("FailsBuild", func(t *testing.T) { @@ -625,7 +626,7 @@ RUN exit 1`, envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) - require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) }) t.Run("BadDevcontainer", func(t *testing.T) { t.Parallel() @@ -638,7 +639,7 @@ RUN exit 1`, _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }}) - require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) }) t.Run("NoImageOrDockerfile", func(t *testing.T) { t.Parallel() @@ -971,7 +972,7 @@ func setupPassthroughRegistry(t *testing.T, image string, opts *setupPassthrough func TestNoMethodFails(t *testing.T) { _, err := runEnvbuilder(t, runOpts{env: []string{}}) - require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) } func TestDockerfileBuildContext(t *testing.T) { @@ -1157,6 +1158,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), + envbuilderEnv("VERBOSE", "1"), }}) require.ErrorContains(t, err, "error probing build cache: uncached RUN command") // Then: it should fail to build the image and nothing should be pushed @@ -1168,6 +1170,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("PUSH_IMAGE", "1"), + envbuilderEnv("VERBOSE", "1"), }}) require.NoError(t, err) diff --git a/internal/magicdir/magicdir.go b/internal/magicdir/magicdir.go new file mode 100644 index 00000000..31bcd7c9 --- /dev/null +++ b/internal/magicdir/magicdir.go @@ -0,0 +1,78 @@ +package magicdir + +import ( + "fmt" + "path/filepath" +) + +const ( + // defaultMagicDirBase is the default working location for envbuilder. + // This is a special directory that must not be modified by the user + // or images. This is intentionally unexported. + defaultMagicDirBase = "/.envbuilder" + + // TempDir is a directory inside the build context inside which + // we place files referenced by MagicDirectives. + TempDir = ".envbuilder.tmp" +) + +var ( + // Default is the default working directory for Envbuilder. + // This defaults to /.envbuilder. It should only be used when Envbuilder + // is known to be running as root inside a container. + Default MagicDir + // Directives are directives automatically appended to Dockerfiles + // when pushing the image. These directives allow the built image to be + // 're-used'. + Directives = fmt.Sprintf(` +COPY --chmod=0755 %[1]s/envbuilder %[2]s/bin/envbuilder +COPY --chmod=0644 %[1]s/image %[2]s/image +USER root +WORKDIR / +ENTRYPOINT ["%[2]s/bin/envbuilder"] +`, TempDir, defaultMagicDirBase) +) + +// MagicDir is a working directory for envbuilder. It +// will also be present in images built by envbuilder. +type MagicDir struct { + base string +} + +// At returns a MagicDir rooted at filepath.Join(paths...) +func At(paths ...string) MagicDir { + if len(paths) == 0 { + return MagicDir{} + } + return MagicDir{base: filepath.Join(paths...)} +} + +// Join returns the result of filepath.Join([m.Path, paths...]). +func (m MagicDir) Join(paths ...string) string { + return filepath.Join(append([]string{m.Path()}, paths...)...) +} + +// String returns the string representation of the MagicDir. +func (m MagicDir) Path() string { + // Instead of the zero value, use defaultMagicDir. + if m.base == "" { + return defaultMagicDirBase + } + return m.base +} + +// Built is a file that is created in the workspace +// when envbuilder has already been run. This is used +// to skip building when a container is restarting. +// e.g. docker stop -> docker start +func (m MagicDir) Built() string { + return m.Join("built") +} + +// Image is a file that is created in the image when +// envbuilder has already been run. This is used to skip +// the destructive initial build step when 'resuming' envbuilder +// from a previously built image. +func (m MagicDir) Image() string { + return m.Join("image") +} diff --git a/internal/magicdir/magicdir_internal_test.go b/internal/magicdir/magicdir_internal_test.go new file mode 100644 index 00000000..43b66ba0 --- /dev/null +++ b/internal/magicdir/magicdir_internal_test.go @@ -0,0 +1,38 @@ +package magicdir + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_MagicDir(t *testing.T) { + t.Parallel() + + t.Run("Default", func(t *testing.T) { + t.Parallel() + require.Equal(t, defaultMagicDirBase+"/foo", Default.Join("foo")) + require.Equal(t, defaultMagicDirBase, Default.Path()) + require.Equal(t, defaultMagicDirBase+"/built", Default.Built()) + require.Equal(t, defaultMagicDirBase+"/image", Default.Image()) + }) + + t.Run("ZeroValue", func(t *testing.T) { + t.Parallel() + var md MagicDir + require.Equal(t, defaultMagicDirBase+"/foo", md.Join("foo")) + require.Equal(t, defaultMagicDirBase, md.Path()) + require.Equal(t, defaultMagicDirBase+"/built", md.Built()) + require.Equal(t, defaultMagicDirBase+"/image", md.Image()) + }) + + t.Run("At", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + md := At(tmpDir) + require.Equal(t, tmpDir+"/foo", md.Join("foo")) + require.Equal(t, tmpDir, md.Path()) + require.Equal(t, tmpDir+"/built", md.Built()) + require.Equal(t, tmpDir+"/image", md.Image()) + }) +} diff --git a/options/defaults.go b/options/defaults.go index 42e48063..df3d436c 100644 --- a/options/defaults.go +++ b/options/defaults.go @@ -7,24 +7,28 @@ import ( "github.com/go-git/go-billy/v5/osfs" giturls "github.com/chainguard-dev/git-urls" - "github.com/coder/envbuilder/constants" "github.com/coder/envbuilder/internal/chmodfs" + "github.com/coder/envbuilder/internal/magicdir" ) +// EmptyWorkspaceDir is the path to a workspace that has +// nothing going on... it's empty! +var EmptyWorkspaceDir = "/workspaces/empty" + // DefaultWorkspaceFolder returns the default workspace folder // for a given repository URL. func DefaultWorkspaceFolder(repoURL string) string { if repoURL == "" { - return constants.EmptyWorkspaceDir + return EmptyWorkspaceDir } parsed, err := giturls.Parse(repoURL) if err != nil { - return constants.EmptyWorkspaceDir + return EmptyWorkspaceDir } name := strings.Split(parsed.Path, "/") hasOwnerAndRepo := len(name) >= 2 if !hasOwnerAndRepo { - return constants.EmptyWorkspaceDir + return EmptyWorkspaceDir } repo := strings.TrimSuffix(name[len(name)-1], ".git") return fmt.Sprintf("/workspaces/%s", repo) @@ -55,7 +59,13 @@ func (o *Options) SetDefaults() { if o.WorkspaceFolder == "" { o.WorkspaceFolder = DefaultWorkspaceFolder(o.GitURL) } + if o.RemoteRepoDir == "" { + o.RemoteRepoDir = magicdir.Default.Join("repo") + } if o.BinaryPath == "" { o.BinaryPath = "/.envbuilder/bin/envbuilder" } + if o.MagicDirBase == "" { + o.MagicDirBase = magicdir.Default.Path() + } } diff --git a/options/defaults_test.go b/options/defaults_test.go index 48783585..8c9946f6 100644 --- a/options/defaults_test.go +++ b/options/defaults_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/assert" - "github.com/coder/envbuilder/constants" "github.com/coder/envbuilder/options" "github.com/stretchr/testify/require" ) @@ -44,7 +43,7 @@ func TestDefaultWorkspaceFolder(t *testing.T) { { name: "empty", gitURL: "", - expected: constants.EmptyWorkspaceDir, + expected: options.EmptyWorkspaceDir, }, } for _, tt := range successTests { @@ -70,7 +69,7 @@ func TestDefaultWorkspaceFolder(t *testing.T) { for _, tt := range invalidTests { t.Run(tt.name, func(t *testing.T) { dir := options.DefaultWorkspaceFolder(tt.invalidURL) - require.Equal(t, constants.EmptyWorkspaceDir, dir) + require.Equal(t, options.EmptyWorkspaceDir, dir) }) } } @@ -84,7 +83,9 @@ func TestOptions_SetDefaults(t *testing.T) { IgnorePaths: []string{"/var/run", "/product_uuid", "/product_name"}, Filesystem: chmodfs.New(osfs.New("/")), GitURL: "", - WorkspaceFolder: constants.EmptyWorkspaceDir, + WorkspaceFolder: options.EmptyWorkspaceDir, + MagicDirBase: "/.envbuilder", + RemoteRepoDir: "/.envbuilder/repo", BinaryPath: "/.envbuilder/bin/envbuilder", } diff --git a/options/options.go b/options/options.go index d7bd66b3..a0058cd3 100644 --- a/options/options.go +++ b/options/options.go @@ -7,7 +7,6 @@ import ( "os" "strings" - "github.com/coder/envbuilder/constants" "github.com/coder/envbuilder/log" "github.com/coder/serpent" "github.com/go-git/go-billy/v5" @@ -166,6 +165,11 @@ type Options struct { // attempting to probe the build cache. This is only relevant when // GetCachedImage is true. BinaryPath string + + // MagicDirBase is the path to the directory where all envbuilder files should be + // stored. By default, this is set to `/.envbuilder`. This is intentionally + // excluded from the CLI options. + MagicDirBase string } const envPrefix = "ENVBUILDER_" @@ -456,10 +460,10 @@ func (o *Options) CLI() serpent.OptionSet { "working on the same repository.", }, { - Flag: "remote-repo-dir", - Env: WithEnvPrefix("REMOTE_REPO_DIR"), - Value: serpent.StringOf(&o.RemoteRepoDir), - Default: constants.MagicRemoteRepoDir, + Flag: "remote-repo-dir", + Env: WithEnvPrefix("REMOTE_REPO_DIR"), + Value: serpent.StringOf(&o.RemoteRepoDir), + // Default: magicdir.Default.Join("repo"), // TODO: reinstate once legacy opts are removed. Hidden: true, Description: "Specify the destination directory for the cloned repo when using remote repo build mode.", },