feat: add deployment foundation and cross-device handoff
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"regexp"
|
||||
"server-deploy/internal/debian"
|
||||
"server-deploy/internal/inspect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Disk struct {
|
||||
State string `json:"state"`
|
||||
AvailableBytes uint64 `json:"availableBytes"`
|
||||
}
|
||||
type Distribution struct {
|
||||
State string `json:"state"`
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Codename string `json:"codename"`
|
||||
}
|
||||
type Resource struct {
|
||||
Path string `json:"path"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
type Report struct {
|
||||
Runtime inspect.Report `json:"runtime"`
|
||||
Distribution Distribution `json:"distribution"`
|
||||
Privilege string `json:"privilege"`
|
||||
Disk Disk `json:"disk"`
|
||||
Resources []Resource `json:"resources"`
|
||||
Inventory debian.Snapshot `json:"inventory"`
|
||||
}
|
||||
type Proposal struct {
|
||||
Executable bool `json:"executable"`
|
||||
Blockers []string `json:"blockers"`
|
||||
ProposedChanges []string `json:"proposedChanges"`
|
||||
Impacts []string `json:"impacts"`
|
||||
}
|
||||
|
||||
var resourcePaths = []string{"var/lib/docker", "var/lib/containerd", "etc/docker", "var/lib/server-deploy", "etc/apt/sources.list.d/docker.sources", "etc/apt/sources.list.d/docker.list"}
|
||||
|
||||
// Probe reads metadata and a bounded os-release file. It never sources shell
|
||||
// files or invokes executables. Disk describes /var/lib, not an arbitrary target.
|
||||
func Probe(runtime inspect.Report, files fs.FS, uid int, disk Disk) Report {
|
||||
r := Report{Runtime: runtime, Distribution: Distribution{State: "not_checked"}, Privilege: "not_checked", Disk: Disk{State: "not_checked"}, Resources: []Resource{}, Inventory: debian.Snapshot{State: "not_checked", Packages: []debian.Installed{}}}
|
||||
if runtime.OS != "linux" {
|
||||
return r
|
||||
}
|
||||
r.Disk = disk
|
||||
r.Privilege = "non_root"
|
||||
if uid == 0 {
|
||||
r.Privilege = "root"
|
||||
} else if uid < 0 {
|
||||
r.Privilege = "unknown"
|
||||
}
|
||||
r.Distribution = readDistribution(files)
|
||||
r.Inventory = debian.Inventory(files)
|
||||
for _, p := range resourcePaths {
|
||||
r.Resources = append(r.Resources, Resource{Path: "/" + p, State: resourceState(files, p)})
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Plan is a proposal, not an executable or approved installation plan. Missing
|
||||
// package inventory/version locks and host identity always block execution.
|
||||
func Plan(r Report) Proposal {
|
||||
p := Proposal{Blockers: []string{}, ProposedChanges: []string{}, Impacts: []string{}}
|
||||
if r.Runtime.OS != "linux" || (r.Runtime.Architecture != "amd64" && r.Runtime.Architecture != "arm64") {
|
||||
p.Blockers = append(p.Blockers, "unsupported_platform")
|
||||
}
|
||||
if r.Distribution.State != "observed" {
|
||||
p.Blockers = append(p.Blockers, "distribution_unverified")
|
||||
} else if r.Distribution.ID != "ubuntu" || !supportedSuite(r.Distribution) {
|
||||
p.Blockers = append(p.Blockers, "unsupported_distribution")
|
||||
}
|
||||
if r.Privilege != "root" {
|
||||
p.Blockers = append(p.Blockers, "root_required")
|
||||
}
|
||||
if r.Runtime.SystemdRuntime != "present" {
|
||||
p.Blockers = append(p.Blockers, "systemd_unverified")
|
||||
}
|
||||
if r.Disk.State != "observed" {
|
||||
p.Blockers = append(p.Blockers, "disk_unverified")
|
||||
} else if r.Disk.AvailableBytes < 5<<30 {
|
||||
p.Blockers = append(p.Blockers, "disk_below_bootstrap_floor")
|
||||
}
|
||||
if r.Runtime.DockerClient != "missing" {
|
||||
p.Blockers = append(p.Blockers, "existing_or_unknown_runtime_requires_review")
|
||||
}
|
||||
// Validate the complete path set as well: an incomplete report is not clean.
|
||||
seen := make(map[string]bool)
|
||||
resourcesClean := len(r.Resources) == len(resourcePaths)
|
||||
for _, v := range r.Resources {
|
||||
if v.State != "missing" || seen[v.Path] {
|
||||
resourcesClean = false
|
||||
}
|
||||
seen[v.Path] = true
|
||||
}
|
||||
for _, path := range resourcePaths {
|
||||
if !seen["/"+path] {
|
||||
resourcesClean = false
|
||||
}
|
||||
}
|
||||
if !resourcesClean {
|
||||
p.Blockers = append(p.Blockers, "existing_resources_require_review")
|
||||
}
|
||||
if r.Inventory.State != "observed" {
|
||||
p.Blockers = append(p.Blockers, "package_inventory_unverified")
|
||||
} else if len(r.Inventory.Packages) > 0 {
|
||||
p.Blockers = append(p.Blockers, "existing_packages_require_review")
|
||||
}
|
||||
if len(p.Blockers) == 0 {
|
||||
p.ProposedChanges = []string{"configure_verified_docker_apt_source", "install_version_locked_docker_packages", "verify_local_engine_and_compose"}
|
||||
p.Impacts = []string{"apt_configuration_and_package_database_changes", "docker_service_may_start_during_install", "docker_may_change_host_network_firewall_rules", "system_disk_usage_increases"}
|
||||
}
|
||||
p.Blockers = append(p.Blockers, "host_identity_unverified", "package_versions_unresolved", "repository_trust_unverified", "network_and_firewall_unverified", "installation_executor_unimplemented")
|
||||
return p
|
||||
}
|
||||
|
||||
func supportedSuite(d Distribution) bool {
|
||||
return map[string]string{"22.04": "jammy", "24.04": "noble", "26.04": "resolute"}[d.Version] == d.Codename && d.Codename != ""
|
||||
}
|
||||
|
||||
var releaseValue = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,63}$`)
|
||||
|
||||
func readDistribution(files fs.FS) Distribution {
|
||||
initial, err := fs.Stat(files, "etc/os-release")
|
||||
if err != nil {
|
||||
return Distribution{State: "unknown"}
|
||||
}
|
||||
if !initial.Mode().IsRegular() || initial.Size() > 65536 {
|
||||
return Distribution{State: "invalid"}
|
||||
}
|
||||
f, err := files.Open("etc/os-release")
|
||||
if err != nil {
|
||||
return Distribution{State: "unknown"}
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return Distribution{State: "unknown"}
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Size() > 65536 {
|
||||
return Distribution{State: "invalid"}
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(f, 65537))
|
||||
if err != nil {
|
||||
return Distribution{State: "unknown"}
|
||||
}
|
||||
if len(raw) > 65536 {
|
||||
return Distribution{State: "invalid"}
|
||||
}
|
||||
values := map[string]string{}
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
key, value, ok := strings.Cut(strings.TrimSpace(line), "=")
|
||||
if key != "ID" && key != "VERSION_ID" && key != "VERSION_CODENAME" {
|
||||
continue
|
||||
}
|
||||
if !ok || values[key] != "" {
|
||||
return Distribution{State: "invalid"}
|
||||
}
|
||||
if len(value) >= 2 && ((value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'')) {
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
if !releaseValue.MatchString(value) {
|
||||
return Distribution{State: "invalid"}
|
||||
}
|
||||
values[key] = value
|
||||
}
|
||||
if len(values) != 3 {
|
||||
return Distribution{State: "invalid"}
|
||||
}
|
||||
return Distribution{State: "observed", ID: values["ID"], Version: values["VERSION_ID"], Codename: values["VERSION_CODENAME"]}
|
||||
}
|
||||
|
||||
// Walk directory entries to observe dangling/intermediate links as existing
|
||||
// resources instead of following them and misreporting a clean install target.
|
||||
func resourceState(files fs.FS, path string) string {
|
||||
parent := "."
|
||||
parts := strings.Split(path, "/")
|
||||
for i, part := range parts {
|
||||
entries, err := fs.ReadDir(files, parent)
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
found := false
|
||||
for _, entry := range entries {
|
||||
if entry.Name() != part {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if i == len(parts)-1 || entry.Type()&fs.ModeSymlink != 0 || !entry.IsDir() {
|
||||
return "present"
|
||||
}
|
||||
if parent == "." {
|
||||
parent = part
|
||||
} else {
|
||||
parent += "/" + part
|
||||
}
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return "missing"
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
Reference in New Issue
Block a user