diff --git a/Makefile b/Makefile index 28827efc..8bd3f6b5 100644 --- a/Makefile +++ b/Makefile @@ -33,10 +33,10 @@ build: scripts/envbuilder-$(GOARCH) update-golden-files: .gen-golden .gen-golden: $(GOLDEN_FILES) $(GO_SRC_FILES) $(GO_TEST_FILES) - go test . -update + go test ./options -update @touch "$@" -docs: options.go +docs: options/options.go options/options_test.go go run ./scripts/docsgen/main.go .PHONY: test diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go index 77405536..24d63fd5 100644 --- a/cmd/envbuilder/main.go +++ b/cmd/envbuilder/main.go @@ -8,6 +8,8 @@ import ( "slices" "strings" + "github.com/coder/envbuilder/options" + "github.com/coder/coder/v2/codersdk" "github.com/coder/envbuilder" "github.com/coder/envbuilder/internal/log" @@ -23,47 +25,47 @@ func main() { cmd := envbuilderCmd() err := cmd.Invoke().WithOS().Run() if err != nil { - fmt.Fprintf(os.Stderr, "error: %v", err) + _, _ = fmt.Fprintf(os.Stderr, "error: %v", err) os.Exit(1) } } func envbuilderCmd() serpent.Command { - var options envbuilder.Options + var o options.Options cmd := serpent.Command{ Use: "envbuilder", - Options: options.CLI(), + Options: o.CLI(), Handler: func(inv *serpent.Invocation) error { - options.Logger = log.New(os.Stderr, options.Verbose) - if options.CoderAgentURL != "" { - if options.CoderAgentToken == "" { + o.Logger = log.New(os.Stderr, o.Verbose) + if o.CoderAgentURL != "" { + if o.CoderAgentToken == "" { return errors.New("CODER_AGENT_URL must be set if CODER_AGENT_TOKEN is set") } - u, err := url.Parse(options.CoderAgentURL) + u, err := url.Parse(o.CoderAgentURL) if err != nil { return fmt.Errorf("unable to parse CODER_AGENT_URL as URL: %w", err) } - coderLog, closeLogs, err := log.Coder(inv.Context(), u, options.CoderAgentToken) + coderLog, closeLogs, err := log.Coder(inv.Context(), u, o.CoderAgentToken) if err == nil { - options.Logger = log.Wrap(options.Logger, coderLog) + o.Logger = log.Wrap(o.Logger, coderLog) defer closeLogs() // This adds the envbuilder subsystem. // If telemetry is enabled in a Coder deployment, // this will be reported and help us understand // envbuilder usage. - if !slices.Contains(options.CoderAgentSubsystem, string(codersdk.AgentSubsystemEnvbuilder)) { - options.CoderAgentSubsystem = append(options.CoderAgentSubsystem, string(codersdk.AgentSubsystemEnvbuilder)) - _ = os.Setenv("CODER_AGENT_SUBSYSTEM", strings.Join(options.CoderAgentSubsystem, ",")) + if !slices.Contains(o.CoderAgentSubsystem, string(codersdk.AgentSubsystemEnvbuilder)) { + o.CoderAgentSubsystem = append(o.CoderAgentSubsystem, string(codersdk.AgentSubsystemEnvbuilder)) + _ = os.Setenv("CODER_AGENT_SUBSYSTEM", strings.Join(o.CoderAgentSubsystem, ",")) } } else { // Failure to log to Coder should cause a fatal error. - options.Logger(log.LevelError, "unable to send logs to Coder: %s", err.Error()) + o.Logger(log.LevelError, "unable to send logs to Coder: %s", err.Error()) } } - err := envbuilder.Run(inv.Context(), options) + err := envbuilder.Run(inv.Context(), o) if err != nil { - options.Logger(log.LevelError, "error: %s", err) + o.Logger(log.LevelError, "error: %s", err) } return err }, diff --git a/envbuilder.go b/envbuilder.go index 62c4279e..3cbdab04 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -24,8 +24,7 @@ import ( "syscall" "time" - "github.com/kballard/go-shellquote" - "github.com/mattn/go-isatty" + "github.com/coder/envbuilder/options" "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/GoogleContainerTools/kaniko/pkg/creds" @@ -46,6 +45,8 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/kballard/go-shellquote" + "github.com/mattn/go-isatty" "github.com/sirupsen/logrus" "github.com/tailscale/hujson" "golang.org/x/xerrors" @@ -83,45 +84,45 @@ type DockerConfig configfile.ConfigFile // Logger is the logf to use for all operations. // Filesystem is the filesystem to use for all operations. // Defaults to the host filesystem. -func Run(ctx context.Context, options Options) error { +func Run(ctx context.Context, opts options.Options) error { // Temporarily removed these from the default settings to prevent conflicts // between current and legacy environment variables that add default values. // Once the legacy environment variables are phased out, this can be // reinstated to the previous default values. - if len(options.IgnorePaths) == 0 { - options.IgnorePaths = []string{ + if len(opts.IgnorePaths) == 0 { + opts.IgnorePaths = []string{ "/var/run", // KinD adds these paths to pods, so ignore them by default. "/product_uuid", "/product_name", } } - if options.InitScript == "" { - options.InitScript = "sleep infinity" + if opts.InitScript == "" { + opts.InitScript = "sleep infinity" } - if options.InitCommand == "" { - options.InitCommand = "/bin/sh" + if opts.InitCommand == "" { + opts.InitCommand = "/bin/sh" } - if options.CacheRepo == "" && options.PushImage { + if opts.CacheRepo == "" && opts.PushImage { return fmt.Errorf("--cache-repo must be set when using --push-image") } // Default to the shell! - initArgs := []string{"-c", options.InitScript} - if options.InitArgs != "" { + initArgs := []string{"-c", opts.InitScript} + if opts.InitArgs != "" { var err error - initArgs, err = shellquote.Split(options.InitArgs) + initArgs, err = shellquote.Split(opts.InitArgs) if err != nil { return fmt.Errorf("parse init args: %w", err) } } - if options.Filesystem == nil { - options.Filesystem = &osfsWithChmod{osfs.New("/")} + if opts.Filesystem == nil { + opts.Filesystem = &osfsWithChmod{osfs.New("/")} } - if options.WorkspaceFolder == "" { - f, err := DefaultWorkspaceFolder(options.GitURL) + if opts.WorkspaceFolder == "" { + f, err := DefaultWorkspaceFolder(opts.GitURL) if err != nil { return err } - options.WorkspaceFolder = f + opts.WorkspaceFolder = f } stageNumber := 0 @@ -129,22 +130,22 @@ func Run(ctx context.Context, options Options) error { now := time.Now() stageNumber++ stageNum := stageNumber - options.Logger(log.LevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) + opts.Logger(log.LevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) return func(format string, args ...any) { - options.Logger(log.LevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) + opts.Logger(log.LevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) } } - options.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) + opts.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) var caBundle []byte - if options.SSLCertBase64 != "" { + if opts.SSLCertBase64 != "" { certPool, err := x509.SystemCertPool() if err != nil { return xerrors.Errorf("get global system cert pool: %w", err) } - data, err := base64.StdEncoding.DecodeString(options.SSLCertBase64) + data, err := base64.StdEncoding.DecodeString(opts.SSLCertBase64) if err != nil { return xerrors.Errorf("base64 decode ssl cert: %w", err) } @@ -155,8 +156,8 @@ func Run(ctx context.Context, options Options) error { caBundle = data } - if options.DockerConfigBase64 != "" { - decoded, err := base64.StdEncoding.DecodeString(options.DockerConfigBase64) + if opts.DockerConfigBase64 != "" { + decoded, err := base64.StdEncoding.DecodeString(opts.DockerConfigBase64) if err != nil { return fmt.Errorf("decode docker config: %w", err) } @@ -177,10 +178,10 @@ func Run(ctx context.Context, options Options) error { var fallbackErr error var cloned bool - if options.GitURL != "" { + if opts.GitURL != "" { endStage := startStage("📦 Cloning %s to %s...", - newColor(color.FgCyan).Sprintf(options.GitURL), - newColor(color.FgCyan).Sprintf(options.WorkspaceFolder), + newColor(color.FgCyan).Sprintf(opts.GitURL), + newColor(color.FgCyan).Sprintf(opts.WorkspaceFolder), ) reader, writer := io.Pipe() @@ -198,28 +199,28 @@ func Run(ctx context.Context, options Options) error { if line == "" { continue } - options.Logger(log.LevelInfo, "#1: %s", strings.TrimSpace(line)) + opts.Logger(log.LevelInfo, "#1: %s", strings.TrimSpace(line)) } } }() cloneOpts := CloneRepoOptions{ - Path: options.WorkspaceFolder, - Storage: options.Filesystem, - Insecure: options.Insecure, + Path: opts.WorkspaceFolder, + Storage: opts.Filesystem, + Insecure: opts.Insecure, Progress: writer, - SingleBranch: options.GitCloneSingleBranch, - Depth: int(options.GitCloneDepth), + SingleBranch: opts.GitCloneSingleBranch, + Depth: int(opts.GitCloneDepth), CABundle: caBundle, } - cloneOpts.RepoAuth = SetupRepoAuth(&options) - if options.GitHTTPProxyURL != "" { + cloneOpts.RepoAuth = SetupRepoAuth(&opts) + if opts.GitHTTPProxyURL != "" { cloneOpts.ProxyOptions = transport.ProxyOptions{ - URL: options.GitHTTPProxyURL, + URL: opts.GitHTTPProxyURL, } } - cloneOpts.RepoURL = options.GitURL + cloneOpts.RepoURL = opts.GitURL cloned, fallbackErr = CloneRepo(ctx, cloneOpts) if fallbackErr == nil { @@ -229,19 +230,19 @@ func Run(ctx context.Context, options Options) error { endStage("📦 The repository already exists!") } } else { - options.Logger(log.LevelError, "Failed to clone repository: %s", fallbackErr.Error()) - options.Logger(log.LevelError, "Falling back to the default image...") + opts.Logger(log.LevelError, "Failed to clone repository: %s", fallbackErr.Error()) + opts.Logger(log.LevelError, "Falling back to the default image...") } } defaultBuildParams := func() (*devcontainer.Compiled, error) { dockerfile := filepath.Join(MagicDir, "Dockerfile") - file, err := options.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0o644) + file, err := opts.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return nil, err } defer file.Close() - if options.FallbackImage == "" { + if opts.FallbackImage == "" { if fallbackErr != nil { return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage) } @@ -249,7 +250,7 @@ func Run(ctx context.Context, options Options) error { // don't support parsing a multiline error. return nil, ErrNoFallbackImage } - content := "FROM " + options.FallbackImage + content := "FROM " + opts.FallbackImage _, err = file.Write([]byte(content)) if err != nil { return nil, err @@ -267,19 +268,19 @@ func Run(ctx context.Context, options Options) error { devcontainerPath string ) - if options.DockerfilePath == "" { + if opts.DockerfilePath == "" { // Only look for a devcontainer if a Dockerfile wasn't specified. // devcontainer is a standard, so it's reasonable to be the default. var devcontainerDir string var err error - devcontainerPath, devcontainerDir, err = findDevcontainerJSON(options) + devcontainerPath, devcontainerDir, err = findDevcontainerJSON(opts) if err != nil { - options.Logger(log.LevelError, "Failed to locate devcontainer.json: %s", err.Error()) - options.Logger(log.LevelError, "Falling back to the default image...") + opts.Logger(log.LevelError, "Failed to locate devcontainer.json: %s", err.Error()) + opts.Logger(log.LevelError, "Falling back to the default image...") } else { // We know a devcontainer exists. // Let's parse it and use it! - file, err := options.Filesystem.Open(devcontainerPath) + file, err := opts.Filesystem.Open(devcontainerPath) if err != nil { return fmt.Errorf("open devcontainer.json: %w", err) } @@ -296,32 +297,32 @@ func Run(ctx context.Context, options Options) error { if err != nil { return fmt.Errorf("no Dockerfile or image found: %w", err) } - options.Logger(log.LevelInfo, "No Dockerfile or image specified; falling back to the default image...") + opts.Logger(log.LevelInfo, "No Dockerfile or image specified; falling back to the default image...") fallbackDockerfile = defaultParams.DockerfilePath } - buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false, os.LookupEnv) + buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv) if err != nil { return fmt.Errorf("compile devcontainer.json: %w", err) } scripts = devContainer.LifecycleScripts } else { - options.Logger(log.LevelError, "Failed to parse devcontainer.json: %s", err.Error()) - options.Logger(log.LevelError, "Falling back to the default image...") + opts.Logger(log.LevelError, "Failed to parse devcontainer.json: %s", err.Error()) + opts.Logger(log.LevelError, "Falling back to the default image...") } } } else { // If a Dockerfile was specified, we use that. - dockerfilePath := filepath.Join(options.WorkspaceFolder, options.DockerfilePath) + dockerfilePath := filepath.Join(opts.WorkspaceFolder, opts.DockerfilePath) // If the dockerfilePath is specified and deeper than the base of WorkspaceFolder AND the BuildContextPath is // not defined, show a warning dockerfileDir := filepath.Dir(dockerfilePath) - if dockerfileDir != filepath.Clean(options.WorkspaceFolder) && options.BuildContextPath == "" { - options.Logger(log.LevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.WorkspaceFolder) - options.Logger(log.LevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) + if dockerfileDir != filepath.Clean(opts.WorkspaceFolder) && opts.BuildContextPath == "" { + opts.Logger(log.LevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, opts.WorkspaceFolder) + opts.Logger(log.LevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) } - dockerfile, err := options.Filesystem.Open(dockerfilePath) + dockerfile, err := opts.Filesystem.Open(dockerfilePath) if err == nil { content, err := io.ReadAll(dockerfile) if err != nil { @@ -330,7 +331,7 @@ func Run(ctx context.Context, options Options) error { buildParams = &devcontainer.Compiled{ DockerfilePath: dockerfilePath, DockerfileContent: string(content), - BuildContext: filepath.Join(options.WorkspaceFolder, options.BuildContextPath), + BuildContext: filepath.Join(opts.WorkspaceFolder, opts.BuildContextPath), } } } @@ -347,17 +348,17 @@ func Run(ctx context.Context, options Options) error { HijackLogrus(func(entry *logrus.Entry) { for _, line := range strings.Split(entry.Message, "\r") { - options.Logger(log.LevelInfo, "#%d: %s", stageNumber, color.HiBlackString(line)) + opts.Logger(log.LevelInfo, "#%d: %s", stageNumber, color.HiBlackString(line)) } }) var closeAfterBuild func() // Allows quick testing of layer caching using a local directory! - if options.LayerCacheDir != "" { + if opts.LayerCacheDir != "" { cfg := &configuration.Configuration{ Storage: configuration.Storage{ "filesystem": configuration.Parameters{ - "rootdirectory": options.LayerCacheDir, + "rootdirectory": opts.LayerCacheDir, }, }, } @@ -380,31 +381,31 @@ func Run(ctx context.Context, options Options) error { go func() { err := srv.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { - options.Logger(log.LevelError, "Failed to serve registry: %s", err.Error()) + opts.Logger(log.LevelError, "Failed to serve registry: %s", err.Error()) } }() closeAfterBuild = func() { _ = srv.Close() _ = listener.Close() } - if options.CacheRepo != "" { - options.Logger(log.LevelWarn, "Overriding cache repo with local registry...") + if opts.CacheRepo != "" { + opts.Logger(log.LevelWarn, "Overriding cache repo with local registry...") } - options.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port) + opts.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port) } - // IgnorePaths in the Kaniko options doesn't properly ignore paths. + // IgnorePaths in the Kaniko opts doesn't properly ignore paths. // 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, - options.WorkspaceFolder, + opts.WorkspaceFolder, // See: https://github.com/coder/envbuilder/issues/37 "/etc/resolv.conf", - }, options.IgnorePaths...) + }, opts.IgnorePaths...) - if options.LayerCacheDir != "" { - ignorePaths = append(ignorePaths, options.LayerCacheDir) + if opts.LayerCacheDir != "" { + ignorePaths = append(ignorePaths, opts.LayerCacheDir) } for _, ignorePath := range ignorePaths { @@ -417,7 +418,7 @@ func Run(ctx context.Context, options Options) error { // In order to allow 'resuming' envbuilder, embed the binary into the image // if it is being pushed - if options.PushImage { + if opts.PushImage { exePath, err := os.Executable() if err != nil { return xerrors.Errorf("get exe path: %w", err) @@ -443,10 +444,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // ignorePrefixes is a superset of ignorePaths that we pass to kaniko's // IgnoreList. ignorePrefixes := append([]string{"/dev", "/proc", "/sys"}, ignorePaths...) - restoreMounts, err := ebutil.TempRemount(options.Logger, tempRemountDest, ignorePrefixes...) + restoreMounts, err := ebutil.TempRemount(opts.Logger, tempRemountDest, ignorePrefixes...) defer func() { // restoreMounts should never be nil if err := restoreMounts(); err != nil { - options.Logger(log.LevelError, "restore mounts: %s", err.Error()) + opts.Logger(log.LevelError, "restore mounts: %s", err.Error()) } }() if err != nil { @@ -455,8 +456,8 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) skippedRebuild := false build := func() (v1.Image, error) { - _, err := options.Filesystem.Stat(MagicFile) - if err == nil && options.SkipRebuild { + _, err := opts.Filesystem.Stat(MagicFile) + if err == nil && opts.SkipRebuild { endStage := startStage("🏗️ Skipping build because of cache...") imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent) if err != nil { @@ -479,7 +480,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // It's possible that the container will already have files in it, and // we don't want to merge a new container with the old one. - if err := maybeDeleteFilesystem(options.Logger, options.ForceSafe); err != nil { + if err := maybeDeleteFilesystem(opts.Logger, opts.ForceSafe); err != nil { return nil, fmt.Errorf("delete filesystem: %w", err) } @@ -492,18 +493,18 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) go func() { scanner := bufio.NewScanner(stdoutReader) for scanner.Scan() { - options.Logger(log.LevelInfo, "%s", scanner.Text()) + opts.Logger(log.LevelInfo, "%s", scanner.Text()) } }() go func() { scanner := bufio.NewScanner(stderrReader) for scanner.Scan() { - options.Logger(log.LevelInfo, "%s", scanner.Text()) + opts.Logger(log.LevelInfo, "%s", scanner.Text()) } }() cacheTTL := time.Hour * 24 * 7 - if options.CacheTTLDays != 0 { - cacheTTL = time.Hour * 24 * time.Duration(options.CacheTTLDays) + if opts.CacheTTLDays != 0 { + cacheTTL = time.Hour * 24 * time.Duration(opts.CacheTTLDays) } // At this point we have all the context, we can now build! @@ -512,10 +513,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) registryMirror = strings.Split(val, ";") } var destinations []string - if options.CacheRepo != "" { - destinations = append(destinations, options.CacheRepo) + if opts.CacheRepo != "" { + destinations = append(destinations, opts.CacheRepo) } - opts := &config.KanikoOptions{ + kOpts := &config.KanikoOptions{ // Boilerplate! CustomPlatform: platforms.Format(platforms.Normalize(platforms.DefaultSpec())), SnapshotMode: "redo", @@ -523,7 +524,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) RunStdout: stdoutWriter, RunStderr: stderrWriter, Destinations: destinations, - NoPush: !options.PushImage || len(destinations) == 0, + NoPush: !opts.PushImage || len(destinations) == 0, CacheRunLayers: true, CacheCopyLayers: true, CompressedCaching: true, @@ -535,18 +536,18 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) CacheOptions: config.CacheOptions{ // Cache for a week by default! CacheTTL: cacheTTL, - CacheDir: options.BaseImageCacheDir, + CacheDir: opts.BaseImageCacheDir, }, ForceUnpack: true, BuildArgs: buildParams.BuildArgs, - CacheRepo: options.CacheRepo, - Cache: options.CacheRepo != "" || options.BaseImageCacheDir != "", + CacheRepo: opts.CacheRepo, + Cache: opts.CacheRepo != "" || opts.BaseImageCacheDir != "", DockerfilePath: buildParams.DockerfilePath, DockerfileContent: buildParams.DockerfileContent, RegistryOptions: config.RegistryOptions{ - Insecure: options.Insecure, - InsecurePull: options.Insecure, - SkipTLSVerify: options.Insecure, + Insecure: opts.Insecure, + InsecurePull: opts.Insecure, + SkipTLSVerify: opts.Insecure, // Enables registry mirror features in Kaniko, see more in link below // https://github.com/GoogleContainerTools/kaniko?tab=readme-ov-file#flag---registry-mirror // Related to PR #114 @@ -556,12 +557,12 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) SrcContext: buildParams.BuildContext, // For cached image utilization, produce reproducible builds. - Reproducible: options.PushImage, + Reproducible: opts.PushImage, } - if options.GetCachedImage { + if opts.GetCachedImage { endStage := startStage("🏗️ Checking for cached image...") - image, err := executor.DoCacheProbe(opts) + image, err := executor.DoCacheProbe(kOpts) if err != nil { return nil, xerrors.Errorf("get cached image: %w", err) } @@ -570,19 +571,19 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) return nil, xerrors.Errorf("get cached image digest: %w", err) } endStage("🏗️ Found cached image!") - _, _ = fmt.Fprintf(os.Stdout, "ENVBUILDER_CACHED_IMAGE=%s@%s\n", options.CacheRepo, digest.String()) + _, _ = fmt.Fprintf(os.Stdout, "ENVBUILDER_CACHED_IMAGE=%s@%s\n", kOpts.CacheRepo, digest.String()) os.Exit(0) } endStage := startStage("🏗️ Building image...") - image, err := executor.DoBuild(opts) + image, err := executor.DoBuild(kOpts) if err != nil { return nil, xerrors.Errorf("do build: %w", err) } endStage("🏗️ Built image!") - if options.PushImage { + if opts.PushImage { endStage = startStage("🏗️ Pushing image...") - if err := executor.DoPush(image, opts); err != nil { + if err := executor.DoPush(image, kOpts); err != nil { return nil, xerrors.Errorf("do push: %w", err) } endStage("🏗️ Pushed image!") @@ -611,13 +612,13 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) fallback = true fallbackErr = err case strings.Contains(err.Error(), "unexpected status code 401 Unauthorized"): - options.Logger(log.LevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") + opts.Logger(log.LevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") } - if !fallback || options.ExitOnBuildFailure { + if !fallback || opts.ExitOnBuildFailure { return err } - options.Logger(log.LevelError, "Failed to build: %s", err) - options.Logger(log.LevelError, "Falling back to the default image...") + opts.Logger(log.LevelError, "Failed to build: %s", err) + opts.Logger(log.LevelError, "Falling back to the default image...") buildParams, err = defaultBuildParams() if err != nil { return err @@ -638,7 +639,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // Create the magic file to indicate that this build // has already been ran before! - file, err := options.Filesystem.Create(MagicFile) + file, err := opts.Filesystem.Create(MagicFile) if err != nil { return fmt.Errorf("create magic file: %w", err) } @@ -664,10 +665,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) if err != nil { return fmt.Errorf("unmarshal metadata: %w", err) } - options.Logger(log.LevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...") + opts.Logger(log.LevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...") for _, container := range devContainer { if container.RemoteUser != "" { - options.Logger(log.LevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser) + opts.Logger(log.LevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser) configFile.Config.User = container.RemoteUser } @@ -688,11 +689,11 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) } } - // Sanitize the environment of any options! - unsetOptionsEnv() + // Sanitize the environment of any opts! + options.UnsetEnv() // Remove the Docker config secret file! - if options.DockerConfigBase64 != "" { + if opts.DockerConfigBase64 != "" { c := filepath.Join(MagicDir, "config.json") err = os.Remove(c) if err != nil { @@ -741,7 +742,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) } sort.Strings(envKeys) for _, envVar := range envKeys { - value := devcontainer.SubstituteVars(env[envVar], options.WorkspaceFolder, os.LookupEnv) + value := devcontainer.SubstituteVars(env[envVar], opts.WorkspaceFolder, os.LookupEnv) os.Setenv(envVar, value) } } @@ -751,10 +752,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // in the export. We should have generated a complete set of environment // on the intial build, so exporting environment variables a second time // isn't useful anyway. - if options.ExportEnvFile != "" && !skippedRebuild { - exportEnvFile, err := os.Create(options.ExportEnvFile) + if opts.ExportEnvFile != "" && !skippedRebuild { + exportEnvFile, err := os.Create(opts.ExportEnvFile) if err != nil { - return fmt.Errorf("failed to open EXPORT_ENV_FILE %q: %w", options.ExportEnvFile, err) + return fmt.Errorf("failed to open EXPORT_ENV_FILE %q: %w", opts.ExportEnvFile, err) } envKeys := make([]string, 0, len(allEnvKeys)) @@ -774,7 +775,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) username = buildParams.User } if username == "" { - options.Logger(log.LevelWarn, "#3: no user specified, using root") + opts.Logger(log.LevelWarn, "#3: no user specified, using root") } userInfo, err := getUser(username) @@ -791,13 +792,13 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // // We need to change the ownership of the files to the user that will // be running the init script. - if chownErr := filepath.Walk(options.WorkspaceFolder, func(path string, _ os.FileInfo, err error) error { + if chownErr := filepath.Walk(opts.WorkspaceFolder, func(path string, _ os.FileInfo, err error) error { if err != nil { return err } return os.Chown(path, userInfo.uid, userInfo.gid) }); chownErr != nil { - options.Logger(log.LevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error()) + opts.Logger(log.LevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error()) endStage("⚠️ Failed to the ownership of the workspace, you may need to fix this manually!") } else { endStage("👤 Updated the ownership of the workspace!") @@ -814,18 +815,18 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) } return os.Chown(path, userInfo.uid, userInfo.gid) }); chownErr != nil { - options.Logger(log.LevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error()) + opts.Logger(log.LevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error()) endStage("⚠️ Failed to update ownership of %s, you may need to fix this manually!", userInfo.user.HomeDir) } else { endStage("🏡 Updated ownership of %s!", userInfo.user.HomeDir) } } - err = os.MkdirAll(options.WorkspaceFolder, 0o755) + err = os.MkdirAll(opts.WorkspaceFolder, 0o755) if err != nil { return fmt.Errorf("create workspace folder: %w", err) } - err = os.Chdir(options.WorkspaceFolder) + err = os.Chdir(opts.WorkspaceFolder) if err != nil { return fmt.Errorf("change directory: %w", err) } @@ -837,7 +838,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // exec systemd as the init command, but that doesn't mean we should // run the lifecycle scripts as root. os.Setenv("HOME", userInfo.user.HomeDir) - if err := execLifecycleScripts(ctx, options, scripts, skippedRebuild, userInfo); err != nil { + if err := execLifecycleScripts(ctx, opts, scripts, skippedRebuild, userInfo); err != nil { return err } @@ -846,11 +847,11 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) // // This is useful for hooking into the environment for a specific // init to PID 1. - if options.SetupScript != "" { + if opts.SetupScript != "" { // We execute the initialize script as the root user! os.Setenv("HOME", "/root") - options.Logger(log.LevelInfo, "=== Running the setup command %q as the root user...", options.SetupScript) + opts.Logger(log.LevelInfo, "=== Running the setup command %q as the root user...", opts.SetupScript) envKey := "ENVBUILDER_ENV" envFile := filepath.Join("/", MagicDir, "environ") @@ -860,12 +861,12 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) } _ = file.Close() - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", options.SetupScript) + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", opts.SetupScript) cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", envKey, envFile), fmt.Sprintf("TARGET_USER=%s", userInfo.user.Username), ) - cmd.Dir = options.WorkspaceFolder + cmd.Dir = opts.WorkspaceFolder // This allows for a really nice and clean experience to experiement with! // e.g. docker run --it --rm -e INIT_SCRIPT bash ... if isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stdin.Fd()) { @@ -877,7 +878,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) go func() { scanner := bufio.NewScanner(&buf) for scanner.Scan() { - options.Logger(log.LevelInfo, "%s", scanner.Text()) + opts.Logger(log.LevelInfo, "%s", scanner.Text()) } }() @@ -907,7 +908,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) key := pair[0] switch key { case "INIT_COMMAND": - options.InitCommand = pair[1] + opts.InitCommand = pair[1] updatedCommand = true case "INIT_ARGS": initArgs, err = shellquote.Split(pair[1]) @@ -943,9 +944,9 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath) return fmt.Errorf("set uid: %w", err) } - options.Logger(log.LevelInfo, "=== Running the init command %s %+v as the %q user...", options.InitCommand, initArgs, userInfo.user.Username) + opts.Logger(log.LevelInfo, "=== Running the init command %s %+v as the %q user...", opts.InitCommand, initArgs, userInfo.user.Username) - err = syscall.Exec(options.InitCommand, append([]string{options.InitCommand}, initArgs...), os.Environ()) + err = syscall.Exec(opts.InitCommand, append([]string{opts.InitCommand}, initArgs...), os.Environ()) if err != nil { return fmt.Errorf("exec init script: %w", err) } @@ -1039,7 +1040,7 @@ func execOneLifecycleScript( func execLifecycleScripts( ctx context.Context, - options Options, + options options.Options, scripts devcontainer.LifecycleScripts, skippedRebuild bool, userInfo userInfo, @@ -1093,25 +1094,6 @@ func createPostStartScript(path string, postStartCommand devcontainer.LifecycleS return nil } -// unsetOptionsEnv unsets all environment variables that are used -// to configure the options. -func unsetOptionsEnv() { - var o Options - for _, opt := range o.CLI() { - if opt.Env == "" { - continue - } - // Do not strip options that do not have the magic prefix! - // For example, CODER_AGENT_URL, CODER_AGENT_TOKEN, CODER_AGENT_SUBSYSTEM. - if !strings.HasPrefix(opt.Env, envPrefix) { - continue - } - // Strip both with and without prefix. - os.Unsetenv(opt.Env) - os.Unsetenv(strings.TrimPrefix(opt.Env, envPrefix)) - } -} - func newColor(value ...color.Attribute) *color.Color { c := color.New(value...) c.EnableColor() @@ -1126,7 +1108,7 @@ func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } -func findDevcontainerJSON(options Options) (string, string, error) { +func findDevcontainerJSON(options options.Options) (string, string, error) { // 0. Check if custom devcontainer directory or path is provided. if options.DevcontainerDir != "" || options.DevcontainerJSONPath != "" { devcontainerDir := options.DevcontainerDir diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index 65edb9cd..3af4b5e4 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -3,6 +3,8 @@ package envbuilder import ( "testing" + "github.com/coder/envbuilder/options" + "github.com/go-git/go-billy/v5/memfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,7 +20,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs := memfs.New() // when - _, _, err := findDevcontainerJSON(Options{ + _, _, err := findDevcontainerJSON(options.Options{ Filesystem: fs, WorkspaceFolder: "/workspace", }) @@ -36,7 +38,7 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - _, _, err = findDevcontainerJSON(Options{ + _, _, err = findDevcontainerJSON(options.Options{ Filesystem: fs, WorkspaceFolder: "/workspace", }) @@ -56,7 +58,7 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{ Filesystem: fs, WorkspaceFolder: "/workspace", }) @@ -78,7 +80,7 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{ Filesystem: fs, WorkspaceFolder: "/workspace", DevcontainerDir: "experimental-devcontainer", @@ -101,7 +103,7 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{ Filesystem: fs, WorkspaceFolder: "/workspace", DevcontainerJSONPath: "experimental.json", @@ -124,7 +126,7 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{ Filesystem: fs, WorkspaceFolder: "/workspace", }) @@ -146,7 +148,7 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{ Filesystem: fs, WorkspaceFolder: "/workspace", }) diff --git a/git.go b/git.go index f28cab8d..eb75b654 100644 --- a/git.go +++ b/git.go @@ -9,6 +9,8 @@ import ( "os" "strings" + "github.com/coder/envbuilder/options" + giturls "github.com/chainguard-dev/git-urls" "github.com/coder/envbuilder/internal/log" "github.com/go-git/go-billy/v5" @@ -177,7 +179,7 @@ func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback { // If SSH_KNOWN_HOSTS is not set, the SSH auth method will be configured // to accept and log all host keys. Otherwise, host key checking will be // performed as usual. -func SetupRepoAuth(options *Options) transport.AuthMethod { +func SetupRepoAuth(options *options.Options) transport.AuthMethod { if options.GitURL == "" { options.Logger(log.LevelInfo, "#1: ❔ No Git URL supplied!") return nil diff --git a/git_test.go b/git_test.go index 38efee1a..842cf2c9 100644 --- a/git_test.go +++ b/git_test.go @@ -12,6 +12,8 @@ import ( "regexp" "testing" + "github.com/coder/envbuilder/options" + "github.com/coder/envbuilder" "github.com/coder/envbuilder/internal/log" "github.com/coder/envbuilder/testutil/gittest" @@ -265,7 +267,7 @@ func TestCloneRepoSSH(t *testing.T) { func TestSetupRepoAuth(t *testing.T) { t.Setenv("SSH_AUTH_SOCK", "") t.Run("Empty", func(t *testing.T) { - opts := &envbuilder.Options{ + opts := &options.Options{ Logger: testLog(t), } auth := envbuilder.SetupRepoAuth(opts) @@ -273,7 +275,7 @@ func TestSetupRepoAuth(t *testing.T) { }) t.Run("HTTP/NoAuth", func(t *testing.T) { - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "http://host.tld/repo", Logger: testLog(t), } @@ -282,7 +284,7 @@ func TestSetupRepoAuth(t *testing.T) { }) t.Run("HTTP/BasicAuth", func(t *testing.T) { - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "http://host.tld/repo", GitUsername: "user", GitPassword: "pass", @@ -296,7 +298,7 @@ func TestSetupRepoAuth(t *testing.T) { }) t.Run("HTTPS/BasicAuth", func(t *testing.T) { - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "https://host.tld/repo", GitUsername: "user", GitPassword: "pass", @@ -311,7 +313,7 @@ func TestSetupRepoAuth(t *testing.T) { t.Run("SSH/WithScheme", func(t *testing.T) { kPath := writeTestPrivateKey(t) - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "ssh://host.tld/repo", GitSSHPrivateKeyPath: kPath, Logger: testLog(t), @@ -323,7 +325,7 @@ func TestSetupRepoAuth(t *testing.T) { t.Run("SSH/NoScheme", func(t *testing.T) { kPath := writeTestPrivateKey(t) - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "git@host.tld:repo/path", GitSSHPrivateKeyPath: kPath, Logger: testLog(t), @@ -336,7 +338,7 @@ func TestSetupRepoAuth(t *testing.T) { t.Run("SSH/OtherScheme", func(t *testing.T) { // Anything that is not https:// or http:// is treated as SSH. kPath := writeTestPrivateKey(t) - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "git://git@host.tld:repo/path", GitSSHPrivateKeyPath: kPath, Logger: testLog(t), @@ -348,7 +350,7 @@ func TestSetupRepoAuth(t *testing.T) { t.Run("SSH/GitUsername", func(t *testing.T) { kPath := writeTestPrivateKey(t) - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "host.tld:12345/repo/path", GitSSHPrivateKeyPath: kPath, GitUsername: "user", @@ -361,7 +363,7 @@ func TestSetupRepoAuth(t *testing.T) { t.Run("SSH/PrivateKey", func(t *testing.T) { kPath := writeTestPrivateKey(t) - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "ssh://git@host.tld:repo/path", GitSSHPrivateKeyPath: kPath, Logger: testLog(t), @@ -376,7 +378,7 @@ func TestSetupRepoAuth(t *testing.T) { }) t.Run("SSH/NoAuthMethods", func(t *testing.T) { - opts := &envbuilder.Options{ + opts := &options.Options{ GitURL: "ssh://git@host.tld:repo/path", Logger: testLog(t), } diff --git a/integration/integration_test.go b/integration/integration_test.go index 29723573..e62bac02 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -21,11 +21,14 @@ import ( "testing" "time" + "github.com/coder/envbuilder/options" + "github.com/coder/envbuilder" "github.com/coder/envbuilder/devcontainer/features" "github.com/coder/envbuilder/testutil/gittest" "github.com/coder/envbuilder/testutil/mwtest" "github.com/coder/envbuilder/testutil/registrytest" + clitypes "github.com/docker/cli/cli/config/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -71,7 +74,7 @@ func TestInitScriptInitCommand(t *testing.T) { "Dockerfile": fmt.Sprintf("FROM %s\nRUN unlink /bin/sh", testImageAlpine), }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("INIT_SCRIPT", fmt.Sprintf(`wget -O - %q`, initSrv.URL)), @@ -85,7 +88,7 @@ func TestInitScriptInitCommand(t *testing.T) { } require.NoError(t, ctx.Err(), "init script did not execute for prefixed env vars") - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), fmt.Sprintf(`INIT_SCRIPT=wget -O - %q`, initSrv.URL), @@ -129,7 +132,7 @@ RUN printf "%%s\n" \ "Dockerfile": dockerFile, }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) @@ -160,7 +163,7 @@ RUN mkdir -p /myapp/somedir \ "Dockerfile": dockerFile, }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) @@ -178,7 +181,7 @@ func TestForceSafe(t *testing.T) { "Dockerfile": "FROM " + testImageAlpine, }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), "KANIKO_DIR=/not/envbuilder", envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), @@ -194,7 +197,7 @@ func TestForceSafe(t *testing.T) { "Dockerfile": "FROM " + testImageAlpine, }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), "KANIKO_DIR=/not/envbuilder", envbuilderEnv("FORCE_SAFE", "true"), @@ -213,7 +216,7 @@ func TestFailsGitAuth(t *testing.T) { username: "kyle", password: "testing", }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }}) require.ErrorContains(t, err, "authentication required") @@ -228,7 +231,7 @@ func TestSucceedsGitAuth(t *testing.T) { username: "kyle", password: "testing", }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("GIT_USERNAME", "kyle"), @@ -252,7 +255,7 @@ func TestSucceedsGitAuthInURL(t *testing.T) { u, err := url.Parse(srv.URL) require.NoError(t, err) u.User = url.UserPassword("kyle", "testing") - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", u.String()), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) @@ -330,7 +333,7 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) { ".devcontainer/feature3/install.sh": "echo $GRAPE > /test3output", }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }}) require.NoError(t, err) @@ -352,7 +355,7 @@ func TestBuildFromDockerfile(t *testing.T) { "Dockerfile": "FROM " + testImageAlpine, }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString([]byte(`{"experimental": "enabled"}`))), @@ -374,7 +377,7 @@ func TestBuildPrintBuildOutput(t *testing.T) { "Dockerfile": "FROM " + testImageAlpine + "\nRUN echo hello", }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) @@ -408,7 +411,7 @@ func TestBuildIgnoreVarRunSecrets(t *testing.T) { require.NoError(t, err) t.Run("ReadWrite", func(t *testing.T) { - ctr, err := runEnvbuilder(t, options{ + ctr, err := runEnvbuilder(t, runOpts{ env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), @@ -422,7 +425,7 @@ func TestBuildIgnoreVarRunSecrets(t *testing.T) { }) t.Run("ReadOnly", func(t *testing.T) { - ctr, err := runEnvbuilder(t, options{ + ctr, err := runEnvbuilder(t, runOpts{ env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), @@ -443,7 +446,7 @@ func TestBuildWithSetupScript(t *testing.T) { "Dockerfile": "FROM " + testImageAlpine, }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("SETUP_SCRIPT", "echo \"INIT_ARGS=-c 'echo hi > /wow && sleep infinity'\" >> $ENVBUILDER_ENV"), @@ -469,7 +472,7 @@ func TestBuildFromDevcontainerInCustomPath(t *testing.T) { ".devcontainer/custom/Dockerfile": "FROM " + testImageUbuntu, }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DEVCONTAINER_DIR", ".devcontainer/custom"), }}) @@ -494,7 +497,7 @@ func TestBuildFromDevcontainerInSubfolder(t *testing.T) { ".devcontainer/subfolder/Dockerfile": "FROM " + testImageUbuntu, }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }}) require.NoError(t, err) @@ -518,7 +521,7 @@ func TestBuildFromDevcontainerInRoot(t *testing.T) { "Dockerfile": "FROM " + testImageUbuntu, }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }}) require.NoError(t, err) @@ -534,7 +537,7 @@ func TestBuildCustomCertificates(t *testing.T) { }, tls: true, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("SSL_CERT_BASE64", base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{ @@ -555,7 +558,7 @@ func TestBuildStopStartCached(t *testing.T) { "Dockerfile": "FROM " + testImageAlpine, }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("SKIP_REBUILD", "true"), @@ -586,7 +589,7 @@ func TestCloneFailsFallback(t *testing.T) { t.Parallel() t.Run("BadRepo", func(t *testing.T) { t.Parallel() - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", "bad-value"), }}) require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) @@ -603,7 +606,7 @@ func TestBuildFailsFallback(t *testing.T) { "Dockerfile": "bad syntax", }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) @@ -619,7 +622,7 @@ func TestBuildFailsFallback(t *testing.T) { RUN exit 1`, }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) @@ -633,7 +636,7 @@ RUN exit 1`, ".devcontainer/devcontainer.json": "not json", }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }}) require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) @@ -645,7 +648,7 @@ RUN exit 1`, ".devcontainer/devcontainer.json": "{}", }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("FALLBACK_IMAGE", testImageAlpine), }}) @@ -663,7 +666,7 @@ func TestExitBuildOnFailure(t *testing.T) { "Dockerfile": "bad syntax", }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("FALLBACK_IMAGE", testImageAlpine), @@ -697,7 +700,7 @@ func TestContainerEnv(t *testing.T) { ".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nENV FROM_DOCKERFILE=foo", }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("EXPORT_ENV_FILE", "/env"), }}) @@ -730,7 +733,7 @@ func TestUnsetOptionsEnv(t *testing.T) { ".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nENV FROM_DOCKERFILE=foo", }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), "GIT_URL", srv.URL, envbuilderEnv("GIT_PASSWORD", "supersecret"), @@ -741,13 +744,13 @@ func TestUnsetOptionsEnv(t *testing.T) { require.NoError(t, err) output := execContainer(t, ctr, "cat /root/env.txt") - var os envbuilder.Options + var os options.Options for _, s := range strings.Split(strings.TrimSpace(output), "\n") { for _, o := range os.CLI() { if strings.HasPrefix(s, o.Env) { assert.Fail(t, "environment variable should be stripped when running init script", s) } - optWithoutPrefix := strings.TrimPrefix(o.Env, envbuilder.WithEnvPrefix("")) + optWithoutPrefix := strings.TrimPrefix(o.Env, options.WithEnvPrefix("")) if strings.HasPrefix(s, optWithoutPrefix) { assert.Fail(t, "environment variable should be stripped when running init script", s) } @@ -777,7 +780,7 @@ func TestLifecycleScripts(t *testing.T) { ".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nUSER nobody", }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }}) require.NoError(t, err) @@ -816,7 +819,7 @@ RUN chmod +x /bin/init.sh USER nobody`, }, }) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("POST_START_SCRIPT_PATH", "/tmp/post-start.sh"), envbuilderEnv("INIT_COMMAND", "/bin/init.sh"), @@ -850,7 +853,7 @@ func TestPrivateRegistry(t *testing.T) { "Dockerfile": "FROM " + image, }, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), }}) @@ -879,7 +882,7 @@ func TestPrivateRegistry(t *testing.T) { }) require.NoError(t, err) - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString(config)), @@ -911,7 +914,7 @@ func TestPrivateRegistry(t *testing.T) { }) require.NoError(t, err) - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"), envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString(config)), @@ -968,7 +971,7 @@ func setupPassthroughRegistry(t *testing.T, image string, opts *setupPassthrough } func TestNoMethodFails(t *testing.T) { - _, err := runEnvbuilder(t, options{env: []string{}}) + _, err := runEnvbuilder(t, runOpts{env: []string{}}) require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error()) } @@ -1042,7 +1045,7 @@ COPY %s .`, testImageAlpine, inclFile) srv := createGitServer(t, gitServerOptions{ files: tc.files, }) - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("DOCKERFILE_PATH", tc.dockerfilePath), envbuilderEnv("BUILD_CONTEXT_PATH", tc.buildContextPath), @@ -1090,7 +1093,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with GET_CACHED_IMAGE - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1101,7 +1104,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with PUSH_IMAGE set - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), }}) @@ -1112,7 +1115,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "MANIFEST_UNKNOWN", "expected image to not be present before build + push") // Then: re-running envbuilder with GET_CACHED_IMAGE should succeed - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1150,7 +1153,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with GET_CACHED_IMAGE - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1161,7 +1164,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with PUSH_IMAGE set - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("PUSH_IMAGE", "1"), @@ -1184,7 +1187,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), } // Then: re-running envbuilder with GET_CACHED_IMAGE should succeed - ctrID, err := runEnvbuilder(t, options{env: []string{ + ctrID, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1283,7 +1286,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with GET_CACHED_IMAGE - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1294,7 +1297,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with PUSH_IMAGE set - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("PUSH_IMAGE", "1"), @@ -1307,7 +1310,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.NoError(t, err, "expected image to be present after build + push") // Then: re-running envbuilder with GET_CACHED_IMAGE should succeed - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1351,7 +1354,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with GET_CACHED_IMAGE - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1362,7 +1365,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with PUSH_IMAGE set - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("PUSH_IMAGE", "1"), @@ -1404,7 +1407,7 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with GET_CACHED_IMAGE - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1416,7 +1419,7 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine), require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") // When: we run envbuilder with PUSH_IMAGE set - ctrID, err := runEnvbuilder(t, options{env: []string{ + ctrID, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("PUSH_IMAGE", "1"), @@ -1432,7 +1435,7 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine), require.NoError(t, err, "expected image to be present after build + push") // Then: re-running envbuilder with GET_CACHED_IMAGE should succeed - _, err = runEnvbuilder(t, options{env: []string{ + _, err = runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", testRepo), envbuilderEnv("GET_CACHED_IMAGE", "1"), @@ -1463,7 +1466,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), }) // When: we run envbuilder with PUSH_IMAGE set but no cache repo set - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("PUSH_IMAGE", "1"), }}) @@ -1499,7 +1502,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine), notRegURL := strings.TrimPrefix(notRegSrv.URL, "http://") + "/test" // When: we run envbuilder with PUSH_IMAGE set - _, err := runEnvbuilder(t, options{env: []string{ + _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), envbuilderEnv("CACHE_REPO", notRegURL), envbuilderEnv("PUSH_IMAGE", "1"), @@ -1535,7 +1538,7 @@ USER test // Run envbuilder with a Docker volume mounted to homedir volName := fmt.Sprintf("%s%d-home", t.Name(), time.Now().Unix()) - ctr, err := runEnvbuilder(t, options{env: []string{ + ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), }, volumes: map[string]string{volName: "/home/test"}}) require.NoError(t, err) @@ -1656,7 +1659,7 @@ func cleanOldEnvbuilders() { } } -type options struct { +type runOpts struct { binds []string env []string volumes map[string]string @@ -1664,7 +1667,7 @@ type options struct { // runEnvbuilder starts the envbuilder container with the given environment // variables and returns the container ID. -func runEnvbuilder(t *testing.T, options options) (string, error) { +func runEnvbuilder(t *testing.T, opts runOpts) (string, error) { t.Helper() ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -1673,7 +1676,7 @@ func runEnvbuilder(t *testing.T, options options) (string, error) { cli.Close() }) mounts := make([]mount.Mount, 0) - for volName, volPath := range options.volumes { + for volName, volPath := range opts.volumes { mounts = append(mounts, mount.Mount{ Type: mount.TypeVolume, Source: volName, @@ -1689,13 +1692,13 @@ func runEnvbuilder(t *testing.T, options options) (string, error) { } ctr, err := cli.ContainerCreate(ctx, &container.Config{ Image: "envbuilder:latest", - Env: options.env, + Env: opts.env, Labels: map[string]string{ testContainerLabel: "true", }, }, &container.HostConfig{ NetworkMode: container.NetworkMode("host"), - Binds: options.binds, + Binds: opts.binds, Mounts: mounts, }, nil, nil, "") require.NoError(t, err) @@ -1784,5 +1787,5 @@ func streamContainerLogs(t *testing.T, cli *client.Client, containerID string) ( } func envbuilderEnv(env string, value string) string { - return fmt.Sprintf("%s=%s", envbuilder.WithEnvPrefix(env), value) + return fmt.Sprintf("%s=%s", options.WithEnvPrefix(env), value) } diff --git a/options.go b/options/options.go similarity index 97% rename from options.go rename to options/options.go index 76eddb60..dd5ee8b9 100644 --- a/options.go +++ b/options/options.go @@ -1,6 +1,7 @@ -package envbuilder +package options import ( + "os" "strings" "github.com/coder/envbuilder/internal/log" @@ -493,3 +494,22 @@ func skipDeprecatedOptions(options []serpent.Option) []serpent.Option { return activeOptions } + +// UnsetEnv unsets all environment variables that are used +// to configure the options. +func UnsetEnv() { + var o Options + for _, opt := range o.CLI() { + if opt.Env == "" { + continue + } + // Do not strip options that do not have the magic prefix! + // For example, CODER_AGENT_URL, CODER_AGENT_TOKEN, CODER_AGENT_SUBSYSTEM. + if !strings.HasPrefix(opt.Env, envPrefix) { + continue + } + // Strip both with and without prefix. + _ = os.Unsetenv(opt.Env) + _ = os.Unsetenv(strings.TrimPrefix(opt.Env, envPrefix)) + } +} diff --git a/options_test.go b/options/options_test.go similarity index 88% rename from options_test.go rename to options/options_test.go index e32af9e6..bf7a216c 100644 --- a/options_test.go +++ b/options/options_test.go @@ -1,4 +1,4 @@ -package envbuilder_test +package options_test import ( "bytes" @@ -6,7 +6,8 @@ import ( "os" "testing" - "github.com/coder/envbuilder" + "github.com/coder/envbuilder/options" + "github.com/coder/serpent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,50 +17,50 @@ import ( func TestEnvOptionParsing(t *testing.T) { t.Run("string", func(t *testing.T) { const val = "setup.sh" - t.Setenv(envbuilder.WithEnvPrefix("SETUP_SCRIPT"), val) + t.Setenv(options.WithEnvPrefix("SETUP_SCRIPT"), val) o := runCLI() require.Equal(t, o.SetupScript, val) }) t.Run("int", func(t *testing.T) { - t.Setenv(envbuilder.WithEnvPrefix("CACHE_TTL_DAYS"), "7") + t.Setenv(options.WithEnvPrefix("CACHE_TTL_DAYS"), "7") o := runCLI() require.Equal(t, o.CacheTTLDays, int64(7)) }) t.Run("string array", func(t *testing.T) { - t.Setenv(envbuilder.WithEnvPrefix("IGNORE_PATHS"), "/var,/temp") + t.Setenv(options.WithEnvPrefix("IGNORE_PATHS"), "/var,/temp") o := runCLI() require.Equal(t, o.IgnorePaths, []string{"/var", "/temp"}) }) t.Run("bool", func(t *testing.T) { t.Run("lowercase", func(t *testing.T) { - t.Setenv(envbuilder.WithEnvPrefix("SKIP_REBUILD"), "true") - t.Setenv(envbuilder.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "false") + t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "true") + t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "false") o := runCLI() require.True(t, o.SkipRebuild) require.False(t, o.GitCloneSingleBranch) }) t.Run("uppercase", func(t *testing.T) { - t.Setenv(envbuilder.WithEnvPrefix("SKIP_REBUILD"), "TRUE") - t.Setenv(envbuilder.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "FALSE") + t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "TRUE") + t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "FALSE") o := runCLI() require.True(t, o.SkipRebuild) require.False(t, o.GitCloneSingleBranch) }) t.Run("numeric", func(t *testing.T) { - t.Setenv(envbuilder.WithEnvPrefix("SKIP_REBUILD"), "1") - t.Setenv(envbuilder.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "0") + t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "1") + t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "0") o := runCLI() require.True(t, o.SkipRebuild) require.False(t, o.GitCloneSingleBranch) }) t.Run("empty", func(t *testing.T) { - t.Setenv(envbuilder.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "") + t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "") o := runCLI() require.False(t, o.GitCloneSingleBranch) }) @@ -142,7 +143,7 @@ var updateCLIOutputGoldenFiles = flag.Bool("update", false, "update options CLI // TestCLIOutput tests that the default CLI output is as expected. func TestCLIOutput(t *testing.T) { - var o envbuilder.Options + var o options.Options cmd := serpent.Command{ Use: "envbuilder", Options: o.CLI(), @@ -171,8 +172,8 @@ func TestCLIOutput(t *testing.T) { } } -func runCLI() envbuilder.Options { - var o envbuilder.Options +func runCLI() options.Options { + var o options.Options cmd := serpent.Command{ Options: o.CLI(), Handler: func(inv *serpent.Invocation) error { diff --git a/testdata/options.golden b/options/testdata/options.golden similarity index 100% rename from testdata/options.golden rename to options/testdata/options.golden diff --git a/scripts/docsgen/main.go b/scripts/docsgen/main.go index c79995cf..83d992c4 100644 --- a/scripts/docsgen/main.go +++ b/scripts/docsgen/main.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "github.com/coder/envbuilder" + "github.com/coder/envbuilder/options" ) const ( @@ -26,7 +26,7 @@ func main() { panic("start or end section comments not found in the file.") } - var options envbuilder.Options + var options options.Options mkd := "\n## Environment Variables\n\n" + options.Markdown() modifiedContent := readmeContent[:startIndex+len(startSection)] + mkd + readmeContent[endIndex:]