test(installer): add OpenSSH distro harness
Все проверки выполнены успешно
CI / test (push) Successful in 3m33s
Docker / Build and publish worker image (push) Successful in 18m37s
Все проверки выполнены успешно
CI / test (push) Successful in 3m33s
Docker / Build and publish worker image (push) Successful in 18m37s
Этот коммит содержится в:
259
internal/sshinstall/plan.go
Обычный файл
259
internal/sshinstall/plan.go
Обычный файл
@@ -0,0 +1,259 @@
|
||||
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"},
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user