chore: add the --ephemeral server flag (#16126)

Another PR to address https://github.com/coder/coder/issues/15109.

Changes:
- Introduces the `--ephemeral` flag, which changes the Coder config
directory to a temporary location. The config directory is where the
built-in PostgreSQL stores its data, so using a new one results in a
deployment with a fresh state.

The `--ephemeral` flag is set to replace the `--in-memory` flag once the
in-memory database is removed.
This commit is contained in:
Hugo Dutka
2025-01-20 14:31:16 +01:00
committed by GitHub
parent d8fbbcbd36
commit 23cf61aff6
10 changed files with 95 additions and 5 deletions
+32 -4
View File
@@ -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").
+1 -1
View File
@@ -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
}
+37
View File
@@ -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")
+4
View File
@@ -446,6 +446,10 @@ cacheDir: [cache dir]
# Controls whether data will be stored in an in-memory database.
# (default: <unset>, 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: <unset>, 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])