Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion internal/api/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,22 @@ type ClusterStatus struct {
// component breakdown and, when state=failed, the reconcile error.
BundleResult *BundleResult `json:"bundleResult"`
BundleProgress *BundleProgress `json:"bundleProgress"`
FailureReason string `json:"failureReason"`
// Drift check: forge compares the components it installed against what is
// actually running in the vCluster. Nil unless the workspace is an active
// platform-bundle one that has had a successful apply.
BundleHealth *BundleHealth `json:"bundleHealth"`
FailureReason string `json:"failureReason"`
}

// BundleHealth reports whether an `active` workspace still contains the bundle
// components forge installed into it. A workspace can lose them without forge
// noticing (provisioning is request-driven, never drift-correcting), which used
// to surface as a healthy-looking workspace with no apps in it.
type BundleHealth struct {
// "ok" | "degraded" | "unreachable"
Status string `json:"status"`
// Components from the last successful apply that are no longer running.
Missing []string `json:"missing"`
}

// BundleResult is the { succeeded, failed } breakdown of a bundle apply,
Expand Down
21 changes: 21 additions & 0 deletions internal/render/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package render
import (
"fmt"
"io"
"strings"
"time"

"github.com/jedib0t/go-pretty/v6/table"
Expand Down Expand Up @@ -43,6 +44,15 @@ func Status(w io.Writer, s *api.ClusterStatus) {

t.AppendRow(table.Row{"deployments", fmt.Sprintf("%d ready", s.DeploymentsReady)})

if h := s.BundleHealth; h != nil {
switch h.Status {
case "degraded":
t.AppendRow(table.Row{"bundle", Red(fmt.Sprintf("degraded — %d missing", len(h.Missing)))})
case "unreachable":
t.AppendRow(table.Row{"bundle", Yellow("unreachable")})
}
}

if s.Quota != nil {
cpu, mem, storage := s.Quota["cpu"], s.Quota["memory"], s.Quota["storage"]
t.AppendRow(table.Row{"quota", fmt.Sprintf("%s CPU / %s / %s", cpu, mem, storage)})
Expand All @@ -54,6 +64,17 @@ func Status(w io.Writer, s *api.ClusterStatus) {
StatusLine(w, StatusWarn, "Workspace", "Paused")
DetailLine(w, StatusWarn, "Next push or "+Command("grounds cluster up")+" resumes it.")
}

// The workspace says `active`, but components forge installed are gone. This
// is what an emptied vCluster looks like — previously it just showed up as
// "0 deployments" and nothing said anything was wrong.
if h := s.BundleHealth; h != nil && h.Status == "degraded" {
fmt.Fprintln(w)
StatusLine(w, StatusError, "Workspace", "Bundle components missing")
DetailLine(w, StatusError, strings.Join(h.Missing, ", "))
DetailLine(w, StatusError, "The workspace is running but its bundle base is gone. "+
Command("grounds cluster reset")+" rebuilds it.")
}
}

func fmtUTC(t *time.Time) string {
Expand Down
37 changes: 37 additions & 0 deletions internal/render/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,40 @@ func TestStatus_PausedShowsWarning(t *testing.T) {
t.Errorf("no warning detail\n%s", out)
}
}

// An emptied vCluster used to render as a perfectly healthy "active, 0 ready"
// workspace — nothing told the engineer their bundle base was gone.
func TestStatus_BundleDegraded(t *testing.T) {
SetEnabled(true)
buf := &bytes.Buffer{}
Status(buf, &api.ClusterStatus{
Namespace: "vcluster-x",
State: "active",
Profile: "platform-bundle",
DeploymentsReady: 0,
BundleHealth: &api.BundleHealth{
Status: "degraded",
Missing: []string{"velocity", "minestom-lobby"},
},
})
out := buf.String()
for _, want := range []string{"degraded", "2 missing", "velocity", "minestom-lobby", "grounds cluster reset"} {
if !strings.Contains(out, want) {
t.Errorf("missing %q\n%s", want, out)
}
}
}

func TestStatus_BundleOkIsQuiet(t *testing.T) {
SetEnabled(true)
buf := &bytes.Buffer{}
Status(buf, &api.ClusterStatus{
Namespace: "vcluster-x",
State: "active",
Profile: "platform-bundle",
BundleHealth: &api.BundleHealth{Status: "ok"},
})
if out := buf.String(); strings.Contains(out, "degraded") || strings.Contains(out, "reset") {
t.Errorf("healthy workspace must not warn:\n%s", out)
}
}
Loading