Files
worker/internal/sshinstall/detect.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

222 строки
6.3 KiB
Go

// 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
}