Gitfed
bastien-mrq/gitfed / cmd / gitfed-install / scan.go
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
}