feat(installer): multi-instance install with full env resolution
Все проверки выполнены успешно
CI / test (push) Successful in 1m0s
Docker / Build and publish worker image (push) Successful in 16m19s

Rework `rsmon-worker install` so one host can run several isolated
workers and so the installer consumes the full worker env-var set.

- main.go now loads .env before dispatching management commands, so
  install/deploy read the same environment as the runtime.
- New --name flag installs a co-located worker as rsmon-worker-<name>
  with its own binary (/usr/local/bin/rsmon-worker-<name>), config
  (/etc/rsmon-worker-<name>), data dir (/var/lib/rsmon-worker-<name>),
  and systemd unit. Named instances require an explicit WORKER_PORT.
- Configuration is resolved flags > --env-file > process env/.env
  (godotenv) > defaults; the resolved set is written as a stable,
  systemd-safe 0600 env file.
- WORKER_LOGIN/WORKER_PASSWORD default to a generated admin password
  (printed once) when both are unset; XOR is rejected.
- The generated unit is now hardened (After=docker.service, CAP_NET_RAW,
  ProtectSystem=full, ReadWritePaths=data dir) and parameterized by
  instance; the Docker unit is namespaced by instance too.
- install creates the data + config directories and prints a summary
  (unit, binary, env file, data dir, console URL, generated password).
- New flags: --name, --host, --port, --login, --password/--password-file.
- Tests: resolvePaths, validateInstanceName, resolveInstallEnv
  precedence/XOR/port-required, renderEnvFile, validateEnvValue, plus
  named-instance unit assertions. End-to-end verified by installing and
  removing a throwaway --name instance.
- docs/install.md documents the tool, config sources/precedence,
  single- and multi-instance flows, the exact actions performed, the
  generated unit, options, and uninstall.
Этот коммит содержится в:
root
2026-08-03 12:15:29 +03:00
родитель dde720eb44
Коммит 9b82c9f82f
6 изменённых файлов: 812 добавлений и 93 удалений

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

@@ -36,10 +36,14 @@ var (
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
// Load .env before any subcommand dispatch so management commands
// (install/deploy) read the same environment as the worker runtime.
// godotenv.Load only fills variables not already present in the
// process environment, so an explicit export or sudo -E still wins.
loadDotEnv()
if handled, code := dispatchManagementCommand(os.Args[1:]); handled {
os.Exit(code)
}
loadDotEnv()
versionFlag := flag.Bool("version", false, "Print version and exit")
noWeb := flag.Bool("no-web", false, "Disable the local web UI (Phase 1 ships with it on)")

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

@@ -28,18 +28,24 @@ func installCommand(args []string) int {
fs := flag.NewFlagSet("install", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
var opts installer.InstallOptions
var tokenFile string
var tokenFile, passwordFile string
fs.StringVar(&opts.Binary, "binary", "", "worker binary to install (default: this executable)")
fs.StringVar(&opts.EnvFile, "env-file", "", "existing worker environment file")
fs.StringVar(&opts.Token, "token", "", "worker API token")
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", installer.DefaultURL, "RSMon server URL")
fs.StringVar(&opts.URL, "url", "", "RSMon server URL (RSMON_URL; default https://rsmon.ru)")
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 [--url URL] [--no-start]")
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 {
@@ -57,11 +63,14 @@ func installCommand(args []string) int {
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
}
fmt.Fprintln(os.Stdout, "rsmon-worker installed")
return 0
}