diff --git a/constants/constants.go b/constants/constants.go new file mode 100644 index 00000000..ccdfcb8c --- /dev/null +++ b/constants/constants.go @@ -0,0 +1,31 @@ +package constants + +import ( + "errors" + "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") +) diff --git a/envbuilder.go b/envbuilder.go index 7c5c380a..cafc4fcd 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -24,6 +24,7 @@ import ( "syscall" "time" + "github.com/coder/envbuilder/constants" "github.com/coder/envbuilder/git" "github.com/coder/envbuilder/options" @@ -53,31 +54,6 @@ import ( "golang.org/x/xerrors" ) -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") -) - // DockerConfig represents the Docker configuration file. type DockerConfig configfile.ConfigFile @@ -171,7 +147,7 @@ func Run(ctx context.Context, opts options.Options) error { if err != nil { return fmt.Errorf("parse docker config: %w", err) } - err = os.WriteFile(filepath.Join(MagicDir, "config.json"), decoded, 0o644) + err = os.WriteFile(filepath.Join(constants.MagicDir, "config.json"), decoded, 0o644) if err != nil { return fmt.Errorf("write docker config: %w", err) } @@ -237,7 +213,7 @@ func Run(ctx context.Context, opts options.Options) error { } defaultBuildParams := func() (*devcontainer.Compiled, error) { - dockerfile := filepath.Join(MagicDir, "Dockerfile") + dockerfile := filepath.Join(constants.MagicDir, "Dockerfile") file, err := opts.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return nil, err @@ -245,11 +221,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(), ErrNoFallbackImage) + return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), constants.ErrNoFallbackImage) } // We can't use errors.Join here because our tests // don't support parsing a multiline error. - return nil, ErrNoFallbackImage + return nil, constants.ErrNoFallbackImage } content := "FROM " + opts.FallbackImage _, err = file.Write([]byte(content)) @@ -259,7 +235,7 @@ func Run(ctx context.Context, opts options.Options) error { return &devcontainer.Compiled{ DockerfilePath: dockerfile, DockerfileContent: content, - BuildContext: MagicDir, + BuildContext: constants.MagicDir, }, nil } @@ -301,7 +277,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, MagicDir, fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv) + buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, constants.MagicDir, fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv) if err != nil { return fmt.Errorf("compile devcontainer.json: %w", err) } @@ -399,7 +375,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{ - MagicDir, + constants.MagicDir, opts.WorkspaceFolder, // See: https://github.com/coder/envbuilder/issues/37 "/etc/resolv.conf", @@ -441,7 +417,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) } // temp move of all ro mounts - tempRemountDest := filepath.Join("/", MagicDir, "mnt") + tempRemountDest := filepath.Join("/", constants.MagicDir, "mnt") // ignorePrefixes is a superset of ignorePaths that we pass to kaniko's // IgnoreList. ignorePrefixes := append([]string{"/dev", "/proc", "/sys"}, ignorePaths...) @@ -457,7 +433,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) skippedRebuild := false build := func() (v1.Image, error) { - _, err := opts.Filesystem.Stat(MagicFile) + _, err := opts.Filesystem.Stat(constants.MagicFile) if err == nil && opts.SkipRebuild { endStage := startStage("🏗️ Skipping build because of cache...") imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent) @@ -640,7 +616,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // Create the magic file to indicate that this build // has already been ran before! - file, err := opts.Filesystem.Create(MagicFile) + file, err := opts.Filesystem.Create(constants.MagicFile) if err != nil { return fmt.Errorf("create magic file: %w", err) } @@ -695,7 +671,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // Remove the Docker config secret file! if opts.DockerConfigBase64 != "" { - c := filepath.Join(MagicDir, "config.json") + c := filepath.Join(constants.MagicDir, "config.json") err = os.Remove(c) if err != nil { if !errors.Is(err, fs.ErrNotExist) { @@ -855,7 +831,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) opts.Logger(log.LevelInfo, "=== Running the setup command %q as the root user...", opts.SetupScript) envKey := "ENVBUILDER_ENV" - envFile := filepath.Join("/", MagicDir, "environ") + envFile := filepath.Join("/", constants.MagicDir, "environ") file, err := os.Create(envFile) if err != nil { return fmt.Errorf("create environ file: %w", err) @@ -958,7 +934,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // for a given repository URL. func DefaultWorkspaceFolder(repoURL string) (string, error) { if repoURL == "" { - return EmptyWorkspaceDir, nil + return constants.EmptyWorkspaceDir, nil } parsed, err := giturls.Parse(repoURL) if err != nil { @@ -967,7 +943,7 @@ func DefaultWorkspaceFolder(repoURL string) (string, error) { name := strings.Split(parsed.Path, "/") hasOwnerAndRepo := len(name) >= 2 if !hasOwnerAndRepo { - return EmptyWorkspaceDir, nil + return constants.EmptyWorkspaceDir, nil } repo := strings.TrimSuffix(name[len(name)-1], ".git") return fmt.Sprintf("/workspaces/%s", repo), nil @@ -1180,7 +1156,7 @@ func findDevcontainerJSON(options options.Options) (string, string, error) { // folks from unwittingly deleting their entire root directory. func maybeDeleteFilesystem(logger log.Func, force bool) error { kanikoDir, ok := os.LookupEnv("KANIKO_DIR") - if !ok || strings.TrimSpace(kanikoDir) != MagicDir { + if !ok || strings.TrimSpace(kanikoDir) != constants.MagicDir { if force { bailoutSecs := 10 logger(log.LevelWarn, "WARNING! BYPASSING SAFETY CHECK! THIS WILL DELETE YOUR ROOT FILESYSTEM!") @@ -1190,7 +1166,7 @@ func maybeDeleteFilesystem(logger log.Func, force bool) error { <-time.After(time.Second) } } else { - logger(log.LevelError, "KANIKO_DIR is not set to %s. Bailing!\n", MagicDir) + logger(log.LevelError, "KANIKO_DIR is not set to %s. Bailing!\n", constants.MagicDir) logger(log.LevelError, "To bypass this check, set FORCE_SAFE=true.") return errors.New("safety check failed") } diff --git a/envbuilder_test.go b/envbuilder_test.go index 6af599c9..0545bfce 100644 --- a/envbuilder_test.go +++ b/envbuilder_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/coder/envbuilder" + "github.com/coder/envbuilder/constants" + "github.com/stretchr/testify/require" ) @@ -38,7 +40,7 @@ func TestDefaultWorkspaceFolder(t *testing.T) { { name: "empty", gitURL: "", - expected: envbuilder.EmptyWorkspaceDir, + expected: constants.EmptyWorkspaceDir, }, } for _, tt := range successTests { @@ -66,7 +68,7 @@ func TestDefaultWorkspaceFolder(t *testing.T) { t.Run(tt.name, func(t *testing.T) { dir, err := envbuilder.DefaultWorkspaceFolder(tt.invalidURL) require.NoError(t, err) - require.Equal(t, envbuilder.EmptyWorkspaceDir, dir) + require.Equal(t, constants.EmptyWorkspaceDir, dir) }) } } diff --git a/integration/integration_test.go b/integration/integration_test.go index e62bac02..b3fe7bef 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -21,10 +21,10 @@ import ( "testing" "time" - "github.com/coder/envbuilder/options" - "github.com/coder/envbuilder" + "github.com/coder/envbuilder/constants" "github.com/coder/envbuilder/devcontainer/features" + "github.com/coder/envbuilder/options" "github.com/coder/envbuilder/testutil/gittest" "github.com/coder/envbuilder/testutil/mwtest" "github.com/coder/envbuilder/testutil/registrytest" @@ -366,7 +366,7 @@ 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(envbuilder.MagicDir, "config.json")) + output = execContainer(t, ctr, "stat "+filepath.Join(constants.MagicDir, "config.json")) require.Contains(t, output, "No such file or directory") } @@ -592,7 +592,7 @@ func TestCloneFailsFallback(t *testing.T) { _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", "bad-value"), }}) - require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) }) } @@ -610,7 +610,7 @@ func TestBuildFailsFallback(t *testing.T) { envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) - require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) require.ErrorContains(t, err, "dockerfile parse error") }) t.Run("FailsBuild", func(t *testing.T) { @@ -626,7 +626,7 @@ RUN exit 1`, envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) - require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) }) t.Run("BadDevcontainer", func(t *testing.T) { t.Parallel() @@ -639,7 +639,7 @@ RUN exit 1`, _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }}) - require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) }) t.Run("NoImageOrDockerfile", func(t *testing.T) { t.Parallel() @@ -972,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, envbuilder.ErrNoFallbackImage.Error()) + require.ErrorContains(t, err, constants.ErrNoFallbackImage.Error()) } func TestDockerfileBuildContext(t *testing.T) {