Skip to content

Commit 352fb0b

Browse files
gustavobertoiclaude
andcommitted
feat(cli): wire shared status to the M2 ledger (spec 03)
- `shared status` [--json]: read-only projection of the shared-service ledger (alias, status, ref count, consuming projects) with a best-effort self-healing reconcile (skipped silently when the daemon is down — the ledger view stays truthful). Replaces the stub. - buildManager helper: assembles a workspace.Manager from the CWD (config + read-only docker client keyed by context + state ledger + embedded templates), degrading to ledger-only when no daemon is reachable. - `shared gc`/`doctor` remain stubs pending the up-saga (they stop/reconcile against the daemon); `up`/`down` likewise await the saga + the host provisioning-port design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8a8011f commit 352fb0b

3 files changed

Lines changed: 116 additions & 5 deletions

File tree

internal/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func NewRootCmd(opts Options) *cobra.Command {
6868
newConfigCmd(g),
6969
newGenerateCmd(g),
7070
newTemplateCmd(g),
71+
newSharedCmd(g),
7172
newAliasCmd(g),
7273
newVersionCmd(),
7374
)

internal/cli/shared.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/open-source-cloud/devstack/internal/config"
11+
"github.com/open-source-cloud/devstack/internal/docker"
12+
"github.com/open-source-cloud/devstack/internal/state"
13+
"github.com/open-source-cloud/devstack/internal/workspace"
14+
"github.com/open-source-cloud/devstack/internal/xdg"
15+
)
16+
17+
// newSharedCmd wires `shared status|gc|doctor`. `status` is the M2 read-only
18+
// projection of the ledger (shared services + ref counts + consuming projects);
19+
// `gc`/`doctor` (which stop/reconcile against the daemon) land with the up saga.
20+
func newSharedCmd(g *GlobalOpts) *cobra.Command {
21+
cmd := &cobra.Command{
22+
Use: "shared",
23+
Short: "Inspect and reclaim shared services",
24+
}
25+
cmd.AddCommand(
26+
newSharedStatusCmd(g),
27+
stub("gc", "Reclaim unused shared services", "M2 (up saga)"),
28+
stub("doctor", "Reconcile the ledger against live containers", "M2 (up saga)"),
29+
)
30+
return cmd
31+
}
32+
33+
func newSharedStatusCmd(g *GlobalOpts) *cobra.Command {
34+
return &cobra.Command{
35+
Use: "status",
36+
Short: "Show shared-service ref counts and consuming projects",
37+
Args: cobra.NoArgs,
38+
RunE: func(cmd *cobra.Command, _ []string) error {
39+
mgr, closeFn, err := buildManager(cmd)
40+
if err != nil {
41+
return err
42+
}
43+
defer closeFn()
44+
45+
// Best-effort self-healing reconcile: prune refs for projects whose
46+
// containers are no longer live. Skipped silently if the daemon is down
47+
// (the ledger view below is still truthful about recorded refs).
48+
_, _ = mgr.Reconcile(cmd.Context())
49+
50+
rows, err := mgr.Status()
51+
if err != nil {
52+
return err
53+
}
54+
if g.JSON {
55+
return writeJSON(cmd, map[string]any{"shared": rows})
56+
}
57+
w := cmd.OutOrStdout()
58+
if len(rows) == 0 {
59+
fmt.Fprintln(w, "no shared services recorded yet (run `devstack up`)")
60+
return nil
61+
}
62+
for _, r := range rows {
63+
fmt.Fprintf(w, "%-20s %-10s refs=%d %v\n", r.Alias, r.Status, r.RefCount, r.Projects)
64+
}
65+
return nil
66+
},
67+
}
68+
}
69+
70+
// buildManager assembles a workspace.Manager from the current directory: the
71+
// loaded workspace, the read-only docker client (context keys the ledger), the
72+
// machine-global state ledger, and the embedded template source. The returned
73+
// closer releases the ledger and the docker client.
74+
func buildManager(cmd *cobra.Command) (*workspace.Manager, func(), error) {
75+
cwd, err := os.Getwd()
76+
if err != nil {
77+
return nil, nil, err
78+
}
79+
model, err := config.Load(cwd)
80+
if err != nil {
81+
return nil, nil, err
82+
}
83+
84+
ctx := cmd.Context()
85+
ctxName := state.DefaultContext
86+
var dockerClient docker.Client
87+
if c, err := docker.NewClient(ctx); err == nil {
88+
dockerClient = c
89+
ctxName = c.ContextName()
90+
} else {
91+
// No daemon client: ledger reads still work; reconcile degrades to a no-op.
92+
dockerClient = &docker.MockClient{Context: ctxName}
93+
}
94+
95+
db, err := state.Open(ctx, xdg.DataHome(), ctxName)
96+
if err != nil {
97+
if c, ok := dockerClient.(interface{ Close() error }); ok {
98+
_ = c.Close()
99+
}
100+
return nil, nil, err
101+
}
102+
103+
mgr := &workspace.Manager{
104+
Model: model,
105+
DB: db,
106+
Docker: dockerClient,
107+
Source: builtinSource(),
108+
LockPath: filepath.Join(xdg.RuntimeDir(), "devstack.lock"),
109+
}
110+
closeFn := func() {
111+
db.Close()
112+
_ = dockerClient.Close()
113+
}
114+
return mgr, closeFn, nil
115+
}

internal/cli/stubs.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ func addStubCommands(root *cobra.Command, _ *GlobalOpts) {
4040
stub("sync", "Sync (fetch/pull) all workspace repos", "M3"),
4141
stub("status", "Status table across all workspace repos", "M3"),
4242
),
43-
stub("shared", "Inspect and reclaim shared services", "M2",
44-
stub("status", "Show shared-service ref counts and health", "M2"),
45-
stub("gc", "Reclaim unused shared services", "M2"),
46-
stub("doctor", "Reconcile the ledger against live containers", "M2"),
47-
),
4843
stub("secrets", "Secrets providers (login, keygen)", "M4",
4944
stub("login", "Authenticate a secrets provider", "M4"),
5045
stub("keygen", "Generate an age/SOPS key", "M4"),

0 commit comments

Comments
 (0)