detached b7cdfee

feat: add gordon config show command

commit
b7cdfeeda2510d72997b6d33ce5537cc3e442085
author
brice · · 2026-03-14 03:20 UTC
parent
590fa2e
signature
unsigned
4 files · +235 -0
A docs/cli/config.md +59 -0
@@ -0,0 +1,59 @@
1+# Config Commands
2+
3+Inspect Gordon server configuration.
4+
5+Remote targeting uses client config or an active remote by default.
6+Use `--remote` and `--token` to override. See [CLI Overview](./index.md).
7+
8+## gordon config
9+
10+### Subcommands
11+
12+| Subcommand | Description |
13+|------------|-------------|
14+| `show` | Show server configuration |
15+
16+---
17+
18+## gordon config show
19+
20+Display the full Gordon server configuration including server settings,
21+auto-route, network isolation, routes, and external routes.
22+
23+```bash
24+gordon config show
25+gordon config show --json
26+gordon config show --remote https://gordon.mydomain.com --token $TOKEN
27+```
28+
29+### Flags
30+
31+| Flag | Description |
32+|------|-------------|
33+| `--json` | Output as JSON |
34+
35+### JSON Output
36+
37+```json
38+{
39+ "server": {
40+ "port": 1111,
41+ "registry_port": 5000,
42+ "registry_domain": "reg.example.com",
43+ "data_dir": "/var/lib/gordon"
44+ },
45+ "auto_route": {
46+ "enabled": true
47+ },
48+ "network_isolation": {
49+ "enabled": true,
50+ "prefix": "gordon_"
51+ },
52+ "routes": [
53+ {"domain": "app.example.com", "image": "myapp:latest"}
54+ ],
55+ "external_routes": {
56+ "reg.example.com": "localhost:5000"
57+ }
58+}
59+```
@@ -0,0 +1,59 @@
1+# Config Commands
2+
3+Inspect Gordon server configuration.
4+
5+Remote targeting uses client config or an active remote by default.
6+Use `--remote` and `--token` to override. See [CLI Overview](./index.md).
7+
8+## gordon config
9+
10+### Subcommands
11+
12+| Subcommand | Description |
13+|------------|-------------|
14+| `show` | Show server configuration |
15+
16+---
17+
18+## gordon config show
19+
20+Display the full Gordon server configuration including server settings,
21+auto-route, network isolation, routes, and external routes.
22+
23+```bash
24+gordon config show
25+gordon config show --json
26+gordon config show --remote https://gordon.mydomain.com --token $TOKEN
27+```
28+
29+### Flags
30+
31+| Flag | Description |
32+|------|-------------|
33+| `--json` | Output as JSON |
34+
35+### JSON Output
36+
37+```json
38+{
39+ "server": {
40+ "port": 1111,
41+ "registry_port": 5000,
42+ "registry_domain": "reg.example.com",
43+ "data_dir": "/var/lib/gordon"
44+ },
45+ "auto_route": {
46+ "enabled": true
47+ },
48+ "network_isolation": {
49+ "enabled": true,
50+ "prefix": "gordon_"
51+ },
52+ "routes": [
53+ {"domain": "app.example.com", "image": "myapp:latest"}
54+ ],
55+ "external_routes": {
56+ "reg.example.com": "localhost:5000"
57+ }
58+}
59+```
M docs/cli/index.md +1 -0
@@ -36,6 +36,7 @@ Management commands run locally through in-process services by default. Add `--r
3636 | `gordon status` | Show Gordon server status | [status](./status.md) |
3737 | `gordon backups` | Manage database backups | [backup](./backup.md) |
3838 | `gordon images` | List and prune images | [images](./images.md) |
39+| `gordon config show` | Show server configuration | [config](./config.md) |
3940
4041 ## Client Commands
4142
@@ -36,6 +36,7 @@ Management commands run locally through in-process services by default. Add `--r
36 | `gordon status` | Show Gordon server status | [status](./status.md) |36 | `gordon status` | Show Gordon server status | [status](./status.md) |
37 | `gordon backups` | Manage database backups | [backup](./backup.md) |37 | `gordon backups` | Manage database backups | [backup](./backup.md) |
38 | `gordon images` | List and prune images | [images](./images.md) |38 | `gordon images` | List and prune images | [images](./images.md) |
39+| `gordon config show` | Show server configuration | [config](./config.md) |
39 40
40 ## Client Commands41 ## Client Commands
41 42
A internal/adapters/in/cli/config.go +171 -0
@@ -0,0 +1,171 @@
1+package cli
2+
3+import (
4+ "context"
5+ "fmt"
6+ "io"
7+ "sort"
8+
9+ "github.com/bnema/gordon/internal/adapters/in/cli/remote"
10+ "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
11+ "github.com/spf13/cobra"
12+)
13+
14+func newConfigCmd() *cobra.Command {
15+ cmd := &cobra.Command{
16+ Use: "config",
17+ Short: "Inspect Gordon configuration",
18+ }
19+
20+ cmd.AddCommand(newConfigShowCmd())
21+
22+ return cmd
23+}
24+
25+func newConfigShowCmd() *cobra.Command {
26+ var jsonOut bool
27+
28+ cmd := &cobra.Command{
29+ Use: "show",
30+ Short: "Show server configuration",
31+ Long: `Display the full Gordon server configuration including server settings,
32+auto-route, network isolation, routes, and external routes.
33+
34+Examples:
35+ gordon config show
36+ gordon config show --json
37+ gordon config show --remote https://gordon.mydomain.com --token $TOKEN`,
38+ RunE: func(cmd *cobra.Command, _ []string) error {
39+ ctx := context.Background()
40+ handle, err := resolveControlPlane(configPath)
41+ if err != nil {
42+ return err
43+ }
44+ defer handle.close()
45+ return runConfigShow(ctx, handle.plane, cmd.OutOrStdout(), jsonOut)
46+ },
47+ }
48+
49+ cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
50+
51+ return cmd
52+}
53+
54+func runConfigShow(ctx context.Context, cp ControlPlane, out io.Writer, jsonOut bool) error {
55+ config, err := cp.GetConfig(ctx)
56+ if err != nil {
57+ return fmt.Errorf("failed to get config: %w", err)
58+ }
59+
60+ if jsonOut {
61+ return writeJSON(out, config)
62+ }
63+
64+ return renderConfigTable(out, config)
65+}
66+
67+func renderConfigTable(out io.Writer, config *remote.Config) error {
68+ if err := renderConfigSummary(out, config); err != nil {
69+ return err
70+ }
71+ if err := renderConfigRoutes(out, config); err != nil {
72+ return err
73+ }
74+ return renderConfigExternalRoutes(out, config)
75+}
76+
77+func renderConfigSummary(out io.Writer, config *remote.Config) error {
78+ if err := cliWriteLine(out, cliRenderTitle("Gordon Configuration")); err != nil {
79+ return err
80+ }
81+ if err := cliWriteLine(out, ""); err != nil {
82+ return err
83+ }
84+ if err := cliWriteLine(out, cliRenderMeta("Server Port:", fmt.Sprintf("%d", config.Server.Port))); err != nil {
85+ return err
86+ }
87+ if err := cliWriteLine(out, cliRenderMeta("Registry Port:", fmt.Sprintf("%d", config.Server.RegistryPort))); err != nil {
88+ return err
89+ }
90+ if err := cliWriteLine(out, cliRenderMeta("Registry Domain:", config.Server.RegistryDomain)); err != nil {
91+ return err
92+ }
93+ if err := cliWriteLine(out, cliRenderMeta("Data Directory:", config.Server.DataDir)); err != nil {
94+ return err
95+ }
96+ if err := cliWriteLine(out, cliRenderMeta("Auto-Route:", fmt.Sprintf("%v", config.AutoRoute.Enabled))); err != nil {
97+ return err
98+ }
99+ if err := cliWriteLine(out, cliRenderMeta("Network Isolation:", fmt.Sprintf("%v", config.NetworkIsolation.Enabled))); err != nil {
100+ return err
101+ }
102+ if config.NetworkIsolation.Prefix != "" {
103+ if err := cliWriteLine(out, cliRenderMeta("Network Prefix:", config.NetworkIsolation.Prefix)); err != nil {
104+ return err
105+ }
106+ }
107+ return nil
108+}
109+
110+func renderConfigRoutes(out io.Writer, config *remote.Config) error {
111+ if err := cliWriteLine(out, ""); err != nil {
112+ return err
113+ }
114+ if len(config.Routes) == 0 {
115+ if err := cliWriteLine(out, cliRenderMuted("No routes configured")); err != nil {
116+ return err
117+ }
118+ } else {
119+ if err := cliWriteLine(out, cliRenderTitle("Routes")); err != nil {
120+ return err
121+ }
122+ rows := make([][]string, 0, len(config.Routes))
123+ for _, route := range config.Routes {
124+ rows = append(rows, []string{route.Domain, route.Image})
125+ }
126+ table := components.NewTable(
127+ components.WithColumns([]components.TableColumn{
128+ {Title: "Domain", Width: 30},
129+ {Title: "Image", Width: 45},
130+ }),
131+ components.WithRows(rows),
132+ )
133+ if err := cliWriteLine(out, table.View()); err != nil {
134+ return err
135+ }
136+ }
137+ return nil
138+}
139+
140+func renderConfigExternalRoutes(out io.Writer, config *remote.Config) error {
141+ if len(config.ExternalRoutes) > 0 {
142+ if err := cliWriteLine(out, ""); err != nil {
143+ return err
144+ }
145+ if err := cliWriteLine(out, cliRenderTitle("External Routes")); err != nil {
146+ return err
147+ }
148+ domains := make([]string, 0, len(config.ExternalRoutes))
149+ for d := range config.ExternalRoutes {
150+ domains = append(domains, d)
151+ }
152+ sort.Strings(domains)
153+
154+ rows := make([][]string, 0, len(domains))
155+ for _, d := range domains {
156+ rows = append(rows, []string{d, config.ExternalRoutes[d]})
157+ }
158+ table := components.NewTable(
159+ components.WithColumns([]components.TableColumn{
160+ {Title: "Domain", Width: 30},
161+ {Title: "Target", Width: 45},
162+ }),
163+ components.WithRows(rows),
164+ )
165+ if err := cliWriteLine(out, table.View()); err != nil {
166+ return err
167+ }
168+ }
169+
170+ return nil
171+}
@@ -0,0 +1,171 @@
1+package cli
2+
3+import (
4+ "context"
5+ "fmt"
6+ "io"
7+ "sort"
8+
9+ "github.com/bnema/gordon/internal/adapters/in/cli/remote"
10+ "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
11+ "github.com/spf13/cobra"
12+)
13+
14+func newConfigCmd() *cobra.Command {
15+ cmd := &cobra.Command{
16+ Use: "config",
17+ Short: "Inspect Gordon configuration",
18+ }
19+
20+ cmd.AddCommand(newConfigShowCmd())
21+
22+ return cmd
23+}
24+
25+func newConfigShowCmd() *cobra.Command {
26+ var jsonOut bool
27+
28+ cmd := &cobra.Command{
29+ Use: "show",
30+ Short: "Show server configuration",
31+ Long: `Display the full Gordon server configuration including server settings,
32+auto-route, network isolation, routes, and external routes.
33+
34+Examples:
35+ gordon config show
36+ gordon config show --json
37+ gordon config show --remote https://gordon.mydomain.com --token $TOKEN`,
38+ RunE: func(cmd *cobra.Command, _ []string) error {
39+ ctx := context.Background()
40+ handle, err := resolveControlPlane(configPath)
41+ if err != nil {
42+ return err
43+ }
44+ defer handle.close()
45+ return runConfigShow(ctx, handle.plane, cmd.OutOrStdout(), jsonOut)
46+ },
47+ }
48+
49+ cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
50+
51+ return cmd
52+}
53+
54+func runConfigShow(ctx context.Context, cp ControlPlane, out io.Writer, jsonOut bool) error {
55+ config, err := cp.GetConfig(ctx)
56+ if err != nil {
57+ return fmt.Errorf("failed to get config: %w", err)
58+ }
59+
60+ if jsonOut {
61+ return writeJSON(out, config)
62+ }
63+
64+ return renderConfigTable(out, config)
65+}
66+
67+func renderConfigTable(out io.Writer, config *remote.Config) error {
68+ if err := renderConfigSummary(out, config); err != nil {
69+ return err
70+ }
71+ if err := renderConfigRoutes(out, config); err != nil {
72+ return err
73+ }
74+ return renderConfigExternalRoutes(out, config)
75+}
76+
77+func renderConfigSummary(out io.Writer, config *remote.Config) error {
78+ if err := cliWriteLine(out, cliRenderTitle("Gordon Configuration")); err != nil {
79+ return err
80+ }
81+ if err := cliWriteLine(out, ""); err != nil {
82+ return err
83+ }
84+ if err := cliWriteLine(out, cliRenderMeta("Server Port:", fmt.Sprintf("%d", config.Server.Port))); err != nil {
85+ return err
86+ }
87+ if err := cliWriteLine(out, cliRenderMeta("Registry Port:", fmt.Sprintf("%d", config.Server.RegistryPort))); err != nil {
88+ return err
89+ }
90+ if err := cliWriteLine(out, cliRenderMeta("Registry Domain:", config.Server.RegistryDomain)); err != nil {
91+ return err
92+ }
93+ if err := cliWriteLine(out, cliRenderMeta("Data Directory:", config.Server.DataDir)); err != nil {
94+ return err
95+ }
96+ if err := cliWriteLine(out, cliRenderMeta("Auto-Route:", fmt.Sprintf("%v", config.AutoRoute.Enabled))); err != nil {
97+ return err
98+ }
99+ if err := cliWriteLine(out, cliRenderMeta("Network Isolation:", fmt.Sprintf("%v", config.NetworkIsolation.Enabled))); err != nil {
100+ return err
101+ }
102+ if config.NetworkIsolation.Prefix != "" {
103+ if err := cliWriteLine(out, cliRenderMeta("Network Prefix:", config.NetworkIsolation.Prefix)); err != nil {
104+ return err
105+ }
106+ }
107+ return nil
108+}
109+
110+func renderConfigRoutes(out io.Writer, config *remote.Config) error {
111+ if err := cliWriteLine(out, ""); err != nil {
112+ return err
113+ }
114+ if len(config.Routes) == 0 {
115+ if err := cliWriteLine(out, cliRenderMuted("No routes configured")); err != nil {
116+ return err
117+ }
118+ } else {
119+ if err := cliWriteLine(out, cliRenderTitle("Routes")); err != nil {
120+ return err
121+ }
122+ rows := make([][]string, 0, len(config.Routes))
123+ for _, route := range config.Routes {
124+ rows = append(rows, []string{route.Domain, route.Image})
125+ }
126+ table := components.NewTable(
127+ components.WithColumns([]components.TableColumn{
128+ {Title: "Domain", Width: 30},
129+ {Title: "Image", Width: 45},
130+ }),
131+ components.WithRows(rows),
132+ )
133+ if err := cliWriteLine(out, table.View()); err != nil {
134+ return err
135+ }
136+ }
137+ return nil
138+}
139+
140+func renderConfigExternalRoutes(out io.Writer, config *remote.Config) error {
141+ if len(config.ExternalRoutes) > 0 {
142+ if err := cliWriteLine(out, ""); err != nil {
143+ return err
144+ }
145+ if err := cliWriteLine(out, cliRenderTitle("External Routes")); err != nil {
146+ return err
147+ }
148+ domains := make([]string, 0, len(config.ExternalRoutes))
149+ for d := range config.ExternalRoutes {
150+ domains = append(domains, d)
151+ }
152+ sort.Strings(domains)
153+
154+ rows := make([][]string, 0, len(domains))
155+ for _, d := range domains {
156+ rows = append(rows, []string{d, config.ExternalRoutes[d]})
157+ }
158+ table := components.NewTable(
159+ components.WithColumns([]components.TableColumn{
160+ {Title: "Domain", Width: 30},
161+ {Title: "Target", Width: 45},
162+ }),
163+ components.WithRows(rows),
164+ )
165+ if err := cliWriteLine(out, table.View()); err != nil {
166+ return err
167+ }
168+ }
169+
170+ return nil
171+}
M internal/adapters/in/cli/root.go +4 -0
@@ -129,6 +129,10 @@ Commands are organized by where they run:
129129 bootstrapCmd.GroupID = groupManage
130130 rootCmd.AddCommand(bootstrapCmd)
131131
132+ configCmd := newConfigCmd()
133+ configCmd.GroupID = groupManage
134+ rootCmd.AddCommand(configCmd)
135+
132136 // Client-only commands (no server needed)
133137 remotesCmd := newRemotesCmd()
134138 remotesCmd.GroupID = groupClient
@@ -129,6 +129,10 @@ Commands are organized by where they run:
129 bootstrapCmd.GroupID = groupManage129 bootstrapCmd.GroupID = groupManage
130 rootCmd.AddCommand(bootstrapCmd)130 rootCmd.AddCommand(bootstrapCmd)
131 131
132+ configCmd := newConfigCmd()
133+ configCmd.GroupID = groupManage
134+ rootCmd.AddCommand(configCmd)
135+
132 // Client-only commands (no server needed)136 // Client-only commands (no server needed)
133 remotesCmd := newRemotesCmd()137 remotesCmd := newRemotesCmd()
134 remotesCmd.GroupID = groupClient138 remotesCmd.GroupID = groupClient

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close