feat(installer): activate source builds atomically
Все проверки выполнены успешно
CI / test (push) Successful in 4m30s
Docker / Build and publish worker image (push) Successful in 17m26s

Этот коммит содержится в:
Gleb Tv
2026-08-13 02:26:37 +03:00
родитель bd6070ee1f
Коммит 674a7d82bf
11 изменённых файлов: 2534 добавлений и 86 удалений

Просмотреть файл

@@ -50,11 +50,23 @@ type SourceInstallOptions struct {
ToolchainDir string
// StageBinary is where the built worker binary is written. It must
// be absolute and defaults to <BuildDir>/rsmon-worker. The running
// service and its config are NOT touched by this work package.
// service and its config are NOT touched until Activation runs.
StageBinary string
// SessionTimeout bounds each remote command. 0 uses the default
// (30 minutes); the build step can legitimately run for minutes.
SessionTimeout time.Duration
// Activation drives work package 4: after the staging build, the
// staged binary, validated environment, data dir, and the detected
// init's service definition are installed atomically and the worker
// is started and verified (process + /healthz). Any failure rolls
// back to the prior working install. Empty keeps SourceInstall at the
// staging boundary and touches no service configuration.
Activation ActivationOptions
// activationEnv is the once-rendered activation environment, computed
// during option normalization so the env file is read and rendered
// exactly once per run (no TOCTOU between preflight and activation).
activationEnv []byte
}
// SourceInstallResult is what a source installation resolved to. The
@@ -70,6 +82,10 @@ type SourceInstallResult struct {
ResolvedCommit string
RecordFile string
StageBinary string
// Activation is set when the staged binary was atomically installed
// and the worker started/verified. It records the installed layout
// and the supervisor used. Nil when Activation was not requested.
Activation *ActivationResult
}
// defaultToolchainDir is the standard Go installation prefix.
@@ -142,14 +158,20 @@ func (e *sourceExecutor) fileProber() sshinstall.FileProber {
// branch, builds the worker to a staging path, and only then records the
// resolved branch and commit.
//
// The running service, its configuration, and its data directory are
// deliberately untouched: atomic activation and rollback are the next
// work package. Every remote step runs with the same privilege path as
// `deploy` (root, passwordless sudo, or sudo -S), every interpolated
// value is single-quoted, every step script fails closed (`set -eu` or
// explicit `&&`/retry), and every failure returns a bounded, actionable
// error. Each remote command is capped by SessionTimeout and its stdout
// is size-bounded.
// When Activation.Activate is set, the staged binary is then atomically
// installed together with the validated environment, data directory, and
// the detected init's service definition, and the worker is started and
// verified (process + /healthz); any activation/start/health failure
// rolls back to the prior working install. Without activation the
// running service, its configuration, and its data directory are
// deliberately untouched (the staging boundary of work package 3).
//
// Every remote step runs with the same privilege path as `deploy` (root,
// passwordless sudo, or sudo -S), every interpolated value is
// single-quoted, every step script fails closed (`set -eu` or explicit
// `&&`/retry), and every failure returns a bounded, actionable error.
// Each remote command is capped by SessionTimeout and its stdout is
// size-bounded.
func SourceInstall(opts SourceInstallOptions) (*SourceInstallResult, error) {
opts, err := normalizeSourceOptions(opts)
if err != nil {
@@ -276,6 +298,15 @@ func SourceInstall(opts SourceInstallOptions) (*SourceInstallResult, error) {
if _, err := executor.runPrivileged("sh -c " + shellQuote(commitRecordScript(plan.BuildDir, branch, commit))); err != nil {
return nil, fmt.Errorf("record resolved commit: %w", err)
}
// Work package 4: atomically install the staged build and activate
// the worker. On any failure the remote script restores the previous
// working install and this step returns a bounded error.
if opts.Activation.Activate {
if err := executor.activateWorker(opts, result); err != nil {
return nil, err
}
}
return result, nil
}
@@ -316,6 +347,20 @@ func normalizeSourceOptions(o SourceInstallOptions) (SourceInstallOptions, error
if o.SessionTimeout <= 0 {
o.SessionTimeout = defaultSessionTimeout
}
if o.Activation.Activate {
// Render (and validate) the activation environment exactly once
// here. The rendered bytes are reused at activation time, so the
// env file is read a single time and cannot change between the
// preflight and the remote install (env-file TOCTOU).
env, err := o.Activation.renderEnv()
if err != nil {
return o, fmt.Errorf("activation: %w", err)
}
if err := validateRenderedEnv(env); err != nil {
return o, fmt.Errorf("activation: %w", err)
}
o.activationEnv = env
}
return o, nil
}