Gitfed
bastien-mrq/gitfed / cmd / gitfed-install / podstatus.go
package main

import (
	"regexp"
	"strconv"
	"strings"
)

type podPhase int

const (
	podUnknown podPhase = iota
	podStarting
	podReady
	podErrImageNeverPull
	podOtherError
)

type podStatus struct {
	Name                             string
	ReadyContainers, TotalContainers int
	RawStatus                        string
	Phase                            podPhase
}

// parsePodStatus reads one line of `kubectl -n gitfed get pods --no-headers`
// output: NAME  READY  STATUS  RESTARTS  AGE. Returns ok=false if out has
// no pod line yet (nothing scheduled) rather than a zero-value guess.
func parsePodStatus(out string) (podStatus, bool) {
	var line string
	for _, l := range strings.Split(strings.TrimSpace(out), "\n") {
		if strings.HasPrefix(strings.TrimSpace(l), "gitfed-") {
			line = l
			break
		}
	}
	if line == "" {
		return podStatus{}, false
	}
	fields := strings.Fields(line)
	if len(fields) < 3 {
		return podStatus{}, false
	}

	ps := podStatus{Name: fields[0], RawStatus: fields[2]}
	if ready, total, ok := parseReadyCount(fields[1]); ok {
		ps.ReadyContainers, ps.TotalContainers = ready, total
	}

	switch {
	case ps.ReadyContainers == ps.TotalContainers && ps.TotalContainers > 0 && ps.RawStatus == "Running":
		ps.Phase = podReady
	case ps.RawStatus == "ErrImageNeverPull":
		ps.Phase = podErrImageNeverPull
	case ps.RawStatus == "Pending" || ps.RawStatus == "ContainerCreating" || ps.RawStatus == "Running":
		ps.Phase = podStarting
	default:
		ps.Phase = podOtherError
	}
	return ps, true
}

func parseReadyCount(s string) (ready, total int, ok bool) {
	a, b, found := strings.Cut(s, "/")
	if !found {
		return 0, 0, false
	}
	ready, err1 := strconv.Atoi(a)
	total, err2 := strconv.Atoi(b)
	return ready, total, err1 == nil && err2 == nil
}

var deploymentImageRe = regexp.MustCompile(`image:\s*(gitfed:\S+)`)

// expectedImageTag extracts the image reference deployment.yaml asks for
// — both containers (server/web) always carry the same tag (see
// deploy/k8s/deployment.yaml), so the first match is authoritative.
func expectedImageTag(deploymentYAML string) (string, bool) {
	m := deploymentImageRe.FindStringSubmatch(deploymentYAML)
	if m == nil {
		return "", false
	}
	return m[1], true
}

var importedImageRe = regexp.MustCompile(`(?:docker\.io/library/)?(gitfed:\S+)`)

// importedImageTags extracts every "gitfed:X.Y.Z"-shaped reference out of
// `k3s ctr images ls` output, however containerd chose to qualify it
// (bare "gitfed:1.2.4" or "docker.io/library/gitfed:1.2.4" are both seen
// in practice depending on how the image was built/imported).
func importedImageTags(ctrImagesOutput string) []string {
	var tags []string
	seen := map[string]bool{}
	for _, m := range importedImageRe.FindAllStringSubmatch(ctrImagesOutput, -1) {
		if !seen[m[1]] {
			seen[m[1]] = true
			tags = append(tags, m[1])
		}
	}
	return tags
}

type tagDiagnosis struct {
	Expected string
	Imported []string
	Matches  bool
}

// diagnoseImageTag is screen 7b's core logic: is the tag deployment.yaml
// asks for actually among what's been imported into containerd? This is
// the automated version of the manual `grep`/`k3s ctr images ls` compare
// INSTALL.md's troubleshooting section walks through by hand.
func diagnoseImageTag(deploymentYAML, ctrImagesOutput string) tagDiagnosis {
	expected, _ := expectedImageTag(deploymentYAML)
	imported := importedImageTags(ctrImagesOutput)
	d := tagDiagnosis{Expected: expected, Imported: imported}
	for _, tag := range imported {
		if tag == expected {
			d.Matches = true
			break
		}
	}
	return d
}