Files
worker/cmd/rsmon-worker/management.go
Gleb Tv 674a7d82bf
Все проверки выполнены успешно
CI / test (push) Successful in 4m30s
Docker / Build and publish worker image (push) Successful in 17m26s
feat(installer): activate source builds atomically
2026-08-13 02:26:37 +03:00

277 строки
14 KiB
Go

package main
import (
"errors"
"flag"
"fmt"
"os"
"strings"
"rocketgit.ru/rsmon/worker/internal/installer"
)
func dispatchManagementCommand(args []string) (bool, int) {
if len(args) == 0 {
return false, 0
}
switch args[0] {
case "install":
return true, installCommand(args[1:])
case "deploy":
return true, deployCommand(args[1:])
case "source-install":
return true, sourceInstallCommand(args[1:])
default:
return false, 0
}
}
func installCommand(args []string) int {
fs := flag.NewFlagSet("install", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
var opts installer.InstallOptions
var tokenFile, passwordFile string
fs.StringVar(&opts.Binary, "binary", "", "worker binary to install (default: this executable)")
fs.StringVar(&opts.EnvFile, "env-file", "", "worker environment file (systemd-safe KEY=VALUE; overrides .env/process env)")
fs.StringVar(&opts.Token, "token", "", "worker API token (RSMON_TOKEN)")
fs.StringVar(&opts.Token, "api-key", "", "worker API token (alias for --token)")
fs.StringVar(&tokenFile, "token-file", "", "file containing the worker API token")
fs.StringVar(&opts.URL, "url", "", "RSMon server URL (RSMON_URL; default https://rsmon.ru)")
fs.StringVar(&opts.PublicURL, "public-url", "", "advertised public origin (PUBLIC_URL; scheme + host, no path)")
fs.StringVar(&opts.Host, "host", "", "operator console bind address (WORKER_HOST; default 127.0.0.1)")
fs.StringVar(&opts.Port, "port", "", "operator console port (WORKER_PORT; required with --name)")
fs.StringVar(&opts.Login, "login", "", "operator console login (WORKER_LOGIN; default admin with a generated password)")
fs.StringVar(&opts.Password, "password", "", "operator console password (WORKER_PASSWORD)")
fs.StringVar(&passwordFile, "password-file", "", "file containing the operator console password")
fs.StringVar(&opts.Name, "name", "", "instance name: installs a co-located worker (rsmon-worker-<name>) with its own config/data/unit/port")
fs.BoolVar(&opts.Docker, "docker", false, "run the prebuilt Docker image instead of the binary")
fs.StringVar(&opts.Image, "image", installer.DefaultImage, "immutable Docker repository@sha256 digest required with --docker")
fs.BoolVar(&opts.NoStart, "no-start", false, "install and enable without starting")
fs.Usage = func() {
fmt.Fprintln(fs.Output(), "Usage: rsmon-worker install [--token TOKEN|--env-file FILE] [--name NAME] [--port PORT] [options]")
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if fs.NArg() != 0 {
fs.Usage()
return 2
}
var err error
if opts.Token, err = secretValue(opts.Token, tokenFile); err != nil {
fmt.Fprintf(os.Stderr, "install failed: %v\n", err)
return 1
}
if opts.Password, err = secretValue(opts.Password, passwordFile); err != nil {
fmt.Fprintf(os.Stderr, "install failed: %v\n", err)
return 1
}
if err := installer.Install(opts); err != nil {
fmt.Fprintf(os.Stderr, "install failed: %v\n", err)
return 1
}
return 0
}
func deployCommand(args []string) int {
fs := flag.NewFlagSet("deploy", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
var opts installer.DeployOptions
var tokenFile, passwordFile, passphraseFile, sudoPasswordFile string
fs.StringVar(&opts.Host, "host", "", "SSH server hostname or address")
fs.IntVar(&opts.Port, "port", 22, "SSH server port")
fs.StringVar(&opts.User, "user", "", "SSH username")
fs.StringVar(&opts.IdentityFile, "identity-file", "", "SSH private key path")
fs.StringVar(&opts.KeyPassphrase, "key-passphrase", "", "SSH private key passphrase")
fs.StringVar(&passphraseFile, "key-passphrase-file", "", "file containing the private key passphrase")
fs.StringVar(&opts.Password, "password", "", "SSH login password")
fs.StringVar(&passwordFile, "password-file", "", "file containing the SSH login password")
fs.StringVar(&opts.SudoPassword, "sudo-password", "", "remote sudo password")
fs.StringVar(&sudoPasswordFile, "sudo-password-file", "", "file containing the remote sudo password")
fs.StringVar(&opts.KnownHostsFile, "known-hosts", "", "known_hosts path (default: ~/.ssh/known_hosts)")
fs.StringVar(&opts.HostKeyFingerprint, "host-key-fingerprint", "", "expected SHA256 SSH host-key fingerprint")
fs.BoolVar(&opts.InsecureHostKey, "insecure-host-key", false, "disable SSH host-key verification (unsafe)")
fs.StringVar(&opts.Binary, "binary", "", "worker binary to upload (default: this executable)")
fs.StringVar(&opts.Token, "token", "", "worker API token")
fs.StringVar(&opts.Token, "api-key", "", "worker API token (alias for --token)")
fs.StringVar(&tokenFile, "token-file", "", "file containing the worker API token")
fs.StringVar(&opts.URL, "url", installer.DefaultURL, "RSMon server URL")
fs.BoolVar(&opts.Docker, "docker", false, "install the prebuilt Docker image remotely")
fs.StringVar(&opts.Image, "image", installer.DefaultImage, "immutable Docker repository@sha256 digest required with --docker")
fs.BoolVar(&opts.NoStart, "no-start", false, "install and enable without starting")
fs.Usage = func() {
fmt.Fprintln(fs.Output(), "Usage: rsmon-worker deploy --host HOST --user USER --token TOKEN [SSH options]")
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if fs.NArg() != 0 {
fs.Usage()
return 2
}
var err error
if opts.Token, err = secretValue(opts.Token, tokenFile); err != nil {
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
return 1
}
if opts.Password, err = secretValue(opts.Password, passwordFile); err != nil {
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
return 1
}
if opts.KeyPassphrase, err = secretValue(opts.KeyPassphrase, passphraseFile); err != nil {
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
return 1
}
if opts.SudoPassword, err = secretValue(opts.SudoPassword, sudoPasswordFile); err != nil {
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
return 1
}
if err := installer.Deploy(opts); err != nil {
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
return 1
}
fmt.Fprintln(os.Stdout, "rsmon-worker deployed")
return 0
}
func secretValue(direct, path string) (string, error) {
if direct != "" && path != "" {
return "", fmt.Errorf("a secret and its file option cannot both be set")
}
if path == "" {
return direct, nil
}
b, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read secret file: %w", err)
}
return strings.TrimRight(string(b), "\r\n"), nil
}
// sourceInstallCommand drives the remote source installer
// (docs/source-installation.md): prerequisites, verified Go toolchain,
// clone/update, resolved branch/commit record, a staging build, and (work
// package 4) the atomic activation of the staged binary, validated
// environment, data dir, and the detected init's service definition,
// followed by a start and a process + /healthz verification. It reuses
// the deploy SSH options. Any activation/start/health failure rolls back
// to the prior working install; --no-activate keeps the staging-only
// behavior.
func sourceInstallCommand(args []string) int {
fs := flag.NewFlagSet("source-install", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
var opts installer.SourceInstallOptions
var passphraseFile, passwordFile, sudoPasswordFile, tokenFile, workerPasswordFile string
var noActivate bool
fs.StringVar(&opts.Host, "host", "", "SSH server hostname or address")
fs.IntVar(&opts.Port, "port", 22, "SSH server port")
fs.StringVar(&opts.User, "user", "", "SSH username")
fs.StringVar(&opts.IdentityFile, "identity-file", "", "SSH private key path")
fs.StringVar(&opts.KeyPassphrase, "key-passphrase", "", "SSH private key passphrase")
fs.StringVar(&passphraseFile, "key-passphrase-file", "", "file containing the private key passphrase")
fs.StringVar(&opts.Password, "password", "", "SSH login password")
fs.StringVar(&passwordFile, "password-file", "", "file containing the SSH login password")
fs.StringVar(&opts.SudoPassword, "sudo-password", "", "remote sudo password")
fs.StringVar(&sudoPasswordFile, "sudo-password-file", "", "file containing the remote sudo password")
fs.StringVar(&opts.KnownHostsFile, "known-hosts", "", "known_hosts path (default: ~/.ssh/known_hosts)")
fs.StringVar(&opts.HostKeyFingerprint, "host-key-fingerprint", "", "expected SHA256 SSH host-key fingerprint")
fs.BoolVar(&opts.InsecureHostKey, "insecure-host-key", false, "disable SSH host-key verification (unsafe)")
fs.StringVar(&opts.Repo, "repo", "", "worker repository to clone/update (default: public rocketgit.ru repo)")
fs.StringVar(&opts.Branch, "branch", "", "branch to build; empty resolves the remote default branch")
fs.StringVar(&opts.GoVersion, "go-version", "", "Go toolchain version (default: pinned 1.26.0)")
fs.StringVar(&opts.GoArch, "go-arch", "", "Go download archive suffix; empty derives it from the remote architecture")
fs.StringVar(&opts.BuildDir, "build-dir", "", "remote clone/build directory (default /opt/rsmon-worker-src)")
fs.StringVar(&opts.GoModuleProxy, "go-proxy", "", "GOPROXY for the remote build (default: Go default)")
fs.StringVar(&opts.ToolchainDir, "toolchain-dir", "", "remote Go install path ending in /go (default /usr/local/go)")
fs.StringVar(&opts.StageBinary, "stage-binary", "", "staging binary path (default <build-dir>/rsmon-worker)")
fs.DurationVar(&opts.SessionTimeout, "session-timeout", 0, "per-remote-command timeout (default 30m; 0 uses the default)")
fs.BoolVar(&noActivate, "no-activate", false, "stop after the staging build and do not install/start a service (activation is the default)")
fs.StringVar(&opts.Activation.Name, "name", "", "instance name: activates rsmon-worker-<name> with its own config/data/unit/port")
fs.StringVar(&opts.Activation.URL, "url", installer.DefaultURL, "RSMon server URL (RSMON_URL)")
fs.StringVar(&opts.Activation.Token, "token", "", "worker API token (RSMON_TOKEN; required for activation)")
fs.StringVar(&opts.Activation.Token, "api-key", "", "worker API token (alias for --token)")
fs.StringVar(&tokenFile, "token-file", "", "file containing the worker API token")
fs.StringVar(&opts.Activation.PublicURL, "public-url", "", "advertised public origin (PUBLIC_URL; scheme + host, no path)")
fs.StringVar(&opts.Activation.Host, "worker-host", "", "worker webapp bind address (WORKER_HOST; default 127.0.0.1)")
fs.StringVar(&opts.Activation.Port, "worker-port", "", "worker webapp bind port (WORKER_PORT; default 27401, required with --name)")
fs.StringVar(&opts.Activation.Login, "worker-login", "", "operator console login (WORKER_LOGIN; default admin with a generated password)")
fs.StringVar(&opts.Activation.Password, "worker-password", "", "operator console password (WORKER_PASSWORD)")
fs.StringVar(&workerPasswordFile, "worker-password-file", "", "file containing the operator console password")
fs.StringVar(&opts.Activation.EnvFile, "env-file", "", "local systemd-safe env file uploaded for activation (overrides the individual knobs)")
fs.BoolVar(&opts.Activation.NoStart, "no-start", false, "install the binary, env, data dir, and service definition without starting the worker")
fs.Usage = func() {
fmt.Fprintln(fs.Output(), "Usage: rsmon-worker source-install --host HOST --user USER [--token TOKEN|--token-file FILE] [SSH options] [source options]")
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if fs.NArg() != 0 {
fs.Usage()
return 2
}
var err error
if opts.KeyPassphrase, err = secretValue(opts.KeyPassphrase, passphraseFile); err != nil {
fmt.Fprintf(os.Stderr, "source-install failed: %v\n", err)
return 1
}
if opts.Password, err = secretValue(opts.Password, passwordFile); err != nil {
fmt.Fprintf(os.Stderr, "source-install failed: %v\n", err)
return 1
}
if opts.SudoPassword, err = secretValue(opts.SudoPassword, sudoPasswordFile); err != nil {
fmt.Fprintf(os.Stderr, "source-install failed: %v\n", err)
return 1
}
if opts.Activation.Token, err = secretValue(opts.Activation.Token, tokenFile); err != nil {
fmt.Fprintf(os.Stderr, "source-install failed: %v\n", err)
return 1
}
if opts.Activation.Password, err = secretValue(opts.Activation.Password, workerPasswordFile); err != nil {
fmt.Fprintf(os.Stderr, "source-install failed: %v\n", err)
return 1
}
opts.Activation.Activate = !noActivate
res, err := installer.SourceInstall(opts)
if err != nil {
fmt.Fprintf(os.Stderr, "source-install failed: %v\n", err)
return 1
}
fmt.Printf("source install complete\n")
fmt.Printf(" distro: %s\n", res.Detection.Summarize())
fmt.Printf(" toolchain: %s (%s) at %s\n", res.Plan.Toolchain.Version, res.GoArch, res.ToolchainDir)
fmt.Printf(" branch: %s\n", res.ResolvedBranch)
fmt.Printf(" commit: %s\n", res.ResolvedCommit)
fmt.Printf(" record file: %s\n", res.RecordFile)
fmt.Printf(" staged build: %s\n", res.StageBinary)
if res.Activation != nil {
fmt.Printf(" installed to: %s\n", res.Activation.Binary)
fmt.Printf(" env file: %s (mode 0600)\n", res.Activation.EnvFile)
fmt.Printf(" data dir: %s\n", res.Activation.DataDir)
if res.Activation.UnitFile != "" {
fmt.Printf(" service: %s (%s)\n", res.Activation.UnitFile, res.Activation.UnitName)
} else {
fmt.Println(" service: none (no supported init system detected; supervisor-managed)")
}
if res.Activation.Started {
fmt.Printf(" status: running (supervisor=%s, /healthz verified)\n", res.Activation.Supervisor)
} else {
fmt.Printf(" status: installed (not started, --no-start)\n")
}
} else {
fmt.Println(" status: not installed as a service (--no-activate)")
}
return 0
}