package main
import (
"net"
"os/exec"
"runtime"
"strconv"
"time"
)
// checkStatus is a scan item's outcome — deliberately three-valued rather
// than a bool, since "not found" and "found but going to be skipped" both
// render differently from a hard failure (see screens_intro.go).
type checkStatus int
const (
statusOK checkStatus = iota
statusMissing
statusWarn
)
type checkItem struct {
Label string
Detail string
Status checkStatus
}
// scanResult is everything screen 1 ("Analyse de la machine") reports —
// gathered once, non-destructively: nothing here changes any state on the
// machine, it only observes.
type scanResult struct {
OS, Arch string
HasK3s bool
HasDocker bool
HasKubectl bool
HasCertManager bool // only meaningful if HasKubectl && a cluster answers
HasSudo bool
PortsBusy []int // any of 80/443/2222 already bound by something else
}
// requiredPorts are the ports gitfed's own deployment needs free: 80/443
// for Traefik (HTTP-01 challenge + normal traffic), 2222 for gitfed's own
// git+ssh (deliberately not 22, see INSTALL.md).
var requiredPorts = []int{80, 443, 2222}
func runScan() scanResult {
r := scanResult{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}
r.HasK3s = commandExists("k3s")
r.HasDocker = commandExists("docker")
r.HasKubectl = commandExists("kubectl") || r.HasK3s // k3s bundles its own "k3s kubectl"
r.HasSudo = hasPasswordlessSudo()
if r.HasKubectl {
r.HasCertManager = certManagerPresent()
}
for _, p := range requiredPorts {
if portBusy(p) {
r.PortsBusy = append(r.PortsBusy, p)
}
}
return r
}
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
// hasPasswordlessSudo checks non-interactively (-n) so this scan never
// blocks on a password prompt or leaves one dangling — a "no" here just
// means the wizard will prompt for a password later, when it actually
// needs to run something privileged.
func hasPasswordlessSudo() bool {
cmd := exec.Command("sudo", "-n", "true")
return cmd.Run() == nil
}
// certManagerPresent asks the cluster (not just "is the binary installed",
// there is no separate binary) whether the cert-manager namespace exists.
func certManagerPresent() bool {
cmd := exec.Command("kubectl", "get", "namespace", "cert-manager", "--no-headers", "--ignore-not-found")
out, err := cmd.Output()
return err == nil && len(out) > 0
}
// portBusy reports whether something is already listening on port p, by
// attempting (and immediately releasing) a bind — the same check the real
// service will do implicitly when it starts, just surfaced early instead
// of failing later with a less obvious error.
func portBusy(port int) bool {
portStr := strconv.Itoa(port)
l, err := net.Listen("tcp", ":"+portStr)
if err != nil {
return true
}
_ = l.Close()
// hostPort-style services (like gitfed's own git+ssh) bind on all
// interfaces, not just the wildcard the tcp Listen above already
// covers — a quick dial confirms nothing answers there either.
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+portStr, 200*time.Millisecond)
if err == nil {
_ = conn.Close()
return true
}
return false
}