Все проверки выполнены успешно
CI / test (push) Successful in 3m13s
Docker / Build and publish worker image (push) Successful in 10m35s
299 строки
11 KiB
Go
299 строки
11 KiB
Go
package sshinstall
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"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"
|
|
|
|
var (
|
|
// goVersionPattern bounds Go toolchain version strings that are
|
|
// interpolated into remote shell commands and download URLs.
|
|
goVersionPattern = regexp.MustCompile(`^[0-9][0-9A-Za-z._-]*$`)
|
|
// goArchPattern bounds Go download archive suffixes (e.g. "amd64")
|
|
// that are interpolated into remote shell commands and URLs.
|
|
goArchPattern = regexp.MustCompile(`^[0-9A-Za-z][0-9A-Za-z_-]*$`)
|
|
)
|
|
|
|
// ValidGoVersion reports whether a Go toolchain version uses only safe
|
|
// characters (digits, letters, dots, dashes, underscores) and starts
|
|
// with a digit. Versions are embedded in remote shell commands and
|
|
// download URLs, so the charset is the injection boundary.
|
|
func ValidGoVersion(v string) bool { return goVersionPattern.MatchString(v) }
|
|
|
|
// ValidGoArch reports whether a Go download archive suffix uses only
|
|
// safe characters. Suffixes are embedded in remote shell commands and
|
|
// download URLs, so the charset is the injection boundary.
|
|
func ValidGoArch(a string) bool { return goArchPattern.MatchString(a) }
|
|
|
|
// 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 // branch to build; empty means "the remote default branch"
|
|
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 != "" && !ValidGoArch(goarch) {
|
|
return SourcePlan{}, fmt.Errorf("invalid Go architecture %q: only letters, digits, dashes, and underscores are allowed", goarch)
|
|
}
|
|
if goarch == "" {
|
|
var err error
|
|
goarch, err = GoArch(opts.UnameM)
|
|
if err != nil {
|
|
return SourcePlan{}, err
|
|
}
|
|
}
|
|
if version := strings.TrimSpace(opts.GoVersion); version != "" && !ValidGoVersion(version) {
|
|
return SourcePlan{}, fmt.Errorf("invalid Go version %q: only digits, letters, dots, dashes, and underscores are allowed", version)
|
|
}
|
|
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)
|
|
// An empty branch means "build the remote's default branch" (the
|
|
// public repo currently publishes master). The executor resolves and
|
|
// records the remote default; a non-empty branch is pinned and must
|
|
// exist on the remote.
|
|
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 https is
|
|
// accepted (the default repository is https), and userinfo such as
|
|
// `user:pass@host` is rejected so credentials can never reach the remote
|
|
// clone command or the clone's config. An empty value is accepted here
|
|
// (it falls back to the default repository when planning).
|
|
func ValidateRepoURL(repo string) error {
|
|
if repo == "" {
|
|
return nil
|
|
}
|
|
return validateRepoURL(repo)
|
|
}
|
|
|
|
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)
|
|
}
|
|
if u.User != nil {
|
|
return fmt.Errorf("repository URL %q must not contain userinfo", repo)
|
|
}
|
|
if u.Scheme != "https" {
|
|
return fmt.Errorf("repository URL %q must use the https scheme", repo)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 {
|
|
checkoutDetail := "check out branch " + p.Branch + " and record the resolved commit"
|
|
if p.Branch == "" {
|
|
checkoutDetail = "check out the remote default branch and record the resolved commit"
|
|
}
|
|
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: checkoutDetail},
|
|
{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"},
|
|
}
|
|
}
|