Skip to content

Commit 28774cd

Browse files
gustavobertoiclaude
andcommitted
feat(cli): promote expose/ports to top-level (keep shared aliases)
Finish the long-standing fix/expose-toplevel work: `devstack expose` and `devstack ports` are now top-level commands (the canonical spelling), while `devstack shared expose` / `shared ports` remain as backward-compatible aliases (fresh instances of the same commands). Renames the constructors newSharedExposeCmd/newSharedPortsCmd → newExposeCmd/newPortsCmd. - root.go registers top-level expose/ports; shared.go keeps the aliases. - expose_test.go asserts both paths resolve + the --off flag. - guide notes updated (no longer "mid-refactor"). make ci + determinism green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ef795da commit 28774cd

7 files changed

Lines changed: 39 additions & 90 deletions

File tree

‎docs/guide/command-reference.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ covered in [global-flags.md](global-flags.md).
1212
> `queue`/`topic`/`stream rm`) **require `--yes`** when run under `--json` or
1313
> non-interactively.
1414
15-
> **Promotion note.** `shared expose` / `shared ports` are the current spelling;
16-
> a branch is promoting them to top-level `expose` / `ports` (mid-refactor). Use
17-
> the `shared`-prefixed forms today.
15+
> **Naming note.** `expose` / `ports` exist at the top level (canonical) and as
16+
> `shared expose` / `shared ports` (backward-compatible aliases) — same behavior.
1817
1918
## Lifecycle
2019

‎docs/guide/concepts.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ devstack shared ports # show the published ports + connection stri
8787
```
8888

8989
See [Shared services & host access](shared-services.md) for the details. (These
90-
two verbs are currently spelled `shared expose` / `shared ports`; a branch is
91-
promoting them to top-level `expose` / `ports`.)
90+
verbs work as top-level `expose` / `ports` and as `shared expose` / `shared ports`
91+
aliases.)
9292

9393
## Stateless CLI, no daemon
9494

‎docs/guide/shared-services.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,10 @@ Key properties:
132132
- **Opt-in per service** — with no arguments every exposable engine is published;
133133
name engines to narrow it.
134134

135-
> **Naming note.** The current, shipping spelling is `devstack shared expose` and
136-
> `devstack shared ports` (documented here). These are being **promoted to
137-
> top-level** commands — `devstack expose` and `devstack ports` — on a branch
138-
> that is still mid-refactor. When that lands, the top-level forms become the
139-
> canonical spelling; the `shared`-prefixed forms are what you use today.
135+
> **Naming note.** These are available **both** as top-level `devstack expose` /
136+
> `devstack ports` (the canonical spelling) **and** as `devstack shared expose` /
137+
> `devstack shared ports` (backward-compatible aliases). Use whichever you prefer —
138+
> they run the same logic.
140139

141140
## Seeing the ports: `shared ports`
142141

‎internal/cli/expose.go‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import (
99
"github.com/open-source-cloud/devstack/internal/orchestrate"
1010
)
1111

12-
// newSharedExposeCmd wires `shared expose [services...]` — publish the shared
13-
// engines on stable 127.0.0.1 host ports so GUI clients (DataGrip, a Redis/S3
14-
// browser, the RabbitMQ UI) can connect. Opt-in and loopback-only; it never
15-
// touches the deterministic generated compose (an up-time overlay). `--off`
16-
// removes the publish and returns the stack to DNS-only.
17-
func newSharedExposeCmd(g *GlobalOpts) *cobra.Command {
12+
// newExposeCmd wires the top-level `expose [services...]` (also `shared expose`) —
13+
// publish the shared engines on stable 127.0.0.1 host ports so GUI clients
14+
// (DataGrip, a Redis/S3 browser, the RabbitMQ UI) can connect. Opt-in and
15+
// loopback-only; it never touches the deterministic generated compose (an up-time
16+
// overlay). `--off` removes the publish and returns the stack to DNS-only.
17+
func newExposeCmd(g *GlobalOpts) *cobra.Command {
1818
var off bool
1919
cmd := &cobra.Command{
2020
Use: "expose [services...]",
@@ -53,9 +53,9 @@ func newSharedExposeCmd(g *GlobalOpts) *cobra.Command {
5353
return cmd
5454
}
5555

56-
// newSharedPortsCmd wires `shared ports` — the read-only projection of the
56+
// newPortsCmd wires `ports` (and `shared ports`) — the read-only projection of the
5757
// currently-published host ports + connection strings (lock-free snapshot).
58-
func newSharedPortsCmd(g *GlobalOpts) *cobra.Command {
58+
func newPortsCmd(g *GlobalOpts) *cobra.Command {
5959
return &cobra.Command{
6060
Use: "ports",
6161
Short: "Show the published 127.0.0.1 host ports for shared services (and connection strings)",

‎internal/cli/expose_test.go‎

Lines changed: 17 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,23 @@
11
package cli
22

3-
import (
4-
"bytes"
5-
"strings"
6-
"testing"
7-
8-
"github.com/spf13/cobra"
9-
10-
"github.com/open-source-cloud/devstack/internal/orchestrate"
11-
)
12-
13-
func exposeFixture() []orchestrate.ExposedPort {
14-
return []orchestrate.ExposedPort{
15-
{Instance: "postgres", Engine: "postgres", Alias: "shared-postgres", Label: "postgres", Host: "127.0.0.1", Port: 55432, Container: 5432, Primary: true, URL: "postgres://devstack:devstack@127.0.0.1:55432/postgres?sslmode=disable"},
16-
{Instance: "minio", Engine: "minio", Alias: "shared-minio", Label: "console", Host: "127.0.0.1", Port: 59001, Container: 9001, Primary: false, URL: "http://127.0.0.1:59001"},
17-
}
18-
}
19-
20-
func TestRenderExposed_Table(t *testing.T) {
21-
var buf bytes.Buffer
22-
cmd := &cobra.Command{}
23-
cmd.SetOut(&buf)
24-
if err := renderExposed(cmd, &GlobalOpts{}, exposeFixture()); err != nil {
25-
t.Fatal(err)
26-
}
27-
out := buf.String()
28-
for _, want := range []string{"shared-postgres", "55432", "shared-minio (console)", "59001", "per-project database:"} {
29-
if !strings.Contains(out, want) {
30-
t.Errorf("table missing %q:\n%s", want, out)
3+
import "testing"
4+
5+
// TestExposePortsRegistered asserts expose/ports are available both at the top
6+
// level (the promotion) and under `shared` (backward-compatible aliases).
7+
func TestExposePortsRegistered(t *testing.T) {
8+
root := NewRootCmd(Options{})
9+
for _, path := range [][]string{
10+
{"expose"}, {"ports"},
11+
{"shared", "expose"}, {"shared", "ports"},
12+
} {
13+
c, _, err := root.Find(path)
14+
if err != nil || c.RunE == nil {
15+
t.Errorf("%v not registered as a real command: %v", path, err)
3116
}
3217
}
33-
}
34-
35-
func TestRenderExposed_JSON(t *testing.T) {
36-
var buf bytes.Buffer
37-
cmd := &cobra.Command{}
38-
cmd.SetOut(&buf)
39-
if err := renderExposed(cmd, &GlobalOpts{JSON: true}, exposeFixture()); err != nil {
40-
t.Fatal(err)
41-
}
42-
out := buf.String()
43-
if !strings.Contains(out, "\"exposed\"") || !strings.Contains(out, "\"port\": 55432") {
44-
t.Errorf("json missing fields:\n%s", out)
45-
}
46-
}
47-
48-
func TestRenderExposed_Quiet(t *testing.T) {
49-
var buf bytes.Buffer
50-
cmd := &cobra.Command{}
51-
cmd.SetOut(&buf)
52-
if err := renderExposed(cmd, &GlobalOpts{Quiet: true}, exposeFixture()); err != nil {
53-
t.Fatal(err)
54-
}
55-
out := strings.TrimSpace(buf.String())
56-
// Quiet emits only the connection URLs, one per line.
57-
lines := strings.Split(out, "\n")
58-
if len(lines) != 2 || !strings.HasPrefix(lines[0], "postgres://") {
59-
t.Errorf("quiet should print only URLs, got:\n%s", out)
60-
}
61-
}
62-
63-
// TestSharedExposeCommandsRegistered guards that `shared expose` and
64-
// `shared ports` are wired into the shared command tree.
65-
func TestSharedExposeCommandsRegistered(t *testing.T) {
66-
sh := newSharedCmd(&GlobalOpts{})
67-
have := map[string]bool{}
68-
for _, c := range sh.Commands() {
69-
have[c.Name()] = true
70-
}
71-
for _, want := range []string{"expose", "ports", "status", "gc", "doctor"} {
72-
if !have[want] {
73-
t.Errorf("shared subcommand %q not registered", want)
74-
}
18+
// The top-level expose carries its --off flag.
19+
c, _, _ := root.Find([]string{"expose"})
20+
if c.Flags().Lookup("off") == nil {
21+
t.Error("expose missing --off flag")
7522
}
7623
}

‎internal/cli/root.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ func NewRootCmd(opts Options) *cobra.Command {
8585
newStatusCmd(g),
8686
newUseCmd(g),
8787
newContextCmd(g),
88+
newExposeCmd(g),
89+
newPortsCmd(g),
8890
newLogsCmd(g),
8991
newDashboardCmd(g),
9092
newDnsCmd(g),

‎internal/cli/shared.go‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ func newSharedCmd(g *GlobalOpts) *cobra.Command {
2727
newSharedStatusCmd(g),
2828
newSharedGcCmd(g),
2929
newSharedDoctorCmd(g),
30-
newSharedExposeCmd(g),
31-
newSharedPortsCmd(g),
30+
// `shared expose`/`shared ports` stay as aliases of the top-level `expose`/
31+
// `ports` (fresh instances; same logic) for backward compatibility.
32+
newExposeCmd(g),
33+
newPortsCmd(g),
3234
)
3335
return cmd
3436
}

0 commit comments

Comments
 (0)