Все проверки выполнены успешно
CI / test (push) Successful in 3m33s
Docker / Build and publish worker image (push) Successful in 18m37s
222 строки
6.3 KiB
Go
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
|
|
}
|