Files
worker/internal/sshinstall/plan.go
Gleb Tv 4651deb280
Все проверки выполнены успешно
CI / test (push) Successful in 3m33s
Docker / Build and publish worker image (push) Successful in 18m37s
test(installer): add OpenSSH distro harness
2026-08-12 22:00:14 +03:00

260 строки
9.4 KiB
Go

package sshinstall
import (
"fmt"
"net/url"
"strings"
)
// DefaultGoVersion is the pinned Go toolchain version the source
// installer plans by default. It is overridable through install options
// when a host needs a different toolchain.
const DefaultGoVersion = "1.26.0"
// DefaultRepo is the publicly readable worker repository. It requires no
// source credential and is the plan's default clone URL.
const DefaultRepo = "https://rocketgit.ru/rsmon/worker.git"
// DefaultBranch is the branch the source installer checks out and
// builds.
const DefaultBranch = "main"
// Toolchain is a pinned, checksum-verified Go toolchain download for a
// remote Linux architecture. The SHA-256 is baked for the default
// toolchain version so planning never needs the network.
type Toolchain struct {
Version string // e.g. "1.26.0"
Arch string // download archive suffix, e.g. "linux-amd64"
URL string // direct download URL
SHA256 string // published SHA-256 (64 lowercase hex)
}
// toolchainSHA pins the official go.dev SHA-256 checksums for the
// default Go version per Linux archive. Sources: https://go.dev/dl/
// (?mode=json) published sums for DefaultGoVersion. Keep this in sync
// with DefaultGoVersion.
var toolchainSHA = map[string]string{
"linux-386": "35e2ec7a7ae6905a1fae5459197b70e3fcbc5e0a786a7d6ba8e49bcd38ad2e26",
"linux-amd64": "aac1b08a0fb0c4e0a7c1555beb7b59180b05dfc5a3d62e40e9de90cd42f88235",
"linux-arm64": "bd03b743eb6eb4193ea3c3fd3956546bf0e3ca5b7076c8226334afe6b75704cd",
"linux-armv6l": "3f6b48d96f0d8dff77e4625aa179e0449f6bbe79b6986bfa711c2cfc1257ebd8",
"linux-loong64": "33947cd7686f1cd5f097d2a5a30427a4ade114ea00b7570c85a2abf1af3d0507",
"linux-mips": "a4ece61d4bac43b6983fde2c6b9cfc1af7f0d5d6a073219583d4e93b11559c25",
"linux-mips64": "197c2e97fa9ec1ad05998e0982d1a1ae761980df154424e5f29f3912e9ea4e5e",
"linux-mips64le": "61c52b4ab0dceae29f10df29045483596c3f06810c9b511e8336a97428a95a1b",
"linux-mipsle": "b3a13cc5a5f9250b02cf4ba19914c90c7034e68a5ccb9affa5198aadbcedac9a",
"linux-ppc64": "ef7232a49101d163a93bac34d03bfbc4fb18f75d7526d77ac307e16d9d83c300",
"linux-ppc64le": "3066b2284b554da76cf664d217490792ba6f292ec0fc20bf9615e173cc0d2800",
"linux-riscv64": "ab9226ecddda0f682365c949114b653a66c2e9330e7b8d3edea80858437d2ff2",
"linux-s390x": "d62137f11530b97f3503453ad7d9e570af070770599fb8054f4e8cd0e905a453",
}
// GoArch maps a remote `uname -m` value to the Go download archive
// suffix used by go.dev. Every suffix it can return must have a pinned
// checksum in toolchainSHA; the table-consistency test enforces that.
// Unknown values error.
func GoArch(unameM string) (string, error) {
switch strings.ToLower(strings.TrimSpace(unameM)) {
case "x86_64", "amd64":
return "amd64", nil
case "aarch64", "arm64":
return "arm64", nil
case "armv6l", "armv7l":
return "armv6l", nil
case "i386", "i486", "i586", "i686", "386":
return "386", nil
case "loongarch64":
return "loong64", nil
case "mips":
return "mips", nil
case "mipsel":
return "mipsle", nil
case "mips64":
return "mips64", nil
case "mips64el":
return "mips64le", nil
case "ppc64":
return "ppc64", nil
case "ppc64le":
return "ppc64le", nil
case "riscv64":
return "riscv64", nil
case "s390x":
return "s390x", nil
default:
return "", fmt.Errorf("unsupported machine architecture %q", unameM)
}
}
// ToolchainFor returns the pinned, checksum-verified Go toolchain for a
// remote Linux architecture. A non-default version has no baked
// checksum yet and must be resolved through a checksum source by the
// executor work package.
func ToolchainFor(goarch, version string) (Toolchain, error) {
if version == "" {
version = DefaultGoVersion
}
suffix := "linux-" + goarch
t := Toolchain{
Version: version,
Arch: suffix,
URL: "https://go.dev/dl/go" + version + "." + suffix + ".tar.gz",
}
if version != DefaultGoVersion {
return t, fmt.Errorf("no baked checksum for Go %s; only %s is pinned (resolve %s via the checksum source)",
version, DefaultGoVersion, suffix)
}
sha, ok := toolchainSHA[suffix]
if !ok {
return Toolchain{}, fmt.Errorf("no pinned Go %s toolchain for %s", version, suffix)
}
t.SHA256 = sha
return t, nil
}
// SourceOptions are the operator-configurable knobs that the source
// plan is resolved against. Every field is optional; empty values fall
// back to the pinned defaults.
type SourceOptions struct {
Repo string // clone URL; default DefaultRepo
Branch string // default DefaultBranch
GoVersion string // default DefaultGoVersion
GoArch string // go archive suffix; when empty, derived from UnameM
UnameM string // remote `uname -m` output; required unless GoArch set
BuildDir string // remote clone/build directory
GoModuleProxy string // GOPROXY override; empty keeps the Go default
}
// SourcePlan is the pure, resolved source-install plan. Producing it
// never touches the network or the remote host; the executor work
// package turns it into remote commands.
type SourcePlan struct {
Repo string
Branch string
BuildDir string
GoModuleProxy string
Toolchain Toolchain
Packages []string // packages to install via the package manager
InitSystem InitSystem
}
// PlanSource resolves a source-install plan for a detected host. It
// returns an error before any remote mutation could happen when the
// target cannot be planned (unknown distro, unsupported architecture,
// malformed repository).
func PlanSource(d Detection, opts SourceOptions) (SourcePlan, error) {
if d.PackageManager == PkgUnknown {
return SourcePlan{}, fmt.Errorf("unsupported distro %q: no package manager", d.ID)
}
goarch := strings.TrimSpace(opts.GoArch)
if goarch == "" {
var err error
goarch, err = GoArch(opts.UnameM)
if err != nil {
return SourcePlan{}, err
}
}
toolchain, err := ToolchainFor(goarch, opts.GoVersion)
if err != nil {
return SourcePlan{}, err
}
repo := strings.TrimSpace(opts.Repo)
if repo == "" {
repo = DefaultRepo
}
if err := validateRepoURL(repo); err != nil {
return SourcePlan{}, err
}
branch := strings.TrimSpace(opts.Branch)
if branch == "" {
branch = DefaultBranch
}
buildDir := strings.TrimSpace(opts.BuildDir)
if buildDir == "" {
buildDir = "/opt/rsmon-worker-src"
}
if !strings.HasPrefix(buildDir, "/") {
return SourcePlan{}, fmt.Errorf("build directory must be absolute, got %q", buildDir)
}
return SourcePlan{
Repo: repo,
Branch: branch,
BuildDir: buildDir,
GoModuleProxy: strings.TrimSpace(opts.GoModuleProxy),
Toolchain: toolchain,
Packages: packagePrereqs(d.PackageManager),
InitSystem: d.InitSystem,
}, nil
}
// packagePrereqs returns the minimal package set the source installer
// plans to install for a package manager: git, CA certificates, and
// download/archive tools. It never plans a C compiler or build-essential
// because the worker builds with CGO disabled.
func packagePrereqs(pkg PackageManager) []string {
switch pkg {
case PkgApk:
return []string{"git", "ca-certificates", "curl", "tar", "gzip"}
case PkgApt:
return []string{"git", "ca-certificates", "curl", "tar", "gzip"}
case PkgPacman:
return []string{"git", "ca-certificates", "curl", "tar", "gzip"}
case PkgDnf, PkgYum:
return []string{"git", "ca-certificates", "curl", "tar", "gzip"}
default:
return nil
}
}
// validateRepoURL rejects repository references that could smuggle a
// command or a non-remote scheme into the clone step. Only http(s) and
// the git protocol are accepted; the default repository is https.
func validateRepoURL(repo string) error {
if strings.ContainsAny(repo, "\r\n\t ") {
return fmt.Errorf("repository URL %q contains whitespace", repo)
}
u, err := url.Parse(repo)
if err != nil || u.Host == "" {
return fmt.Errorf("repository URL %q is not an absolute clone URL", repo)
}
switch u.Scheme {
case "https", "http", "git":
return nil
default:
return fmt.Errorf("repository URL %q uses unsupported scheme %q", repo, u.Scheme)
}
}
// StepKind identifies one ordered remote step the executor will run.
type StepKind string
const (
StepInstallPackages StepKind = "install-packages"
StepInstallToolchain StepKind = "install-toolchain"
StepCloneSource StepKind = "clone-source"
StepCheckoutBranch StepKind = "checkout-branch"
StepBuildWorker StepKind = "build-worker"
StepInstallService StepKind = "install-service"
)
// Step is one ordered, pure planning step. The executor maps each step
// to remote commands; planning does not execute anything.
type Step struct {
Kind StepKind
Detail string
Packages []string // only for StepInstallPackages
}
// Steps returns the ordered source-install plan as stable, reviewable
// steps. It is the contract the executor work package implements.
func (p SourcePlan) Steps() []Step {
return []Step{
{Kind: StepInstallPackages, Detail: "install minimal build prerequisites", Packages: p.Packages},
{Kind: StepInstallToolchain, Detail: "install pinned Go " + p.Toolchain.Version + " (" + p.Toolchain.Arch + ") and verify SHA-256"},
{Kind: StepCloneSource, Detail: "clone " + p.Repo + " into " + p.BuildDir},
{Kind: StepCheckoutBranch, Detail: "check out branch " + p.Branch + " and record the resolved commit"},
{Kind: StepBuildWorker, Detail: "build the worker binary with CGO_ENABLED=0 and trimpath"},
{Kind: StepInstallService, Detail: "atomically install the binary, env, data dir, and " + string(p.InitSystem) + " service definition"},
}
}