test(installer): add OpenSSH distro harness
Все проверки выполнены успешно
CI / test (push) Successful in 3m33s
Docker / Build and publish worker image (push) Successful in 18m37s

Этот коммит содержится в:
Gleb Tv
2026-08-12 22:00:14 +03:00
родитель cb23f123ae
Коммит 4651deb280
18 изменённых файлов: 2408 добавлений и 21 удалений

221
internal/sshinstall/detect.go Обычный файл
Просмотреть файл

@@ -0,0 +1,221 @@
// Package sshinstall contains the pure detection and planning layer for
// installing the worker from source over SSH. It never touches the
// network or a remote host; the executor that turns a plan into remote
// commands is a later work package (docs/source-installation.md).
//
// The Docker/OpenSSH harness that exercises detection against real
// distro containers lives in internal/installer/harness.
package sshinstall
import (
"fmt"
"strings"
)
// Distro is a supported remote Linux distribution family.
type Distro string
const (
DistroUnknown Distro = "unknown"
DistroAlpine Distro = "alpine"
DistroDebian Distro = "debian"
DistroUbuntu Distro = "ubuntu"
DistroArch Distro = "arch"
DistroCentOS Distro = "centos"
DistroFedora Distro = "fedora"
DistroRocky Distro = "rocky"
DistroAlma Distro = "alma"
DistroRHEL Distro = "rhel"
)
// PackageManager is the remote package manager used to install build
// prerequisites.
type PackageManager string
const (
PkgUnknown PackageManager = ""
PkgApk PackageManager = "apk"
PkgApt PackageManager = "apt"
PkgPacman PackageManager = "pacman"
PkgDnf PackageManager = "dnf"
PkgYum PackageManager = "yum"
)
// InitSystem is the remote init system. The source installer must know
// it before it can install a service definition; Alpine's OpenRC and the
// systemd distros take different paths.
type InitSystem string
const (
InitUnknown InitSystem = ""
InitSystemd InitSystem = "systemd"
InitOpenRC InitSystem = "openrc"
)
// Detection is the resolved identity of a remote Linux host.
type Detection struct {
ID string // os-release ID (lowercase, e.g. "alpine")
Name string // os-release NAME (pretty, may be empty)
VersionID string // os-release VERSION_ID (may be empty)
Distro Distro
PackageManager PackageManager
InitSystem InitSystem
}
// FileProber reports which of the given absolute paths exist on the
// remote host. Detection uses it to distinguish init systems and
// packaging hints without parsing the full command surface. A nil
// prober behaves as if nothing exists.
type FileProber func(paths ...string) map[string]bool
// Detect resolves the distro, package manager, and init system of a
// remote host from its /etc/os-release contents and a path prober.
// Pure and deterministic: no command execution happens here.
func Detect(osRelease string, probe FileProber) Detection {
d := parseOSRelease(osRelease)
d.PackageManager = packageManagerFor(d.ID)
d.InitSystem = initFor(d.ID, probe)
return d
}
// parseOSRelease extracts the fields the installer cares about from the
// /etc/os-release text. Unknown keys are ignored.
func parseOSRelease(data string) Detection {
var d Detection
for _, line := range strings.Split(data, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
// os-release requires KEY=VALUE with no whitespace around the
// equals sign. A spaced assignment is a malformed line and must
// not be interpreted.
if strings.ContainsAny(key, " \t") {
continue
}
value = unquoteOSValue(value)
switch strings.TrimSpace(key) {
case "ID":
d.ID = strings.ToLower(strings.TrimSpace(value))
case "NAME":
d.Name = strings.TrimSpace(value)
case "VERSION_ID":
d.VersionID = strings.TrimSpace(value)
}
}
d.Distro = distroForID(d.ID)
return d
}
// unquoteOSValue strips the surrounding quotes os-release permits and
// unescapes the two escapes the spec defines. Unquoted values pass
// through unchanged.
func unquoteOSValue(value string) string {
value = strings.TrimSpace(value)
if len(value) >= 2 && (value[0] == '"' || value[0] == '\'') && value[len(value)-1] == value[0] {
value = value[1 : len(value)-1]
}
value = strings.ReplaceAll(value, `\"`, `"`)
value = strings.ReplaceAll(value, `\'`, `'`)
value = strings.ReplaceAll(value, `\\`, `\`)
value = strings.ReplaceAll(value, `\$`, `$`)
value = strings.ReplaceAll(value, "`", "")
return value
}
// distroForID maps an os-release ID to a supported distro family.
func distroForID(id string) Distro {
switch id {
case "alpine":
return DistroAlpine
case "debian":
return DistroDebian
case "ubuntu", "linuxmint", "elementary":
return DistroUbuntu
case "arch", "archarm", "manjaro", "endeavouros":
return DistroArch
case "centos":
return DistroCentOS
case "fedora":
return DistroFedora
case "rocky":
return DistroRocky
case "almalinux":
return DistroAlma
case "rhel", "redhat":
return DistroRHEL
default:
return DistroUnknown
}
}
// packageManagerFor maps an os-release ID to its package manager.
// CentOS-family versions differ (dnf on 8+, yum on 7); the planner
// defaults to dnf and the CentOS work package will refine it.
func packageManagerFor(id string) PackageManager {
switch distroForID(id) {
case DistroAlpine:
return PkgApk
case DistroDebian, DistroUbuntu:
return PkgApt
case DistroArch:
return PkgPacman
case DistroCentOS, DistroFedora, DistroRocky, DistroAlma, DistroRHEL:
return PkgDnf
default:
return PkgUnknown
}
}
// initFor infers the init system from well-known filesystem markers. In
// a container the PID 1 command is not a reliable signal (sshd or the
// runtime command runs first), so the installer uses the packaging
// markers systemd and OpenRC leave behind.
func initFor(_ string, probe FileProber) InitSystem {
if probe == nil {
return InitUnknown
}
present := probe(
"/run/systemd/system",
"/usr/lib/systemd/system",
"/etc/systemd/system",
"/sbin/openrc",
"/etc/init.d",
)
if present["/run/systemd/system"] || present["/usr/lib/systemd/system"] || present["/etc/systemd/system"] {
return InitSystemd
}
if present["/sbin/openrc"] || present["/etc/init.d"] {
return InitOpenRC
}
return InitUnknown
}
// Summarize returns a single-line, operator-readable description of the
// detection result.
func (d Detection) Summarize() string {
return fmt.Sprintf("%s (id=%s version=%s, pkg=%s, init=%s)",
displayName(d), d.ID, displayValue(d.VersionID),
displayValue(string(d.PackageManager)), displayValue(string(d.InitSystem)))
}
func displayName(d Detection) string {
if d.Name != "" {
return d.Name
}
if d.ID != "" {
return d.ID
}
return string(d.Distro)
}
func displayValue(v string) string {
if v == "" {
return "unknown"
}
return v
}

120
internal/sshinstall/detect_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,120 @@
package sshinstall
import (
"strings"
"testing"
)
// prober returns a FileProber backed by a fixed path set.
func prober(exist ...string) FileProber {
want := make(map[string]bool, len(exist))
for _, p := range exist {
want[p] = true
}
return func(paths ...string) map[string]bool {
out := make(map[string]bool, len(paths))
for _, p := range paths {
out[p] = want[p]
}
return out
}
}
func TestParseOSReleaseQuoting(t *testing.T) {
d := parseOSRelease(`NAME="Ubuntu 24.04 LTS"
VERSION_ID="24.04"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 24.04 LTS"
`)
if d.ID != "ubuntu" || d.Name != "Ubuntu 24.04 LTS" || d.VersionID != "24.04" {
t.Fatalf("parseOSRelease = %+v", d)
}
if d.Distro != DistroUbuntu {
t.Fatalf("distro = %q, want ubuntu", d.Distro)
}
}
func TestParseOSReleaseSingleQuotedAndEmpty(t *testing.T) {
d := parseOSRelease("# comment\nID='alpine'\nNAME=Alpine\n\n")
if d.ID != "alpine" || d.Name != "Alpine" {
t.Fatalf("parseOSRelease = %+v", d)
}
if d.Distro != DistroAlpine {
t.Fatalf("distro = %q, want alpine", d.Distro)
}
}
func TestParseOSReleaseGarbage(t *testing.T) {
d := parseOSRelease("not an os-release file\nID = spaced\nFOO=bar\n")
if d.ID != "" {
t.Fatalf("ID = %q, want empty", d.ID)
}
if d.Distro != DistroUnknown {
t.Fatalf("distro = %q, want unknown", d.Distro)
}
}
func TestPackageManagerFor(t *testing.T) {
cases := map[string]PackageManager{
"alpine": PkgApk,
"debian": PkgApt,
"ubuntu": PkgApt,
"arch": PkgPacman,
"centos": PkgDnf,
"fedora": PkgDnf,
"rocky": PkgDnf,
"almalinux": PkgDnf,
"rhel": PkgDnf,
"nonsense": PkgUnknown,
"": PkgUnknown,
}
for id, want := range cases {
if got := packageManagerFor(id); got != want {
t.Fatalf("packageManagerFor(%q) = %q, want %q", id, got, want)
}
}
}
func TestDetectInitMarkers(t *testing.T) {
osRelease := "ID=arch\nNAME=Arch Linux\n"
if got := Detect(osRelease, prober("/usr/lib/systemd/system")); got.InitSystem != InitSystemd {
t.Fatalf("arch systemd markers not detected: %+v", got)
}
if got := Detect(osRelease, nil).InitSystem; got != InitUnknown {
t.Fatalf("nil prober should yield unknown init, got %q", got)
}
alpine := Detect("ID=alpine\n", prober("/sbin/openrc", "/etc/init.d"))
if alpine.InitSystem != InitOpenRC {
t.Fatalf("alpine openrc markers not detected: %+v", alpine)
}
if alpine.PackageManager != PkgApk {
t.Fatalf("alpine pkg = %q, want apk", alpine.PackageManager)
}
ubuntu := Detect("ID=ubuntu\n", prober("/run/systemd/system"))
if ubuntu.InitSystem != InitSystemd || ubuntu.PackageManager != PkgApt {
t.Fatalf("ubuntu detection = %+v", ubuntu)
}
}
func TestDetectSystemdWinsOverOpenRC(t *testing.T) {
// A host that carries both markers must be reported as systemd so a
// systemd distro running a containerized openrc stub is not misplanned.
d := Detect("ID=ubuntu\n", prober("/usr/lib/systemd/system", "/sbin/openrc"))
if d.InitSystem != InitSystemd {
t.Fatalf("init = %q, want systemd", d.InitSystem)
}
}
func TestDetectionSummarize(t *testing.T) {
d := Detect("ID=alpine\nNAME=Alpine Linux\n", prober("/sbin/openrc"))
s := d.Summarize()
for _, want := range []string{"Alpine Linux", "id=alpine", "pkg=apk", "init=openrc"} {
if !strings.Contains(s, want) {
t.Fatalf("Summarize() = %q missing %q", s, want)
}
}
}

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"},
}
}

243
internal/sshinstall/plan_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,243 @@
package sshinstall
import (
"strings"
"testing"
)
func TestGoArch(t *testing.T) {
cases := map[string]string{
"x86_64": "amd64",
"X86_64": "amd64",
"amd64": "amd64",
"aarch64": "arm64",
"arm64": "arm64",
"armv7l": "armv6l",
"armv6l": "armv6l",
"i686": "386",
"i386": "386",
"loongarch64": "loong64",
"mips": "mips",
"mipsel": "mipsle",
"mips64": "mips64",
"mips64el": "mips64le",
"ppc64": "ppc64",
"ppc64le": "ppc64le",
"riscv64": "riscv64",
"s390x": "s390x",
}
for in, want := range cases {
got, err := GoArch(in)
if err != nil || got != want {
t.Fatalf("GoArch(%q) = %q, %v; want %q", in, got, err, want)
}
}
for _, bad := range []string{"", "sparc", "x86", "mips64el-le"} {
if _, err := GoArch(bad); err == nil {
t.Fatalf("GoArch(%q) succeeded", bad)
}
}
}
// TestToolchainTableConsistentWithGoArch keeps GoArch and toolchainSHA in
// sync: every `uname -m` mapping must resolve to a pinned checksum, and
// every pinned checksum must be reachable through some `uname -m` value.
func TestToolchainTableConsistentWithGoArch(t *testing.T) {
// unameExamples maps a Go archive suffix to a real `uname -m` value
// that GoArch accepts for it.
unameExamples := map[string]string{
"amd64": "x86_64",
"arm64": "aarch64",
"armv6l": "armv7l",
"386": "i686",
"loong64": "loongarch64",
"mips": "mips",
"mipsle": "mipsel",
"mips64": "mips64",
"mips64le": "mips64el",
"ppc64": "ppc64",
"ppc64le": "ppc64le",
"riscv64": "riscv64",
"s390x": "s390x",
}
for archSuffix, unameValue := range unameExamples {
tc, err := ToolchainFor(archSuffix, DefaultGoVersion)
if err != nil {
t.Fatalf("GoArch(%q) = %q has no pinned checksum: %v", unameValue, archSuffix, err)
}
if tc.SHA256 == "" || tc.Arch != "linux-"+archSuffix {
t.Fatalf("ToolchainFor(%q) = %+v", archSuffix, tc)
}
}
for suffix := range toolchainSHA {
arch := strings.TrimPrefix(suffix, "linux-")
if _, ok := unameExamples[arch]; !ok {
t.Fatalf("checksum %q is not reachable through any GoArch `uname -m` mapping", suffix)
}
}
}
func TestToolchainFor(t *testing.T) {
tc, err := ToolchainFor("amd64", "")
if err != nil {
t.Fatal(err)
}
if tc.Version != DefaultGoVersion {
t.Fatalf("version = %q, want %q", tc.Version, DefaultGoVersion)
}
if tc.Arch != "linux-amd64" {
t.Fatalf("arch = %q, want linux-amd64", tc.Arch)
}
if tc.URL != "https://go.dev/dl/go1.26.0.linux-amd64.tar.gz" {
t.Fatalf("url = %q", tc.URL)
}
if len(tc.SHA256) != 64 {
t.Fatalf("sha = %q, want 64 hex chars", tc.SHA256)
}
for _, c := range tc.SHA256 {
hexDigit := c >= '0' && c <= '9' || c >= 'a' && c <= 'f'
if !hexDigit {
t.Fatalf("sha %q contains non-lowercase-hex char", tc.SHA256)
}
}
}
func TestToolchainForArm64Pinned(t *testing.T) {
tc, err := ToolchainFor("arm64", DefaultGoVersion)
if err != nil {
t.Fatal(err)
}
if tc.SHA256 == "" || tc.Arch != "linux-arm64" {
t.Fatalf("arm64 toolchain not pinned: %+v", tc)
}
}
func TestToolchainForRejectsUnpinnedVersion(t *testing.T) {
if _, err := ToolchainFor("amd64", "1.27.0"); err == nil {
t.Fatal("unpinned Go version accepted")
}
if _, err := ToolchainFor("sparc", ""); err == nil {
t.Fatal("unknown architecture accepted")
}
}
func TestPlanSourceAlpine(t *testing.T) {
d := Detect("ID=alpine\n", prober("/sbin/openrc"))
p, err := PlanSource(d, SourceOptions{UnameM: "x86_64"})
if err != nil {
t.Fatal(err)
}
if p.Repo != DefaultRepo || p.Branch != DefaultBranch {
t.Fatalf("plan defaults wrong: %+v", p)
}
if p.Toolchain.Arch != "linux-amd64" {
t.Fatalf("toolchain = %+v", p.Toolchain)
}
if len(p.Packages) != 5 || p.Packages[0] != "git" {
t.Fatalf("alpine packages = %v", p.Packages)
}
if p.InitSystem != InitOpenRC {
t.Fatalf("init = %q, want openrc", p.InitSystem)
}
steps := p.Steps()
if len(steps) != 6 {
t.Fatalf("steps = %d, want 6", len(steps))
}
if steps[0].Kind != StepInstallPackages || steps[1].Kind != StepInstallToolchain {
t.Fatalf("step order wrong: %+v", steps)
}
if !strings.Contains(steps[1].Detail, "Go 1.26.0") || !strings.Contains(steps[1].Detail, "linux-amd64") {
t.Fatalf("toolchain step detail = %q", steps[1].Detail)
}
if !strings.Contains(steps[5].Detail, "openrc") {
t.Fatalf("install step detail = %q", steps[5].Detail)
}
}
func TestPlanSourceUbuntuArch(t *testing.T) {
ubuntu := Detect("ID=ubuntu\n", prober("/usr/lib/systemd/system"))
p, err := PlanSource(ubuntu, SourceOptions{UnameM: "aarch64"})
if err != nil {
t.Fatal(err)
}
if p.Toolchain.Arch != "linux-arm64" || p.InitSystem != InitSystemd {
t.Fatalf("ubuntu plan = %+v", p)
}
arch := Detect("ID=arch\n", prober("/usr/lib/systemd/system"))
p, err = PlanSource(arch, SourceOptions{UnameM: "x86_64"})
if err != nil {
t.Fatal(err)
}
if p.Packages[0] != "git" || p.InitSystem != InitSystemd {
t.Fatalf("arch plan = %+v", p)
}
}
func TestPlanSourceExplicitOverrides(t *testing.T) {
d := Detect("ID=ubuntu\n", nil)
p, err := PlanSource(d, SourceOptions{
Repo: "https://example.test/worker.git",
Branch: "release-1.0",
GoArch: "arm64",
BuildDir: "/srv/rsmon",
GoModuleProxy: "https://proxy.golang.org,direct",
})
if err != nil {
t.Fatal(err)
}
if p.Repo != "https://example.test/worker.git" || p.Branch != "release-1.0" ||
p.BuildDir != "/srv/rsmon" || p.GoModuleProxy != "https://proxy.golang.org,direct" {
t.Fatalf("overrides not applied: %+v", p)
}
if p.Toolchain.Arch != "linux-arm64" {
t.Fatalf("toolchain = %+v", p.Toolchain)
}
}
func TestPlanSourceErrors(t *testing.T) {
unknown := Detect("ID=weirdos\n", nil)
if _, err := PlanSource(unknown, SourceOptions{UnameM: "x86_64"}); err == nil {
t.Fatal("unknown distro planned")
}
ubuntu := Detect("ID=ubuntu\n", nil)
if _, err := PlanSource(ubuntu, SourceOptions{}); err == nil {
t.Fatal("missing architecture planned")
}
for _, repo := range []string{"file:///tmp/worker.git", "not-a-url", "https://ex ample/x"} {
if _, err := PlanSource(ubuntu, SourceOptions{UnameM: "x86_64", Repo: repo}); err == nil {
t.Fatalf("invalid repo %q planned", repo)
}
}
if _, err := PlanSource(ubuntu, SourceOptions{UnameM: "x86_64", BuildDir: "relative"}); err == nil {
t.Fatal("relative build dir planned")
}
}
func TestPackagePrereqsNeverIncludeCompiler(t *testing.T) {
for _, pkg := range []PackageManager{PkgApk, PkgApt, PkgPacman, PkgDnf} {
for _, name := range packagePrereqs(pkg) {
switch name {
case "build-essential", "gcc", "g++", "base-devel", "gcc-c++", "make":
t.Fatalf("plan includes compiler package %q", name)
}
}
}
if got := packagePrereqs(PkgUnknown); len(got) != 0 {
t.Fatalf("unknown package manager planned packages %v", got)
}
}
func TestValidateRepoURLSchemes(t *testing.T) {
for _, ok := range []string{"https://rocketgit.ru/rsmon/worker.git", "http://x/y", "git://example.test/r"} {
if err := validateRepoURL(ok); err != nil {
t.Fatalf("validateRepoURL(%q): %v", ok, err)
}
}
for _, bad := range []string{"ssh://h@x/r", "s3://bucket/key", "x y", ""} {
if err := validateRepoURL(bad); err == nil {
t.Fatalf("validateRepoURL(%q) succeeded", bad)
}
}
}