v2.4.2 7da87b2

secrets.go RawHistoryFolder

1package cli
2
3import (
4	"context"
5	"fmt"
6	"strings"
7
8	"gordon/internal/adapters/in/cli/ui/components"
9	"gordon/internal/adapters/in/cli/ui/styles"
10
11	"github.com/spf13/cobra"
12)
13
14// newSecretsCmd creates the secrets command group.
15func newSecretsCmd() *cobra.Command {
16	cmd := &cobra.Command{
17		Use:   "secrets",
18		Short: "Manage secrets",
19		Long: `Manage secrets (environment variables) for routes.
20
21Secrets are stored per-domain and injected into containers as environment variables.
22
23When targeting a remote Gordon instance (via --remote flag or GORDON_REMOTE env var),
24these commands operate on the remote server.`,
25	}
26
27	cmd.AddCommand(newSecretsListCmd())
28	cmd.AddCommand(newSecretsSetCmd())
29	cmd.AddCommand(newSecretsRemoveCmd())
30
31	return cmd
32}
33
34// newSecretsListCmd creates the secrets list command.
35func newSecretsListCmd() *cobra.Command {
36	return &cobra.Command{
37		Use:   "list <domain>",
38		Short: "List secrets for a domain",
39		Long: `List all secret keys configured for a domain.
40
41Note: Only secret keys are shown, not values (for security).
42
43Examples:
44  gordon secrets list app.mydomain.com
45  gordon --remote https://gordon.mydomain.com secrets list api.mydomain.com`,
46		Args: cobra.ExactArgs(1),
47		RunE: func(cmd *cobra.Command, args []string) error {
48			ctx := context.Background()
49			secretDomain := args[0]
50
51			var keys []string
52			var err error
53
54			client, isRemote := GetRemoteClient()
55			if isRemote {
56				keys, err = client.ListSecrets(ctx, secretDomain)
57			} else {
58				local, localErr := GetLocalServices(configPath)
59				if localErr != nil {
60					return fmt.Errorf("failed to initialize local services: %w", localErr)
61				}
62				keys, err = local.GetSecretService().ListKeys(ctx, secretDomain)
63			}
64
65			if err != nil {
66				return fmt.Errorf("failed to list secrets: %w", err)
67			}
68
69			if len(keys) == 0 {
70				fmt.Println(styles.Theme.Muted.Render(fmt.Sprintf("No secrets configured for %s", secretDomain)))
71				return nil
72			}
73
74			title := fmt.Sprintf("Secrets for %s", secretDomain)
75			if !isRemote {
76				title = fmt.Sprintf("Secrets for %s (local)", secretDomain)
77			}
78			fmt.Println(styles.Theme.Title.Render(title))
79			fmt.Println()
80
81			// Build table rows
82			rows := make([][]string, len(keys))
83			for i, key := range keys {
84				rows[i] = []string{key, styles.Theme.Muted.Render("(hidden)")}
85			}
86
87			// Render table
88			table := components.NewTable(
89				components.WithColumns([]components.TableColumn{
90					{Title: "Key", Width: 30},
91					{Title: "Value", Width: 20},
92				}),
93				components.WithRows(rows),
94			)
95
96			fmt.Println(table.View())
97
98			return nil
99		},
100	}
101}
102
103// newSecretsSetCmd creates the secrets set command.
104func newSecretsSetCmd() *cobra.Command {
105	return &cobra.Command{
106		Use:   "set <domain> <KEY=value>...",
107		Short: "Set secrets for a domain",
108		Long: `Set one or more secrets for a domain.
109
110Secrets are specified as KEY=value pairs. Multiple secrets can be set at once.
111
112Examples:
113  gordon secrets set app.mydomain.com DATABASE_URL=postgres://localhost/db
114  gordon secrets set app.mydomain.com API_KEY=secret123 DEBUG=false
115  gordon --remote https://gordon.mydomain.com secrets set api.mydomain.com TOKEN=abc`,
116		Args: cobra.MinimumNArgs(2),
117		RunE: func(cmd *cobra.Command, args []string) error {
118			ctx := context.Background()
119			secretDomain := args[0]
120			pairs := args[1:]
121
122			// Parse KEY=value pairs
123			secrets := make(map[string]string)
124			for _, pair := range pairs {
125				parts := strings.SplitN(pair, "=", 2)
126				if len(parts) != 2 {
127					fmt.Println(styles.RenderError(fmt.Sprintf("Invalid format: %s (expected KEY=value)", pair)))
128					return nil
129				}
130				secrets[parts[0]] = parts[1]
131			}
132
133			client, isRemote := GetRemoteClient()
134			if isRemote {
135				if err := client.SetSecrets(ctx, secretDomain, secrets); err != nil {
136					return fmt.Errorf("failed to set secrets: %w", err)
137				}
138			} else {
139				local, err := GetLocalServices(configPath)
140				if err != nil {
141					return fmt.Errorf("failed to initialize local services: %w", err)
142				}
143				if err := local.GetSecretService().Set(ctx, secretDomain, secrets); err != nil {
144					return fmt.Errorf("failed to set secrets: %w", err)
145				}
146			}
147
148			if len(secrets) == 1 {
149				for key := range secrets {
150					fmt.Println(styles.RenderSuccess(fmt.Sprintf("Secret set: %s", key)))
151				}
152			} else {
153				fmt.Println(styles.RenderSuccess(fmt.Sprintf("Set %d secrets for %s", len(secrets), secretDomain)))
154			}
155
156			return nil
157		},
158	}
159}
160
161// newSecretsRemoveCmd creates the secrets remove command.
162func newSecretsRemoveCmd() *cobra.Command {
163	var force bool
164
165	cmd := &cobra.Command{
166		Use:   "remove <domain> <key>",
167		Short: "Remove a secret",
168		Long: `Remove a secret from a domain.
169
170Examples:
171  gordon secrets remove app.mydomain.com OLD_API_KEY
172  gordon secrets remove app.mydomain.com OLD_API_KEY --force`,
173		Args: cobra.ExactArgs(2),
174		RunE: func(cmd *cobra.Command, args []string) error {
175			ctx := context.Background()
176			secretDomain := args[0]
177			key := args[1]
178
179			// Confirm unless --force
180			if !force {
181				confirmed, err := components.RunConfirm(
182					fmt.Sprintf("Remove secret '%s' from %s?", key, secretDomain),
183				)
184				if err != nil {
185					return err
186				}
187				if !confirmed {
188					fmt.Println(styles.Theme.Muted.Render("Cancelled"))
189					return nil
190				}
191			}
192
193			client, isRemote := GetRemoteClient()
194			if isRemote {
195				if err := client.DeleteSecret(ctx, secretDomain, key); err != nil {
196					return fmt.Errorf("failed to remove secret: %w", err)
197				}
198			} else {
199				local, err := GetLocalServices(configPath)
200				if err != nil {
201					return fmt.Errorf("failed to initialize local services: %w", err)
202				}
203				if err := local.GetSecretService().Delete(ctx, secretDomain, key); err != nil {
204					return fmt.Errorf("failed to remove secret: %w", err)
205				}
206			}
207
208			fmt.Println(styles.RenderSuccess(fmt.Sprintf("Secret removed: %s", key)))
209			return nil
210		},
211	}
212
213	cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation")
214
215	return cmd
216}

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 8

Tags 106

↑↓ moveenter openesc close