generated from mathias/template-go-web
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f7c9ab1eb | ||
|
|
3e28dcf69f |
@@ -53,6 +53,70 @@ func HostsFromNodes(nodesJSON []byte) ([]Host, error) {
|
|||||||
return hosts, nil
|
return hosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flux is the reconciliation state of a Flux Kustomization.
|
||||||
|
type Flux struct {
|
||||||
|
Ready bool
|
||||||
|
Reason string
|
||||||
|
Revision string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FluxStatus parses a Flux Kustomization object into its reconcile state.
|
||||||
|
func FluxStatus(kustJSON []byte) (Flux, error) {
|
||||||
|
var k struct {
|
||||||
|
Status struct {
|
||||||
|
Conditions []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
} `json:"conditions"`
|
||||||
|
LastAppliedRevision string `json:"lastAppliedRevision"`
|
||||||
|
} `json:"status"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(kustJSON, &k); err != nil {
|
||||||
|
return Flux{}, fmt.Errorf("parse kustomization: %w", err)
|
||||||
|
}
|
||||||
|
f := Flux{Revision: shortRev(k.Status.LastAppliedRevision)}
|
||||||
|
for _, c := range k.Status.Conditions {
|
||||||
|
if c.Type == "Ready" {
|
||||||
|
f.Ready = c.Status == "True"
|
||||||
|
f.Reason = c.Reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FluxNode renders the Flux reconcile state as a stage node.
|
||||||
|
func FluxNode(f Flux) Node {
|
||||||
|
if f.Ready {
|
||||||
|
return Node{
|
||||||
|
Title: "⟳ Flux · reconciled · " + f.Revision,
|
||||||
|
Pill: "var(--green)",
|
||||||
|
Tags: []string{"live · k8s"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Node{
|
||||||
|
Title: "⟳ Flux · " + f.Reason,
|
||||||
|
Pill: "var(--coral)",
|
||||||
|
Tags: []string{"live · k8s"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortRev turns a Flux revision "main@sha1:<full>" into "main@<short>".
|
||||||
|
func shortRev(rev string) string {
|
||||||
|
at := strings.Index(rev, "@")
|
||||||
|
if at < 0 {
|
||||||
|
return rev
|
||||||
|
}
|
||||||
|
branch, sha := rev[:at], rev[at+1:]
|
||||||
|
if c := strings.LastIndex(sha, ":"); c >= 0 {
|
||||||
|
sha = sha[c+1:]
|
||||||
|
}
|
||||||
|
if len(sha) > 7 {
|
||||||
|
sha = sha[:7]
|
||||||
|
}
|
||||||
|
return branch + "@" + sha
|
||||||
|
}
|
||||||
|
|
||||||
// Deploy is the live state of a Kubernetes Deployment.
|
// Deploy is the live state of a Kubernetes Deployment.
|
||||||
type Deploy struct {
|
type Deploy struct {
|
||||||
Image string
|
Image string
|
||||||
|
|||||||
@@ -98,6 +98,34 @@ func TestNamespaceSummary_CapsWithMore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFluxStatus_ParsesReadyReasonAndShortRevision(t *testing.T) {
|
||||||
|
k := []byte(`{"status":{
|
||||||
|
"conditions":[{"type":"Ready","status":"True","reason":"ReconciliationSucceeded","message":"Applied revision: main@sha1:7a51d1d13944"}],
|
||||||
|
"lastAppliedRevision":"main@sha1:7a51d1d13944c481b04fc434d9a2aca2a25632e4"}}`)
|
||||||
|
|
||||||
|
f, err := atlas.FluxStatus(k)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FluxStatus: %v", err)
|
||||||
|
}
|
||||||
|
if !f.Ready || f.Reason != "ReconciliationSucceeded" || f.Revision != "main@7a51d1d" {
|
||||||
|
t.Fatalf("flux = %+v", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFluxNode_GreenWhenReadyCoralWhenNot(t *testing.T) {
|
||||||
|
ready := atlas.FluxNode(atlas.Flux{Ready: true, Reason: "ReconciliationSucceeded", Revision: "main@7a51d1d"})
|
||||||
|
if ready.Pill != "var(--green)" {
|
||||||
|
t.Fatalf("ready pill = %q", ready.Pill)
|
||||||
|
}
|
||||||
|
if ready.Title != "⟳ Flux · reconciled · main@7a51d1d" {
|
||||||
|
t.Fatalf("title = %q", ready.Title)
|
||||||
|
}
|
||||||
|
notReady := atlas.FluxNode(atlas.Flux{Ready: false, Reason: "BuildFailed"})
|
||||||
|
if notReady.Pill != "var(--coral)" {
|
||||||
|
t.Fatalf("not-ready pill = %q", notReady.Pill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeployState_ParsesImageAndReplicas(t *testing.T) {
|
func TestDeployState_ParsesImageAndReplicas(t *testing.T) {
|
||||||
dep := []byte(`{"spec":{"replicas":2,"template":{"spec":{"containers":[
|
dep := []byte(`{"spec":{"replicas":2,"template":{"spec":{"containers":[
|
||||||
{"name":"cad-atlas","image":"localhost:5000/cad-atlas:3ff922a"}]}}},
|
{"name":"cad-atlas","image":"localhost:5000/cad-atlas:3ff922a"}]}}},
|
||||||
|
|||||||
@@ -73,21 +73,21 @@ func RecentRuns(tasksJSON []byte, n int) ([]RunDot, error) {
|
|||||||
|
|
||||||
// aggregateState folds per-job outcomes into a run outcome.
|
// aggregateState folds per-job outcomes into a run outcome.
|
||||||
func aggregateState(jobs []Job) string {
|
func aggregateState(jobs []Job) string {
|
||||||
allSucceeded := true
|
pending := false
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
switch j.State() {
|
switch j.State() {
|
||||||
case "failure", "cancelled", "error":
|
case "failure", "cancelled", "error":
|
||||||
return "failure"
|
return "failure"
|
||||||
case "success":
|
case "success", "skipped": // completed OK — skipped (e.g. deploy on a tag push) doesn't block
|
||||||
default:
|
default:
|
||||||
allSucceeded = false
|
pending = true // running / in_progress / waiting / queued / unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allSucceeded {
|
if pending {
|
||||||
return "success"
|
|
||||||
}
|
|
||||||
return "running"
|
return "running"
|
||||||
}
|
}
|
||||||
|
return "success"
|
||||||
|
}
|
||||||
|
|
||||||
// LatestRunJobs parses a Gitea `/actions/tasks` response (per-job entries,
|
// LatestRunJobs parses a Gitea `/actions/tasks` response (per-job entries,
|
||||||
// newest first) and returns the newest run with its jobs in pipeline order.
|
// newest first) and returns the newest run with its jobs in pipeline order.
|
||||||
|
|||||||
@@ -89,6 +89,16 @@ func TestRecentRuns_GroupsRunsNewestFirstWithAggregateState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunState_SkippedJobsCountAsOK(t *testing.T) {
|
||||||
|
// A tag-push run skips the deploy job; the run still succeeded.
|
||||||
|
s := atlas.RunSummary{Jobs: []atlas.Job{
|
||||||
|
{Status: "skipped"}, {Status: "success"}, {Status: "success"},
|
||||||
|
}}
|
||||||
|
if s.State() != "success" {
|
||||||
|
t.Fatalf("skipped+success run state = %q, want success", s.State())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLatestRunJobs_ErrorsWhenEmpty(t *testing.T) {
|
func TestLatestRunJobs_ErrorsWhenEmpty(t *testing.T) {
|
||||||
if _, err := atlas.LatestRunJobs([]byte(`{"workflow_runs":[]}`)); err == nil {
|
if _, err := atlas.LatestRunJobs([]byte(`{"workflow_runs":[]}`)); err == nil {
|
||||||
t.Fatal("expected error on empty, got nil")
|
t.Fatal("expected error on empty, got nil")
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ func Deployment() ([]byte, error) {
|
|||||||
return get("/apis/apps/v1/namespaces/cad-atlas/deployments/cad-atlas")
|
return get("/apis/apps/v1/namespaces/cad-atlas/deployments/cad-atlas")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FluxKustomization returns the raw JSON for the Flux `apps` Kustomization that
|
||||||
|
// reconciles this repo's manifests.
|
||||||
|
func FluxKustomization() ([]byte, error) {
|
||||||
|
return get("/apis/kustomize.toolkit.fluxcd.io/v1/namespaces/flux-system/kustomizations/apps")
|
||||||
|
}
|
||||||
|
|
||||||
func get(path string) ([]byte, error) {
|
func get(path string) ([]byte, error) {
|
||||||
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
|
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||||
if host == "" || port == "" {
|
if host == "" || port == "" {
|
||||||
|
|||||||
+14
-2
@@ -56,11 +56,17 @@ func NewHandler() http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ld.deploy != nil || ld.flux != nil {
|
||||||
|
var live []atlas.Node
|
||||||
if ld.deploy != nil {
|
if ld.deploy != nil {
|
||||||
node := atlas.DeployNode(*ld.deploy)
|
live = append(live, atlas.DeployNode(*ld.deploy))
|
||||||
|
}
|
||||||
|
if ld.flux != nil {
|
||||||
|
live = append(live, atlas.FluxNode(*ld.flux))
|
||||||
|
}
|
||||||
for i := range a.Stages {
|
for i := range a.Stages {
|
||||||
if a.Stages[i].Generate == "deploy-state" {
|
if a.Stages[i].Generate == "deploy-state" {
|
||||||
a.Stages[i].Nodes = append([]atlas.Node{node}, a.Stages[i].Nodes...)
|
a.Stages[i].Nodes = append(live, a.Stages[i].Nodes...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +82,7 @@ type liveOverlayData struct {
|
|||||||
ns string
|
ns string
|
||||||
run *atlas.RunSummary
|
run *atlas.RunSummary
|
||||||
deploy *atlas.Deploy
|
deploy *atlas.Deploy
|
||||||
|
flux *atlas.Flux
|
||||||
timeline []atlas.RunDot
|
timeline []atlas.RunDot
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +128,11 @@ func liveOverlay() liveOverlayData {
|
|||||||
d.deploy = &dep
|
d.deploy = &dep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if raw, err := cluster.FluxKustomization(); err == nil {
|
||||||
|
if f, err := atlas.FluxStatus(raw); err == nil {
|
||||||
|
d.flux = &f
|
||||||
|
}
|
||||||
|
}
|
||||||
liveCache = d
|
liveCache = d
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user