feat(installer): activate source builds atomically
Все проверки выполнены успешно
CI / test (push) Successful in 4m30s
Docker / Build and publish worker image (push) Successful in 17m26s
Все проверки выполнены успешно
CI / test (push) Successful in 4m30s
Docker / Build and publish worker image (push) Successful in 17m26s
Этот коммит содержится в:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user