diff --git a/cli/server.go b/cli/server.go index 9bb4cfb0a72f2..48f049c163b3b 100644 --- a/cli/server.go +++ b/cli/server.go @@ -391,6 +391,21 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } defer httpServers.Close() + if vals.EphemeralDeployment.Value() { + r.globalConfig = filepath.Join(os.TempDir(), fmt.Sprintf("coder_ephemeral_%d", time.Now().UnixMilli())) + if err := os.MkdirAll(r.globalConfig, 0o700); err != nil { + return xerrors.Errorf("create ephemeral deployment directory: %w", err) + } + cliui.Infof(inv.Stdout, "Using an ephemeral deployment directory (%s)", r.globalConfig) + defer func() { + cliui.Infof(inv.Stdout, "Removing ephemeral deployment directory...") + if err := os.RemoveAll(r.globalConfig); err != nil { + cliui.Errorf(inv.Stderr, "Failed to remove ephemeral deployment directory: %v", err) + } else { + cliui.Infof(inv.Stdout, "Removed ephemeral deployment directory") + } + }() + } config := r.createConfig() builtinPostgres := false @@ -398,7 +413,16 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. if !vals.InMemoryDatabase && vals.PostgresURL == "" { var closeFunc func() error cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)", config.PostgresPath()) - pgURL, closeFunc, err := startBuiltinPostgres(ctx, config, logger) + customPostgresCacheDir := "" + // By default, built-in PostgreSQL will use the Coder root directory + // for its cache. However, when a deployment is ephemeral, the root + // directory is wiped clean on shutdown, defeating the purpose of using + // it as a cache. So here we use a cache directory that will not get + // removed on restart. + if vals.EphemeralDeployment.Value() { + customPostgresCacheDir = cacheDir + } + pgURL, closeFunc, err := startBuiltinPostgres(ctx, config, logger, customPostgresCacheDir) if err != nil { return err } @@ -1202,7 +1226,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. ctx, cancel := inv.SignalNotifyContext(ctx, InterruptSignals...) defer cancel() - url, closePg, err := startBuiltinPostgres(ctx, cfg, logger) + url, closePg, err := startBuiltinPostgres(ctx, cfg, logger, "") if err != nil { return err } @@ -1949,7 +1973,7 @@ func embeddedPostgresURL(cfg config.Root) (string, error) { return fmt.Sprintf("postgres://coder@localhost:%s/coder?sslmode=disable&password=%s", pgPort, pgPassword), nil } -func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logger) (string, func() error, error) { +func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logger, customCacheDir string) (string, func() error, error) { usr, err := user.Current() if err != nil { return "", nil, err @@ -1976,6 +2000,10 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg return "", nil, xerrors.Errorf("parse postgres port: %w", err) } + cachePath := filepath.Join(cfg.PostgresPath(), "cache") + if customCacheDir != "" { + cachePath = filepath.Join(customCacheDir, "postgres") + } stdlibLogger := slog.Stdlib(ctx, logger.Named("postgres"), slog.LevelDebug) ep := embeddedpostgres.NewDatabase( embeddedpostgres.DefaultConfig(). @@ -1983,7 +2011,7 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg BinariesPath(filepath.Join(cfg.PostgresPath(), "bin")). DataPath(filepath.Join(cfg.PostgresPath(), "data")). RuntimePath(filepath.Join(cfg.PostgresPath(), "runtime")). - CachePath(filepath.Join(cfg.PostgresPath(), "cache")). + CachePath(cachePath). Username("coder"). Password(pgPassword). Database("coder"). diff --git a/cli/server_createadminuser.go b/cli/server_createadminuser.go index ed9c7b9bcc921..40d65507dc087 100644 --- a/cli/server_createadminuser.go +++ b/cli/server_createadminuser.go @@ -54,7 +54,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command { if newUserDBURL == "" { cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)", cfg.PostgresPath()) - url, closePg, err := startBuiltinPostgres(ctx, cfg, logger) + url, closePg, err := startBuiltinPostgres(ctx, cfg, logger, "") if err != nil { return err } diff --git a/cli/server_test.go b/cli/server_test.go index 0dba63e7c2fe3..40ea2f0b707cf 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -177,6 +177,43 @@ func TestServer(t *testing.T) { return err == nil && rawURL != "" }, superDuperLong, testutil.IntervalFast, "failed to get access URL") }) + t.Run("EphemeralDeployment", func(t *testing.T) { + t.Parallel() + if testing.Short() { + t.SkipNow() + } + + inv, _ := clitest.New(t, + "server", + "--http-address", ":0", + "--access-url", "http://example.com", + "--ephemeral", + ) + pty := ptytest.New(t).Attach(inv) + + // Embedded postgres takes a while to fire up. + const superDuperLong = testutil.WaitSuperLong * 3 + ctx, cancelFunc := context.WithCancel(testutil.Context(t, superDuperLong)) + errCh := make(chan error, 1) + go func() { + errCh <- inv.WithContext(ctx).Run() + }() + pty.ExpectMatch("Using an ephemeral deployment directory") + rootDirLine := pty.ReadLine(ctx) + rootDir := strings.TrimPrefix(rootDirLine, "Using an ephemeral deployment directory") + rootDir = strings.TrimSpace(rootDir) + rootDir = strings.TrimPrefix(rootDir, "(") + rootDir = strings.TrimSuffix(rootDir, ")") + require.NotEmpty(t, rootDir) + require.DirExists(t, rootDir) + + pty.ExpectMatchContext(ctx, "View the Web UI") + + cancelFunc() + <-errCh + + require.NoDirExists(t, rootDir) + }) t.Run("BuiltinPostgresURL", func(t *testing.T) { t.Parallel() root, _ := clitest.New(t, "server", "postgres-builtin-url") diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 2323d958b537d..96a03c5b1f05e 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -446,6 +446,10 @@ cacheDir: [cache dir] # Controls whether data will be stored in an in-memory database. # (default: , type: bool) inMemoryDatabase: false +# Controls whether Coder data, including built-in Postgres, will be stored in a +# temporary directory and deleted when the server is stopped. +# (default: , type: bool) +ephemeralDeployment: false # Type of auth to use when connecting to postgres. For AWS RDS, using IAM # authentication (awsiamrds) is recommended. # (default: password, type: enum[password\|awsiamrds]) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 49e459f5d1a52..3b53b81d2192a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10875,6 +10875,9 @@ const docTemplate = `{ "enable_terraform_debug_mode": { "type": "boolean" }, + "ephemeral_deployment": { + "type": "boolean" + }, "experiments": { "type": "array", "items": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c3472ca8b9bf0..743e7404c08ec 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9718,6 +9718,9 @@ "enable_terraform_debug_mode": { "type": "boolean" }, + "ephemeral_deployment": { + "type": "boolean" + }, "experiments": { "type": "array", "items": { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index c6696ac1780c4..e1c0b977c00d2 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -350,6 +350,7 @@ type DeploymentValues struct { ProxyTrustedOrigins serpent.StringArray `json:"proxy_trusted_origins,omitempty" typescript:",notnull"` CacheDir serpent.String `json:"cache_directory,omitempty" typescript:",notnull"` InMemoryDatabase serpent.Bool `json:"in_memory_database,omitempty" typescript:",notnull"` + EphemeralDeployment serpent.Bool `json:"ephemeral_deployment,omitempty" typescript:",notnull"` PostgresURL serpent.String `json:"pg_connection_url,omitempty" typescript:",notnull"` PostgresAuth string `json:"pg_auth,omitempty" typescript:",notnull"` OAuth2 OAuth2Config `json:"oauth2,omitempty" typescript:",notnull"` @@ -2282,6 +2283,15 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Value: &c.InMemoryDatabase, YAML: "inMemoryDatabase", }, + { + Name: "Ephemeral Deployment", + Description: "Controls whether Coder data, including built-in Postgres, will be stored in a temporary directory and deleted when the server is stopped.", + Flag: "ephemeral", + Env: "CODER_EPHEMERAL", + Hidden: true, + Value: &c.EphemeralDeployment, + YAML: "ephemeralDeployment", + }, { Name: "Postgres Connection URL", Description: "URL of a PostgreSQL database. If empty, PostgreSQL binaries will be downloaded from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access the built-in database with \"coder server postgres-builtin-url\". Note that any special characters in the URL must be URL-encoded.", diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index c25e513431002..66e85f3f6978a 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -224,6 +224,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "user": {} }, "enable_terraform_debug_mode": true, + "ephemeral_deployment": true, "experiments": [ "string" ], diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 2a1c88e38a797..db6fc2a51f58e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1867,6 +1867,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "user": {} }, "enable_terraform_debug_mode": true, + "ephemeral_deployment": true, "experiments": [ "string" ], @@ -2336,6 +2337,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "user": {} }, "enable_terraform_debug_mode": true, + "ephemeral_deployment": true, "experiments": [ "string" ], @@ -2646,6 +2648,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `disable_path_apps` | boolean | false | | | | `docs_url` | [serpent.URL](#serpenturl) | false | | | | `enable_terraform_debug_mode` | boolean | false | | | +| `ephemeral_deployment` | boolean | false | | | | `experiments` | array of string | false | | | | `external_auth` | [serpent.Struct-array_codersdk_ExternalAuthConfig](#serpentstruct-array_codersdk_externalauthconfig) | false | | | | `external_token_encryption_keys` | array of string | false | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2250d1b0d3221..d5093587ad527 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -623,6 +623,7 @@ export interface DeploymentValues { readonly proxy_trusted_origins?: string; readonly cache_directory?: string; readonly in_memory_database?: boolean; + readonly ephemeral_deployment?: boolean; readonly pg_connection_url?: string; readonly pg_auth?: string; readonly oauth2?: OAuth2Config;