Offer only what this machine can deliver
The installer knew one distribution: Debian, with docker, on a new enough release. Everything else it would have offered and then failed at. Three facts now decide what the matrix offers, and each is read from the machine rather than assumed: - The container runtime. Docker where the machine has one, podman on the Red Hat family, which ships no docker at all. Both go through the same compose plugin: compose speaks the Docker API and podman serves it, so there is one compose file and one deployment path, not two. Telling a Fedora operator to add Docker's own repository to a machine that already has a container runtime would have been the wrong trade. - glibc. The server binary is downloaded, not built here, and it is linked against 2.39. Rocky 9 (2.34), Debian 12 and Ubuntu 22.04 cannot run it, so the host shape is refused there with the version found and the container shape named as the answer -- rather than installing a file that cannot start. - The operating system itself. This compiles for macOS and Windows because Go compiles anything, and on either it would read no os-release, find no systemd, and describe a machine that does not exist. It now says what it is and exits. Two bugs the other distributions found, both of which Debian could not have: - The survey reported the first thing in the way and stopped, so on Fedora it asked to start podman.socket, and then -- having done it -- asked for the compose plugin. Needs are named now, not described, and reported together. - apply used the survey taken before dependencies were installed, so on a machine that had no runtime at all it installed podman and then reached for docker. It re-surveys after resolving, and stops if containers still are not usable. The lab takes DISTRO now: debian13, debian12, ubuntu2404, fedora, rocky9, arch, each with its own disk and ssh port so several can be up at once. The cases no longer say "docker" either. install-local passes on Debian 13, Fedora 43 and Rocky 9 -- 20 checks each, ending with a sign-in to the webmail the installer put there.
This commit is contained in:
+170
-29
@@ -32,12 +32,13 @@ import (
|
||||
// anybody anything.
|
||||
type Facts struct {
|
||||
OS OSInfo
|
||||
Root bool // running as uid 0
|
||||
Systemd bool // systemd is pid 1
|
||||
Docker Docker //
|
||||
Node Node //
|
||||
Ports []Port // the ports the suite wants, and who holds them
|
||||
DiskFreeGB int // on the filesystem that would hold the install
|
||||
Root bool // running as uid 0
|
||||
Systemd bool // systemd is pid 1
|
||||
Runtime Runtime // docker or podman, whichever this machine can use
|
||||
Node Node //
|
||||
Glibc Glibc // what a downloaded binary has to be able to run against
|
||||
Ports []Port // the ports the suite wants, and who holds them
|
||||
DiskFreeGB int // on the filesystem that would hold the install
|
||||
MemoryMB int
|
||||
Existing string // path of a previous install's state file, or ""
|
||||
}
|
||||
@@ -50,13 +51,42 @@ type OSInfo struct {
|
||||
Family string // "debian", "rhel", "arch", "suse", "" when unknown
|
||||
}
|
||||
|
||||
// Docker is whether containers are a real option for this user.
|
||||
type Docker struct {
|
||||
Present bool
|
||||
Usable bool // the daemon answers *as this user*, which is the part that matters
|
||||
Version string //
|
||||
Compose string // "v2" when `docker compose` works, "" otherwise
|
||||
// Runtime is whether containers are a real option for this user, and with
|
||||
// what. Docker and podman are both first-class: podman is what the Red Hat
|
||||
// family ships, and refusing to see it would mean telling a Fedora or Rocky
|
||||
// operator to install Docker from a third-party repository on a machine that
|
||||
// already has a container runtime.
|
||||
type Runtime struct {
|
||||
Kind string // "docker", "podman", or "" when neither can be used
|
||||
Present bool // one of them is installed, whether or not it works
|
||||
Usable bool // it answers *as this user*, which is the part that matters
|
||||
Version string
|
||||
Compose string // "v2" when compose works, "" otherwise
|
||||
Socket string // for podman, the API socket compose is pointed at
|
||||
Why string // why it is unusable, in a sentence fit to show someone
|
||||
|
||||
// What is missing, named rather than described, so the caller does not
|
||||
// have to read Why to decide what to do. Reporting only the first thing
|
||||
// in the way meant fixing one of them and being told about the next.
|
||||
NeedsSocket bool
|
||||
NeedsCompose bool
|
||||
}
|
||||
|
||||
// Glibc is the C library a downloaded binary is linked against. The release
|
||||
// binaries are built in a current Debian, and a host install on an older
|
||||
// distribution would put a binary on the machine that cannot start.
|
||||
type Glibc struct {
|
||||
Version string // "2.39"
|
||||
Major int
|
||||
Minor int
|
||||
}
|
||||
|
||||
// AtLeast answers whether this glibc is new enough for a given version.
|
||||
func (g Glibc) AtLeast(major, minor int) bool {
|
||||
if g.Major != major {
|
||||
return g.Major > major
|
||||
}
|
||||
return g.Minor >= minor
|
||||
}
|
||||
|
||||
// Node is what a host install of the webmail would run on.
|
||||
@@ -106,8 +136,9 @@ func Survey(ctx context.Context) Facts {
|
||||
f := Facts{
|
||||
OS: readOSRelease("/etc/os-release"),
|
||||
Systemd: isSystemd(),
|
||||
Docker: surveyDocker(ctx),
|
||||
Runtime: surveyRuntime(ctx),
|
||||
Node: surveyNode(ctx),
|
||||
Glibc: surveyGlibc(ctx),
|
||||
MemoryMB: memoryMB(),
|
||||
}
|
||||
if u, err := user.Current(); err == nil {
|
||||
@@ -166,14 +197,37 @@ func isSystemd() bool {
|
||||
return err == nil && st.IsDir()
|
||||
}
|
||||
|
||||
func surveyDocker(ctx context.Context) Docker {
|
||||
var d Docker
|
||||
bin, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
d.Why = "docker is not installed"
|
||||
// surveyRuntime looks for docker first and podman second, and reports the
|
||||
// one that actually answers. Preferring docker where both exist keeps a
|
||||
// machine that has been set up for docker working the way its operator
|
||||
// expects; podman is what the Red Hat family ships, and is no less
|
||||
// first-class for being second in the list.
|
||||
func surveyRuntime(ctx context.Context) Runtime {
|
||||
if r := surveyDocker(ctx); r.Usable {
|
||||
return r
|
||||
} else if d := r; d.Present {
|
||||
// Docker is installed but broken. Podman may still be here and
|
||||
// working, and saying so is more use than reporting the broken one.
|
||||
if p := surveyPodman(ctx); p.Usable {
|
||||
return p
|
||||
}
|
||||
return d
|
||||
}
|
||||
d.Present = true
|
||||
if p := surveyPodman(ctx); p.Present {
|
||||
return p
|
||||
}
|
||||
return Runtime{Why: "neither docker nor podman is installed"}
|
||||
}
|
||||
|
||||
func surveyDocker(ctx context.Context) Runtime {
|
||||
r := Runtime{Kind: "docker"}
|
||||
bin, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
r.Kind = ""
|
||||
r.Why = "docker is not installed"
|
||||
return r
|
||||
}
|
||||
r.Present = true
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -182,22 +236,109 @@ func surveyDocker(ctx context.Context) Docker {
|
||||
// answers the question that matters: can *this user* run containers?
|
||||
out, err := exec.CommandContext(ctx, bin, "version", "--format", "{{.Server.Version}}").CombinedOutput()
|
||||
if err != nil {
|
||||
d.Why = firstLine(string(out))
|
||||
if d.Why == "" {
|
||||
d.Why = "the docker daemon did not answer"
|
||||
r.Why = firstLine(string(out))
|
||||
if r.Why == "" {
|
||||
r.Why = "the docker daemon did not answer"
|
||||
}
|
||||
return d
|
||||
return r
|
||||
}
|
||||
d.Usable = true
|
||||
d.Version = strings.TrimSpace(string(out))
|
||||
r.Usable = true
|
||||
r.Version = strings.TrimSpace(string(out))
|
||||
|
||||
if err := exec.CommandContext(ctx, bin, "compose", "version").Run(); err == nil {
|
||||
d.Compose = "v2"
|
||||
r.Compose = "v2"
|
||||
} else {
|
||||
d.Usable = false
|
||||
d.Why = "docker compose (v2) is not available"
|
||||
r.Usable = false
|
||||
r.NeedsCompose = true
|
||||
r.Why = "docker compose (v2) is not available"
|
||||
}
|
||||
return d
|
||||
return r
|
||||
}
|
||||
|
||||
// surveyPodman reports podman and the API socket compose can be pointed at.
|
||||
// Compose speaks the Docker API, and podman serves it, so the same compose
|
||||
// plugin drives either -- which is why this installer does not carry two
|
||||
// deployment paths for one compose file.
|
||||
func surveyPodman(ctx context.Context) Runtime {
|
||||
r := Runtime{Kind: "podman"}
|
||||
bin, err := exec.LookPath("podman")
|
||||
if err != nil {
|
||||
r.Kind = ""
|
||||
r.Why = "podman is not installed"
|
||||
return r
|
||||
}
|
||||
r.Present = true
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := exec.CommandContext(ctx, bin, "version", "--format", "{{.Version}}").CombinedOutput()
|
||||
if err != nil {
|
||||
r.Why = firstLine(string(out))
|
||||
if r.Why == "" {
|
||||
r.Why = "podman did not answer"
|
||||
}
|
||||
return r
|
||||
}
|
||||
r.Version = strings.TrimSpace(string(out))
|
||||
|
||||
// Two things make compose work here, and both are checked before
|
||||
// reporting: the API socket podman.socket serves, which is not running
|
||||
// on a fresh machine, and the compose plugin itself.
|
||||
for _, path := range []string{"/run/podman/podman.sock"} {
|
||||
if st, err := os.Stat(path); err == nil && st.Mode()&os.ModeSocket != 0 {
|
||||
r.Socket = path
|
||||
}
|
||||
}
|
||||
r.NeedsSocket = r.Socket == ""
|
||||
if _, err := os.Stat(ComposePluginPath); err == nil {
|
||||
r.Compose = "v2"
|
||||
} else {
|
||||
r.NeedsCompose = true
|
||||
}
|
||||
switch {
|
||||
case r.NeedsSocket && r.NeedsCompose:
|
||||
r.Why = "podman's API socket is not running (podman.socket), and the compose plugin is not installed"
|
||||
case r.NeedsSocket:
|
||||
r.Why = "podman's API socket is not running (podman.socket)"
|
||||
case r.NeedsCompose:
|
||||
r.Why = "the compose plugin is not installed"
|
||||
default:
|
||||
r.Usable = true
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ComposePluginPath is where this installer puts the compose plugin, and
|
||||
// where it looks for one it put there before. The same file serves docker
|
||||
// and podman.
|
||||
const ComposePluginPath = "/usr/local/lib/docker/cli-plugins/docker-compose"
|
||||
|
||||
// surveyGlibc reads the C library's version, which decides whether a
|
||||
// downloaded binary can run here at all. The release binaries are built in a
|
||||
// current Debian; an older distribution can host containers perfectly well
|
||||
// and still be unable to start that file.
|
||||
func surveyGlibc(ctx context.Context) Glibc {
|
||||
var g Glibc
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
// getconf is in glibc itself; ldd --version is the fallback for a
|
||||
// machine where it is missing.
|
||||
out, err := exec.CommandContext(ctx, "getconf", "GNU_LIBC_VERSION").Output()
|
||||
if err != nil || len(out) == 0 {
|
||||
out, err = exec.CommandContext(ctx, "ldd", "--version").Output()
|
||||
if err != nil {
|
||||
return g
|
||||
}
|
||||
}
|
||||
m := regexp.MustCompile(`([0-9]+)\.([0-9]+)`).FindStringSubmatch(firstLine(string(out)))
|
||||
if m == nil {
|
||||
return g
|
||||
}
|
||||
g.Major, _ = strconv.Atoi(m[1])
|
||||
g.Minor, _ = strconv.Atoi(m[2])
|
||||
g.Version = m[1] + "." + m[2]
|
||||
return g
|
||||
}
|
||||
|
||||
var nodeVersion = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)`)
|
||||
|
||||
Reference in New Issue
Block a user