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
Этот коммит содержится в:
221
internal/sshinstall/detect.go
Обычный файл
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
|
||||
}
|
||||
Ссылка в новой задаче
Block a user