From 15e00e2f33ba7631adba47906cb75e47c52dbed7 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 24 Apr 2024 13:24:49 +0000 Subject: [PATCH 01/20] Refactor options structure --- cmd/envbuilder/main.go | 29 +-- envbuilder.go | 395 ++++++++++------------------------------- envbuilder_test.go | 33 ---- options.go | 311 ++++++++++++++++++++++++++++++++ options_test.go | 74 ++++++++ 5 files changed, 499 insertions(+), 343 deletions(-) create mode 100644 options.go create mode 100644 options_test.go diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go index 7e18be2d..689528e5 100644 --- a/cmd/envbuilder/main.go +++ b/cmd/envbuilder/main.go @@ -48,7 +48,7 @@ func main() { client.SDK.HTTPClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: options.Insecure, + InsecureSkipVerify: options.GetBool("Insecure"), }, }, } @@ -68,20 +68,23 @@ func main() { os.Setenv("CODER_AGENT_SUBSYSTEM", subsystems) } - options.Logger = func(level codersdk.LogLevel, format string, args ...interface{}) { - output := fmt.Sprintf(format, args...) - fmt.Fprintln(cmd.ErrOrStderr(), output) - if sendLogs != nil { - sendLogs(cmd.Context(), agentsdk.Log{ - CreatedAt: time.Now(), - Output: output, - Level: level, - }) - } + deps := envbuilder.Dependencies{ + Logger: func(level codersdk.LogLevel, format string, args ...interface{}) { + output := fmt.Sprintf(format, args...) + fmt.Fprintln(cmd.ErrOrStderr(), output) + if sendLogs != nil { + sendLogs(cmd.Context(), agentsdk.Log{ + CreatedAt: time.Now(), + Output: output, + Level: level, + }) + } + }, } - err := envbuilder.Run(cmd.Context(), options) + + err := envbuilder.Run(cmd.Context(), options, deps) if err != nil { - options.Logger(codersdk.LogLevelError, "error: %s", err) + deps.Logger(codersdk.LogLevelError, "error: %s", err) } return err }, diff --git a/envbuilder.go b/envbuilder.go index 7ae8936f..e3a0453f 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -18,7 +18,6 @@ import ( "os/exec" "os/user" "path/filepath" - "reflect" "sort" "strconv" "strings" @@ -77,152 +76,9 @@ var ( MagicFile = filepath.Join(MagicDir, "built") ) -type Options struct { - // SetupScript is the script to run before the init script. - // It runs as the root user regardless of the user specified - // in the devcontainer.json file. - - // SetupScript is ran as the root user prior to the init script. - // It is used to configure envbuilder dynamically during the runtime. - // e.g. specifying whether to start `systemd` or `tiny init` for PID 1. - SetupScript string `env:"SETUP_SCRIPT"` - - // InitScript is the script to run to initialize the workspace. - InitScript string `env:"INIT_SCRIPT"` - - // InitCommand is the command to run to initialize the workspace. - InitCommand string `env:"INIT_COMMAND"` - - // InitArgs are the arguments to pass to the init command. - // They are split according to `/bin/sh` rules with - // https://github.com/kballard/go-shellquote - InitArgs string `env:"INIT_ARGS"` - - // CacheRepo is the name of the container registry - // to push the cache image to. If this is empty, the cache - // will not be pushed. - CacheRepo string `env:"CACHE_REPO"` - - // BaseImageCacheDir is the path to a directory where the base - // image can be found. This should be a read-only directory - // solely mounted for the purpose of caching the base image. - BaseImageCacheDir string `env:"BASE_IMAGE_CACHE_DIR"` - - // LayerCacheDir is the path to a directory where built layers - // will be stored. This spawns an in-memory registry to serve - // the layers from. - // - // It will override CacheRepo if both are specified. - LayerCacheDir string `env:"LAYER_CACHE_DIR"` - - // DevcontainerDir is a path to the folder containing - // the devcontainer.json file that will be used to build the - // workspace and can either be an absolute path or a path - // relative to the workspace folder. If not provided, defaults to - // `.devcontainer`. - DevcontainerDir string `env:"DEVCONTAINER_DIR"` - - // DevcontainerJSONPath is a path to a devcontainer.json file - // that is either an absolute path or a path relative to - // DevcontainerDir. This can be used in cases where one wants - // to substitute an edited devcontainer.json file for the one - // that exists in the repo. - DevcontainerJSONPath string `env:"DEVCONTAINER_JSON_PATH"` - - // DockerfilePath is a relative path to the Dockerfile that - // will be used to build the workspace. This is an alternative - // to using a devcontainer that some might find simpler. - DockerfilePath string `env:"DOCKERFILE_PATH"` - - // CacheTTLDays is the number of days to use cached layers before - // expiring them. Defaults to 7 days. - CacheTTLDays int `env:"CACHE_TTL_DAYS"` - - // DockerConfigBase64 is a base64 encoded Docker config - // file that will be used to pull images from private - // container registries. - DockerConfigBase64 string `env:"DOCKER_CONFIG_BASE64"` - - // FallbackImage specifies an alternative image to use when neither - // an image is declared in the devcontainer.json file nor a Dockerfile is present. - // If there's a build failure (from a faulty Dockerfile) or a misconfiguration, - // this image will be the substitute. - // Set `ExitOnBuildFailure` to true to halt the container if the build faces an issue. - FallbackImage string `env:"FALLBACK_IMAGE"` - - // ExitOnBuildFailure terminates the container upon a build failure. - // This is handy when preferring the `FALLBACK_IMAGE` in cases where - // no devcontainer.json or image is provided. However, it ensures - // that the container stops if the build process encounters an error. - ExitOnBuildFailure bool `env:"EXIT_ON_BUILD_FAILURE"` - - // ForceSafe ignores any filesystem safety checks. - // This could cause serious harm to your system! - // This is used in cases where bypass is needed - // to unblock customers! - ForceSafe bool `env:"FORCE_SAFE"` - - // Insecure bypasses TLS verification when cloning - // and pulling from container registries. - Insecure bool `env:"INSECURE"` - - // IgnorePaths is a comma separated list of paths - // to ignore when building the workspace. - IgnorePaths []string `env:"IGNORE_PATHS"` - - // SkipRebuild skips building if the MagicFile exists. - // This is used to skip building when a container is - // restarting. e.g. docker stop -> docker start - // This value can always be set to true - even if the - // container is being started for the first time. - SkipRebuild bool `env:"SKIP_REBUILD"` - - // GitURL is the URL of the Git repository to clone. - // This is optional! - GitURL string `env:"GIT_URL"` - - // GitCloneDepth is the depth to use when cloning - // the Git repository. - GitCloneDepth int `env:"GIT_CLONE_DEPTH"` - - // GitCloneSingleBranch clones only a single branch - // of the Git repository. - GitCloneSingleBranch bool `env:"GIT_CLONE_SINGLE_BRANCH"` - - // GitUsername is the username to use for Git authentication. - // This is optional! - GitUsername string `env:"GIT_USERNAME"` - - // GitPassword is the password to use for Git authentication. - // This is optional! - GitPassword string `env:"GIT_PASSWORD"` - - // GitHTTPProxyURL is the url for the http proxy. - // This is optional! - GitHTTPProxyURL string `env:"GIT_HTTP_PROXY_URL"` - - // WorkspaceFolder is the path to the workspace folder - // that will be built. This is optional! - WorkspaceFolder string `env:"WORKSPACE_FOLDER"` - - // SSLCertBase64 is the content of an SSL cert file. - // This is useful for self-signed certificates. - SSLCertBase64 string `env:"SSL_CERT_BASE64"` - - // ExportEnvFile is an optional file path to a .env file where - // envbuilder will dump environment variables from devcontainer.json and - // the built container image. - ExportEnvFile string `env:"EXPORT_ENV_FILE"` - - // PostStartScriptPath is the path to a script that will be created by - // envbuilder based on the `postStartCommand` in devcontainer.json, if any - // is specified (otherwise the script is not created). If this is set, the - // specified InitCommand should check for the presence of this script and - // execute it after successful startup. - PostStartScriptPath string `env:"POST_START_SCRIPT_PATH"` - +type Dependencies struct { // Logger is the logger to use for all operations. - Logger func(level codersdk.LogLevel, format string, args ...interface{}) + Logger func(level codersdk.LogLevel, format string, args ...any) // Filesystem is the filesystem to use for all operations. // Defaults to the host filesystem. @@ -233,47 +89,36 @@ type Options struct { type DockerConfig configfile.ConfigFile // Run runs the envbuilder. -func Run(ctx context.Context, options Options) error { - if options.InitScript == "" { - options.InitScript = "sleep infinity" - } - if options.InitCommand == "" { - options.InitCommand = "/bin/sh" - } - if options.IgnorePaths == nil { - // Kubernetes frequently stores secrets in /var/run/secrets, and - // other applications might as well. This seems to be a sensible - // default, but if that changes, it's simple to adjust. - options.IgnorePaths = []string{"/var/run"} - } +func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { // Default to the shell! - initArgs := []string{"-c", options.InitScript} - if options.InitArgs != "" { + initArgs := []string{"-c", options.GetString("InitScript")} + if options.GetString("InitArgs") != "" { var err error - initArgs, err = shellquote.Split(options.InitArgs) + initArgs, err = shellquote.Split(options.GetString("InitArgs")) if err != nil { return fmt.Errorf("parse init args: %w", err) } } - if options.Filesystem == nil { - options.Filesystem = &osfsWithChmod{osfs.New("/")} + if deps.Filesystem == nil { + deps.Filesystem = &osfsWithChmod{osfs.New("/")} } - if options.WorkspaceFolder == "" { + if options.GetString("WorkspaceFolder") == "" { var err error - options.WorkspaceFolder, err = DefaultWorkspaceFolder(options.GitURL) + folder, err := DefaultWorkspaceFolder(options.GetString("GitURL")) + options.SetString("WorkspaceFolder", folder) if err != nil { return err } } - logf := options.Logger + logf := deps.Logger stageNumber := 1 - startStage := func(format string, args ...interface{}) func(format string, args ...interface{}) { + startStage := func(format string, args ...any) func(format string, args ...any) { now := time.Now() stageNum := stageNumber stageNumber++ logf(codersdk.LogLevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) - return func(format string, args ...interface{}) { + return func(format string, args ...any) { logf(codersdk.LogLevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) } } @@ -281,12 +126,12 @@ func Run(ctx context.Context, options Options) error { logf(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) var caBundle []byte - if options.SSLCertBase64 != "" { + if options.GetString("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(options.GetString("SSLCertBase64")) if err != nil { return xerrors.Errorf("base64 decode ssl cert: %w", err) } @@ -297,8 +142,8 @@ func Run(ctx context.Context, options Options) error { caBundle = data } - if options.DockerConfigBase64 != "" { - decoded, err := base64.StdEncoding.DecodeString(options.DockerConfigBase64) + if options.GetString("DockerConfigBase64") != "" { + decoded, err := base64.StdEncoding.DecodeString(options.GetString("DockerConfigBase64")) if err != nil { return fmt.Errorf("decode docker config: %w", err) } @@ -319,10 +164,10 @@ func Run(ctx context.Context, options Options) error { var fallbackErr error var cloned bool - if options.GitURL != "" { + if options.GetString("GitURL") != "" { endStage := startStage("📦 Cloning %s to %s...", - newColor(color.FgCyan).Sprintf(options.GitURL), - newColor(color.FgCyan).Sprintf(options.WorkspaceFolder), + newColor(color.FgCyan).Sprintf(options.GetString("GitURL")), + newColor(color.FgCyan).Sprintf(options.GetString("WorkspaceFolder")), ) reader, writer := io.Pipe() @@ -346,34 +191,34 @@ func Run(ctx context.Context, options Options) error { }() cloneOpts := CloneRepoOptions{ - Path: options.WorkspaceFolder, - Storage: options.Filesystem, - Insecure: options.Insecure, + Path: options.GetString("WorkspaceFolder"), + Storage: deps.Filesystem, + Insecure: options.GetBool("Insecure"), Progress: writer, - SingleBranch: options.GitCloneSingleBranch, - Depth: options.GitCloneDepth, + SingleBranch: options.GetBool("GitCloneSingleBranch"), + Depth: options.GetInt("GitCloneDepth"), CABundle: caBundle, } - if options.GitUsername != "" || options.GitPassword != "" { - gitURL, err := url.Parse(options.GitURL) + if options.GetString("GitUsername") != "" || options.GetString("GitPassword") != "" { + gitURL, err := url.Parse(options.GetString("GitURL")) if err != nil { return fmt.Errorf("parse git url: %w", err) } - gitURL.User = url.UserPassword(options.GitUsername, options.GitPassword) - options.GitURL = gitURL.String() + gitURL.User = url.UserPassword(options.GetString("GitUsername"), options.GetString("GitPassword")) + options.SetString("GitURL", gitURL.String()) cloneOpts.RepoAuth = &githttp.BasicAuth{ - Username: options.GitUsername, - Password: options.GitPassword, + Username: options.GetString("GitUsername"), + Password: options.GetString("GitPassword"), } } - if options.GitHTTPProxyURL != "" { + if options.GetString("GitHTTPProxyURL") != "" { cloneOpts.ProxyOptions = transport.ProxyOptions{ - URL: options.GitHTTPProxyURL, + URL: options.GetString("GitHTTPProxyURL"), } } - cloneOpts.RepoURL = options.GitURL + cloneOpts.RepoURL = options.GetString("GitURL") cloned, fallbackErr = CloneRepo(ctx, cloneOpts) if fallbackErr == nil { @@ -390,12 +235,12 @@ func Run(ctx context.Context, options Options) error { defaultBuildParams := func() (*devcontainer.Compiled, error) { dockerfile := filepath.Join(MagicDir, "Dockerfile") - file, err := options.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0644) + file, err := deps.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err } defer file.Close() - if options.FallbackImage == "" { + if options.GetString("FallbackImage") == "" { if fallbackErr != nil { return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage) } @@ -403,7 +248,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 " + options.GetString("FallbackImage") _, err = file.Write([]byte(content)) if err != nil { return nil, err @@ -419,28 +264,28 @@ func Run(ctx context.Context, options Options) error { buildParams *devcontainer.Compiled scripts devcontainer.LifecycleScripts ) - if options.DockerfilePath == "" { + if options.GetString("DockerFilepath") == "" { // Only look for a devcontainer if a Dockerfile wasn't specified. // devcontainer is a standard, so it's reasonable to be the default. - devcontainerDir := options.DevcontainerDir + devcontainerDir := options.GetString("DevcontainerDir") if devcontainerDir == "" { devcontainerDir = ".devcontainer" } if !filepath.IsAbs(devcontainerDir) { - devcontainerDir = filepath.Join(options.WorkspaceFolder, devcontainerDir) + devcontainerDir = filepath.Join(options.GetString("WorkspaceFolder"), devcontainerDir) } - devcontainerPath := options.DevcontainerJSONPath + devcontainerPath := options.GetString("DevcontainerJSONPath") if devcontainerPath == "" { devcontainerPath = "devcontainer.json" } if !filepath.IsAbs(devcontainerPath) { devcontainerPath = filepath.Join(devcontainerDir, devcontainerPath) } - _, err := options.Filesystem.Stat(devcontainerPath) + _, err := deps.Filesystem.Stat(devcontainerPath) if err == nil { // We know a devcontainer exists. // Let's parse it and use it! - file, err := options.Filesystem.Open(devcontainerPath) + file, err := deps.Filesystem.Open(devcontainerPath) if err != nil { return fmt.Errorf("open devcontainer.json: %w", err) } @@ -460,7 +305,7 @@ func Run(ctx context.Context, options Options) error { logf(codersdk.LogLevelInfo, "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) + buildParams, err = devContainer.Compile(deps.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.GetString("WorkspaceFolder"), false) if err != nil { return fmt.Errorf("compile devcontainer.json: %w", err) } @@ -472,8 +317,8 @@ func Run(ctx context.Context, options Options) error { } } else { // If a Dockerfile was specified, we use that. - dockerfilePath := filepath.Join(options.WorkspaceFolder, options.DockerfilePath) - dockerfile, err := options.Filesystem.Open(dockerfilePath) + dockerfilePath := filepath.Join(options.GetString("WorkspaceFolder"), options.GetString("DockerFilepath")) + dockerfile, err := deps.Filesystem.Open(dockerfilePath) if err == nil { content, err := io.ReadAll(dockerfile) if err != nil { @@ -482,7 +327,7 @@ func Run(ctx context.Context, options Options) error { buildParams = &devcontainer.Compiled{ DockerfilePath: dockerfilePath, DockerfileContent: string(content), - BuildContext: options.WorkspaceFolder, + BuildContext: options.GetString("WorkspaceFolder"), } } } @@ -505,11 +350,11 @@ func Run(ctx context.Context, options Options) error { var closeAfterBuild func() // Allows quick testing of layer caching using a local directory! - if options.LayerCacheDir != "" { + if options.GetString("LayerCacheDir") != "" { cfg := &configuration.Configuration{ Storage: configuration.Storage{ "filesystem": configuration.Parameters{ - "rootdirectory": options.LayerCacheDir, + "rootdirectory": options.GetString("LayerCacheDir"), }, }, } @@ -545,10 +390,10 @@ func Run(ctx context.Context, options Options) error { _ = srv.Close() _ = listener.Close() } - if options.CacheRepo != "" { + if options.GetString("CacheRepo") != "" { logf(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") } - options.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port) + options.SetString("CacheRepo", fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port)) } // IgnorePaths in the Kaniko options doesn't properly ignore paths. @@ -556,11 +401,11 @@ func Run(ctx context.Context, options Options) error { // https://github.com/GoogleContainerTools/kaniko/blob/63be4990ca5a60bdf06ddc4d10aa4eca0c0bc714/cmd/executor/cmd/root.go#L136 ignorePaths := append([]string{ MagicDir, - options.LayerCacheDir, - options.WorkspaceFolder, + options.GetString("LayerCacheDir"), + options.GetString("WorkspaceFolder"), // See: https://github.com/coder/envbuilder/issues/37 "/etc/resolv.conf", - }, options.IgnorePaths...) + }, options.GetStringSlice("IgnorePaths")...) for _, ignorePath := range ignorePaths { util.AddToDefaultIgnoreList(util.IgnoreListEntry{ @@ -571,8 +416,8 @@ func Run(ctx context.Context, options Options) error { skippedRebuild := false build := func() (v1.Image, error) { - _, err := options.Filesystem.Stat(MagicFile) - if err == nil && options.SkipRebuild { + _, err := deps.Filesystem.Stat(MagicFile) + if err == nil && options.GetBool("SkipRebuild") { endStage := startStage("🏗️ Skipping build because of cache...") imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent) if err != nil { @@ -619,8 +464,8 @@ func Run(ctx context.Context, options Options) error { } }() cacheTTL := time.Hour * 24 * 7 - if options.CacheTTLDays != 0 { - cacheTTL = time.Hour * 24 * time.Duration(options.CacheTTLDays) + if options.GetInt("CacheTTLDays") != 0 { + cacheTTL = time.Hour * 24 * time.Duration(options.GetInt("CacheTTLDays")) } endStage := startStage("🏗️ Building image...") @@ -648,18 +493,18 @@ func Run(ctx context.Context, options Options) error { CacheOptions: config.CacheOptions{ // Cache for a week by default! CacheTTL: cacheTTL, - CacheDir: options.BaseImageCacheDir, + CacheDir: options.GetString("BaseImageCacheDir"), }, ForceUnpack: true, BuildArgs: buildParams.BuildArgs, - CacheRepo: options.CacheRepo, - Cache: options.CacheRepo != "" || options.BaseImageCacheDir != "", + CacheRepo: options.GetString("CacheRepo"), + Cache: options.GetString("CacheRepo") != "" || options.GetString("BaseImageCacheDir") != "", DockerfilePath: buildParams.DockerfilePath, DockerfileContent: buildParams.DockerfileContent, RegistryOptions: config.RegistryOptions{ - Insecure: options.Insecure, - InsecurePull: options.Insecure, - SkipTLSVerify: options.Insecure, + Insecure: options.GetBool("Insecure"), + InsecurePull: options.GetBool("Insecure"), + SkipTLSVerify: options.GetBool("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 @@ -697,7 +542,7 @@ func Run(ctx context.Context, options Options) error { case strings.Contains(err.Error(), "unexpected status code 401 Unauthorized"): logf(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") } - if !fallback || options.ExitOnBuildFailure { + if !fallback || options.GetBool("ExitOnBuildFailure") { return err } logf(codersdk.LogLevelError, "Failed to build: %s", err) @@ -718,7 +563,7 @@ func Run(ctx context.Context, options Options) error { // Create the magic file to indicate that this build // has already been ran before! - file, err := options.Filesystem.Create(MagicFile) + file, err := deps.Filesystem.Create(MagicFile) if err != nil { return fmt.Errorf("create magic file: %w", err) } @@ -769,10 +614,10 @@ func Run(ctx context.Context, options Options) error { } // Sanitize the environment of any options! - unsetOptionsEnv() + unsetOptionsEnv(options) // Remove the Docker config secret file! - if options.DockerConfigBase64 != "" { + if options.GetString("DockerConfigBase64") != "" { err = os.Remove(filepath.Join(MagicDir, "config.json")) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove docker config: %w", err) @@ -809,7 +654,7 @@ func Run(ctx context.Context, options Options) error { } sort.Strings(envKeys) for _, envVar := range envKeys { - value := devcontainer.SubstituteVars(env[envVar], options.WorkspaceFolder) + value := devcontainer.SubstituteVars(env[envVar], options.GetString("WorkspaceFolder")) os.Setenv(envVar, value) } } @@ -819,10 +664,10 @@ func Run(ctx context.Context, options Options) error { // 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 options.GetString("ExportEnvFile") != "" && !skippedRebuild { + exportEnvFile, err := os.Create(options.GetString("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", options.GetString("ExportEnvFile"), err) } envKeys := make([]string, 0, len(allEnvKeys)) @@ -859,7 +704,7 @@ func Run(ctx context.Context, options Options) error { // // We need to change the ownership of the files to the user that will // be running the init script. - filepath.Walk(options.WorkspaceFolder, func(path string, info os.FileInfo, err error) error { + filepath.Walk(options.GetString("WorkspaceFolder"), func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -868,11 +713,11 @@ func Run(ctx context.Context, options Options) error { endStage("👤 Updated the ownership of the workspace!") } - err = os.MkdirAll(options.WorkspaceFolder, 0755) + err = os.MkdirAll(options.GetString("WorkspaceFolder"), 0755) if err != nil { return fmt.Errorf("create workspace folder: %w", err) } - err = os.Chdir(options.WorkspaceFolder) + err = os.Chdir(options.GetString("WorkspaceFolder")) if err != nil { return fmt.Errorf("change directory: %w", err) } @@ -884,7 +729,7 @@ func Run(ctx context.Context, options Options) error { // 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, options, deps, scripts, skippedRebuild, userInfo); err != nil { return err } @@ -893,11 +738,11 @@ func Run(ctx context.Context, options Options) error { // // This is useful for hooking into the environment for a specific // init to PID 1. - if options.SetupScript != "" { + if options.GetString("SetupScript") != "" { // We execute the initialize script as the root user! os.Setenv("HOME", "/root") - logf(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.SetupScript) + logf(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.GetString("SetupScript")) envKey := "ENVBUILDER_ENV" envFile := filepath.Join("/", MagicDir, "environ") @@ -907,12 +752,12 @@ func Run(ctx context.Context, options Options) error { } _ = file.Close() - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", options.SetupScript) + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", options.GetString("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 = options.GetString("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()) { @@ -954,7 +799,7 @@ func Run(ctx context.Context, options Options) error { key := pair[0] switch key { case "INIT_COMMAND": - options.InitCommand = pair[1] + options.SetString("InitCommand", pair[1]) updatedCommand = true case "INIT_ARGS": initArgs, err = shellquote.Split(pair[1]) @@ -990,9 +835,9 @@ func Run(ctx context.Context, options Options) error { return fmt.Errorf("set uid: %w", err) } - logf(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.InitCommand, initArgs, userInfo.user.Username) + logf(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.GetString("InitCommand"), initArgs, userInfo.user.Username) - err = syscall.Exec(options.InitCommand, append([]string{options.InitCommand}, initArgs...), os.Environ()) + err = syscall.Exec(options.GetString("InitCommand"), append([]string{options.GetString("InitCommand")}, initArgs...), os.Environ()) if err != nil { return fmt.Errorf("exec init script: %w", err) } @@ -1063,7 +908,7 @@ func findUser(nameOrID string) (*user.User, error) { func execOneLifecycleScript( ctx context.Context, - logf func(level codersdk.LogLevel, format string, args ...interface{}), + logf func(level codersdk.LogLevel, format string, args ...any), s devcontainer.LifecycleScript, scriptName string, userInfo userInfo, @@ -1081,38 +926,39 @@ func execOneLifecycleScript( func execLifecycleScripts( ctx context.Context, - options Options, + options OptionsMap, + deps Dependencies, scripts devcontainer.LifecycleScripts, skippedRebuild bool, userInfo userInfo, ) error { - if options.PostStartScriptPath != "" { - _ = os.Remove(options.PostStartScriptPath) + if options.GetString("PostStartScriptPath") != "" { + _ = os.Remove(options.GetString("PostStartScriptPath")) } if !skippedRebuild { - if err := execOneLifecycleScript(ctx, options.Logger, scripts.OnCreateCommand, "onCreateCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, deps.Logger, scripts.OnCreateCommand, "onCreateCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } } - if err := execOneLifecycleScript(ctx, options.Logger, scripts.UpdateContentCommand, "updateContentCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, deps.Logger, scripts.UpdateContentCommand, "updateContentCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } - if err := execOneLifecycleScript(ctx, options.Logger, scripts.PostCreateCommand, "postCreateCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, deps.Logger, scripts.PostCreateCommand, "postCreateCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } if !scripts.PostStartCommand.IsEmpty() { // If PostStartCommandPath is set, the init command is responsible // for running the postStartCommand. Otherwise, we execute it now. - if options.PostStartScriptPath != "" { - if err := createPostStartScript(options.PostStartScriptPath, scripts.PostStartCommand); err != nil { + if options.GetString("PostStartScriptPath") != "" { + if err := createPostStartScript(options.GetString("PostStartScriptPath"), scripts.PostStartCommand); err != nil { return fmt.Errorf("failed to create post-start script: %w", err) } } else { - _ = execOneLifecycleScript(ctx, options.Logger, scripts.PostStartCommand, "postStartCommand", userInfo) + _ = execOneLifecycleScript(ctx, deps.Logger, scripts.PostStartCommand, "postStartCommand", userInfo) } } return nil @@ -1135,56 +981,11 @@ func createPostStartScript(path string, postStartCommand devcontainer.LifecycleS return nil } -// OptionsFromEnv returns a set of options from environment variables. -func OptionsFromEnv(getEnv func(string) (string, bool)) Options { - options := Options{} - - val := reflect.ValueOf(&options).Elem() - typ := val.Type() - - for i := 0; i < val.NumField(); i++ { - field := val.Field(i) - fieldTyp := typ.Field(i) - env := fieldTyp.Tag.Get("env") - if env == "" { - continue - } - e, ok := getEnv(env) - if !ok { - continue - } - switch fieldTyp.Type.Kind() { - case reflect.String: - field.SetString(e) - case reflect.Bool: - v, _ := strconv.ParseBool(e) - field.SetBool(v) - case reflect.Int: - v, _ := strconv.ParseInt(e, 10, 64) - field.SetInt(v) - case reflect.Slice: - field.Set(reflect.ValueOf(strings.Split(e, ","))) - default: - panic(fmt.Sprintf("unsupported type %s in OptionsFromEnv", fieldTyp.Type.String())) - } - } - - return options -} - // unsetOptionsEnv unsets all environment variables that are used // to configure the options. -func unsetOptionsEnv() { - val := reflect.ValueOf(&Options{}).Elem() - typ := val.Type() - - for i := 0; i < val.NumField(); i++ { - fieldTyp := typ.Field(i) - env := fieldTyp.Tag.Get("env") - if env == "" { - continue - } - os.Unsetenv(env) +func unsetOptionsEnv(options OptionsMap) { + for _, option := range options { + os.Unsetenv(option.Env) } } diff --git a/envbuilder_test.go b/envbuilder_test.go index ecd9d663..e38f0a4d 100644 --- a/envbuilder_test.go +++ b/envbuilder_test.go @@ -17,36 +17,3 @@ func TestDefaultWorkspaceFolder(t *testing.T) { require.NoError(t, err) require.Equal(t, envbuilder.EmptyWorkspaceDir, dir) } - -func TestSystemOptions(t *testing.T) { - t.Parallel() - opts := map[string]string{ - "INIT_SCRIPT": "echo hello", - "CACHE_REPO": "kylecarbs/testing", - "CACHE_TTL_DAYS": "30", - "DEVCONTAINER_JSON_PATH": "/tmp/devcontainer.json", - "DOCKERFILE_PATH": "Dockerfile", - "FALLBACK_IMAGE": "ubuntu:latest", - "FORCE_SAFE": "true", - "INSECURE": "false", - "GIT_CLONE_DEPTH": "1", - "GIT_URL": "https://github.com/coder/coder", - "WORKSPACE_FOLDER": "/workspaces/coder", - "GIT_HTTP_PROXY_URL": "http://company-proxy.com:8081", - } - env := envbuilder.OptionsFromEnv(func(s string) (string, bool) { - return opts[s], true - }) - require.Equal(t, "echo hello", env.InitScript) - require.Equal(t, "kylecarbs/testing", env.CacheRepo) - require.Equal(t, "/tmp/devcontainer.json", env.DevcontainerJSONPath) - require.Equal(t, 30, env.CacheTTLDays) - require.Equal(t, "Dockerfile", env.DockerfilePath) - require.Equal(t, "ubuntu:latest", env.FallbackImage) - require.True(t, env.ForceSafe) - require.False(t, env.Insecure) - require.Equal(t, 1, env.GitCloneDepth) - require.Equal(t, "https://github.com/coder/coder", env.GitURL) - require.Equal(t, "/workspaces/coder", env.WorkspaceFolder) - require.Equal(t, "http://company-proxy.com:8081", env.GitHTTPProxyURL) -} diff --git a/options.go b/options.go new file mode 100644 index 00000000..f3b0009f --- /dev/null +++ b/options.go @@ -0,0 +1,311 @@ +package envbuilder + +import ( + "fmt" + "strconv" + "strings" +) + +type option struct { + Env string + Value any + Detail string +} + +type OptionsMap map[string]option + +var defaultOptions = OptionsMap{ + "SetupScript": option{ + Env: "SETUP_SCRIPT", + Value: "", + Detail: `SetupScript is the script to run before the init script. + It runs as the root user regardless of the user specified + in the devcontainer.json file. + + SetupScript is ran as the root user prior to the init script. + It is used to configure envbuilder dynamically during the runtime. + e.g. specifying whether to start ` + "`systemd`" + ` or ` + "`tiny init`" + ` for PID 1.`, + }, + "InitScript": option{ + Env: "INIT_SCRIPT", + Value: "sleep infinity", + Detail: "InitScript is the script to run to initialize the workspace.", + }, + "InitCommand": option{ + Env: "INIT_COMMAND", + Value: "/bin/sh", + Detail: "InitCommand is the command to run to initialize the workspace.", + }, + "InitArgs": option{ + Env: "INIT_ARGS", + Value: "", + Detail: `InitArgs are the arguments to pass to the init command. + They are split according to ` + "`/bin/sh`" + ` rules with + https://github.com/kballard/go-shellquote`, + }, + "CacheRepo": option{ + Env: "CACHE_REPO", + Value: "", + Detail: `CacheRepo is the name of the container registry + to push the cache image to. If this is empty, the cache + will not be pushed.`, + }, + "BaseImageCacheDir": option{ + Env: "BASE_IMAGE_CACHE_DIR", + Value: "", + Detail: `BaseImageCacheDir is the path to a directory where the base + image can be found. This should be a read-only directory + solely mounted for the purpose of caching the base image.`, + }, + "LayerCacheDir": option{ + Env: "LAYER_CACHE_DIR", + Value: "", + Detail: `LayerCacheDir is the path to a directory where built layers + will be stored. This spawns an in-memory registry to serve + the layers from.`, + }, + "DevcontainerDir": option{ + Env: "DEVCONTAINER_DIR", + Value: "", + Detail: `DevcontainerDir is a path to the folder containing + the devcontainer.json file that will be used to build the + workspace and can either be an absolute path or a path + relative to the workspace folder. If not provided, defaults to + ` + "`.devcontainer`" + `.`, + }, + "DevcontainerJSONPath": option{ + Env: "DEVCONTAINER_JSON_PATH", + Value: "", + Detail: `DevcontainerJSONPath is a path to a devcontainer.json file + that is either an absolute path or a path relative to + DevcontainerDir. This can be used in cases where one wants + to substitute an edited devcontainer.json file for the one + that exists in the repo.`, + }, + "DockerfilePath": option{ + Env: "DOCKERFILE_PATH", + Value: "", + Detail: `DockerfilePath is a relative path to the Dockerfile that + will be used to build the workspace. This is an alternative + to using a devcontainer that some might find simpler.`, + }, + "CacheTTLDays": option{ + Env: "CACHE_TTL_DAYS", + Value: 0, + Detail: `CacheTTLDays is the number of days to use cached layers before + expiring them. Defaults to 7 days.`, + }, + "DockerConfigBase64": option{ + Env: "DOCKER_CONFIG_BASE64", + Value: "", + Detail: `DockerConfigBase64 is a base64 encoded Docker config + file that will be used to pull images from private + container registries.`, + }, + "FallbackImage": option{ + Env: "FALLBACK_IMAGE", + Value: "", + Detail: `FallbackImage specifies an alternative image to use when neither + an image is declared in the devcontainer.json file nor a Dockerfile is present. + If there's a build failure (from a faulty Dockerfile) or a misconfiguration, + this image will be the substitute. + Set ` + "`ExitOnBuildFailure`" + ` to true to halt the container if the build faces an issue.`, + }, + "ExitOnBuildFailure": option{ + Env: "EXIT_ON_BUILD_FAILURE", + Value: false, + Detail: `ExitOnBuildFailure terminates the container upon a build failure. + This is handy when preferring the ` + "`FALLBACK_IMAGE`" + ` in cases where + no devcontainer.json or image is provided. However, it ensures + that the container stops if the build process encounters an error.`, + }, + "ForceSafe": option{ + Env: "FORCE_SAFE", + Value: false, + Detail: `ForceSafe ignores any filesystem safety checks. + This could cause serious harm to your system! + This is used in cases where bypass is needed + to unblock customers!`, + }, + "Insecure": option{ + Env: "INSECURE", + Value: false, + Detail: `Insecure bypasses TLS verification when cloning + and pulling from container registries.`, + }, + "IgnorePaths": option{ + Env: "IGNORE_PATHS", + // Kubernetes frequently stores secrets in /var/run/secrets, and + // other applications might as well. This seems to be a sensible + // default, but if that changes, it's simple to adjust. + Value: []string{"/var/run"}, + Detail: `IgnorePaths is a comma separated list of paths + to ignore when building the workspace.`, + }, + "SkipRebuild": option{ + Env: "SKIP_REBUILD", + Value: false, + Detail: `SkipRebuild skips building if the MagicFile exists. + This is used to skip building when a container is + restarting. e.g. docker stop -> docker start + This value can always be set to true - even if the + container is being started for the first time.`, + }, + "GitURL": option{ + Env: "GIT_URL", + Value: "", + Detail: `GitURL is the URL of the Git repository to clone. + This is optional!`, + }, + "GitCloneDepth": option{ + Env: "GIT_CLONE_DEPTH", + Value: 0, + Detail: `GitCloneDepth is the depth to use when cloning + the Git repository.`, + }, + "GitCloneSingleBranch": option{ + Env: "GIT_CLONE_SINGLE_BRANCH", + Value: false, + Detail: `GitCloneSingleBranch clones only a single branch + of the Git repository.`, + }, + "GitUsername": option{ + Env: "GIT_USERNAME", + Value: "", + Detail: `GitUsername is the username to use for Git authentication. + This is optional!`, + }, + "GitPassword": option{ + Env: "GIT_PASSWORD", + Value: "", + Detail: `GitPassword is the password to use for Git authentication. + This is optional!`, + }, + "GitHTTPProxyURL": option{ + Env: "GIT_HTTP_PROXY_URL", + Value: "", + Detail: `GitHTTPProxyURL is the url for the http proxy. + This is optional!`, + }, + "WorkspaceFolder": option{ + Env: "WORKSPACE_FOLDER", + Value: "", + Detail: `WorkspaceFolder is the path to the workspace folder + that will be built. This is optional!`, + }, + "SSLCertBase64": option{ + Env: "SSL_CERT_BASE64", + Value: "", + Detail: `SSLCertBase64 is the content of an SSL cert file. + This is useful for self-signed certificates.`, + }, + "ExportEnvFile": option{ + Env: "EXPORT_ENV_FILE", + Value: "", + Detail: `ExportEnvFile is an optional file path to a .env file where + envbuilder will dump environment variables from devcontainer.json and + the built container image.`, + }, + "PostStartScriptPath": option{ + Env: "POST_START_SCRIPT_PATH", + Value: "", + Detail: `PostStartScriptPath is the path to a script that will be created by + envbuilder based on the ` + "`postStartCommand`" + ` in devcontainer.json, if any + is specified (otherwise the script is not created). If this is set, the + specified InitCommand should check for the presence of this script and + execute it after successful startup.`, + }, +} + +func OptionsFromEnv(getEnv func(string) (string, bool)) OptionsMap { + options := defaultOptions + + for key, option := range options { + value, ok := getEnv(option.Env) + if !ok || value == "" { + continue + } + + switch v := options[key].Value.(type) { + case string: + options.SetString(key, value) + case int: + intValue, _ := strconv.Atoi(value) + options.SetInt(key, intValue) + case bool: + boolValue, _ := strconv.ParseBool(value) + options.SetBool(key, boolValue) + case []string: + options.SetStringSlice(key, strings.Split(value, ",")) + default: + panic(fmt.Sprintf("unsupported type %T", v)) + } + } + + return options +} + +func (o OptionsMap) get(key string) any { + val, ok := o[key] + if !ok { + panic(fmt.Sprintf("key %q not found in options", key)) + } + return val.Value +} + +func (o OptionsMap) GetString(key string) string { + val := o.get(key) + if val == nil { + return "" + } + return val.(string) +} + +func (o OptionsMap) GetBool(key string) bool { + val := o.get(key) + if val == nil { + return false + } + return val.(bool) +} + +func (o OptionsMap) GetInt(key string) int { + val := o.get(key) + if val == nil { + return 0 + } + return val.(int) +} + +func (o OptionsMap) GetStringSlice(key string) []string { + val := o.get(key) + if val == nil { + return nil + } + return val.([]string) +} + +func (o OptionsMap) set(key string, value any) { + val, ok := o[key] + if !ok { + panic(fmt.Sprintf("key %q not found in options", key)) + } + val.Value = value + o[key] = val +} + +func (o OptionsMap) SetString(key string, value string) { + o.set(key, value) +} + +func (o OptionsMap) SetStringSlice(key string, value []string) { + o.set(key, value) +} + +func (o OptionsMap) SetBool(key string, value bool) { + o.set(key, value) +} + +func (o OptionsMap) SetInt(key string, value int) { + o.set(key, value) +} diff --git a/options_test.go b/options_test.go new file mode 100644 index 00000000..7086e511 --- /dev/null +++ b/options_test.go @@ -0,0 +1,74 @@ +package envbuilder_test + +import ( + "testing" + + "github.com/coder/envbuilder" + "github.com/stretchr/testify/require" +) + +func TestOptionsFromEnv(t *testing.T) { + t.Parallel() + env := map[string]string{ + "SETUP_SCRIPT": "echo setup script", + "INIT_SCRIPT": "echo init script", + "INIT_COMMAND": "/bin/bash", + "INIT_ARGS": "-c 'echo init args'", + "CACHE_REPO": "kylecarbs/testing", + "BASE_IMAGE_CACHE_DIR": "/tmp/cache", + "LAYER_CACHE_DIR": "/tmp/cache", + "DEVCONTAINER_DIR": "/tmp/devcontainer", + "DEVCONTAINER_JSON_PATH": "/tmp/devcontainer.json", + "DOCKERFILE_PATH": "Dockerfile", + "CACHE_TTL_DAYS": "30", + "DOCKER_CONFIG_BASE64": "dGVzdA==", + "FALLBACK_IMAGE": "ubuntu:latest", + "EXIT_ON_BUILD_FAILURE": "true", + "FORCE_SAFE": "true", + "INSECURE": "false", + "IGNORE_PATHS": "/tmp,/var", + "SKIP_REBUILD": "true", + "GIT_URL": "https://github.com/coder/coder", + "GIT_CLONE_DEPTH": "1", + "GIT_CLONE_SINGLE_BRANCH": "true", + "GIT_USERNAME": "kylecarbs", + "GIT_PASSWORD": "password", + "GIT_HTTP_PROXY_URL": "http://company-proxy.com:8081", + "WORKSPACE_FOLDER": "/workspaces/coder", + "SSL_CERT_BASE64": "dGVzdA==", + "EXPORT_ENV_FILE": "/tmp/env", + "POST_START_SCRIPT_PATH": "/tmp/poststart.sh", + } + options := envbuilder.OptionsFromEnv(func(s string) (string, bool) { + return env[s], true + }) + + require.Equal(t, env["SETUP_SCRIPT"], options.GetString("SetupScript")) + require.Equal(t, env["INIT_SCRIPT"], options.GetString("InitScript")) + require.Equal(t, env["INIT_COMMAND"], options.GetString("InitCommand")) + require.Equal(t, env["INIT_ARGS"], options.GetString("InitArgs")) + require.Equal(t, env["CACHE_REPO"], options.GetString("CacheRepo")) + require.Equal(t, env["BASE_IMAGE_CACHE_DIR"], options.GetString("BaseImageCacheDir")) + require.Equal(t, env["LAYER_CACHE_DIR"], options.GetString("LayerCacheDir")) + require.Equal(t, env["DEVCONTAINER_DIR"], options.GetString("DevcontainerDir")) + require.Equal(t, env["DEVCONTAINER_JSON_PATH"], options.GetString("DevcontainerJSONPath")) + require.Equal(t, env["DOCKERFILE_PATH"], options.GetString("DockerfilePath")) + require.Equal(t, 30, options.GetInt("CacheTTLDays")) + require.Equal(t, env["DOCKER_CONFIG_BASE64"], options.GetString("DockerConfigBase64")) + require.Equal(t, env["FALLBACK_IMAGE"], options.GetString("FallbackImage")) + require.True(t, options.GetBool("ExitOnBuildFailure")) + require.True(t, options.GetBool("ForceSafe")) + require.False(t, options.GetBool("Insecure")) + require.Equal(t, options.GetStringSlice("IgnorePaths"), []string{"/tmp", "/var"}) + require.True(t, options.GetBool("SkipRebuild")) + require.Equal(t, env["GIT_URL"], options.GetString("GitURL")) + require.Equal(t, 1, options.GetInt("GitCloneDepth")) + require.True(t, options.GetBool("GitCloneSingleBranch")) + require.Equal(t, env["GIT_USERNAME"], options.GetString("GitUsername")) + require.Equal(t, env["GIT_PASSWORD"], options.GetString("GitPassword")) + require.Equal(t, env["GIT_HTTP_PROXY_URL"], options.GetString("GitHTTPProxyURL")) + require.Equal(t, env["WORKSPACE_FOLDER"], options.GetString("WorkspaceFolder")) + require.Equal(t, env["SSL_CERT_BASE64"], options.GetString("SSLCertBase64")) + require.Equal(t, env["EXPORT_ENV_FILE"], options.GetString("ExportEnvFile")) + require.Equal(t, env["POST_START_SCRIPT_PATH"], options.GetString("PostStartScriptPath")) +} From 68d4523610c0aee7c298a801dc7a80fc74d72267 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 24 Apr 2024 16:39:26 +0000 Subject: [PATCH 02/20] Fix broken internal test --- envbuilder_internal_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index 155abc5c..88e9ddf4 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -75,7 +75,7 @@ func TestFindDevcontainerJSON(t *testing.T) { // when options := DefaultOptions() options.SetString("WorkspaceFolder", "/workspace") - options.SetString("DevcontainerDir", "/experimental-devcontainer") + options.SetString("DevcontainerDir", "experimental-devcontainer") devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs}) // then From 1cdf3f3d2b47e93f89d663b0805ebab39a4927e3 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 24 Apr 2024 16:56:24 +0000 Subject: [PATCH 03/20] Remove Dependencies struct and pass deps directly as arg --- cmd/envbuilder/main.go | 26 ++++---- envbuilder.go | 130 +++++++++++++++++------------------- envbuilder_internal_test.go | 14 ++-- 3 files changed, 82 insertions(+), 88 deletions(-) diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go index 689528e5..321544dd 100644 --- a/cmd/envbuilder/main.go +++ b/cmd/envbuilder/main.go @@ -68,23 +68,21 @@ func main() { os.Setenv("CODER_AGENT_SUBSYSTEM", subsystems) } - deps := envbuilder.Dependencies{ - Logger: func(level codersdk.LogLevel, format string, args ...interface{}) { - output := fmt.Sprintf(format, args...) - fmt.Fprintln(cmd.ErrOrStderr(), output) - if sendLogs != nil { - sendLogs(cmd.Context(), agentsdk.Log{ - CreatedAt: time.Now(), - Output: output, - Level: level, - }) - } - }, + logger := func(level codersdk.LogLevel, format string, args ...interface{}) { + output := fmt.Sprintf(format, args...) + fmt.Fprintln(cmd.ErrOrStderr(), output) + if sendLogs != nil { + sendLogs(cmd.Context(), agentsdk.Log{ + CreatedAt: time.Now(), + Output: output, + Level: level, + }) + } } - err := envbuilder.Run(cmd.Context(), options, deps) + err := envbuilder.Run(cmd.Context(), options, nil, logger) if err != nil { - deps.Logger(codersdk.LogLevelError, "error: %s", err) + logger(codersdk.LogLevelError, "error: %s", err) } return err }, diff --git a/envbuilder.go b/envbuilder.go index 2e11e46d..cfc9be18 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -76,20 +76,16 @@ var ( MagicFile = filepath.Join(MagicDir, "built") ) -type Dependencies struct { - // Logger is the logger to use for all operations. - Logger func(level codersdk.LogLevel, format string, args ...any) - - // Filesystem is the filesystem to use for all operations. - // Defaults to the host filesystem. - Filesystem billy.Filesystem -} +type Logger func(level codersdk.LogLevel, format string, args ...any) // DockerConfig represents the Docker configuration file. type DockerConfig configfile.ConfigFile // Run runs the envbuilder. -func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { +// Logger is the logger 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 OptionsMap, fs billy.Filesystem, logger Logger) error { // Default to the shell! initArgs := []string{"-c", options.GetString("InitScript")} if options.GetString("InitArgs") != "" { @@ -99,8 +95,8 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { return fmt.Errorf("parse init args: %w", err) } } - if deps.Filesystem == nil { - deps.Filesystem = &osfsWithChmod{osfs.New("/")} + if fs == nil { + fs = &osfsWithChmod{osfs.New("/")} } if options.GetString("WorkspaceFolder") == "" { var err error @@ -110,20 +106,20 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { return err } } - logf := deps.Logger + stageNumber := 1 startStage := func(format string, args ...any) func(format string, args ...any) { now := time.Now() stageNum := stageNumber stageNumber++ - logf(codersdk.LogLevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) + logger(codersdk.LogLevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) return func(format string, args ...any) { - logf(codersdk.LogLevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) + logger(codersdk.LogLevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) } } - logf(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) + logger(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) var caBundle []byte if options.GetString("SSLCertBase64") != "" { @@ -185,14 +181,14 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { if line == "" { continue } - logf(codersdk.LogLevelInfo, "#1: %s", strings.TrimSpace(line)) + logger(codersdk.LogLevelInfo, "#1: %s", strings.TrimSpace(line)) } } }() cloneOpts := CloneRepoOptions{ Path: options.GetString("WorkspaceFolder"), - Storage: deps.Filesystem, + Storage: fs, Insecure: options.GetBool("Insecure"), Progress: writer, SingleBranch: options.GetBool("GitCloneSingleBranch"), @@ -228,14 +224,14 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { endStage("📦 The repository already exists!") } } else { - logf(codersdk.LogLevelError, "Failed to clone repository: %s", fallbackErr.Error()) - logf(codersdk.LogLevelError, "Falling back to the default image...") + logger(codersdk.LogLevelError, "Failed to clone repository: %s", fallbackErr.Error()) + logger(codersdk.LogLevelError, "Falling back to the default image...") } } defaultBuildParams := func() (*devcontainer.Compiled, error) { dockerfile := filepath.Join(MagicDir, "Dockerfile") - file, err := deps.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0644) + file, err := fs.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err } @@ -267,14 +263,14 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { if options.GetString("DockerFilepath") == "" { // Only look for a devcontainer if a Dockerfile wasn't specified. // devcontainer is a standard, so it's reasonable to be the default. - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &deps) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, logger) if err != nil { - logf(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error()) - logf(codersdk.LogLevelError, "Falling back to the default image...") + logger(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error()) + logger(codersdk.LogLevelError, "Falling back to the default image...") } else { // We know a devcontainer exists. // Let's parse it and use it! - file, err := deps.Filesystem.Open(devcontainerPath) + file, err := fs.Open(devcontainerPath) if err != nil { return fmt.Errorf("open devcontainer.json: %w", err) } @@ -291,17 +287,17 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { if err != nil { return fmt.Errorf("no Dockerfile or image found: %w", err) } - logf(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...") + logger(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...") fallbackDockerfile = defaultParams.DockerfilePath } - buildParams, err = devContainer.Compile(deps.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.GetString("WorkspaceFolder"), false) + buildParams, err = devContainer.Compile(fs, devcontainerDir, MagicDir, fallbackDockerfile, options.GetString("WorkspaceFolder"), false) if err != nil { return fmt.Errorf("compile devcontainer.json: %w", err) } scripts = devContainer.LifecycleScripts } else { - logf(codersdk.LogLevelError, "Failed to parse devcontainer.json: %s", err.Error()) - logf(codersdk.LogLevelError, "Falling back to the default image...") + logger(codersdk.LogLevelError, "Failed to parse devcontainer.json: %s", err.Error()) + logger(codersdk.LogLevelError, "Falling back to the default image...") } } } else { @@ -312,11 +308,11 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { // not defined, show a warning dockerfileDir := filepath.Dir(dockerfilePath) if dockerfileDir != filepath.Clean(options.GetString("WorkspaceFolder")) && options.GetString("BuildContextPath") == "" { - logf(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.GetString("WorkspaceFolder")) - logf(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) + logger(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.GetString("WorkspaceFolder")) + logger(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) } - dockerfile, err := deps.Filesystem.Open(dockerfilePath) + dockerfile, err := fs.Open(dockerfilePath) if err == nil { content, err := io.ReadAll(dockerfile) if err != nil { @@ -342,7 +338,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { HijackLogrus(func(entry *logrus.Entry) { for _, line := range strings.Split(entry.Message, "\r") { - logf(codersdk.LogLevelInfo, "#2: %s", color.HiBlackString(line)) + logger(codersdk.LogLevelInfo, "#2: %s", color.HiBlackString(line)) } }) @@ -358,9 +354,9 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { } // Disable all logging from the registry... - logger := logrus.New() - logger.SetOutput(io.Discard) - entry := logrus.NewEntry(logger) + logrusLogger := logrus.New() + logrusLogger.SetOutput(io.Discard) + entry := logrus.NewEntry(logrusLogger) dcontext.SetDefaultLogger(entry) ctx = dcontext.WithLogger(ctx, entry) @@ -381,7 +377,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { go func() { err := srv.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { - logf(codersdk.LogLevelError, "Failed to serve registry: %s", err.Error()) + logger(codersdk.LogLevelError, "Failed to serve registry: %s", err.Error()) } }() closeAfterBuild = func() { @@ -389,7 +385,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { _ = listener.Close() } if options.GetString("CacheRepo") != "" { - logf(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") + logger(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") } options.SetString("CacheRepo", fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port)) } @@ -414,7 +410,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { skippedRebuild := false build := func() (v1.Image, error) { - _, err := deps.Filesystem.Stat(MagicFile) + _, err := fs.Stat(MagicFile) if err == nil && options.GetBool("SkipRebuild") { endStage := startStage("🏗️ Skipping build because of cache...") imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent) @@ -452,13 +448,13 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { go func() { scanner := bufio.NewScanner(stdoutReader) for scanner.Scan() { - logf(codersdk.LogLevelInfo, "%s", scanner.Text()) + logger(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() go func() { scanner := bufio.NewScanner(stderrReader) for scanner.Scan() { - logf(codersdk.LogLevelInfo, "%s", scanner.Text()) + logger(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() cacheTTL := time.Hour * 24 * 7 @@ -538,13 +534,13 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { fallback = true fallbackErr = err case strings.Contains(err.Error(), "unexpected status code 401 Unauthorized"): - logf(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") + logger(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") } if !fallback || options.GetBool("ExitOnBuildFailure") { return err } - logf(codersdk.LogLevelError, "Failed to build: %s", err) - logf(codersdk.LogLevelError, "Falling back to the default image...") + logger(codersdk.LogLevelError, "Failed to build: %s", err) + logger(codersdk.LogLevelError, "Falling back to the default image...") buildParams, err = defaultBuildParams() if err != nil { return err @@ -561,7 +557,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { // Create the magic file to indicate that this build // has already been ran before! - file, err := deps.Filesystem.Create(MagicFile) + file, err := fs.Create(MagicFile) if err != nil { return fmt.Errorf("create magic file: %w", err) } @@ -587,10 +583,10 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { if err != nil { return fmt.Errorf("unmarshal metadata: %w", err) } - logf(codersdk.LogLevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...") + logger(codersdk.LogLevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...") for _, container := range devContainer { if container.RemoteUser != "" { - logf(codersdk.LogLevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser) + logger(codersdk.LogLevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser) configFile.Config.User = container.RemoteUser } @@ -685,7 +681,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { username = buildParams.User } if username == "" { - logf(codersdk.LogLevelWarn, "#3: no user specified, using root") + logger(codersdk.LogLevelWarn, "#3: no user specified, using root") } userInfo, err := getUser(username) @@ -727,7 +723,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { // 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, deps, scripts, skippedRebuild, userInfo); err != nil { + if err := execLifecycleScripts(ctx, options, fs, logger, scripts, skippedRebuild, userInfo); err != nil { return err } @@ -740,7 +736,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { // We execute the initialize script as the root user! os.Setenv("HOME", "/root") - logf(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.GetString("SetupScript")) + logger(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.GetString("SetupScript")) envKey := "ENVBUILDER_ENV" envFile := filepath.Join("/", MagicDir, "environ") @@ -767,7 +763,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { go func() { scanner := bufio.NewScanner(&buf) for scanner.Scan() { - logf(codersdk.LogLevelInfo, "%s", scanner.Text()) + logger(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() @@ -833,7 +829,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error { return fmt.Errorf("set uid: %w", err) } - logf(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.GetString("InitCommand"), initArgs, userInfo.user.Username) + logger(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.GetString("InitCommand"), initArgs, userInfo.user.Username) err = syscall.Exec(options.GetString("InitCommand"), append([]string{options.GetString("InitCommand")}, initArgs...), os.Environ()) if err != nil { @@ -906,7 +902,7 @@ func findUser(nameOrID string) (*user.User, error) { func execOneLifecycleScript( ctx context.Context, - logf func(level codersdk.LogLevel, format string, args ...any), + logger func(level codersdk.LogLevel, format string, args ...any), s devcontainer.LifecycleScript, scriptName string, userInfo userInfo, @@ -914,9 +910,9 @@ func execOneLifecycleScript( if s.IsEmpty() { return nil } - logf(codersdk.LogLevelInfo, "=== Running %s as the %q user...", scriptName, userInfo.user.Username) + logger(codersdk.LogLevelInfo, "=== Running %s as the %q user...", scriptName, userInfo.user.Username) if err := s.Execute(ctx, userInfo.uid, userInfo.gid); err != nil { - logf(codersdk.LogLevelError, "Failed to run %s: %v", scriptName, err) + logger(codersdk.LogLevelError, "Failed to run %s: %v", scriptName, err) return err } return nil @@ -925,7 +921,8 @@ func execOneLifecycleScript( func execLifecycleScripts( ctx context.Context, options OptionsMap, - deps Dependencies, + fs billy.Filesystem, + logger Logger, scripts devcontainer.LifecycleScripts, skippedRebuild bool, userInfo userInfo, @@ -935,16 +932,16 @@ func execLifecycleScripts( } if !skippedRebuild { - if err := execOneLifecycleScript(ctx, deps.Logger, scripts.OnCreateCommand, "onCreateCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, logger, scripts.OnCreateCommand, "onCreateCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } } - if err := execOneLifecycleScript(ctx, deps.Logger, scripts.UpdateContentCommand, "updateContentCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, logger, scripts.UpdateContentCommand, "updateContentCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } - if err := execOneLifecycleScript(ctx, deps.Logger, scripts.PostCreateCommand, "postCreateCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, logger, scripts.PostCreateCommand, "postCreateCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } @@ -956,7 +953,7 @@ func execLifecycleScripts( return fmt.Errorf("failed to create post-start script: %w", err) } } else { - _ = execOneLifecycleScript(ctx, deps.Logger, scripts.PostStartCommand, "postStartCommand", userInfo) + _ = execOneLifecycleScript(ctx, logger, scripts.PostStartCommand, "postStartCommand", userInfo) } } return nil @@ -1001,7 +998,7 @@ func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } -func findDevcontainerJSON(options *OptionsMap, deps *Dependencies) (string, string, error) { +func findDevcontainerJSON(options OptionsMap, fs billy.Filesystem, logger Logger) (string, string, error) { // 0. Check if custom devcontainer directory or path is provided. if options.GetString("DevcontainerDir") != "" || options.GetString("DevcontainerJSONPath") != "" { devcontainerDir := options.GetString("DevcontainerDir") @@ -1032,34 +1029,33 @@ func findDevcontainerJSON(options *OptionsMap, deps *Dependencies) (string, stri // 1. Check `options.WorkspaceFolder`/.devcontainer/devcontainer.json. location := filepath.Join(options.GetString("WorkspaceFolder"), ".devcontainer", "devcontainer.json") - if _, err := deps.Filesystem.Stat(location); err == nil { + if _, err := fs.Stat(location); err == nil { return location, filepath.Dir(location), nil } // 2. Check `options.WorkspaceFolder`/devcontainer.json. location = filepath.Join(options.GetString("WorkspaceFolder"), "devcontainer.json") - if _, err := deps.Filesystem.Stat(location); err == nil { + if _, err := fs.Stat(location); err == nil { return location, filepath.Dir(location), nil } // 3. Check every folder: `options.WorkspaceFolder`/.devcontainer//devcontainer.json. devcontainerDir := filepath.Join(options.GetString("WorkspaceFolder"), ".devcontainer") - fileInfos, err := deps.Filesystem.ReadDir(devcontainerDir) + fileInfos, err := fs.ReadDir(devcontainerDir) if err != nil { return "", "", err } - logf := deps.Logger for _, fileInfo := range fileInfos { if !fileInfo.IsDir() { - logf(codersdk.LogLevelDebug, `%s is a file`, fileInfo.Name()) + logger(codersdk.LogLevelDebug, `%s is a file`, fileInfo.Name()) continue } location := filepath.Join(devcontainerDir, fileInfo.Name(), "devcontainer.json") - if _, err := deps.Filesystem.Stat(location); err != nil { - logf(codersdk.LogLevelDebug, `stat %s failed: %s`, location, err.Error()) + if _, err := fs.Stat(location); err != nil { + logger(codersdk.LogLevelDebug, `stat %s failed: %s`, location, err.Error()) continue } diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index 88e9ddf4..1a103a45 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -20,7 +20,7 @@ func TestFindDevcontainerJSON(t *testing.T) { // when options := DefaultOptions() options.SetString("WorkspaceFolder", "/workspace") - _, _, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs}) + _, _, err := findDevcontainerJSON(options, fs, nil) // then require.Error(t, err) @@ -37,7 +37,7 @@ func TestFindDevcontainerJSON(t *testing.T) { // when options := DefaultOptions() options.SetString("WorkspaceFolder", "/workspace") - _, _, err = findDevcontainerJSON(&options, &Dependencies{Filesystem: fs}) + _, _, err = findDevcontainerJSON(options, fs, nil) // then require.Error(t, err) @@ -55,7 +55,7 @@ func TestFindDevcontainerJSON(t *testing.T) { // when options := DefaultOptions() options.SetString("WorkspaceFolder", "/workspace") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs}) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) // then require.NoError(t, err) @@ -76,7 +76,7 @@ func TestFindDevcontainerJSON(t *testing.T) { options := DefaultOptions() options.SetString("WorkspaceFolder", "/workspace") options.SetString("DevcontainerDir", "experimental-devcontainer") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs}) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) // then require.NoError(t, err) @@ -97,7 +97,7 @@ func TestFindDevcontainerJSON(t *testing.T) { options := DefaultOptions() options.SetString("WorkspaceFolder", "/workspace") options.SetString("DevcontainerJSONPath", "experimental.json") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs}) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) // then require.NoError(t, err) @@ -117,7 +117,7 @@ func TestFindDevcontainerJSON(t *testing.T) { // when options := DefaultOptions() options.SetString("WorkspaceFolder", "/workspace") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs}) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) // then require.NoError(t, err) @@ -137,7 +137,7 @@ func TestFindDevcontainerJSON(t *testing.T) { // when options := DefaultOptions() options.SetString("WorkspaceFolder", "/workspace") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs}) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) // then require.NoError(t, err) From ff1b0d0d7301ba1996001c8303c51852daeefa33 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 24 Apr 2024 16:57:40 +0000 Subject: [PATCH 04/20] Remove unecessary arg --- envbuilder.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index cfc9be18..5461eef2 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -723,7 +723,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo // 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, fs, logger, scripts, skippedRebuild, userInfo); err != nil { + if err := execLifecycleScripts(ctx, options, logger, scripts, skippedRebuild, userInfo); err != nil { return err } @@ -921,7 +921,6 @@ func execOneLifecycleScript( func execLifecycleScripts( ctx context.Context, options OptionsMap, - fs billy.Filesystem, logger Logger, scripts devcontainer.LifecycleScripts, skippedRebuild bool, From 8a626edee3604762f2d28472e0282b95fc605ff2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 24 Apr 2024 18:29:19 +0000 Subject: [PATCH 05/20] Migrate to user serpent --- cmd/envbuilder/main.go | 27 ++- config.go | 280 +++++++++++++++++++++++++++++++ envbuilder.go | 187 +++++++++++---------- envbuilder_internal_test.go | 38 ++--- go.mod | 25 ++- go.sum | 37 +++-- options.go | 319 ------------------------------------ options_test.go | 76 --------- 8 files changed, 445 insertions(+), 544 deletions(-) create mode 100644 config.go delete mode 100644 options.go delete mode 100644 options_test.go diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go index 321544dd..077c79b8 100644 --- a/cmd/envbuilder/main.go +++ b/cmd/envbuilder/main.go @@ -14,7 +14,7 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/envbuilder" - "github.com/spf13/cobra" + "github.com/coder/serpent" // *Never* remove this. Certificates are not bundled as part // of the container, so this is necessary for all connections @@ -23,15 +23,14 @@ import ( ) func main() { - root := &cobra.Command{ - Use: "envbuilder", + config := envbuilder.Config{} + cmd := serpent.Command{ + Use: "envbuilder", + Options: config.Options(), // Hide usage because we don't want to show the // "envbuilder [command] --help" output on error. - SilenceUsage: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - options := envbuilder.OptionsFromEnv(os.LookupEnv) - + Hidden: true, + Handler: func(inv *serpent.Invocation) error { var sendLogs func(ctx context.Context, log ...agentsdk.Log) error agentURL := os.Getenv("CODER_AGENT_URL") agentToken := os.Getenv("CODER_AGENT_TOKEN") @@ -48,13 +47,13 @@ func main() { client.SDK.HTTPClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: options.GetBool("Insecure"), + InsecureSkipVerify: config.Insecure, }, }, } var flushAndClose func(ctx context.Context) error sendLogs, flushAndClose = agentsdk.LogsSender(agentsdk.ExternalLogSourceID, client.PatchLogs, slog.Logger{}) - defer flushAndClose(cmd.Context()) + defer flushAndClose(inv.Context()) // This adds the envbuilder subsystem. // If telemetry is enabled in a Coder deployment, @@ -70,9 +69,9 @@ func main() { logger := func(level codersdk.LogLevel, format string, args ...interface{}) { output := fmt.Sprintf(format, args...) - fmt.Fprintln(cmd.ErrOrStderr(), output) + fmt.Fprintln(inv.Stderr, output) if sendLogs != nil { - sendLogs(cmd.Context(), agentsdk.Log{ + sendLogs(inv.Context(), agentsdk.Log{ CreatedAt: time.Now(), Output: output, Level: level, @@ -80,14 +79,14 @@ func main() { } } - err := envbuilder.Run(cmd.Context(), options, nil, logger) + err := envbuilder.Run(inv.Context(), &config, nil, logger) if err != nil { logger(codersdk.LogLevelError, "error: %s", err) } return err }, } - err := root.Execute() + err := cmd.Invoke().WithOS().Run() if err != nil { fmt.Fprintf(os.Stderr, "error: %v", err) os.Exit(1) diff --git a/config.go b/config.go new file mode 100644 index 00000000..76b2a0dd --- /dev/null +++ b/config.go @@ -0,0 +1,280 @@ +package envbuilder + +import ( + "github.com/coder/serpent" +) + +type Config struct { + SetupScript string + InitScript string + InitCommand string + InitArgs string + CacheRepo string + BaseImageCacheDir string + LayerCacheDir string + DevcontainerDir string + DevcontainerJSONPath string + DockerfilePath string + BuildContextPath string + CacheTTLDays int64 + DockerConfigBase64 string + FallbackImage string + ExitOnBuildFailure bool + ForceSafe bool + Insecure bool + IgnorePaths []string + SkipRebuild bool + GitURL string + GitCloneDepth int64 + GitCloneSingleBranch bool + GitUsername string + GitPassword string + GitHTTPProxyURL string + WorkspaceFolder string + SSLCertBase64 string + ExportEnvFile string + PostStartScriptPath string +} + +func (c *Config) Options() serpent.OptionSet { + return serpent.OptionSet{ + { + Name: "Setup Script", + Env: "SETUP_SCRIPT", + Flag: "setup-script", + Value: serpent.StringOf(&c.SetupScript), + Description: `SetupScript is the script to run before the init script. + It runs as the root user regardless of the user specified + in the devcontainer.json file. + + SetupScript is ran as the root user prior to the init script. + It is used to configure envbuilder dynamically during the runtime. + e.g. specifying whether to start ` + "`systemd`" + ` or ` + "`tiny init`" + ` for PID 1.`, + }, + { + Name: "Init Script", + Env: "INIT_SCRIPT", + Default: "sleep infinity", + Value: serpent.StringOf(&c.InitScript), + Description: "InitScript is the script to run to initialize the workspace.", + }, + { + Name: "Init Command", + Env: "INIT_COMMAND", + Default: "/bin/sh", + Value: serpent.StringOf(&c.InitCommand), + Description: "InitCommand is the command to run to initialize the workspace.", + }, + { + Name: "Init Args", + Env: "INIT_ARGS", + Value: serpent.StringOf(&c.InitArgs), + Description: `InitArgs are the arguments to pass to the init command. + They are split according to ` + "`/bin/sh`" + ` rules with + https://github.com/kballard/go-shellquote`, + }, + { + Name: "Cache Repo", + Env: "CACHE_REPO", + Value: serpent.StringOf(&c.CacheRepo), + Description: `CacheRepo is the name of the container registry + to push the cache image to. If this is empty, the cache + will not be pushed.`, + }, + { + Name: "Base Image Cache Dir", + Env: "BASE_IMAGE_CACHE_DIR", + Value: serpent.StringOf(&c.BaseImageCacheDir), + Description: `BaseImageCacheDir is the path to a directory where the base + image can be found. This should be a read-only directory + solely mounted for the purpose of caching the base image.`, + }, + { + Name: "Layer Cache Dir", + Env: "LAYER_CACHE_DIR", + Value: serpent.StringOf(&c.LayerCacheDir), + Description: `LayerCacheDir is the path to a directory where built layers + will be stored. This spawns an in-memory registry to serve + the layers from.`, + }, + { + Name: "Devcontainer Dir", + Env: "DEVCONTAINER_DIR", + Value: serpent.StringOf(&c.DevcontainerDir), + Description: `DevcontainerDir is a path to the folder containing + the devcontainer.json file that will be used to build the + workspace and can either be an absolute path or a path + relative to the workspace folder. If not provided, defaults to + ` + "`.devcontainer`" + `.`, + }, + { + Name: "Devcontainer JSON Path", + Env: "DEVCONTAINER_JSON_PATH", + Value: serpent.StringOf(&c.DevcontainerJSONPath), + Description: `DevcontainerJSONPath is a path to a devcontainer.json file + that is either an absolute path or a path relative to + DevcontainerDir. This can be used in cases where one wants + to substitute an edited devcontainer.json file for the one + that exists in the repo.`, + }, + { + Name: "Dockerfile Path", + Env: "DOCKERFILE_PATH", + Value: serpent.StringOf(&c.DockerfilePath), + Description: `DockerfilePath is a relative path to the Dockerfile that + will be used to build the workspace. This is an alternative + to using a devcontainer that some might find simpler.`, + }, + { + Name: "Build Context Path", + Env: `BUILD_CONTEXT_PATH`, + Value: serpent.StringOf(&c.BuildContextPath), + Description: `BuildContextPath can be specified when a DockerfilePath is specified outside the base WorkspaceFolder. + This path MUST be relative to the WorkspaceFolder path into which the repo is cloned.`, + }, + { + Name: "Cache TTL Days", + Env: "CACHE_TTL_DAYS", + Value: serpent.Int64Of(&c.CacheTTLDays), + Description: `CacheTTLDays is the number of days to use cached layers before + expiring them. Defaults to 7 days.`, + }, + { + Name: "Docker Config Base64", + Env: "DOCKER_CONFIG_BASE64", + Value: serpent.StringOf(&c.DockerConfigBase64), + Description: `DockerConfigBase64 is a base64 encoded Docker config + file that will be used to pull images from private + container registries.`, + }, + { + Name: "Fallback Image", + Env: "FALLBACK_IMAGE", + Value: serpent.StringOf(&c.FallbackImage), + Description: `FallbackImage specifies an alternative image to use when neither + an image is declared in the devcontainer.json file nor a Dockerfile is present. + If there's a build failure (from a faulty Dockerfile) or a misconfiguration, + this image will be the substitute. + Set ` + "`ExitOnBuildFailure`" + ` to true to halt the container if the build faces an issue.`, + }, + { + Env: "EXIT_ON_BUILD_FAILURE", + Value: serpent.BoolOf(&c.ExitOnBuildFailure), + Description: `ExitOnBuildFailure terminates the container upon a build failure. + This is handy when preferring the ` + "`FALLBACK_IMAGE`" + ` in cases where + no devcontainer.json or image is provided. However, it ensures + that the container stops if the build process encounters an error.`, + }, + { + Name: "Force Safe", + Env: "FORCE_SAFE", + Value: serpent.BoolOf(&c.ForceSafe), + Description: `ForceSafe ignores any filesystem safety checks. + This could cause serious harm to your system! + This is used in cases where bypass is needed + to unblock customers!`, + }, + { + Name: "Insecure", + Env: "INSECURE", + Value: serpent.BoolOf(&c.Insecure), + Description: `Insecure bypasses TLS verification when cloning + and pulling from container registries.`, + }, + { + Name: "Ignore Paths", + Env: "IGNORE_PATHS", + Value: serpent.StringArrayOf(&c.IgnorePaths), + // Kubernetes frequently stores secrets in /var/run/secrets, and + // other applications might as well. This seems to be a sensible + // default, but if that changes, it's simple to adjust. + Default: "/var/run", + Description: `IgnorePaths is a comma separated list of paths + to ignore when building the workspace.`, + }, + { + Name: "Skip Rebuild", + Env: "SKIP_REBUILD", + Value: serpent.BoolOf(&c.SkipRebuild), + Description: `SkipRebuild skips building if the MagicFile exists. + This is used to skip building when a container is + restarting. e.g. docker stop -> docker start + This value can always be set to true - even if the + container is being started for the first time.`, + }, + { + Name: "Git URL", + Env: "GIT_URL", + Value: serpent.StringOf(&c.GitURL), + Description: `GitURL is the URL of the Git repository to clone. + This is optional!`, + }, + { + Name: "Git Clone Depth", + Env: "GIT_CLONE_DEPTH", + Value: serpent.Int64Of(&c.GitCloneDepth), + Description: `GitCloneDepth is the depth to use when cloning + the Git repository.`, + }, + { + Name: "Git Clone Single Branch", + Env: "GIT_CLONE_SINGLE_BRANCH", + Value: serpent.BoolOf(&c.GitCloneSingleBranch), + Description: `GitCloneSingleBranch clones only a single branch + of the Git repository.`, + }, + { + Name: "Git Username", + Env: "GIT_USERNAME", + Value: serpent.StringOf(&c.GitUsername), + Description: `GitUsername is the username to use for Git authentication. + This is optional!`, + }, + { + Name: "Git Password", + Env: "GIT_PASSWORD", + Value: serpent.StringOf(&c.GitPassword), + Description: `GitPassword is the password to use for Git authentication. + This is optional!`, + }, + { + Name: "Git HTTP Proxy URL", + Env: "GIT_HTTP_PROXY_URL", + Value: serpent.StringOf(&c.GitHTTPProxyURL), + Description: `GitHTTPProxyURL is the url for the http proxy. + This is optional!`, + }, + { + Name: "Workspace Folder", + Env: "WORKSPACE_FOLDER", + Value: serpent.StringOf(&c.WorkspaceFolder), + Description: `WorkspaceFolder is the path to the workspace folder + that will be built. This is optional!`, + }, + { + Name: "SSL Cert Base64", + Env: "SSL_CERT_BASE64", + Value: serpent.StringOf(&c.SSLCertBase64), + Description: `SSLCertBase64 is the content of an SSL cert file. + This is useful for self-signed certificates.`, + }, + { + Name: "Export Env File", + Env: "EXPORT_ENV_FILE", + Value: serpent.StringOf(&c.ExportEnvFile), + Description: `ExportEnvFile is an optional file path to a .env file where + envbuilder will dump environment variables from devcontainer.json and + the built container image.`, + }, + { + Name: "Post Start Script Path", + Env: "POST_START_SCRIPT_PATH", + Value: serpent.StringOf(&c.PostStartScriptPath), + Description: `PostStartScriptPath is the path to a script that will be created by + envbuilder based on the ` + "`postStartCommand`" + ` in devcontainer.json, if any + is specified (otherwise the script is not created). If this is set, the + specified InitCommand should check for the presence of this script and + execute it after successful startup.`, + }, + } +} diff --git a/envbuilder.go b/envbuilder.go index 5461eef2..63e735c2 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -18,6 +18,7 @@ import ( "os/exec" "os/user" "path/filepath" + "reflect" "sort" "strconv" "strings" @@ -85,12 +86,12 @@ type DockerConfig configfile.ConfigFile // Logger is the logger 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 OptionsMap, fs billy.Filesystem, logger Logger) error { +func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) error { // Default to the shell! - initArgs := []string{"-c", options.GetString("InitScript")} - if options.GetString("InitArgs") != "" { + initArgs := []string{"-c", c.InitScript} + if c.InitArgs != "" { var err error - initArgs, err = shellquote.Split(options.GetString("InitArgs")) + initArgs, err = shellquote.Split(c.InitArgs) if err != nil { return fmt.Errorf("parse init args: %w", err) } @@ -98,10 +99,10 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo if fs == nil { fs = &osfsWithChmod{osfs.New("/")} } - if options.GetString("WorkspaceFolder") == "" { + if c.WorkspaceFolder == "" { var err error - folder, err := DefaultWorkspaceFolder(options.GetString("GitURL")) - options.SetString("WorkspaceFolder", folder) + folder, err := DefaultWorkspaceFolder(c.GitURL) + c.WorkspaceFolder = folder if err != nil { return err } @@ -122,12 +123,12 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo logger(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) var caBundle []byte - if options.GetString("SSLCertBase64") != "" { + if c.SSLCertBase64 != "" { certPool, err := x509.SystemCertPool() if err != nil { return xerrors.Errorf("get global system cert pool: %w", err) } - data, err := base64.StdEncoding.DecodeString(options.GetString("SSLCertBase64")) + data, err := base64.StdEncoding.DecodeString(c.SSLCertBase64) if err != nil { return xerrors.Errorf("base64 decode ssl cert: %w", err) } @@ -138,8 +139,8 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo caBundle = data } - if options.GetString("DockerConfigBase64") != "" { - decoded, err := base64.StdEncoding.DecodeString(options.GetString("DockerConfigBase64")) + if c.DockerConfigBase64 != "" { + decoded, err := base64.StdEncoding.DecodeString(c.DockerConfigBase64) if err != nil { return fmt.Errorf("decode docker config: %w", err) } @@ -152,7 +153,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo if err != nil { return fmt.Errorf("parse docker config: %w", err) } - err = os.WriteFile(filepath.Join(MagicDir, "config.json"), decoded, 0644) + err = os.WriteFile(filepath.Join(MagicDir, "c.json"), decoded, 0644) if err != nil { return fmt.Errorf("write docker config: %w", err) } @@ -160,10 +161,10 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo var fallbackErr error var cloned bool - if options.GetString("GitURL") != "" { + if c.GitURL != "" { endStage := startStage("📦 Cloning %s to %s...", - newColor(color.FgCyan).Sprintf(options.GetString("GitURL")), - newColor(color.FgCyan).Sprintf(options.GetString("WorkspaceFolder")), + newColor(color.FgCyan).Sprintf(c.GitURL), + newColor(color.FgCyan).Sprintf(c.WorkspaceFolder), ) reader, writer := io.Pipe() @@ -187,34 +188,34 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo }() cloneOpts := CloneRepoOptions{ - Path: options.GetString("WorkspaceFolder"), + Path: c.WorkspaceFolder, Storage: fs, - Insecure: options.GetBool("Insecure"), + Insecure: c.Insecure, Progress: writer, - SingleBranch: options.GetBool("GitCloneSingleBranch"), - Depth: options.GetInt("GitCloneDepth"), + SingleBranch: c.GitCloneSingleBranch, + Depth: int(c.GitCloneDepth), CABundle: caBundle, } - if options.GetString("GitUsername") != "" || options.GetString("GitPassword") != "" { - gitURL, err := url.Parse(options.GetString("GitURL")) + if c.GitUsername != "" || c.GitPassword != "" { + gitURL, err := url.Parse(c.GitURL) if err != nil { return fmt.Errorf("parse git url: %w", err) } - gitURL.User = url.UserPassword(options.GetString("GitUsername"), options.GetString("GitPassword")) - options.SetString("GitURL", gitURL.String()) + gitURL.User = url.UserPassword(c.GitUsername, c.GitPassword) + c.GitURL = gitURL.String() cloneOpts.RepoAuth = &githttp.BasicAuth{ - Username: options.GetString("GitUsername"), - Password: options.GetString("GitPassword"), + Username: c.GitUsername, + Password: c.GitPassword, } } - if options.GetString("GitHTTPProxyURL") != "" { + if c.GitHTTPProxyURL != "" { cloneOpts.ProxyOptions = transport.ProxyOptions{ - URL: options.GetString("GitHTTPProxyURL"), + URL: c.GitHTTPProxyURL, } } - cloneOpts.RepoURL = options.GetString("GitURL") + cloneOpts.RepoURL = c.GitURL cloned, fallbackErr = CloneRepo(ctx, cloneOpts) if fallbackErr == nil { @@ -236,7 +237,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo return nil, err } defer file.Close() - if options.GetString("FallbackImage") == "" { + if c.FallbackImage == "" { if fallbackErr != nil { return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage) } @@ -244,7 +245,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo // don't support parsing a multiline error. return nil, ErrNoFallbackImage } - content := "FROM " + options.GetString("FallbackImage") + content := "FROM " + c.FallbackImage _, err = file.Write([]byte(content)) if err != nil { return nil, err @@ -260,10 +261,10 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo buildParams *devcontainer.Compiled scripts devcontainer.LifecycleScripts ) - if options.GetString("DockerFilepath") == "" { + if c.DockerfilePath == "" { // Only look for a devcontainer if a Dockerfile wasn't specified. // devcontainer is a standard, so it's reasonable to be the default. - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, logger) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(c, fs, logger) if err != nil { logger(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error()) logger(codersdk.LogLevelError, "Falling back to the default image...") @@ -290,7 +291,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo logger(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...") fallbackDockerfile = defaultParams.DockerfilePath } - buildParams, err = devContainer.Compile(fs, devcontainerDir, MagicDir, fallbackDockerfile, options.GetString("WorkspaceFolder"), false) + buildParams, err = devContainer.Compile(fs, devcontainerDir, MagicDir, fallbackDockerfile, c.WorkspaceFolder, false) if err != nil { return fmt.Errorf("compile devcontainer.json: %w", err) } @@ -302,13 +303,13 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo } } else { // If a Dockerfile was specified, we use that. - dockerfilePath := filepath.Join(options.GetString("WorkspaceFolder"), options.GetString("DockerfilePath")) + dockerfilePath := filepath.Join(c.WorkspaceFolder, c.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.GetString("WorkspaceFolder")) && options.GetString("BuildContextPath") == "" { - logger(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.GetString("WorkspaceFolder")) + if dockerfileDir != filepath.Clean(c.WorkspaceFolder) && c.BuildContextPath == "" { + logger(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, c.WorkspaceFolder) logger(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) } @@ -321,7 +322,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo buildParams = &devcontainer.Compiled{ DockerfilePath: dockerfilePath, DockerfileContent: string(content), - BuildContext: filepath.Join(options.GetString("WorkspaceFolder"), options.GetString("BuildContextPath")), + BuildContext: filepath.Join(c.WorkspaceFolder, c.BuildContextPath), } } } @@ -344,11 +345,11 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo var closeAfterBuild func() // Allows quick testing of layer caching using a local directory! - if options.GetString("LayerCacheDir") != "" { + if c.LayerCacheDir != "" { cfg := &configuration.Configuration{ Storage: configuration.Storage{ "filesystem": configuration.Parameters{ - "rootdirectory": options.GetString("LayerCacheDir"), + "rootdirectory": c.LayerCacheDir, }, }, } @@ -384,10 +385,10 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo _ = srv.Close() _ = listener.Close() } - if options.GetString("CacheRepo") != "" { + if c.CacheRepo != "" { logger(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") } - options.SetString("CacheRepo", fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port)) + c.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port) } // IgnorePaths in the Kaniko options doesn't properly ignore paths. @@ -395,11 +396,11 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo // https://github.com/GoogleContainerTools/kaniko/blob/63be4990ca5a60bdf06ddc4d10aa4eca0c0bc714/cmd/executor/cmd/root.go#L136 ignorePaths := append([]string{ MagicDir, - options.GetString("LayerCacheDir"), - options.GetString("WorkspaceFolder"), + c.LayerCacheDir, + c.WorkspaceFolder, // See: https://github.com/coder/envbuilder/issues/37 "/etc/resolv.conf", - }, options.GetStringSlice("IgnorePaths")...) + }, c.IgnorePaths...) for _, ignorePath := range ignorePaths { util.AddToDefaultIgnoreList(util.IgnoreListEntry{ @@ -411,7 +412,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo skippedRebuild := false build := func() (v1.Image, error) { _, err := fs.Stat(MagicFile) - if err == nil && options.GetBool("SkipRebuild") { + if err == nil && c.SkipRebuild { endStage := startStage("🏗️ Skipping build because of cache...") imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent) if err != nil { @@ -458,8 +459,8 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo } }() cacheTTL := time.Hour * 24 * 7 - if options.GetInt("CacheTTLDays") != 0 { - cacheTTL = time.Hour * 24 * time.Duration(options.GetInt("CacheTTLDays")) + if c.CacheTTLDays != 0 { + cacheTTL = time.Hour * 24 * time.Duration(c.CacheTTLDays) } endStage := startStage("🏗️ Building image...") @@ -487,18 +488,18 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo CacheOptions: config.CacheOptions{ // Cache for a week by default! CacheTTL: cacheTTL, - CacheDir: options.GetString("BaseImageCacheDir"), + CacheDir: c.BaseImageCacheDir, }, ForceUnpack: true, BuildArgs: buildParams.BuildArgs, - CacheRepo: options.GetString("CacheRepo"), - Cache: options.GetString("CacheRepo") != "" || options.GetString("BaseImageCacheDir") != "", + CacheRepo: c.CacheRepo, + Cache: c.CacheRepo != "" || c.BaseImageCacheDir != "", DockerfilePath: buildParams.DockerfilePath, DockerfileContent: buildParams.DockerfileContent, RegistryOptions: config.RegistryOptions{ - Insecure: options.GetBool("Insecure"), - InsecurePull: options.GetBool("Insecure"), - SkipTLSVerify: options.GetBool("Insecure"), + Insecure: c.Insecure, + InsecurePull: c.Insecure, + SkipTLSVerify: c.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 @@ -536,7 +537,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo case strings.Contains(err.Error(), "unexpected status code 401 Unauthorized"): logger(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") } - if !fallback || options.GetBool("ExitOnBuildFailure") { + if !fallback || c.ExitOnBuildFailure { return err } logger(codersdk.LogLevelError, "Failed to build: %s", err) @@ -608,11 +609,11 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo } // Sanitize the environment of any options! - unsetOptionsEnv(options) + unsetOptionsEnv() // Remove the Docker config secret file! - if options.GetString("DockerConfigBase64") != "" { - err = os.Remove(filepath.Join(MagicDir, "config.json")) + if c.DockerConfigBase64 != "" { + err = os.Remove(filepath.Join(MagicDir, "c.json")) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove docker config: %w", err) } @@ -648,7 +649,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo } sort.Strings(envKeys) for _, envVar := range envKeys { - value := devcontainer.SubstituteVars(env[envVar], options.GetString("WorkspaceFolder")) + value := devcontainer.SubstituteVars(env[envVar], c.WorkspaceFolder) os.Setenv(envVar, value) } } @@ -658,10 +659,10 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo // 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.GetString("ExportEnvFile") != "" && !skippedRebuild { - exportEnvFile, err := os.Create(options.GetString("ExportEnvFile")) + if c.ExportEnvFile != "" && !skippedRebuild { + exportEnvFile, err := os.Create(c.ExportEnvFile) if err != nil { - return fmt.Errorf("failed to open EXPORT_ENV_FILE %q: %w", options.GetString("ExportEnvFile"), err) + return fmt.Errorf("failed to open EXPORT_ENV_FILE %q: %w", c.ExportEnvFile, err) } envKeys := make([]string, 0, len(allEnvKeys)) @@ -698,7 +699,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo // // We need to change the ownership of the files to the user that will // be running the init script. - filepath.Walk(options.GetString("WorkspaceFolder"), func(path string, info os.FileInfo, err error) error { + filepath.Walk(c.WorkspaceFolder, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -707,11 +708,11 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo endStage("👤 Updated the ownership of the workspace!") } - err = os.MkdirAll(options.GetString("WorkspaceFolder"), 0755) + err = os.MkdirAll(c.WorkspaceFolder, 0755) if err != nil { return fmt.Errorf("create workspace folder: %w", err) } - err = os.Chdir(options.GetString("WorkspaceFolder")) + err = os.Chdir(c.WorkspaceFolder) if err != nil { return fmt.Errorf("change directory: %w", err) } @@ -723,7 +724,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo // 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, logger, scripts, skippedRebuild, userInfo); err != nil { + if err := execLifecycleScripts(ctx, c, logger, scripts, skippedRebuild, userInfo); err != nil { return err } @@ -732,11 +733,11 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo // // This is useful for hooking into the environment for a specific // init to PID 1. - if options.GetString("SetupScript") != "" { + if c.SetupScript != "" { // We execute the initialize script as the root user! os.Setenv("HOME", "/root") - logger(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.GetString("SetupScript")) + logger(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", c.SetupScript) envKey := "ENVBUILDER_ENV" envFile := filepath.Join("/", MagicDir, "environ") @@ -746,12 +747,12 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo } _ = file.Close() - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", options.GetString("SetupScript")) + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", c.SetupScript) cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", envKey, envFile), fmt.Sprintf("TARGET_USER=%s", userInfo.user.Username), ) - cmd.Dir = options.GetString("WorkspaceFolder") + cmd.Dir = c.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()) { @@ -793,7 +794,7 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo key := pair[0] switch key { case "INIT_COMMAND": - options.SetString("InitCommand", pair[1]) + c.InitCommand = pair[1] updatedCommand = true case "INIT_ARGS": initArgs, err = shellquote.Split(pair[1]) @@ -829,9 +830,9 @@ func Run(ctx context.Context, options OptionsMap, fs billy.Filesystem, logger Lo return fmt.Errorf("set uid: %w", err) } - logger(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.GetString("InitCommand"), initArgs, userInfo.user.Username) + logger(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", c.InitCommand, initArgs, userInfo.user.Username) - err = syscall.Exec(options.GetString("InitCommand"), append([]string{options.GetString("InitCommand")}, initArgs...), os.Environ()) + err = syscall.Exec(c.InitCommand, append([]string{c.InitCommand}, initArgs...), os.Environ()) if err != nil { return fmt.Errorf("exec init script: %w", err) } @@ -920,14 +921,14 @@ func execOneLifecycleScript( func execLifecycleScripts( ctx context.Context, - options OptionsMap, + c *Config, logger Logger, scripts devcontainer.LifecycleScripts, skippedRebuild bool, userInfo userInfo, ) error { - if options.GetString("PostStartScriptPath") != "" { - _ = os.Remove(options.GetString("PostStartScriptPath")) + if c.PostStartScriptPath != "" { + _ = os.Remove(c.PostStartScriptPath) } if !skippedRebuild { @@ -947,8 +948,8 @@ func execLifecycleScripts( if !scripts.PostStartCommand.IsEmpty() { // If PostStartCommandPath is set, the init command is responsible // for running the postStartCommand. Otherwise, we execute it now. - if options.GetString("PostStartScriptPath") != "" { - if err := createPostStartScript(options.GetString("PostStartScriptPath"), scripts.PostStartCommand); err != nil { + if c.PostStartScriptPath != "" { + if err := createPostStartScript(c.PostStartScriptPath, scripts.PostStartCommand); err != nil { return fmt.Errorf("failed to create post-start script: %w", err) } } else { @@ -977,9 +978,17 @@ func createPostStartScript(path string, postStartCommand devcontainer.LifecycleS // unsetOptionsEnv unsets all environment variables that are used // to configure the options. -func unsetOptionsEnv(options OptionsMap) { - for _, option := range options { - os.Unsetenv(option.Env) +func unsetOptionsEnv() { + val := reflect.ValueOf(&Config{}).Elem() + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + fieldTyp := typ.Field(i) + env := fieldTyp.Tag.Get("env") + if env == "" { + continue + } + os.Unsetenv(env) } } @@ -997,23 +1006,23 @@ func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } -func findDevcontainerJSON(options OptionsMap, fs billy.Filesystem, logger Logger) (string, string, error) { +func findDevcontainerJSON(c *Config, fs billy.Filesystem, logger Logger) (string, string, error) { // 0. Check if custom devcontainer directory or path is provided. - if options.GetString("DevcontainerDir") != "" || options.GetString("DevcontainerJSONPath") != "" { - devcontainerDir := options.GetString("DevcontainerDir") + if c.DevcontainerDir != "" || c.DevcontainerJSONPath != "" { + devcontainerDir := c.DevcontainerDir if devcontainerDir == "" { devcontainerDir = ".devcontainer" } // If `devcontainerDir` is not an absolute path, assume it is relative to the workspace folder. if !filepath.IsAbs(devcontainerDir) { - devcontainerDir = filepath.Join(options.GetString("WorkspaceFolder"), devcontainerDir) + devcontainerDir = filepath.Join(c.WorkspaceFolder, devcontainerDir) } // An absolute location always takes a precedence. - devcontainerPath := options.GetString("DevcontainerJSONPath") + devcontainerPath := c.DevcontainerJSONPath if filepath.IsAbs(devcontainerPath) { - return options.GetString("DevcontainerJSONPath"), devcontainerDir, nil + return c.DevcontainerJSONPath, devcontainerDir, nil } // If an override is not provided, assume it is just `devcontainer.json`. if devcontainerPath == "" { @@ -1027,19 +1036,19 @@ func findDevcontainerJSON(options OptionsMap, fs billy.Filesystem, logger Logger } // 1. Check `options.WorkspaceFolder`/.devcontainer/devcontainer.json. - location := filepath.Join(options.GetString("WorkspaceFolder"), ".devcontainer", "devcontainer.json") + location := filepath.Join(c.WorkspaceFolder, ".devcontainer", "devcontainer.json") if _, err := fs.Stat(location); err == nil { return location, filepath.Dir(location), nil } // 2. Check `options.WorkspaceFolder`/devcontainer.json. - location = filepath.Join(options.GetString("WorkspaceFolder"), "devcontainer.json") + location = filepath.Join(c.WorkspaceFolder, "devcontainer.json") if _, err := fs.Stat(location); err == nil { return location, filepath.Dir(location), nil } // 3. Check every folder: `options.WorkspaceFolder`/.devcontainer//devcontainer.json. - devcontainerDir := filepath.Join(options.GetString("WorkspaceFolder"), ".devcontainer") + devcontainerDir := filepath.Join(c.WorkspaceFolder, ".devcontainer") fileInfos, err := fs.ReadDir(devcontainerDir) if err != nil { diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index 1a103a45..84cf4896 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -18,9 +18,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs := memfs.New() // when - options := DefaultOptions() - options.SetString("WorkspaceFolder", "/workspace") - _, _, err := findDevcontainerJSON(options, fs, nil) + _, _, err := findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) // then require.Error(t, err) @@ -35,9 +33,7 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - options := DefaultOptions() - options.SetString("WorkspaceFolder", "/workspace") - _, _, err = findDevcontainerJSON(options, fs, nil) + _, _, err = findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) // then require.Error(t, err) @@ -53,9 +49,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/devcontainer.json") // when - options := DefaultOptions() - options.SetString("WorkspaceFolder", "/workspace") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) // then require.NoError(t, err) @@ -73,10 +67,11 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/experimental-devcontainer/devcontainer.json") // when - options := DefaultOptions() - options.SetString("WorkspaceFolder", "/workspace") - options.SetString("DevcontainerDir", "experimental-devcontainer") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) + c := &Config{ + WorkspaceFolder: "/workspace", + DevcontainerDir: "experimental-devcontainer", + } + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(c, fs, nil) // then require.NoError(t, err) @@ -94,10 +89,11 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/experimental.json") // when - options := DefaultOptions() - options.SetString("WorkspaceFolder", "/workspace") - options.SetString("DevcontainerJSONPath", "experimental.json") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) + c := &Config{ + WorkspaceFolder: "/workspace", + DevcontainerJSONPath: "experimental.json", + } + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(c, fs, nil) // then require.NoError(t, err) @@ -115,9 +111,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/devcontainer.json") // when - options := DefaultOptions() - options.SetString("WorkspaceFolder", "/workspace") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) // then require.NoError(t, err) @@ -135,9 +129,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/sample/devcontainer.json") // when - options := DefaultOptions() - options.SetString("WorkspaceFolder", "/workspace") - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) // then require.NoError(t, err) diff --git a/go.mod b/go.mod index 3f3e3ab4..8fa755aa 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/coder/envbuilder -go 1.21.1 +go 1.21.4 -toolchain go1.21.5 +toolchain go1.21.9 // There are a few options we need added to Kaniko! // See: https://github.com/GoogleContainerTools/kaniko/compare/main...coder:kaniko:main @@ -20,6 +20,7 @@ require ( github.com/GoogleContainerTools/kaniko v1.9.2 github.com/breml/rootcerts v0.2.10 github.com/coder/coder/v2 v2.3.3 + github.com/coder/serpent v0.7.0 github.com/containerd/containerd v1.7.11 github.com/distribution/distribution/v3 v3.0.0-20230629214736-bac7f02e02a1 github.com/docker/cli v23.0.5+incompatible @@ -33,11 +34,10 @@ require ( github.com/moby/buildkit v0.11.6 github.com/otiai10/copy v1.14.0 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a golang.org/x/sync v0.6.0 - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 ) require ( @@ -88,12 +88,14 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.21.1 // indirect github.com/aws/smithy-go v1.19.0 // indirect github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230522190001-adf1bafd791a // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect github.com/cilium/ebpf v0.12.3 // indirect github.com/cloudflare/circl v1.3.7 // indirect + github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 // indirect github.com/coder/retry v1.5.1 // indirect github.com/coder/terraform-provider-coder v0.13.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect @@ -161,7 +163,6 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -171,7 +172,9 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect @@ -195,6 +198,7 @@ require ( github.com/moby/sys/symlink v0.2.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/open-policy-agent/opa v0.58.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect @@ -204,6 +208,8 @@ require ( github.com/outcaste-io/ristretto v0.2.3 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/pion/transport/v2 v2.0.0 // indirect + github.com/pion/udp v0.1.4 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -212,6 +218,7 @@ require ( github.com/prometheus/common v0.46.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rootless-containers/rootlesskit v1.1.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect @@ -255,15 +262,15 @@ require ( go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect golang.org/x/crypto v0.19.0 // indirect - golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.18.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 10d41283..06659e2a 100644 --- a/go.sum +++ b/go.sum @@ -198,8 +198,12 @@ github.com/coder/gvisor v0.0.0-20230714132058-be2e4ac102c3 h1:gtuDFa+InmMVUYiurB github.com/coder/gvisor v0.0.0-20230714132058-be2e4ac102c3/go.mod h1:pzr6sy8gDLfVmDAg8OYrlKvGEHw5C3PGTiBXBTCx76Q= github.com/coder/kaniko v0.0.0-20240103181425-f83d15201044 h1:28V9fkQdceB0FzjyavTU6r+II5NwRpJqNdzUSfe6RPU= github.com/coder/kaniko v0.0.0-20240103181425-f83d15201044/go.mod h1:byIUWxhLPDuO0o38iG+ffFWmIhUCSc8/N1INJZhjcUY= +github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= +github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= github.com/coder/retry v1.5.1/go.mod h1:blHMk9vs6LkoRT9ZHyuZo360cufXEhrxqvEzeMtRGoY= +github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0= +github.com/coder/serpent v0.7.0/go.mod h1:REkJ5ZFHQUWFTPLExhXYZ1CaHFjxvGNRlLXLdsI08YA= github.com/coder/tailscale v1.1.1-0.20240214140224-3788ab894ba1 h1:A7dZHNidAVH6Kxn5D3hTEH+iRO8slnM0aRer6/cxlyE= github.com/coder/tailscale v1.1.1-0.20240214140224-3788ab894ba1/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4= github.com/coder/terraform-provider-coder v0.13.0 h1:MjW7O+THAiqIYcxyiuBoGbFEduqgjp7tUZhSkiwGxwo= @@ -509,8 +513,6 @@ github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwso github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -690,6 +692,11 @@ github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2 github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4= +github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= +github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= +github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -723,6 +730,7 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -747,8 +755,6 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= @@ -930,16 +936,16 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -952,13 +958,14 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1009,6 +1016,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1019,6 +1027,7 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -1052,14 +1061,14 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= diff --git a/options.go b/options.go deleted file mode 100644 index 1ff10dfa..00000000 --- a/options.go +++ /dev/null @@ -1,319 +0,0 @@ -package envbuilder - -import ( - "fmt" - "strconv" - "strings" -) - -type Option struct { - Env string - Value any - Detail string -} - -type OptionsMap map[string]Option - -func DefaultOptions() OptionsMap { - return OptionsMap{ - "SetupScript": Option{ - Env: "SETUP_SCRIPT", - Value: "", - Detail: `SetupScript is the script to run before the init script. - It runs as the root user regardless of the user specified - in the devcontainer.json file. - - SetupScript is ran as the root user prior to the init script. - It is used to configure envbuilder dynamically during the runtime. - e.g. specifying whether to start ` + "`systemd`" + ` or ` + "`tiny init`" + ` for PID 1.`, - }, - "InitScript": Option{ - Env: "INIT_SCRIPT", - Value: "sleep infinity", - Detail: "InitScript is the script to run to initialize the workspace.", - }, - "InitCommand": Option{ - Env: "INIT_COMMAND", - Value: "/bin/sh", - Detail: "InitCommand is the command to run to initialize the workspace.", - }, - "InitArgs": Option{ - Env: "INIT_ARGS", - Value: "", - Detail: `InitArgs are the arguments to pass to the init command. - They are split according to ` + "`/bin/sh`" + ` rules with - https://github.com/kballard/go-shellquote`, - }, - "CacheRepo": Option{ - Env: "CACHE_REPO", - Value: "", - Detail: `CacheRepo is the name of the container registry - to push the cache image to. If this is empty, the cache - will not be pushed.`, - }, - "BaseImageCacheDir": Option{ - Env: "BASE_IMAGE_CACHE_DIR", - Value: "", - Detail: `BaseImageCacheDir is the path to a directory where the base - image can be found. This should be a read-only directory - solely mounted for the purpose of caching the base image.`, - }, - "LayerCacheDir": Option{ - Env: "LAYER_CACHE_DIR", - Value: "", - Detail: `LayerCacheDir is the path to a directory where built layers - will be stored. This spawns an in-memory registry to serve - the layers from.`, - }, - "DevcontainerDir": Option{ - Env: "DEVCONTAINER_DIR", - Value: "", - Detail: `DevcontainerDir is a path to the folder containing - the devcontainer.json file that will be used to build the - workspace and can either be an absolute path or a path - relative to the workspace folder. If not provided, defaults to - ` + "`.devcontainer`" + `.`, - }, - "DevcontainerJSONPath": Option{ - Env: "DEVCONTAINER_JSON_PATH", - Value: "", - Detail: `DevcontainerJSONPath is a path to a devcontainer.json file - that is either an absolute path or a path relative to - DevcontainerDir. This can be used in cases where one wants - to substitute an edited devcontainer.json file for the one - that exists in the repo.`, - }, - "DockerfilePath": Option{ - Env: "DOCKERFILE_PATH", - Value: "", - Detail: `DockerfilePath is a relative path to the Dockerfile that - will be used to build the workspace. This is an alternative - to using a devcontainer that some might find simpler.`, - }, - "BuildContextPath": Option{ - Env: `BUILD_CONTEXT_PATH`, - Value: "", - Detail: `BuildContextPath can be specified when a DockerfilePath is specified outside the base WorkspaceFolder. - This path MUST be relative to the WorkspaceFolder path into which the repo is cloned.`, - }, - "CacheTTLDays": Option{ - Env: "CACHE_TTL_DAYS", - Value: 0, - Detail: `CacheTTLDays is the number of days to use cached layers before - expiring them. Defaults to 7 days.`, - }, - "DockerConfigBase64": Option{ - Env: "DOCKER_CONFIG_BASE64", - Value: "", - Detail: `DockerConfigBase64 is a base64 encoded Docker config - file that will be used to pull images from private - container registries.`, - }, - "FallbackImage": Option{ - Env: "FALLBACK_IMAGE", - Value: "", - Detail: `FallbackImage specifies an alternative image to use when neither - an image is declared in the devcontainer.json file nor a Dockerfile is present. - If there's a build failure (from a faulty Dockerfile) or a misconfiguration, - this image will be the substitute. - Set ` + "`ExitOnBuildFailure`" + ` to true to halt the container if the build faces an issue.`, - }, - "ExitOnBuildFailure": Option{ - Env: "EXIT_ON_BUILD_FAILURE", - Value: false, - Detail: `ExitOnBuildFailure terminates the container upon a build failure. - This is handy when preferring the ` + "`FALLBACK_IMAGE`" + ` in cases where - no devcontainer.json or image is provided. However, it ensures - that the container stops if the build process encounters an error.`, - }, - "ForceSafe": Option{ - Env: "FORCE_SAFE", - Value: false, - Detail: `ForceSafe ignores any filesystem safety checks. - This could cause serious harm to your system! - This is used in cases where bypass is needed - to unblock customers!`, - }, - "Insecure": Option{ - Env: "INSECURE", - Value: false, - Detail: `Insecure bypasses TLS verification when cloning - and pulling from container registries.`, - }, - "IgnorePaths": Option{ - Env: "IGNORE_PATHS", - // Kubernetes frequently stores secrets in /var/run/secrets, and - // other applications might as well. This seems to be a sensible - // default, but if that changes, it's simple to adjust. - Value: []string{"/var/run"}, - Detail: `IgnorePaths is a comma separated list of paths - to ignore when building the workspace.`, - }, - "SkipRebuild": Option{ - Env: "SKIP_REBUILD", - Value: false, - Detail: `SkipRebuild skips building if the MagicFile exists. - This is used to skip building when a container is - restarting. e.g. docker stop -> docker start - This value can always be set to true - even if the - container is being started for the first time.`, - }, - "GitURL": Option{ - Env: "GIT_URL", - Value: "", - Detail: `GitURL is the URL of the Git repository to clone. - This is optional!`, - }, - "GitCloneDepth": Option{ - Env: "GIT_CLONE_DEPTH", - Value: 0, - Detail: `GitCloneDepth is the depth to use when cloning - the Git repository.`, - }, - "GitCloneSingleBranch": Option{ - Env: "GIT_CLONE_SINGLE_BRANCH", - Value: false, - Detail: `GitCloneSingleBranch clones only a single branch - of the Git repository.`, - }, - "GitUsername": Option{ - Env: "GIT_USERNAME", - Value: "", - Detail: `GitUsername is the username to use for Git authentication. - This is optional!`, - }, - "GitPassword": Option{ - Env: "GIT_PASSWORD", - Value: "", - Detail: `GitPassword is the password to use for Git authentication. - This is optional!`, - }, - "GitHTTPProxyURL": Option{ - Env: "GIT_HTTP_PROXY_URL", - Value: "", - Detail: `GitHTTPProxyURL is the url for the http proxy. - This is optional!`, - }, - "WorkspaceFolder": Option{ - Env: "WORKSPACE_FOLDER", - Value: "", - Detail: `WorkspaceFolder is the path to the workspace folder - that will be built. This is optional!`, - }, - "SSLCertBase64": Option{ - Env: "SSL_CERT_BASE64", - Value: "", - Detail: `SSLCertBase64 is the content of an SSL cert file. - This is useful for self-signed certificates.`, - }, - "ExportEnvFile": Option{ - Env: "EXPORT_ENV_FILE", - Value: "", - Detail: `ExportEnvFile is an optional file path to a .env file where - envbuilder will dump environment variables from devcontainer.json and - the built container image.`, - }, - "PostStartScriptPath": Option{ - Env: "POST_START_SCRIPT_PATH", - Value: "", - Detail: `PostStartScriptPath is the path to a script that will be created by - envbuilder based on the ` + "`postStartCommand`" + ` in devcontainer.json, if any - is specified (otherwise the script is not created). If this is set, the - specified InitCommand should check for the presence of this script and - execute it after successful startup.`, - }, - } -} - -func OptionsFromEnv(getEnv func(string) (string, bool)) OptionsMap { - options := DefaultOptions() - - for key, option := range options { - value, ok := getEnv(option.Env) - if !ok || value == "" { - continue - } - - switch v := options[key].Value.(type) { - case string: - options.SetString(key, value) - case int: - intValue, _ := strconv.Atoi(value) - options.SetInt(key, intValue) - case bool: - boolValue, _ := strconv.ParseBool(value) - options.SetBool(key, boolValue) - case []string: - options.SetStringSlice(key, strings.Split(value, ",")) - default: - panic(fmt.Sprintf("unsupported type %T", v)) - } - } - - return options -} - -func (o OptionsMap) get(key string) any { - val, ok := o[key] - if !ok { - panic(fmt.Sprintf("key %q not found in options %v", key, o)) - } - return val.Value -} - -func (o OptionsMap) GetString(key string) string { - val := o.get(key) - if val == nil { - return "" - } - return val.(string) -} - -func (o OptionsMap) GetBool(key string) bool { - val := o.get(key) - if val == nil { - return false - } - return val.(bool) -} - -func (o OptionsMap) GetInt(key string) int { - val := o.get(key) - if val == nil { - return 0 - } - return val.(int) -} - -func (o OptionsMap) GetStringSlice(key string) []string { - val := o.get(key) - if val == nil { - return nil - } - return val.([]string) -} - -func (o OptionsMap) set(key string, value any) { - val, ok := o[key] - if !ok { - panic(fmt.Sprintf("key %q not found in options %v", key, o)) - } - val.Value = value - o[key] = val -} - -func (o OptionsMap) SetString(key string, value string) { - o.set(key, value) -} - -func (o OptionsMap) SetStringSlice(key string, value []string) { - o.set(key, value) -} - -func (o OptionsMap) SetBool(key string, value bool) { - o.set(key, value) -} - -func (o OptionsMap) SetInt(key string, value int) { - o.set(key, value) -} diff --git a/options_test.go b/options_test.go deleted file mode 100644 index da8328ca..00000000 --- a/options_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package envbuilder_test - -import ( - "testing" - - "github.com/coder/envbuilder" - "github.com/stretchr/testify/require" -) - -func TestOptionsFromEnv(t *testing.T) { - t.Parallel() - env := map[string]string{ - "SETUP_SCRIPT": "echo setup script", - "INIT_SCRIPT": "echo init script", - "INIT_COMMAND": "/bin/bash", - "INIT_ARGS": "-c 'echo init args'", - "CACHE_REPO": "kylecarbs/testing", - "BASE_IMAGE_CACHE_DIR": "/tmp/cache", - "LAYER_CACHE_DIR": "/tmp/cache", - "DEVCONTAINER_DIR": "/tmp/devcontainer", - "DEVCONTAINER_JSON_PATH": "/tmp/devcontainer.json", - "DOCKERFILE_PATH": "Dockerfile", - "BUILD_CONTEXT_PATH": "/tmp/buildcontext", - "CACHE_TTL_DAYS": "30", - "DOCKER_CONFIG_BASE64": "dGVzdA==", - "FALLBACK_IMAGE": "ubuntu:latest", - "EXIT_ON_BUILD_FAILURE": "true", - "FORCE_SAFE": "true", - "INSECURE": "false", - "IGNORE_PATHS": "/tmp,/var", - "SKIP_REBUILD": "true", - "GIT_URL": "https://github.com/coder/coder", - "GIT_CLONE_DEPTH": "1", - "GIT_CLONE_SINGLE_BRANCH": "true", - "GIT_USERNAME": "kylecarbs", - "GIT_PASSWORD": "password", - "GIT_HTTP_PROXY_URL": "http://company-proxy.com:8081", - "WORKSPACE_FOLDER": "/workspaces/coder", - "SSL_CERT_BASE64": "dGVzdA==", - "EXPORT_ENV_FILE": "/tmp/env", - "POST_START_SCRIPT_PATH": "/tmp/poststart.sh", - } - options := envbuilder.OptionsFromEnv(func(s string) (string, bool) { - return env[s], true - }) - - require.Equal(t, env["SETUP_SCRIPT"], options.GetString("SetupScript")) - require.Equal(t, env["INIT_SCRIPT"], options.GetString("InitScript")) - require.Equal(t, env["INIT_COMMAND"], options.GetString("InitCommand")) - require.Equal(t, env["INIT_ARGS"], options.GetString("InitArgs")) - require.Equal(t, env["CACHE_REPO"], options.GetString("CacheRepo")) - require.Equal(t, env["BASE_IMAGE_CACHE_DIR"], options.GetString("BaseImageCacheDir")) - require.Equal(t, env["LAYER_CACHE_DIR"], options.GetString("LayerCacheDir")) - require.Equal(t, env["DEVCONTAINER_DIR"], options.GetString("DevcontainerDir")) - require.Equal(t, env["DEVCONTAINER_JSON_PATH"], options.GetString("DevcontainerJSONPath")) - require.Equal(t, env["DOCKERFILE_PATH"], options.GetString("DockerfilePath")) - require.Equal(t, env["BUILD_CONTEXT_PATH"], options.GetString("BuildContextPath")) - require.Equal(t, 30, options.GetInt("CacheTTLDays")) - require.Equal(t, env["DOCKER_CONFIG_BASE64"], options.GetString("DockerConfigBase64")) - require.Equal(t, env["FALLBACK_IMAGE"], options.GetString("FallbackImage")) - require.True(t, options.GetBool("ExitOnBuildFailure")) - require.True(t, options.GetBool("ForceSafe")) - require.False(t, options.GetBool("Insecure")) - require.Equal(t, options.GetStringSlice("IgnorePaths"), []string{"/tmp", "/var"}) - require.True(t, options.GetBool("SkipRebuild")) - require.Equal(t, env["GIT_URL"], options.GetString("GitURL")) - require.Equal(t, 1, options.GetInt("GitCloneDepth")) - require.True(t, options.GetBool("GitCloneSingleBranch")) - require.Equal(t, env["GIT_USERNAME"], options.GetString("GitUsername")) - require.Equal(t, env["GIT_PASSWORD"], options.GetString("GitPassword")) - require.Equal(t, env["GIT_HTTP_PROXY_URL"], options.GetString("GitHTTPProxyURL")) - require.Equal(t, env["WORKSPACE_FOLDER"], options.GetString("WorkspaceFolder")) - require.Equal(t, env["SSL_CERT_BASE64"], options.GetString("SSLCertBase64")) - require.Equal(t, env["EXPORT_ENV_FILE"], options.GetString("ExportEnvFile")) - require.Equal(t, env["POST_START_SCRIPT_PATH"], options.GetString("PostStartScriptPath")) -} From 247514f65ffea18a7387f5039e13c2a81a23313e Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 24 Apr 2024 20:04:45 +0000 Subject: [PATCH 06/20] Fix 'Init Args description should end with a period' --- config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.go b/config.go index 76b2a0dd..dc7ba16d 100644 --- a/config.go +++ b/config.go @@ -71,7 +71,7 @@ func (c *Config) Options() serpent.OptionSet { Value: serpent.StringOf(&c.InitArgs), Description: `InitArgs are the arguments to pass to the init command. They are split according to ` + "`/bin/sh`" + ` rules with - https://github.com/kballard/go-shellquote`, + https://github.com/kballard/go-shellquote.`, }, { Name: "Cache Repo", From c8cbdf56cfedd981cc77170e37d3f6252ca0f40d Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 24 Apr 2024 20:10:53 +0000 Subject: [PATCH 07/20] Fix others 'should end with a period' errors --- config.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config.go b/config.go index dc7ba16d..998bd9ea 100644 --- a/config.go +++ b/config.go @@ -172,7 +172,7 @@ func (c *Config) Options() serpent.OptionSet { Description: `ForceSafe ignores any filesystem safety checks. This could cause serious harm to your system! This is used in cases where bypass is needed - to unblock customers!`, + to unblock customers.`, }, { Name: "Insecure", @@ -207,7 +207,7 @@ func (c *Config) Options() serpent.OptionSet { Env: "GIT_URL", Value: serpent.StringOf(&c.GitURL), Description: `GitURL is the URL of the Git repository to clone. - This is optional!`, + This is optional.`, }, { Name: "Git Clone Depth", @@ -228,28 +228,28 @@ func (c *Config) Options() serpent.OptionSet { Env: "GIT_USERNAME", Value: serpent.StringOf(&c.GitUsername), Description: `GitUsername is the username to use for Git authentication. - This is optional!`, + This is optional.`, }, { Name: "Git Password", Env: "GIT_PASSWORD", Value: serpent.StringOf(&c.GitPassword), Description: `GitPassword is the password to use for Git authentication. - This is optional!`, + This is optional.`, }, { Name: "Git HTTP Proxy URL", Env: "GIT_HTTP_PROXY_URL", Value: serpent.StringOf(&c.GitHTTPProxyURL), Description: `GitHTTPProxyURL is the url for the http proxy. - This is optional!`, + This is optional.`, }, { Name: "Workspace Folder", Env: "WORKSPACE_FOLDER", Value: serpent.StringOf(&c.WorkspaceFolder), Description: `WorkspaceFolder is the path to the workspace folder - that will be built. This is optional!`, + that will be built. This is optional.`, }, { Name: "SSL Cert Base64", From e94b8c3636b49ed1bedb9ba432e96389f194dfbe Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 25 Apr 2024 16:18:29 +0000 Subject: [PATCH 08/20] Fix config.json --- envbuilder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envbuilder.go b/envbuilder.go index 5feaffcf..23e4f597 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -153,7 +153,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err if err != nil { return fmt.Errorf("parse docker config: %w", err) } - err = os.WriteFile(filepath.Join(MagicDir, "c.json"), decoded, 0644) + err = os.WriteFile(filepath.Join(MagicDir, "config.json"), decoded, 0644) if err != nil { return fmt.Errorf("write docker config: %w", err) } From f03fa6e413fd15b65e1616b64b7e9ae7c58e4a05 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 25 Apr 2024 18:45:01 +0000 Subject: [PATCH 09/20] Adjust description and flags --- config.go | 249 +++++++++++++++++++++++++----------------------------- 1 file changed, 117 insertions(+), 132 deletions(-) diff --git a/config.go b/config.go index 998bd9ea..2ab27d5b 100644 --- a/config.go +++ b/config.go @@ -39,242 +39,227 @@ type Config struct { func (c *Config) Options() serpent.OptionSet { return serpent.OptionSet{ { - Name: "Setup Script", - Env: "SETUP_SCRIPT", Flag: "setup-script", + Env: "SETUP_SCRIPT", Value: serpent.StringOf(&c.SetupScript), - Description: `SetupScript is the script to run before the init script. - It runs as the root user regardless of the user specified - in the devcontainer.json file. - - SetupScript is ran as the root user prior to the init script. - It is used to configure envbuilder dynamically during the runtime. - e.g. specifying whether to start ` + "`systemd`" + ` or ` + "`tiny init`" + ` for PID 1.`, + Description: "SetupScript is the script to run before the init script. It runs as " + + "the root user regardless of the user specified in the devcontainer.json " + + "file.\n\nSetupScript is ran as the root user prior to the init script. " + + "It is used to configure envbuilder dynamically during the runtime. e.g. " + + "specifying whether to start systemd or tiny init for PID 1.", }, { - Name: "Init Script", + Flag: "init-script", Env: "INIT_SCRIPT", Default: "sleep infinity", Value: serpent.StringOf(&c.InitScript), Description: "InitScript is the script to run to initialize the workspace.", }, { - Name: "Init Command", + Flag: "init-command", Env: "INIT_COMMAND", Default: "/bin/sh", Value: serpent.StringOf(&c.InitCommand), Description: "InitCommand is the command to run to initialize the workspace.", }, { - Name: "Init Args", + Flag: "init-args", Env: "INIT_ARGS", Value: serpent.StringOf(&c.InitArgs), - Description: `InitArgs are the arguments to pass to the init command. - They are split according to ` + "`/bin/sh`" + ` rules with - https://github.com/kballard/go-shellquote.`, + Description: "InitArgs are the arguments to pass to the init command. They are " + + "split according to /bin/sh rules with " + + "https://github.com/kballard/go-shellquote.", }, { - Name: "Cache Repo", + Flag: "cache-repo", Env: "CACHE_REPO", Value: serpent.StringOf(&c.CacheRepo), - Description: `CacheRepo is the name of the container registry - to push the cache image to. If this is empty, the cache - will not be pushed.`, + Description: "CacheRepo is the name of the container registry to push the cache " + + "image to. If this is empty, the cache will not be pushed.", }, { - Name: "Base Image Cache Dir", + Flag: "base-image-cache-dir", Env: "BASE_IMAGE_CACHE_DIR", Value: serpent.StringOf(&c.BaseImageCacheDir), - Description: `BaseImageCacheDir is the path to a directory where the base - image can be found. This should be a read-only directory - solely mounted for the purpose of caching the base image.`, + Description: "BaseImageCacheDir is the path to a directory where the base image " + + "can be found. This should be a read-only directory solely mounted " + + "for the purpose of caching the base image.", }, { - Name: "Layer Cache Dir", + Flag: "layer-cache-dir", Env: "LAYER_CACHE_DIR", Value: serpent.StringOf(&c.LayerCacheDir), - Description: `LayerCacheDir is the path to a directory where built layers - will be stored. This spawns an in-memory registry to serve - the layers from.`, + Description: "LayerCacheDir is the path to a directory where built layers will " + + "be stored. This spawns an in-memory registry to serve the layers " + + "from.", }, { - Name: "Devcontainer Dir", + Flag: "devcontainer-dir", Env: "DEVCONTAINER_DIR", Value: serpent.StringOf(&c.DevcontainerDir), - Description: `DevcontainerDir is a path to the folder containing - the devcontainer.json file that will be used to build the - workspace and can either be an absolute path or a path - relative to the workspace folder. If not provided, defaults to - ` + "`.devcontainer`" + `.`, + Description: "DevcontainerDir is a path to the folder containing the " + + "devcontainer.json file that will be used to build the workspace " + + "and can either be an absolute path or a path relative to the " + + "workspace folder. If not provided, defaults to `.devcontainer`.", }, { - Name: "Devcontainer JSON Path", + Flag: "devcontainer-json-path", Env: "DEVCONTAINER_JSON_PATH", Value: serpent.StringOf(&c.DevcontainerJSONPath), - Description: `DevcontainerJSONPath is a path to a devcontainer.json file - that is either an absolute path or a path relative to - DevcontainerDir. This can be used in cases where one wants - to substitute an edited devcontainer.json file for the one - that exists in the repo.`, + Description: "DevcontainerJSONPath is a path to a devcontainer.json file that " + + "is either an absolute path or a path relative to DevcontainerDir. " + + "This can be used in cases where one wants to substitute an edited " + + "devcontainer.json file for the one that exists in the repo.", }, { - Name: "Dockerfile Path", + Flag: "dockerfile-path", Env: "DOCKERFILE_PATH", Value: serpent.StringOf(&c.DockerfilePath), - Description: `DockerfilePath is a relative path to the Dockerfile that - will be used to build the workspace. This is an alternative - to using a devcontainer that some might find simpler.`, + Description: "DockerfilePath is a relative path to the Dockerfile that will " + + "be used to build the workspace. This is an alternative to using " + + "a devcontainer that some might find simpler.", }, { - Name: "Build Context Path", - Env: `BUILD_CONTEXT_PATH`, + Flag: "build-context-path", + Env: "BUILD_CONTEXT_PATH", Value: serpent.StringOf(&c.BuildContextPath), - Description: `BuildContextPath can be specified when a DockerfilePath is specified outside the base WorkspaceFolder. - This path MUST be relative to the WorkspaceFolder path into which the repo is cloned.`, + Description: "BuildContextPath can be specified when a DockerfilePath is " + + "specified outside the base WorkspaceFolder. This path MUST be " + + "relative to the WorkspaceFolder path into which the repo is cloned.", }, { - Name: "Cache TTL Days", + Flag: "cache-ttl-days", Env: "CACHE_TTL_DAYS", Value: serpent.Int64Of(&c.CacheTTLDays), - Description: `CacheTTLDays is the number of days to use cached layers before - expiring them. Defaults to 7 days.`, + Description: "CacheTTLDays is the number of days to use cached layers before " + + "expiring them. Defaults to 7 days.", }, { - Name: "Docker Config Base64", + Flag: "docker-config-base64", Env: "DOCKER_CONFIG_BASE64", Value: serpent.StringOf(&c.DockerConfigBase64), - Description: `DockerConfigBase64 is a base64 encoded Docker config - file that will be used to pull images from private - container registries.`, + Description: "DockerConfigBase64 is a base64 encoded Docker config file that " + + "will be used to pull images from private container registries.", }, { - Name: "Fallback Image", + Flag: "fallback-image", Env: "FALLBACK_IMAGE", Value: serpent.StringOf(&c.FallbackImage), - Description: `FallbackImage specifies an alternative image to use when neither - an image is declared in the devcontainer.json file nor a Dockerfile is present. - If there's a build failure (from a faulty Dockerfile) or a misconfiguration, - this image will be the substitute. - Set ` + "`ExitOnBuildFailure`" + ` to true to halt the container if the build faces an issue.`, + Description: "FallbackImage specifies an alternative image to use when neither " + + "an image is declared in the devcontainer.json file nor a Dockerfile " + + "is present. If there's a build failure (from a faulty Dockerfile) " + + "or a misconfiguration, this image will be the substitute. Set " + + "ExitOnBuildFailure to true to halt the container if the build " + + "faces an issue.", }, { + Flag: "exit-on-build-failure", Env: "EXIT_ON_BUILD_FAILURE", Value: serpent.BoolOf(&c.ExitOnBuildFailure), - Description: `ExitOnBuildFailure terminates the container upon a build failure. - This is handy when preferring the ` + "`FALLBACK_IMAGE`" + ` in cases where - no devcontainer.json or image is provided. However, it ensures - that the container stops if the build process encounters an error.`, + Description: "ExitOnBuildFailure terminates the container upon a build failure. " + + "This is handy when preferring the FALLBACK_IMAGE in cases where " + + "no devcontainer.json or image is provided. However, it ensures " + + "that the container stops if the build process encounters an error.", }, { - Name: "Force Safe", + Flag: "force-safe", Env: "FORCE_SAFE", Value: serpent.BoolOf(&c.ForceSafe), - Description: `ForceSafe ignores any filesystem safety checks. - This could cause serious harm to your system! - This is used in cases where bypass is needed - to unblock customers.`, + Description: "ForceSafe ignores any filesystem safety checks. This could cause " + + "serious harm to your system! This is used in cases where bypass " + + "is needed to unblock customers.", }, { - Name: "Insecure", + Flag: "insecure", Env: "INSECURE", Value: serpent.BoolOf(&c.Insecure), - Description: `Insecure bypasses TLS verification when cloning - and pulling from container registries.`, + Description: "Insecure bypasses TLS verification when cloning and pulling from " + + "container registries.", }, { - Name: "Ignore Paths", - Env: "IGNORE_PATHS", - Value: serpent.StringArrayOf(&c.IgnorePaths), - // Kubernetes frequently stores secrets in /var/run/secrets, and - // other applications might as well. This seems to be a sensible - // default, but if that changes, it's simple to adjust. + Flag: "ignore-paths", + Env: "IGNORE_PATHS", + Value: serpent.StringArrayOf(&c.IgnorePaths), Default: "/var/run", - Description: `IgnorePaths is a comma separated list of paths - to ignore when building the workspace.`, + Description: "IgnorePaths is a comma separated list of paths to ignore when " + + "building the workspace.", }, { - Name: "Skip Rebuild", + Flag: "skip-rebuild", Env: "SKIP_REBUILD", Value: serpent.BoolOf(&c.SkipRebuild), - Description: `SkipRebuild skips building if the MagicFile exists. - This is used to skip building when a container is - restarting. e.g. docker stop -> docker start - This value can always be set to true - even if the - container is being started for the first time.`, + Description: "SkipRebuild skips building if the MagicFile exists. This is used " + + "to skip building when a container is restarting. e.g. docker stop -> " + + "docker start This value can always be set to true - even if the " + + "container is being started for the first time.", }, { - Name: "Git URL", - Env: "GIT_URL", - Value: serpent.StringOf(&c.GitURL), - Description: `GitURL is the URL of the Git repository to clone. - This is optional.`, + Flag: "git-url", + Env: "GIT_URL", + Value: serpent.StringOf(&c.GitURL), + Description: "GitURL is the URL of the Git repository to clone. This is optional.", }, { - Name: "Git Clone Depth", - Env: "GIT_CLONE_DEPTH", - Value: serpent.Int64Of(&c.GitCloneDepth), - Description: `GitCloneDepth is the depth to use when cloning - the Git repository.`, + Flag: "git-clone-depth", + Env: "GIT_CLONE_DEPTH", + Value: serpent.Int64Of(&c.GitCloneDepth), + Description: "GitCloneDepth is the depth to use when cloning the Git repository.", }, { - Name: "Git Clone Single Branch", - Env: "GIT_CLONE_SINGLE_BRANCH", - Value: serpent.BoolOf(&c.GitCloneSingleBranch), - Description: `GitCloneSingleBranch clones only a single branch - of the Git repository.`, + Flag: "git-clone-single-branch", + Env: "GIT_CLONE_SINGLE_BRANCH", + Value: serpent.BoolOf(&c.GitCloneSingleBranch), + Description: "GitCloneSingleBranch clones only a single branch of the Git repository.", }, { - Name: "Git Username", - Env: "GIT_USERNAME", - Value: serpent.StringOf(&c.GitUsername), - Description: `GitUsername is the username to use for Git authentication. - This is optional.`, + Flag: "git-username", + Env: "GIT_USERNAME", + Value: serpent.StringOf(&c.GitUsername), + Description: "GitUsername is the username to use for Git authentication. This is optional.", }, { - Name: "Git Password", - Env: "GIT_PASSWORD", - Value: serpent.StringOf(&c.GitPassword), - Description: `GitPassword is the password to use for Git authentication. - This is optional.`, + Flag: "git-password", + Env: "GIT_PASSWORD", + Value: serpent.StringOf(&c.GitPassword), + Description: "GitPassword is the password to use for Git authentication. This is optional.", }, { - Name: "Git HTTP Proxy URL", - Env: "GIT_HTTP_PROXY_URL", - Value: serpent.StringOf(&c.GitHTTPProxyURL), - Description: `GitHTTPProxyURL is the url for the http proxy. - This is optional.`, + Flag: "git-http-proxy-url", + Env: "GIT_HTTP_PROXY_URL", + Value: serpent.StringOf(&c.GitHTTPProxyURL), + Description: "GitHTTPProxyURL is the url for the http proxy. This is optional.", }, { - Name: "Workspace Folder", + Flag: "workspace-folder", Env: "WORKSPACE_FOLDER", Value: serpent.StringOf(&c.WorkspaceFolder), - Description: `WorkspaceFolder is the path to the workspace folder - that will be built. This is optional.`, + Description: "WorkspaceFolder is the path to the workspace folder that will " + + "be built. This is optional.", }, { - Name: "SSL Cert Base64", + Flag: "ssl-cert-base64", Env: "SSL_CERT_BASE64", Value: serpent.StringOf(&c.SSLCertBase64), - Description: `SSLCertBase64 is the content of an SSL cert file. - This is useful for self-signed certificates.`, + Description: "SSLCertBase64 is the content of an SSL cert file. This is useful " + + "for self-signed certificates.", }, { - Name: "Export Env File", + Flag: "export-env-file", Env: "EXPORT_ENV_FILE", Value: serpent.StringOf(&c.ExportEnvFile), - Description: `ExportEnvFile is an optional file path to a .env file where - envbuilder will dump environment variables from devcontainer.json and - the built container image.`, + Description: "ExportEnvFile is an optional file path to a .env file where " + + "envbuilder will dump environment variables from devcontainer.json " + + "and the built container image.", }, { - Name: "Post Start Script Path", + Flag: "post-start-script-path", Env: "POST_START_SCRIPT_PATH", Value: serpent.StringOf(&c.PostStartScriptPath), - Description: `PostStartScriptPath is the path to a script that will be created by - envbuilder based on the ` + "`postStartCommand`" + ` in devcontainer.json, if any - is specified (otherwise the script is not created). If this is set, the - specified InitCommand should check for the presence of this script and - execute it after successful startup.`, + Description: "PostStartScriptPath is the path to a script that will be created " + + "by envbuilder based on the postStartCommand in devcontainer.json, " + + "if any is specified (otherwise the script is not created). If this " + + "is set, the specified InitCommand should check for the presence of " + + "this script and execute it after successful startup.", }, } } From 9cd93678eb0712b905b844b6e81f1a82dbe3f804 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 25 Apr 2024 18:58:08 +0000 Subject: [PATCH 10/20] Use options back to reduce PR changes --- cmd/envbuilder/main.go | 8 +- envbuilder.go | 164 ++++++++++++++++++------------------ envbuilder_internal_test.go | 14 +-- config.go => options.go | 62 +++++++------- 4 files changed, 124 insertions(+), 124 deletions(-) rename config.go => options.go (85%) diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go index 077c79b8..55afe391 100644 --- a/cmd/envbuilder/main.go +++ b/cmd/envbuilder/main.go @@ -23,10 +23,10 @@ import ( ) func main() { - config := envbuilder.Config{} + options := envbuilder.Options{} cmd := serpent.Command{ Use: "envbuilder", - Options: config.Options(), + Options: options.CLI(), // Hide usage because we don't want to show the // "envbuilder [command] --help" output on error. Hidden: true, @@ -47,7 +47,7 @@ func main() { client.SDK.HTTPClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: config.Insecure, + InsecureSkipVerify: options.Insecure, }, }, } @@ -79,7 +79,7 @@ func main() { } } - err := envbuilder.Run(inv.Context(), &config, nil, logger) + err := envbuilder.Run(inv.Context(), options, nil, logger) if err != nil { logger(codersdk.LogLevelError, "error: %s", err) } diff --git a/envbuilder.go b/envbuilder.go index 23e4f597..b1349a8a 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -86,12 +86,12 @@ type DockerConfig configfile.ConfigFile // Logger is the logger to use for all operations. // Filesystem is the filesystem to use for all operations. // Defaults to the host filesystem. -func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) error { +func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logger) error { // Default to the shell! - initArgs := []string{"-c", c.InitScript} - if c.InitArgs != "" { + initArgs := []string{"-c", options.InitScript} + if options.InitArgs != "" { var err error - initArgs, err = shellquote.Split(c.InitArgs) + initArgs, err = shellquote.Split(options.InitArgs) if err != nil { return fmt.Errorf("parse init args: %w", err) } @@ -99,10 +99,10 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err if fs == nil { fs = &osfsWithChmod{osfs.New("/")} } - if c.WorkspaceFolder == "" { + if options.WorkspaceFolder == "" { var err error - folder, err := DefaultWorkspaceFolder(c.GitURL) - c.WorkspaceFolder = folder + folder, err := DefaultWorkspaceFolder(options.GitURL) + options.WorkspaceFolder = folder if err != nil { return err } @@ -123,12 +123,12 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err logger(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) var caBundle []byte - if c.SSLCertBase64 != "" { + if options.SSLCertBase64 != "" { certPool, err := x509.SystemCertPool() if err != nil { return xerrors.Errorf("get global system cert pool: %w", err) } - data, err := base64.StdEncoding.DecodeString(c.SSLCertBase64) + data, err := base64.StdEncoding.DecodeString(options.SSLCertBase64) if err != nil { return xerrors.Errorf("base64 decode ssl cert: %w", err) } @@ -139,8 +139,8 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err caBundle = data } - if c.DockerConfigBase64 != "" { - decoded, err := base64.StdEncoding.DecodeString(c.DockerConfigBase64) + if options.DockerConfigBase64 != "" { + decoded, err := base64.StdEncoding.DecodeString(options.DockerConfigBase64) if err != nil { return fmt.Errorf("decode docker config: %w", err) } @@ -161,10 +161,10 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err var fallbackErr error var cloned bool - if c.GitURL != "" { + if options.GitURL != "" { endStage := startStage("📦 Cloning %s to %s...", - newColor(color.FgCyan).Sprintf(c.GitURL), - newColor(color.FgCyan).Sprintf(c.WorkspaceFolder), + newColor(color.FgCyan).Sprintf(options.GitURL), + newColor(color.FgCyan).Sprintf(options.WorkspaceFolder), ) reader, writer := io.Pipe() @@ -188,29 +188,29 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err }() cloneOpts := CloneRepoOptions{ - Path: c.WorkspaceFolder, + Path: options.WorkspaceFolder, Storage: fs, - Insecure: c.Insecure, + Insecure: options.Insecure, Progress: writer, - SingleBranch: c.GitCloneSingleBranch, - Depth: int(c.GitCloneDepth), + SingleBranch: options.GitCloneSingleBranch, + Depth: int(options.GitCloneDepth), CABundle: caBundle, } - if c.GitUsername != "" || c.GitPassword != "" { + if options.GitUsername != "" || options.GitPassword != "" { // NOTE: we previously inserted the credentials into the repo URL. // This was removed in https://github.com/coder/envbuilder/pull/141 cloneOpts.RepoAuth = &githttp.BasicAuth{ - Username: c.GitUsername, - Password: c.GitPassword, + Username: options.GitUsername, + Password: options.GitPassword, } } - if c.GitHTTPProxyURL != "" { + if options.GitHTTPProxyURL != "" { cloneOpts.ProxyOptions = transport.ProxyOptions{ - URL: c.GitHTTPProxyURL, + URL: options.GitHTTPProxyURL, } } - cloneOpts.RepoURL = c.GitURL + cloneOpts.RepoURL = options.GitURL cloned, fallbackErr = CloneRepo(ctx, cloneOpts) if fallbackErr == nil { @@ -232,7 +232,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err return nil, err } defer file.Close() - if c.FallbackImage == "" { + if options.FallbackImage == "" { if fallbackErr != nil { return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage) } @@ -240,7 +240,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err // don't support parsing a multiline error. return nil, ErrNoFallbackImage } - content := "FROM " + c.FallbackImage + content := "FROM " + options.FallbackImage _, err = file.Write([]byte(content)) if err != nil { return nil, err @@ -256,10 +256,10 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err buildParams *devcontainer.Compiled scripts devcontainer.LifecycleScripts ) - if c.DockerfilePath == "" { + if options.DockerfilePath == "" { // Only look for a devcontainer if a Dockerfile wasn't specified. // devcontainer is a standard, so it's reasonable to be the default. - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(c, fs, logger) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, logger) if err != nil { logger(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error()) logger(codersdk.LogLevelError, "Falling back to the default image...") @@ -286,7 +286,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err logger(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...") fallbackDockerfile = defaultParams.DockerfilePath } - buildParams, err = devContainer.Compile(fs, devcontainerDir, MagicDir, fallbackDockerfile, c.WorkspaceFolder, false) + buildParams, err = devContainer.Compile(fs, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false) if err != nil { return fmt.Errorf("compile devcontainer.json: %w", err) } @@ -298,13 +298,13 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err } } else { // If a Dockerfile was specified, we use that. - dockerfilePath := filepath.Join(c.WorkspaceFolder, c.DockerfilePath) + dockerfilePath := filepath.Join(options.WorkspaceFolder, options.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(c.WorkspaceFolder) && c.BuildContextPath == "" { - logger(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, c.WorkspaceFolder) + if dockerfileDir != filepath.Clean(options.WorkspaceFolder) && options.BuildContextPath == "" { + logger(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.WorkspaceFolder) logger(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) } @@ -317,7 +317,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err buildParams = &devcontainer.Compiled{ DockerfilePath: dockerfilePath, DockerfileContent: string(content), - BuildContext: filepath.Join(c.WorkspaceFolder, c.BuildContextPath), + BuildContext: filepath.Join(options.WorkspaceFolder, options.BuildContextPath), } } } @@ -340,11 +340,11 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err var closeAfterBuild func() // Allows quick testing of layer caching using a local directory! - if c.LayerCacheDir != "" { + if options.LayerCacheDir != "" { cfg := &configuration.Configuration{ Storage: configuration.Storage{ "filesystem": configuration.Parameters{ - "rootdirectory": c.LayerCacheDir, + "rootdirectory": options.LayerCacheDir, }, }, } @@ -380,10 +380,10 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err _ = srv.Close() _ = listener.Close() } - if c.CacheRepo != "" { + if options.CacheRepo != "" { logger(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") } - c.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port) + options.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port) } // IgnorePaths in the Kaniko options doesn't properly ignore paths. @@ -391,11 +391,11 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err // https://github.com/GoogleContainerTools/kaniko/blob/63be4990ca5a60bdf06ddc4d10aa4eca0c0bc714/cmd/executor/cmd/root.go#L136 ignorePaths := append([]string{ MagicDir, - c.LayerCacheDir, - c.WorkspaceFolder, + options.LayerCacheDir, + options.WorkspaceFolder, // See: https://github.com/coder/envbuilder/issues/37 "/etc/resolv.conf", - }, c.IgnorePaths...) + }, options.IgnorePaths...) for _, ignorePath := range ignorePaths { util.AddToDefaultIgnoreList(util.IgnoreListEntry{ @@ -407,7 +407,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err skippedRebuild := false build := func() (v1.Image, error) { _, err := fs.Stat(MagicFile) - if err == nil && c.SkipRebuild { + if err == nil && options.SkipRebuild { endStage := startStage("🏗️ Skipping build because of cache...") imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent) if err != nil { @@ -454,8 +454,8 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err } }() cacheTTL := time.Hour * 24 * 7 - if c.CacheTTLDays != 0 { - cacheTTL = time.Hour * 24 * time.Duration(c.CacheTTLDays) + if options.CacheTTLDays != 0 { + cacheTTL = time.Hour * 24 * time.Duration(options.CacheTTLDays) } endStage := startStage("🏗️ Building image...") @@ -483,18 +483,18 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err CacheOptions: config.CacheOptions{ // Cache for a week by default! CacheTTL: cacheTTL, - CacheDir: c.BaseImageCacheDir, + CacheDir: options.BaseImageCacheDir, }, ForceUnpack: true, BuildArgs: buildParams.BuildArgs, - CacheRepo: c.CacheRepo, - Cache: c.CacheRepo != "" || c.BaseImageCacheDir != "", + CacheRepo: options.CacheRepo, + Cache: options.CacheRepo != "" || options.BaseImageCacheDir != "", DockerfilePath: buildParams.DockerfilePath, DockerfileContent: buildParams.DockerfileContent, RegistryOptions: config.RegistryOptions{ - Insecure: c.Insecure, - InsecurePull: c.Insecure, - SkipTLSVerify: c.Insecure, + Insecure: options.Insecure, + InsecurePull: options.Insecure, + SkipTLSVerify: options.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 @@ -532,7 +532,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err case strings.Contains(err.Error(), "unexpected status code 401 Unauthorized"): logger(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") } - if !fallback || c.ExitOnBuildFailure { + if !fallback || options.ExitOnBuildFailure { return err } logger(codersdk.LogLevelError, "Failed to build: %s", err) @@ -607,8 +607,8 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err unsetOptionsEnv() // Remove the Docker config secret file! - if c.DockerConfigBase64 != "" { - err = os.Remove(filepath.Join(MagicDir, "c.json")) + if options.DockerConfigBase64 != "" { + err = os.Remove(filepath.Join(MagicDir, "o.json")) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove docker config: %w", err) } @@ -644,7 +644,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err } sort.Strings(envKeys) for _, envVar := range envKeys { - value := devcontainer.SubstituteVars(env[envVar], c.WorkspaceFolder) + value := devcontainer.SubstituteVars(env[envVar], options.WorkspaceFolder) os.Setenv(envVar, value) } } @@ -654,10 +654,10 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err // 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 c.ExportEnvFile != "" && !skippedRebuild { - exportEnvFile, err := os.Create(c.ExportEnvFile) + if options.ExportEnvFile != "" && !skippedRebuild { + exportEnvFile, err := os.Create(options.ExportEnvFile) if err != nil { - return fmt.Errorf("failed to open EXPORT_ENV_FILE %q: %w", c.ExportEnvFile, err) + return fmt.Errorf("failed to open EXPORT_ENV_FILE %q: %w", options.ExportEnvFile, err) } envKeys := make([]string, 0, len(allEnvKeys)) @@ -694,7 +694,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err // // We need to change the ownership of the files to the user that will // be running the init script. - filepath.Walk(c.WorkspaceFolder, func(path string, info os.FileInfo, err error) error { + filepath.Walk(options.WorkspaceFolder, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -703,11 +703,11 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err endStage("👤 Updated the ownership of the workspace!") } - err = os.MkdirAll(c.WorkspaceFolder, 0755) + err = os.MkdirAll(options.WorkspaceFolder, 0755) if err != nil { return fmt.Errorf("create workspace folder: %w", err) } - err = os.Chdir(c.WorkspaceFolder) + err = os.Chdir(options.WorkspaceFolder) if err != nil { return fmt.Errorf("change directory: %w", err) } @@ -719,7 +719,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err // 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, c, logger, scripts, skippedRebuild, userInfo); err != nil { + if err := execLifecycleScripts(ctx, options, logger, scripts, skippedRebuild, userInfo); err != nil { return err } @@ -728,11 +728,11 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err // // This is useful for hooking into the environment for a specific // init to PID 1. - if c.SetupScript != "" { + if options.SetupScript != "" { // We execute the initialize script as the root user! os.Setenv("HOME", "/root") - logger(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", c.SetupScript) + logger(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.SetupScript) envKey := "ENVBUILDER_ENV" envFile := filepath.Join("/", MagicDir, "environ") @@ -742,12 +742,12 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err } _ = file.Close() - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", c.SetupScript) + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", options.SetupScript) cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", envKey, envFile), fmt.Sprintf("TARGET_USER=%s", userInfo.user.Username), ) - cmd.Dir = c.WorkspaceFolder + cmd.Dir = options.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()) { @@ -789,7 +789,7 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err key := pair[0] switch key { case "INIT_COMMAND": - c.InitCommand = pair[1] + options.InitCommand = pair[1] updatedCommand = true case "INIT_ARGS": initArgs, err = shellquote.Split(pair[1]) @@ -825,9 +825,9 @@ func Run(ctx context.Context, c *Config, fs billy.Filesystem, logger Logger) err return fmt.Errorf("set uid: %w", err) } - logger(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", c.InitCommand, initArgs, userInfo.user.Username) + logger(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.InitCommand, initArgs, userInfo.user.Username) - err = syscall.Exec(c.InitCommand, append([]string{c.InitCommand}, initArgs...), os.Environ()) + err = syscall.Exec(options.InitCommand, append([]string{options.InitCommand}, initArgs...), os.Environ()) if err != nil { return fmt.Errorf("exec init script: %w", err) } @@ -916,14 +916,14 @@ func execOneLifecycleScript( func execLifecycleScripts( ctx context.Context, - c *Config, + options Options, logger Logger, scripts devcontainer.LifecycleScripts, skippedRebuild bool, userInfo userInfo, ) error { - if c.PostStartScriptPath != "" { - _ = os.Remove(c.PostStartScriptPath) + if options.PostStartScriptPath != "" { + _ = os.Remove(options.PostStartScriptPath) } if !skippedRebuild { @@ -943,8 +943,8 @@ func execLifecycleScripts( if !scripts.PostStartCommand.IsEmpty() { // If PostStartCommandPath is set, the init command is responsible // for running the postStartCommand. Otherwise, we execute it now. - if c.PostStartScriptPath != "" { - if err := createPostStartScript(c.PostStartScriptPath, scripts.PostStartCommand); err != nil { + if options.PostStartScriptPath != "" { + if err := createPostStartScript(options.PostStartScriptPath, scripts.PostStartCommand); err != nil { return fmt.Errorf("failed to create post-start script: %w", err) } } else { @@ -974,7 +974,7 @@ func createPostStartScript(path string, postStartCommand devcontainer.LifecycleS // unsetOptionsEnv unsets all environment variables that are used // to configure the options. func unsetOptionsEnv() { - val := reflect.ValueOf(&Config{}).Elem() + val := reflect.ValueOf(&Options{}).Elem() typ := val.Type() for i := 0; i < val.NumField(); i++ { @@ -1001,23 +1001,23 @@ func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } -func findDevcontainerJSON(c *Config, fs billy.Filesystem, logger Logger) (string, string, error) { +func findDevcontainerJSON(options Options, fs billy.Filesystem, logger Logger) (string, string, error) { // 0. Check if custom devcontainer directory or path is provided. - if c.DevcontainerDir != "" || c.DevcontainerJSONPath != "" { - devcontainerDir := c.DevcontainerDir + if options.DevcontainerDir != "" || options.DevcontainerJSONPath != "" { + devcontainerDir := options.DevcontainerDir if devcontainerDir == "" { devcontainerDir = ".devcontainer" } // If `devcontainerDir` is not an absolute path, assume it is relative to the workspace folder. if !filepath.IsAbs(devcontainerDir) { - devcontainerDir = filepath.Join(c.WorkspaceFolder, devcontainerDir) + devcontainerDir = filepath.Join(options.WorkspaceFolder, devcontainerDir) } // An absolute location always takes a precedence. - devcontainerPath := c.DevcontainerJSONPath + devcontainerPath := options.DevcontainerJSONPath if filepath.IsAbs(devcontainerPath) { - return c.DevcontainerJSONPath, devcontainerDir, nil + return options.DevcontainerJSONPath, devcontainerDir, nil } // If an override is not provided, assume it is just `devcontainer.json`. if devcontainerPath == "" { @@ -1031,19 +1031,19 @@ func findDevcontainerJSON(c *Config, fs billy.Filesystem, logger Logger) (string } // 1. Check `options.WorkspaceFolder`/.devcontainer/devcontainer.json. - location := filepath.Join(c.WorkspaceFolder, ".devcontainer", "devcontainer.json") + location := filepath.Join(options.WorkspaceFolder, ".devcontainer", "devcontainer.json") if _, err := fs.Stat(location); err == nil { return location, filepath.Dir(location), nil } // 2. Check `options.WorkspaceFolder`/devcontainer.json. - location = filepath.Join(c.WorkspaceFolder, "devcontainer.json") + location = filepath.Join(options.WorkspaceFolder, "devcontainer.json") if _, err := fs.Stat(location); err == nil { return location, filepath.Dir(location), nil } // 3. Check every folder: `options.WorkspaceFolder`/.devcontainer//devcontainer.json. - devcontainerDir := filepath.Join(c.WorkspaceFolder, ".devcontainer") + devcontainerDir := filepath.Join(options.WorkspaceFolder, ".devcontainer") fileInfos, err := fs.ReadDir(devcontainerDir) if err != nil { diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index 84cf4896..ffa57de4 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -18,7 +18,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs := memfs.New() // when - _, _, err := findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) + _, _, err := findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) // then require.Error(t, err) @@ -33,7 +33,7 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - _, _, err = findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) + _, _, err = findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) // then require.Error(t, err) @@ -49,7 +49,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/devcontainer.json") // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) // then require.NoError(t, err) @@ -67,7 +67,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/experimental-devcontainer/devcontainer.json") // when - c := &Config{ + c := Options{ WorkspaceFolder: "/workspace", DevcontainerDir: "experimental-devcontainer", } @@ -89,7 +89,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/experimental.json") // when - c := &Config{ + c := Options{ WorkspaceFolder: "/workspace", DevcontainerJSONPath: "experimental.json", } @@ -111,7 +111,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/devcontainer.json") // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) // then require.NoError(t, err) @@ -129,7 +129,7 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/sample/devcontainer.json") // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&Config{WorkspaceFolder: "/workspace"}, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) // then require.NoError(t, err) diff --git a/config.go b/options.go similarity index 85% rename from config.go rename to options.go index 2ab27d5b..34089914 100644 --- a/config.go +++ b/options.go @@ -4,7 +4,7 @@ import ( "github.com/coder/serpent" ) -type Config struct { +type Options struct { SetupScript string InitScript string InitCommand string @@ -36,12 +36,12 @@ type Config struct { PostStartScriptPath string } -func (c *Config) Options() serpent.OptionSet { +func (o *Options) CLI() serpent.OptionSet { return serpent.OptionSet{ { Flag: "setup-script", Env: "SETUP_SCRIPT", - Value: serpent.StringOf(&c.SetupScript), + Value: serpent.StringOf(&o.SetupScript), Description: "SetupScript is the script to run before the init script. It runs as " + "the root user regardless of the user specified in the devcontainer.json " + "file.\n\nSetupScript is ran as the root user prior to the init script. " + @@ -52,20 +52,20 @@ func (c *Config) Options() serpent.OptionSet { Flag: "init-script", Env: "INIT_SCRIPT", Default: "sleep infinity", - Value: serpent.StringOf(&c.InitScript), + Value: serpent.StringOf(&o.InitScript), Description: "InitScript is the script to run to initialize the workspace.", }, { Flag: "init-command", Env: "INIT_COMMAND", Default: "/bin/sh", - Value: serpent.StringOf(&c.InitCommand), + Value: serpent.StringOf(&o.InitCommand), Description: "InitCommand is the command to run to initialize the workspace.", }, { Flag: "init-args", Env: "INIT_ARGS", - Value: serpent.StringOf(&c.InitArgs), + Value: serpent.StringOf(&o.InitArgs), Description: "InitArgs are the arguments to pass to the init command. They are " + "split according to /bin/sh rules with " + "https://github.com/kballard/go-shellquote.", @@ -73,14 +73,14 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "cache-repo", Env: "CACHE_REPO", - Value: serpent.StringOf(&c.CacheRepo), + Value: serpent.StringOf(&o.CacheRepo), Description: "CacheRepo is the name of the container registry to push the cache " + "image to. If this is empty, the cache will not be pushed.", }, { Flag: "base-image-cache-dir", Env: "BASE_IMAGE_CACHE_DIR", - Value: serpent.StringOf(&c.BaseImageCacheDir), + Value: serpent.StringOf(&o.BaseImageCacheDir), Description: "BaseImageCacheDir is the path to a directory where the base image " + "can be found. This should be a read-only directory solely mounted " + "for the purpose of caching the base image.", @@ -88,7 +88,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "layer-cache-dir", Env: "LAYER_CACHE_DIR", - Value: serpent.StringOf(&c.LayerCacheDir), + Value: serpent.StringOf(&o.LayerCacheDir), Description: "LayerCacheDir is the path to a directory where built layers will " + "be stored. This spawns an in-memory registry to serve the layers " + "from.", @@ -96,7 +96,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "devcontainer-dir", Env: "DEVCONTAINER_DIR", - Value: serpent.StringOf(&c.DevcontainerDir), + Value: serpent.StringOf(&o.DevcontainerDir), Description: "DevcontainerDir is a path to the folder containing the " + "devcontainer.json file that will be used to build the workspace " + "and can either be an absolute path or a path relative to the " + @@ -105,7 +105,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "devcontainer-json-path", Env: "DEVCONTAINER_JSON_PATH", - Value: serpent.StringOf(&c.DevcontainerJSONPath), + Value: serpent.StringOf(&o.DevcontainerJSONPath), Description: "DevcontainerJSONPath is a path to a devcontainer.json file that " + "is either an absolute path or a path relative to DevcontainerDir. " + "This can be used in cases where one wants to substitute an edited " + @@ -114,7 +114,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "dockerfile-path", Env: "DOCKERFILE_PATH", - Value: serpent.StringOf(&c.DockerfilePath), + Value: serpent.StringOf(&o.DockerfilePath), Description: "DockerfilePath is a relative path to the Dockerfile that will " + "be used to build the workspace. This is an alternative to using " + "a devcontainer that some might find simpler.", @@ -122,7 +122,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "build-context-path", Env: "BUILD_CONTEXT_PATH", - Value: serpent.StringOf(&c.BuildContextPath), + Value: serpent.StringOf(&o.BuildContextPath), Description: "BuildContextPath can be specified when a DockerfilePath is " + "specified outside the base WorkspaceFolder. This path MUST be " + "relative to the WorkspaceFolder path into which the repo is cloned.", @@ -130,21 +130,21 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "cache-ttl-days", Env: "CACHE_TTL_DAYS", - Value: serpent.Int64Of(&c.CacheTTLDays), + Value: serpent.Int64Of(&o.CacheTTLDays), Description: "CacheTTLDays is the number of days to use cached layers before " + "expiring them. Defaults to 7 days.", }, { Flag: "docker-config-base64", Env: "DOCKER_CONFIG_BASE64", - Value: serpent.StringOf(&c.DockerConfigBase64), + Value: serpent.StringOf(&o.DockerConfigBase64), Description: "DockerConfigBase64 is a base64 encoded Docker config file that " + "will be used to pull images from private container registries.", }, { Flag: "fallback-image", Env: "FALLBACK_IMAGE", - Value: serpent.StringOf(&c.FallbackImage), + Value: serpent.StringOf(&o.FallbackImage), Description: "FallbackImage specifies an alternative image to use when neither " + "an image is declared in the devcontainer.json file nor a Dockerfile " + "is present. If there's a build failure (from a faulty Dockerfile) " + @@ -155,7 +155,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "exit-on-build-failure", Env: "EXIT_ON_BUILD_FAILURE", - Value: serpent.BoolOf(&c.ExitOnBuildFailure), + Value: serpent.BoolOf(&o.ExitOnBuildFailure), Description: "ExitOnBuildFailure terminates the container upon a build failure. " + "This is handy when preferring the FALLBACK_IMAGE in cases where " + "no devcontainer.json or image is provided. However, it ensures " + @@ -164,7 +164,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "force-safe", Env: "FORCE_SAFE", - Value: serpent.BoolOf(&c.ForceSafe), + Value: serpent.BoolOf(&o.ForceSafe), Description: "ForceSafe ignores any filesystem safety checks. This could cause " + "serious harm to your system! This is used in cases where bypass " + "is needed to unblock customers.", @@ -172,14 +172,14 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "insecure", Env: "INSECURE", - Value: serpent.BoolOf(&c.Insecure), + Value: serpent.BoolOf(&o.Insecure), Description: "Insecure bypasses TLS verification when cloning and pulling from " + "container registries.", }, { Flag: "ignore-paths", Env: "IGNORE_PATHS", - Value: serpent.StringArrayOf(&c.IgnorePaths), + Value: serpent.StringArrayOf(&o.IgnorePaths), Default: "/var/run", Description: "IgnorePaths is a comma separated list of paths to ignore when " + "building the workspace.", @@ -187,7 +187,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "skip-rebuild", Env: "SKIP_REBUILD", - Value: serpent.BoolOf(&c.SkipRebuild), + Value: serpent.BoolOf(&o.SkipRebuild), Description: "SkipRebuild skips building if the MagicFile exists. This is used " + "to skip building when a container is restarting. e.g. docker stop -> " + "docker start This value can always be set to true - even if the " + @@ -196,57 +196,57 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "git-url", Env: "GIT_URL", - Value: serpent.StringOf(&c.GitURL), + Value: serpent.StringOf(&o.GitURL), Description: "GitURL is the URL of the Git repository to clone. This is optional.", }, { Flag: "git-clone-depth", Env: "GIT_CLONE_DEPTH", - Value: serpent.Int64Of(&c.GitCloneDepth), + Value: serpent.Int64Of(&o.GitCloneDepth), Description: "GitCloneDepth is the depth to use when cloning the Git repository.", }, { Flag: "git-clone-single-branch", Env: "GIT_CLONE_SINGLE_BRANCH", - Value: serpent.BoolOf(&c.GitCloneSingleBranch), + Value: serpent.BoolOf(&o.GitCloneSingleBranch), Description: "GitCloneSingleBranch clones only a single branch of the Git repository.", }, { Flag: "git-username", Env: "GIT_USERNAME", - Value: serpent.StringOf(&c.GitUsername), + Value: serpent.StringOf(&o.GitUsername), Description: "GitUsername is the username to use for Git authentication. This is optional.", }, { Flag: "git-password", Env: "GIT_PASSWORD", - Value: serpent.StringOf(&c.GitPassword), + Value: serpent.StringOf(&o.GitPassword), Description: "GitPassword is the password to use for Git authentication. This is optional.", }, { Flag: "git-http-proxy-url", Env: "GIT_HTTP_PROXY_URL", - Value: serpent.StringOf(&c.GitHTTPProxyURL), + Value: serpent.StringOf(&o.GitHTTPProxyURL), Description: "GitHTTPProxyURL is the url for the http proxy. This is optional.", }, { Flag: "workspace-folder", Env: "WORKSPACE_FOLDER", - Value: serpent.StringOf(&c.WorkspaceFolder), + Value: serpent.StringOf(&o.WorkspaceFolder), Description: "WorkspaceFolder is the path to the workspace folder that will " + "be built. This is optional.", }, { Flag: "ssl-cert-base64", Env: "SSL_CERT_BASE64", - Value: serpent.StringOf(&c.SSLCertBase64), + Value: serpent.StringOf(&o.SSLCertBase64), Description: "SSLCertBase64 is the content of an SSL cert file. This is useful " + "for self-signed certificates.", }, { Flag: "export-env-file", Env: "EXPORT_ENV_FILE", - Value: serpent.StringOf(&c.ExportEnvFile), + Value: serpent.StringOf(&o.ExportEnvFile), Description: "ExportEnvFile is an optional file path to a .env file where " + "envbuilder will dump environment variables from devcontainer.json " + "and the built container image.", @@ -254,7 +254,7 @@ func (c *Config) Options() serpent.OptionSet { { Flag: "post-start-script-path", Env: "POST_START_SCRIPT_PATH", - Value: serpent.StringOf(&c.PostStartScriptPath), + Value: serpent.StringOf(&o.PostStartScriptPath), Description: "PostStartScriptPath is the path to a script that will be created " + "by envbuilder based on the postStartCommand in devcontainer.json, " + "if any is specified (otherwise the script is not created). If this " + From babf6b8127e72b480efa9355ed8110f1a9f07ba0 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 25 Apr 2024 19:01:38 +0000 Subject: [PATCH 11/20] Use logf back to reduce PR changes --- envbuilder.go | 84 +++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index b1349a8a..4827bfd7 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -83,10 +83,10 @@ type Logger func(level codersdk.LogLevel, format string, args ...any) type DockerConfig configfile.ConfigFile // Run runs the envbuilder. -// Logger is the logger to use for all operations. +// 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, fs billy.Filesystem, logger Logger) error { +func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) error { // Default to the shell! initArgs := []string{"-c", options.InitScript} if options.InitArgs != "" { @@ -113,14 +113,14 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge now := time.Now() stageNum := stageNumber stageNumber++ - logger(codersdk.LogLevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) + logf(codersdk.LogLevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) return func(format string, args ...any) { - logger(codersdk.LogLevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) + logf(codersdk.LogLevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) } } - logger(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) + logf(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) var caBundle []byte if options.SSLCertBase64 != "" { @@ -182,7 +182,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge if line == "" { continue } - logger(codersdk.LogLevelInfo, "#1: %s", strings.TrimSpace(line)) + logf(codersdk.LogLevelInfo, "#1: %s", strings.TrimSpace(line)) } } }() @@ -220,8 +220,8 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge endStage("📦 The repository already exists!") } } else { - logger(codersdk.LogLevelError, "Failed to clone repository: %s", fallbackErr.Error()) - logger(codersdk.LogLevelError, "Falling back to the default image...") + logf(codersdk.LogLevelError, "Failed to clone repository: %s", fallbackErr.Error()) + logf(codersdk.LogLevelError, "Falling back to the default image...") } } @@ -259,10 +259,10 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge if options.DockerfilePath == "" { // Only look for a devcontainer if a Dockerfile wasn't specified. // devcontainer is a standard, so it's reasonable to be the default. - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, logger) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, logf) if err != nil { - logger(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error()) - logger(codersdk.LogLevelError, "Falling back to the default image...") + logf(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error()) + logf(codersdk.LogLevelError, "Falling back to the default image...") } else { // We know a devcontainer exists. // Let's parse it and use it! @@ -283,7 +283,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge if err != nil { return fmt.Errorf("no Dockerfile or image found: %w", err) } - logger(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...") + logf(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...") fallbackDockerfile = defaultParams.DockerfilePath } buildParams, err = devContainer.Compile(fs, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false) @@ -292,8 +292,8 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge } scripts = devContainer.LifecycleScripts } else { - logger(codersdk.LogLevelError, "Failed to parse devcontainer.json: %s", err.Error()) - logger(codersdk.LogLevelError, "Falling back to the default image...") + logf(codersdk.LogLevelError, "Failed to parse devcontainer.json: %s", err.Error()) + logf(codersdk.LogLevelError, "Falling back to the default image...") } } } else { @@ -304,8 +304,8 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge // not defined, show a warning dockerfileDir := filepath.Dir(dockerfilePath) if dockerfileDir != filepath.Clean(options.WorkspaceFolder) && options.BuildContextPath == "" { - logger(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.WorkspaceFolder) - logger(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) + logf(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.WorkspaceFolder) + logf(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) } dockerfile, err := fs.Open(dockerfilePath) @@ -334,7 +334,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge HijackLogrus(func(entry *logrus.Entry) { for _, line := range strings.Split(entry.Message, "\r") { - logger(codersdk.LogLevelInfo, "#2: %s", color.HiBlackString(line)) + logf(codersdk.LogLevelInfo, "#2: %s", color.HiBlackString(line)) } }) @@ -373,7 +373,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge go func() { err := srv.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { - logger(codersdk.LogLevelError, "Failed to serve registry: %s", err.Error()) + logf(codersdk.LogLevelError, "Failed to serve registry: %s", err.Error()) } }() closeAfterBuild = func() { @@ -381,7 +381,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge _ = listener.Close() } if options.CacheRepo != "" { - logger(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") + logf(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") } options.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port) } @@ -444,13 +444,13 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge go func() { scanner := bufio.NewScanner(stdoutReader) for scanner.Scan() { - logger(codersdk.LogLevelInfo, "%s", scanner.Text()) + logf(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() go func() { scanner := bufio.NewScanner(stderrReader) for scanner.Scan() { - logger(codersdk.LogLevelInfo, "%s", scanner.Text()) + logf(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() cacheTTL := time.Hour * 24 * 7 @@ -530,13 +530,13 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge fallback = true fallbackErr = err case strings.Contains(err.Error(), "unexpected status code 401 Unauthorized"): - logger(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") + logf(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") } if !fallback || options.ExitOnBuildFailure { return err } - logger(codersdk.LogLevelError, "Failed to build: %s", err) - logger(codersdk.LogLevelError, "Falling back to the default image...") + logf(codersdk.LogLevelError, "Failed to build: %s", err) + logf(codersdk.LogLevelError, "Falling back to the default image...") buildParams, err = defaultBuildParams() if err != nil { return err @@ -579,10 +579,10 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge if err != nil { return fmt.Errorf("unmarshal metadata: %w", err) } - logger(codersdk.LogLevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...") + logf(codersdk.LogLevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...") for _, container := range devContainer { if container.RemoteUser != "" { - logger(codersdk.LogLevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser) + logf(codersdk.LogLevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser) configFile.Config.User = container.RemoteUser } @@ -677,7 +677,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge username = buildParams.User } if username == "" { - logger(codersdk.LogLevelWarn, "#3: no user specified, using root") + logf(codersdk.LogLevelWarn, "#3: no user specified, using root") } userInfo, err := getUser(username) @@ -719,7 +719,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge // 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, logger, scripts, skippedRebuild, userInfo); err != nil { + if err := execLifecycleScripts(ctx, options, logf, scripts, skippedRebuild, userInfo); err != nil { return err } @@ -732,7 +732,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge // We execute the initialize script as the root user! os.Setenv("HOME", "/root") - logger(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.SetupScript) + logf(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.SetupScript) envKey := "ENVBUILDER_ENV" envFile := filepath.Join("/", MagicDir, "environ") @@ -759,7 +759,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge go func() { scanner := bufio.NewScanner(&buf) for scanner.Scan() { - logger(codersdk.LogLevelInfo, "%s", scanner.Text()) + logf(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() @@ -825,7 +825,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logger Logge return fmt.Errorf("set uid: %w", err) } - logger(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.InitCommand, initArgs, userInfo.user.Username) + logf(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.InitCommand, initArgs, userInfo.user.Username) err = syscall.Exec(options.InitCommand, append([]string{options.InitCommand}, initArgs...), os.Environ()) if err != nil { @@ -898,7 +898,7 @@ func findUser(nameOrID string) (*user.User, error) { func execOneLifecycleScript( ctx context.Context, - logger func(level codersdk.LogLevel, format string, args ...any), + logf func(level codersdk.LogLevel, format string, args ...any), s devcontainer.LifecycleScript, scriptName string, userInfo userInfo, @@ -906,9 +906,9 @@ func execOneLifecycleScript( if s.IsEmpty() { return nil } - logger(codersdk.LogLevelInfo, "=== Running %s as the %q user...", scriptName, userInfo.user.Username) + logf(codersdk.LogLevelInfo, "=== Running %s as the %q user...", scriptName, userInfo.user.Username) if err := s.Execute(ctx, userInfo.uid, userInfo.gid); err != nil { - logger(codersdk.LogLevelError, "Failed to run %s: %v", scriptName, err) + logf(codersdk.LogLevelError, "Failed to run %s: %v", scriptName, err) return err } return nil @@ -917,7 +917,7 @@ func execOneLifecycleScript( func execLifecycleScripts( ctx context.Context, options Options, - logger Logger, + logf Logger, scripts devcontainer.LifecycleScripts, skippedRebuild bool, userInfo userInfo, @@ -927,16 +927,16 @@ func execLifecycleScripts( } if !skippedRebuild { - if err := execOneLifecycleScript(ctx, logger, scripts.OnCreateCommand, "onCreateCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, logf, scripts.OnCreateCommand, "onCreateCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } } - if err := execOneLifecycleScript(ctx, logger, scripts.UpdateContentCommand, "updateContentCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, logf, scripts.UpdateContentCommand, "updateContentCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } - if err := execOneLifecycleScript(ctx, logger, scripts.PostCreateCommand, "postCreateCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, logf, scripts.PostCreateCommand, "postCreateCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } @@ -948,7 +948,7 @@ func execLifecycleScripts( return fmt.Errorf("failed to create post-start script: %w", err) } } else { - _ = execOneLifecycleScript(ctx, logger, scripts.PostStartCommand, "postStartCommand", userInfo) + _ = execOneLifecycleScript(ctx, logf, scripts.PostStartCommand, "postStartCommand", userInfo) } } return nil @@ -1001,7 +1001,7 @@ func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } -func findDevcontainerJSON(options Options, fs billy.Filesystem, logger Logger) (string, string, error) { +func findDevcontainerJSON(options Options, fs billy.Filesystem, logf Logger) (string, string, error) { // 0. Check if custom devcontainer directory or path is provided. if options.DevcontainerDir != "" || options.DevcontainerJSONPath != "" { devcontainerDir := options.DevcontainerDir @@ -1052,13 +1052,13 @@ func findDevcontainerJSON(options Options, fs billy.Filesystem, logger Logger) ( for _, fileInfo := range fileInfos { if !fileInfo.IsDir() { - logger(codersdk.LogLevelDebug, `%s is a file`, fileInfo.Name()) + logf(codersdk.LogLevelDebug, `%s is a file`, fileInfo.Name()) continue } location := filepath.Join(devcontainerDir, fileInfo.Name(), "devcontainer.json") if _, err := fs.Stat(location); err != nil { - logger(codersdk.LogLevelDebug, `stat %s failed: %s`, location, err.Error()) + logf(codersdk.LogLevelDebug, `stat %s failed: %s`, location, err.Error()) continue } From a383098f258551fc7f4938da6304d2ba5687345e Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 25 Apr 2024 19:02:11 +0000 Subject: [PATCH 12/20] Remove Hidden --- cmd/envbuilder/main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go index 55afe391..628f56c4 100644 --- a/cmd/envbuilder/main.go +++ b/cmd/envbuilder/main.go @@ -27,9 +27,6 @@ func main() { cmd := serpent.Command{ Use: "envbuilder", Options: options.CLI(), - // Hide usage because we don't want to show the - // "envbuilder [command] --help" output on error. - Hidden: true, Handler: func(inv *serpent.Invocation) error { var sendLogs func(ctx context.Context, log ...agentsdk.Log) error agentURL := os.Getenv("CODER_AGENT_URL") From f11f3f32e41f24fe43ff9d2e6dad1fb7bcad1cb5 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 25 Apr 2024 19:02:57 +0000 Subject: [PATCH 13/20] Document exported values on options.go --- options.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options.go b/options.go index 34089914..1314001e 100644 --- a/options.go +++ b/options.go @@ -4,6 +4,7 @@ import ( "github.com/coder/serpent" ) +// Options contains the configuration for the envbuilder. type Options struct { SetupScript string InitScript string @@ -36,6 +37,7 @@ type Options struct { PostStartScriptPath string } +// Generate CLI options for the envbuilder command. func (o *Options) CLI() serpent.OptionSet { return serpent.OptionSet{ { From e3b205bc58fff55d4c17fb5df4916f2ac059876b Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 25 Apr 2024 19:07:05 +0000 Subject: [PATCH 14/20] Rename c to Options in test --- envbuilder_internal_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index ffa57de4..f72c027d 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -67,11 +67,11 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/experimental-devcontainer/devcontainer.json") // when - c := Options{ + options := Options{ WorkspaceFolder: "/workspace", DevcontainerDir: "experimental-devcontainer", } - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(c, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) // then require.NoError(t, err) @@ -89,11 +89,11 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/experimental.json") // when - c := Options{ + options := Options{ WorkspaceFolder: "/workspace", DevcontainerJSONPath: "experimental.json", } - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(c, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) // then require.NoError(t, err) From 9b5ca70bd0eea07b252d4f009dd44be3b64ad389 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 26 Apr 2024 15:15:05 +0000 Subject: [PATCH 15/20] Use shorter name for variable --- envbuilder.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index 4827bfd7..04de17bb 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -350,9 +350,9 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) } // Disable all logging from the registry... - logrusLogger := logrus.New() - logrusLogger.SetOutput(io.Discard) - entry := logrus.NewEntry(logrusLogger) + l := logrus.New() + l.SetOutput(io.Discard) + entry := logrus.NewEntry(l) dcontext.SetDefaultLogger(entry) ctx = dcontext.WithLogger(ctx, entry) From bb53a5a18909af60579c6dc64b799387c5fe57cd Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 26 Apr 2024 15:23:16 +0000 Subject: [PATCH 16/20] Fix descriptions --- options.go | 58 +++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/options.go b/options.go index 1314001e..b37998f8 100644 --- a/options.go +++ b/options.go @@ -44,7 +44,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "setup-script", Env: "SETUP_SCRIPT", Value: serpent.StringOf(&o.SetupScript), - Description: "SetupScript is the script to run before the init script. It runs as " + + Description: "The script to run before the init script. It runs as " + "the root user regardless of the user specified in the devcontainer.json " + "file.\n\nSetupScript is ran as the root user prior to the init script. " + "It is used to configure envbuilder dynamically during the runtime. e.g. " + @@ -55,20 +55,20 @@ func (o *Options) CLI() serpent.OptionSet { Env: "INIT_SCRIPT", Default: "sleep infinity", Value: serpent.StringOf(&o.InitScript), - Description: "InitScript is the script to run to initialize the workspace.", + Description: "The script to run to initialize the workspace.", }, { Flag: "init-command", Env: "INIT_COMMAND", Default: "/bin/sh", Value: serpent.StringOf(&o.InitCommand), - Description: "InitCommand is the command to run to initialize the workspace.", + Description: "The command to run to initialize the workspace.", }, { Flag: "init-args", Env: "INIT_ARGS", Value: serpent.StringOf(&o.InitArgs), - Description: "InitArgs are the arguments to pass to the init command. They are " + + Description: "The arguments to pass to the init command. They are " + "split according to /bin/sh rules with " + "https://github.com/kballard/go-shellquote.", }, @@ -76,14 +76,14 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "cache-repo", Env: "CACHE_REPO", Value: serpent.StringOf(&o.CacheRepo), - Description: "CacheRepo is the name of the container registry to push the cache " + + Description: "The name of the container registry to push the cache " + "image to. If this is empty, the cache will not be pushed.", }, { Flag: "base-image-cache-dir", Env: "BASE_IMAGE_CACHE_DIR", Value: serpent.StringOf(&o.BaseImageCacheDir), - Description: "BaseImageCacheDir is the path to a directory where the base image " + + Description: "The path to a directory where the base image " + "can be found. This should be a read-only directory solely mounted " + "for the purpose of caching the base image.", }, @@ -91,7 +91,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "layer-cache-dir", Env: "LAYER_CACHE_DIR", Value: serpent.StringOf(&o.LayerCacheDir), - Description: "LayerCacheDir is the path to a directory where built layers will " + + Description: "The path to a directory where built layers will " + "be stored. This spawns an in-memory registry to serve the layers " + "from.", }, @@ -99,7 +99,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "devcontainer-dir", Env: "DEVCONTAINER_DIR", Value: serpent.StringOf(&o.DevcontainerDir), - Description: "DevcontainerDir is a path to the folder containing the " + + Description: "The path to the folder containing the " + "devcontainer.json file that will be used to build the workspace " + "and can either be an absolute path or a path relative to the " + "workspace folder. If not provided, defaults to `.devcontainer`.", @@ -108,7 +108,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "devcontainer-json-path", Env: "DEVCONTAINER_JSON_PATH", Value: serpent.StringOf(&o.DevcontainerJSONPath), - Description: "DevcontainerJSONPath is a path to a devcontainer.json file that " + + Description: "The path to a devcontainer.json file that " + "is either an absolute path or a path relative to DevcontainerDir. " + "This can be used in cases where one wants to substitute an edited " + "devcontainer.json file for the one that exists in the repo.", @@ -117,7 +117,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "dockerfile-path", Env: "DOCKERFILE_PATH", Value: serpent.StringOf(&o.DockerfilePath), - Description: "DockerfilePath is a relative path to the Dockerfile that will " + + Description: "The relative path to the Dockerfile that will " + "be used to build the workspace. This is an alternative to using " + "a devcontainer that some might find simpler.", }, @@ -125,7 +125,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "build-context-path", Env: "BUILD_CONTEXT_PATH", Value: serpent.StringOf(&o.BuildContextPath), - Description: "BuildContextPath can be specified when a DockerfilePath is " + + Description: "Can be specified when a DockerfilePath is " + "specified outside the base WorkspaceFolder. This path MUST be " + "relative to the WorkspaceFolder path into which the repo is cloned.", }, @@ -133,21 +133,21 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "cache-ttl-days", Env: "CACHE_TTL_DAYS", Value: serpent.Int64Of(&o.CacheTTLDays), - Description: "CacheTTLDays is the number of days to use cached layers before " + + Description: "The number of days to use cached layers before " + "expiring them. Defaults to 7 days.", }, { Flag: "docker-config-base64", Env: "DOCKER_CONFIG_BASE64", Value: serpent.StringOf(&o.DockerConfigBase64), - Description: "DockerConfigBase64 is a base64 encoded Docker config file that " + + Description: "The base64 encoded Docker config file that " + "will be used to pull images from private container registries.", }, { Flag: "fallback-image", Env: "FALLBACK_IMAGE", Value: serpent.StringOf(&o.FallbackImage), - Description: "FallbackImage specifies an alternative image to use when neither " + + Description: "Specifies an alternative image to use when neither " + "an image is declared in the devcontainer.json file nor a Dockerfile " + "is present. If there's a build failure (from a faulty Dockerfile) " + "or a misconfiguration, this image will be the substitute. Set " + @@ -158,7 +158,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "exit-on-build-failure", Env: "EXIT_ON_BUILD_FAILURE", Value: serpent.BoolOf(&o.ExitOnBuildFailure), - Description: "ExitOnBuildFailure terminates the container upon a build failure. " + + Description: "Terminates the container upon a build failure. " + "This is handy when preferring the FALLBACK_IMAGE in cases where " + "no devcontainer.json or image is provided. However, it ensures " + "that the container stops if the build process encounters an error.", @@ -167,7 +167,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "force-safe", Env: "FORCE_SAFE", Value: serpent.BoolOf(&o.ForceSafe), - Description: "ForceSafe ignores any filesystem safety checks. This could cause " + + Description: "Ignores any filesystem safety checks. This could cause " + "serious harm to your system! This is used in cases where bypass " + "is needed to unblock customers.", }, @@ -175,7 +175,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "insecure", Env: "INSECURE", Value: serpent.BoolOf(&o.Insecure), - Description: "Insecure bypasses TLS verification when cloning and pulling from " + + Description: "Bypass TLS verification when cloning and pulling from " + "container registries.", }, { @@ -183,14 +183,14 @@ func (o *Options) CLI() serpent.OptionSet { Env: "IGNORE_PATHS", Value: serpent.StringArrayOf(&o.IgnorePaths), Default: "/var/run", - Description: "IgnorePaths is a comma separated list of paths to ignore when " + + Description: "The comma separated list of paths to ignore when " + "building the workspace.", }, { Flag: "skip-rebuild", Env: "SKIP_REBUILD", Value: serpent.BoolOf(&o.SkipRebuild), - Description: "SkipRebuild skips building if the MagicFile exists. This is used " + + Description: "Skip building if the MagicFile exists. This is used " + "to skip building when a container is restarting. e.g. docker stop -> " + "docker start This value can always be set to true - even if the " + "container is being started for the first time.", @@ -199,57 +199,57 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "git-url", Env: "GIT_URL", Value: serpent.StringOf(&o.GitURL), - Description: "GitURL is the URL of the Git repository to clone. This is optional.", + Description: "The URL of the Git repository to clone. This is optional.", }, { Flag: "git-clone-depth", Env: "GIT_CLONE_DEPTH", Value: serpent.Int64Of(&o.GitCloneDepth), - Description: "GitCloneDepth is the depth to use when cloning the Git repository.", + Description: "The depth to use when cloning the Git repository.", }, { Flag: "git-clone-single-branch", Env: "GIT_CLONE_SINGLE_BRANCH", Value: serpent.BoolOf(&o.GitCloneSingleBranch), - Description: "GitCloneSingleBranch clones only a single branch of the Git repository.", + Description: "Clone only a single branch of the Git repository.", }, { Flag: "git-username", Env: "GIT_USERNAME", Value: serpent.StringOf(&o.GitUsername), - Description: "GitUsername is the username to use for Git authentication. This is optional.", + Description: "The username to use for Git authentication. This is optional.", }, { Flag: "git-password", Env: "GIT_PASSWORD", Value: serpent.StringOf(&o.GitPassword), - Description: "GitPassword is the password to use for Git authentication. This is optional.", + Description: "The password to use for Git authentication. This is optional.", }, { Flag: "git-http-proxy-url", Env: "GIT_HTTP_PROXY_URL", Value: serpent.StringOf(&o.GitHTTPProxyURL), - Description: "GitHTTPProxyURL is the url for the http proxy. This is optional.", + Description: "The URL for the HTTP proxy. This is optional.", }, { Flag: "workspace-folder", Env: "WORKSPACE_FOLDER", Value: serpent.StringOf(&o.WorkspaceFolder), - Description: "WorkspaceFolder is the path to the workspace folder that will " + + Description: "The path to the workspace folder that will " + "be built. This is optional.", }, { Flag: "ssl-cert-base64", Env: "SSL_CERT_BASE64", Value: serpent.StringOf(&o.SSLCertBase64), - Description: "SSLCertBase64 is the content of an SSL cert file. This is useful " + + Description: "The content of an SSL cert file. This is useful " + "for self-signed certificates.", }, { Flag: "export-env-file", Env: "EXPORT_ENV_FILE", Value: serpent.StringOf(&o.ExportEnvFile), - Description: "ExportEnvFile is an optional file path to a .env file where " + + Description: "Optional file path to a .env file where " + "envbuilder will dump environment variables from devcontainer.json " + "and the built container image.", }, @@ -257,7 +257,7 @@ func (o *Options) CLI() serpent.OptionSet { Flag: "post-start-script-path", Env: "POST_START_SCRIPT_PATH", Value: serpent.StringOf(&o.PostStartScriptPath), - Description: "PostStartScriptPath is the path to a script that will be created " + + Description: "The path to a script that will be created " + "by envbuilder based on the postStartCommand in devcontainer.json, " + "if any is specified (otherwise the script is not created). If this " + "is set, the specified InitCommand should check for the presence of " + From 2e5aa71deac3280901eeca713e8215b9ab8935e3 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 26 Apr 2024 15:27:28 +0000 Subject: [PATCH 17/20] Nit var declaration --- cmd/envbuilder/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go index 628f56c4..aa47ba01 100644 --- a/cmd/envbuilder/main.go +++ b/cmd/envbuilder/main.go @@ -23,7 +23,7 @@ import ( ) func main() { - options := envbuilder.Options{} + var options envbuilder.Options cmd := serpent.Command{ Use: "envbuilder", Options: options.CLI(), From d67ade53f25692d4d572eef393f6ae42b0ce881e Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 26 Apr 2024 15:30:02 +0000 Subject: [PATCH 18/20] Fix git config json removal --- envbuilder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envbuilder.go b/envbuilder.go index 04de17bb..adaef3ac 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -608,7 +608,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) // Remove the Docker config secret file! if options.DockerConfigBase64 != "" { - err = os.Remove(filepath.Join(MagicDir, "o.json")) + err = os.Remove(filepath.Join(MagicDir, "config.json")) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove docker config: %w", err) } From 866853a80b111c31024b4c056d26ec7b4c12d853 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 26 Apr 2024 15:41:01 +0000 Subject: [PATCH 19/20] Add filesystem and logger back to Options --- cmd/envbuilder/main.go | 6 +-- envbuilder.go | 103 +++++++++++++++++------------------- envbuilder_internal_test.go | 37 +++++++++---- options.go | 7 +++ 4 files changed, 86 insertions(+), 67 deletions(-) diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go index aa47ba01..1f6741c8 100644 --- a/cmd/envbuilder/main.go +++ b/cmd/envbuilder/main.go @@ -64,7 +64,7 @@ func main() { os.Setenv("CODER_AGENT_SUBSYSTEM", subsystems) } - logger := func(level codersdk.LogLevel, format string, args ...interface{}) { + options.Logger = func(level codersdk.LogLevel, format string, args ...interface{}) { output := fmt.Sprintf(format, args...) fmt.Fprintln(inv.Stderr, output) if sendLogs != nil { @@ -76,9 +76,9 @@ func main() { } } - err := envbuilder.Run(inv.Context(), options, nil, logger) + err := envbuilder.Run(inv.Context(), options) if err != nil { - logger(codersdk.LogLevelError, "error: %s", err) + options.Logger(codersdk.LogLevelError, "error: %s", err) } return err }, diff --git a/envbuilder.go b/envbuilder.go index adaef3ac..7a183e75 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -77,8 +77,6 @@ var ( MagicFile = filepath.Join(MagicDir, "built") ) -type Logger func(level codersdk.LogLevel, format string, args ...any) - // DockerConfig represents the Docker configuration file. type DockerConfig configfile.ConfigFile @@ -86,7 +84,7 @@ 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, fs billy.Filesystem, logf Logger) error { +func Run(ctx context.Context, options Options) error { // Default to the shell! initArgs := []string{"-c", options.InitScript} if options.InitArgs != "" { @@ -96,8 +94,8 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) return fmt.Errorf("parse init args: %w", err) } } - if fs == nil { - fs = &osfsWithChmod{osfs.New("/")} + if options.Filesystem == nil { + options.Filesystem = &osfsWithChmod{osfs.New("/")} } if options.WorkspaceFolder == "" { var err error @@ -113,14 +111,14 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) now := time.Now() stageNum := stageNumber stageNumber++ - logf(codersdk.LogLevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) + options.Logger(codersdk.LogLevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...)) return func(format string, args ...any) { - logf(codersdk.LogLevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) + options.Logger(codersdk.LogLevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now)) } } - logf(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) + options.Logger(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) var caBundle []byte if options.SSLCertBase64 != "" { @@ -182,14 +180,14 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) if line == "" { continue } - logf(codersdk.LogLevelInfo, "#1: %s", strings.TrimSpace(line)) + options.Logger(codersdk.LogLevelInfo, "#1: %s", strings.TrimSpace(line)) } } }() cloneOpts := CloneRepoOptions{ Path: options.WorkspaceFolder, - Storage: fs, + Storage: options.Filesystem, Insecure: options.Insecure, Progress: writer, SingleBranch: options.GitCloneSingleBranch, @@ -220,14 +218,14 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) endStage("📦 The repository already exists!") } } else { - logf(codersdk.LogLevelError, "Failed to clone repository: %s", fallbackErr.Error()) - logf(codersdk.LogLevelError, "Falling back to the default image...") + options.Logger(codersdk.LogLevelError, "Failed to clone repository: %s", fallbackErr.Error()) + options.Logger(codersdk.LogLevelError, "Falling back to the default image...") } } defaultBuildParams := func() (*devcontainer.Compiled, error) { dockerfile := filepath.Join(MagicDir, "Dockerfile") - file, err := fs.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0644) + file, err := options.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err } @@ -259,14 +257,14 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) if options.DockerfilePath == "" { // Only look for a devcontainer if a Dockerfile wasn't specified. // devcontainer is a standard, so it's reasonable to be the default. - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, logf) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options) if err != nil { - logf(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error()) - logf(codersdk.LogLevelError, "Falling back to the default image...") + options.Logger(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error()) + options.Logger(codersdk.LogLevelError, "Falling back to the default image...") } else { // We know a devcontainer exists. // Let's parse it and use it! - file, err := fs.Open(devcontainerPath) + file, err := options.Filesystem.Open(devcontainerPath) if err != nil { return fmt.Errorf("open devcontainer.json: %w", err) } @@ -283,17 +281,17 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) if err != nil { return fmt.Errorf("no Dockerfile or image found: %w", err) } - logf(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...") + options.Logger(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...") fallbackDockerfile = defaultParams.DockerfilePath } - buildParams, err = devContainer.Compile(fs, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false) + buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false) if err != nil { return fmt.Errorf("compile devcontainer.json: %w", err) } scripts = devContainer.LifecycleScripts } else { - logf(codersdk.LogLevelError, "Failed to parse devcontainer.json: %s", err.Error()) - logf(codersdk.LogLevelError, "Falling back to the default image...") + options.Logger(codersdk.LogLevelError, "Failed to parse devcontainer.json: %s", err.Error()) + options.Logger(codersdk.LogLevelError, "Falling back to the default image...") } } } else { @@ -304,11 +302,11 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) // not defined, show a warning dockerfileDir := filepath.Dir(dockerfilePath) if dockerfileDir != filepath.Clean(options.WorkspaceFolder) && options.BuildContextPath == "" { - logf(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.WorkspaceFolder) - logf(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) + options.Logger(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.WorkspaceFolder) + options.Logger(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir) } - dockerfile, err := fs.Open(dockerfilePath) + dockerfile, err := options.Filesystem.Open(dockerfilePath) if err == nil { content, err := io.ReadAll(dockerfile) if err != nil { @@ -334,7 +332,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) HijackLogrus(func(entry *logrus.Entry) { for _, line := range strings.Split(entry.Message, "\r") { - logf(codersdk.LogLevelInfo, "#2: %s", color.HiBlackString(line)) + options.Logger(codersdk.LogLevelInfo, "#2: %s", color.HiBlackString(line)) } }) @@ -373,7 +371,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) go func() { err := srv.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { - logf(codersdk.LogLevelError, "Failed to serve registry: %s", err.Error()) + options.Logger(codersdk.LogLevelError, "Failed to serve registry: %s", err.Error()) } }() closeAfterBuild = func() { @@ -381,7 +379,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) _ = listener.Close() } if options.CacheRepo != "" { - logf(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") + options.Logger(codersdk.LogLevelWarn, "Overriding cache repo with local registry...") } options.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port) } @@ -406,7 +404,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) skippedRebuild := false build := func() (v1.Image, error) { - _, err := fs.Stat(MagicFile) + _, err := options.Filesystem.Stat(MagicFile) if err == nil && options.SkipRebuild { endStage := startStage("🏗️ Skipping build because of cache...") imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent) @@ -444,13 +442,13 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) go func() { scanner := bufio.NewScanner(stdoutReader) for scanner.Scan() { - logf(codersdk.LogLevelInfo, "%s", scanner.Text()) + options.Logger(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() go func() { scanner := bufio.NewScanner(stderrReader) for scanner.Scan() { - logf(codersdk.LogLevelInfo, "%s", scanner.Text()) + options.Logger(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() cacheTTL := time.Hour * 24 * 7 @@ -530,13 +528,13 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) fallback = true fallbackErr = err case strings.Contains(err.Error(), "unexpected status code 401 Unauthorized"): - logf(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") + options.Logger(codersdk.LogLevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!") } if !fallback || options.ExitOnBuildFailure { return err } - logf(codersdk.LogLevelError, "Failed to build: %s", err) - logf(codersdk.LogLevelError, "Falling back to the default image...") + options.Logger(codersdk.LogLevelError, "Failed to build: %s", err) + options.Logger(codersdk.LogLevelError, "Falling back to the default image...") buildParams, err = defaultBuildParams() if err != nil { return err @@ -553,7 +551,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) // Create the magic file to indicate that this build // has already been ran before! - file, err := fs.Create(MagicFile) + file, err := options.Filesystem.Create(MagicFile) if err != nil { return fmt.Errorf("create magic file: %w", err) } @@ -579,10 +577,10 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) if err != nil { return fmt.Errorf("unmarshal metadata: %w", err) } - logf(codersdk.LogLevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...") + options.Logger(codersdk.LogLevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...") for _, container := range devContainer { if container.RemoteUser != "" { - logf(codersdk.LogLevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser) + options.Logger(codersdk.LogLevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser) configFile.Config.User = container.RemoteUser } @@ -677,7 +675,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) username = buildParams.User } if username == "" { - logf(codersdk.LogLevelWarn, "#3: no user specified, using root") + options.Logger(codersdk.LogLevelWarn, "#3: no user specified, using root") } userInfo, err := getUser(username) @@ -719,7 +717,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) // 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, logf, scripts, skippedRebuild, userInfo); err != nil { + if err := execLifecycleScripts(ctx, options, scripts, skippedRebuild, userInfo); err != nil { return err } @@ -732,7 +730,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) // We execute the initialize script as the root user! os.Setenv("HOME", "/root") - logf(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.SetupScript) + options.Logger(codersdk.LogLevelInfo, "=== Running the setup command %q as the root user...", options.SetupScript) envKey := "ENVBUILDER_ENV" envFile := filepath.Join("/", MagicDir, "environ") @@ -759,7 +757,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) go func() { scanner := bufio.NewScanner(&buf) for scanner.Scan() { - logf(codersdk.LogLevelInfo, "%s", scanner.Text()) + options.Logger(codersdk.LogLevelInfo, "%s", scanner.Text()) } }() @@ -825,7 +823,7 @@ func Run(ctx context.Context, options Options, fs billy.Filesystem, logf Logger) return fmt.Errorf("set uid: %w", err) } - logf(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.InitCommand, initArgs, userInfo.user.Username) + options.Logger(codersdk.LogLevelInfo, "=== Running the init command %s %+v as the %q user...", options.InitCommand, initArgs, userInfo.user.Username) err = syscall.Exec(options.InitCommand, append([]string{options.InitCommand}, initArgs...), os.Environ()) if err != nil { @@ -917,7 +915,6 @@ func execOneLifecycleScript( func execLifecycleScripts( ctx context.Context, options Options, - logf Logger, scripts devcontainer.LifecycleScripts, skippedRebuild bool, userInfo userInfo, @@ -927,16 +924,16 @@ func execLifecycleScripts( } if !skippedRebuild { - if err := execOneLifecycleScript(ctx, logf, scripts.OnCreateCommand, "onCreateCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, options.Logger, scripts.OnCreateCommand, "onCreateCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } } - if err := execOneLifecycleScript(ctx, logf, scripts.UpdateContentCommand, "updateContentCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, options.Logger, scripts.UpdateContentCommand, "updateContentCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } - if err := execOneLifecycleScript(ctx, logf, scripts.PostCreateCommand, "postCreateCommand", userInfo); err != nil { + if err := execOneLifecycleScript(ctx, options.Logger, scripts.PostCreateCommand, "postCreateCommand", userInfo); err != nil { // skip remaining lifecycle commands return nil } @@ -948,7 +945,7 @@ func execLifecycleScripts( return fmt.Errorf("failed to create post-start script: %w", err) } } else { - _ = execOneLifecycleScript(ctx, logf, scripts.PostStartCommand, "postStartCommand", userInfo) + _ = execOneLifecycleScript(ctx, options.Logger, scripts.PostStartCommand, "postStartCommand", userInfo) } } return nil @@ -1001,7 +998,7 @@ func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } -func findDevcontainerJSON(options Options, fs billy.Filesystem, logf Logger) (string, string, error) { +func findDevcontainerJSON(options Options) (string, string, error) { // 0. Check if custom devcontainer directory or path is provided. if options.DevcontainerDir != "" || options.DevcontainerJSONPath != "" { devcontainerDir := options.DevcontainerDir @@ -1032,33 +1029,33 @@ func findDevcontainerJSON(options Options, fs billy.Filesystem, logf Logger) (st // 1. Check `options.WorkspaceFolder`/.devcontainer/devcontainer.json. location := filepath.Join(options.WorkspaceFolder, ".devcontainer", "devcontainer.json") - if _, err := fs.Stat(location); err == nil { + if _, err := options.Filesystem.Stat(location); err == nil { return location, filepath.Dir(location), nil } // 2. Check `options.WorkspaceFolder`/devcontainer.json. location = filepath.Join(options.WorkspaceFolder, "devcontainer.json") - if _, err := fs.Stat(location); err == nil { + if _, err := options.Filesystem.Stat(location); err == nil { return location, filepath.Dir(location), nil } // 3. Check every folder: `options.WorkspaceFolder`/.devcontainer//devcontainer.json. devcontainerDir := filepath.Join(options.WorkspaceFolder, ".devcontainer") - fileInfos, err := fs.ReadDir(devcontainerDir) + fileInfos, err := options.Filesystem.ReadDir(devcontainerDir) if err != nil { return "", "", err } for _, fileInfo := range fileInfos { if !fileInfo.IsDir() { - logf(codersdk.LogLevelDebug, `%s is a file`, fileInfo.Name()) + options.Logger(codersdk.LogLevelDebug, `%s is a file`, fileInfo.Name()) continue } location := filepath.Join(devcontainerDir, fileInfo.Name(), "devcontainer.json") - if _, err := fs.Stat(location); err != nil { - logf(codersdk.LogLevelDebug, `stat %s failed: %s`, location, err.Error()) + if _, err := options.Filesystem.Stat(location); err != nil { + options.Logger(codersdk.LogLevelDebug, `stat %s failed: %s`, location, err.Error()) continue } diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index f72c027d..d9fd3cb9 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -18,7 +18,10 @@ func TestFindDevcontainerJSON(t *testing.T) { fs := memfs.New() // when - _, _, err := findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) + _, _, err := findDevcontainerJSON(Options{ + Filesystem: fs, + WorkspaceFolder: "/workspace", + }) // then require.Error(t, err) @@ -33,7 +36,10 @@ func TestFindDevcontainerJSON(t *testing.T) { require.NoError(t, err) // when - _, _, err = findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) + _, _, err = findDevcontainerJSON(Options{ + Filesystem: fs, + WorkspaceFolder: "/workspace", + }) // then require.Error(t, err) @@ -49,7 +55,10 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/devcontainer.json") // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + Filesystem: fs, + WorkspaceFolder: "/workspace", + }) // then require.NoError(t, err) @@ -67,11 +76,11 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/experimental-devcontainer/devcontainer.json") // when - options := Options{ + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + Filesystem: fs, WorkspaceFolder: "/workspace", DevcontainerDir: "experimental-devcontainer", - } - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) + }) // then require.NoError(t, err) @@ -89,11 +98,11 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/experimental.json") // when - options := Options{ + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + Filesystem: fs, WorkspaceFolder: "/workspace", DevcontainerJSONPath: "experimental.json", - } - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options, fs, nil) + }) // then require.NoError(t, err) @@ -111,7 +120,10 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/devcontainer.json") // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + Filesystem: fs, + WorkspaceFolder: "/workspace", + }) // then require.NoError(t, err) @@ -129,7 +141,10 @@ func TestFindDevcontainerJSON(t *testing.T) { fs.Create("/workspace/.devcontainer/sample/devcontainer.json") // when - devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{WorkspaceFolder: "/workspace"}, fs, nil) + devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{ + Filesystem: fs, + WorkspaceFolder: "/workspace", + }) // then require.NoError(t, err) diff --git a/options.go b/options.go index b37998f8..9066d64b 100644 --- a/options.go +++ b/options.go @@ -1,7 +1,9 @@ package envbuilder import ( + "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" + "github.com/go-git/go-billy/v5" ) // Options contains the configuration for the envbuilder. @@ -35,6 +37,11 @@ type Options struct { SSLCertBase64 string ExportEnvFile string PostStartScriptPath string + // Logger is the logger to use for all operations. + Logger func(level codersdk.LogLevel, format string, args ...interface{}) + // Filesystem is the filesystem to use for all operations. + // Defaults to the host filesystem. + Filesystem billy.Filesystem } // Generate CLI options for the envbuilder command. From ce4bb56ad6e0d65bfbbc2d5343b199617d7d94cb Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 26 Apr 2024 17:19:25 +0000 Subject: [PATCH 20/20] Fix WorkspaceFolder assignment --- envbuilder.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index 7a183e75..acc666f8 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -98,12 +98,11 @@ func Run(ctx context.Context, options Options) error { options.Filesystem = &osfsWithChmod{osfs.New("/")} } if options.WorkspaceFolder == "" { - var err error - folder, err := DefaultWorkspaceFolder(options.GitURL) - options.WorkspaceFolder = folder + f, err := DefaultWorkspaceFolder(options.GitURL) if err != nil { return err } + options.WorkspaceFolder = f } stageNumber := 1