v2.5.0 b9b6391

root.go RawHistoryFolder

1// Package cli implements the CLI adapter for Gordon.
2// This package provides Cobra commands that delegate to the app layer.
3package cli
4
5import (
6	"context"
7	"fmt"
8
9	"github.com/spf13/cobra"
10
11	"gordon/internal/adapters/in/cli/remote"
12	"gordon/internal/app"
13)
14
15var (
16	// Version information (set at build time)
17	Version   = "dev"
18	Commit    = "unknown"
19	BuildDate = "unknown"
20
21	// Global flags for remote targeting
22	remoteFlag string
23	tokenFlag  string
24)
25
26// NewRootCmd creates the root command for Gordon CLI.
27func NewRootCmd() *cobra.Command {
28	rootCmd := &cobra.Command{
29		Use:   "gordon",
30		Short: "Gordon - A lightweight container deployment platform",
31		Long: `Gordon is a self-contained container deployment platform that combines
32a Docker registry with automatic container deployment capabilities.
33
34It listens for image pushes and automatically deploys containers based on
35configuration rules, making it ideal for single-server deployments.
36
37The CLI can target remote Gordon instances using the --remote flag or
38GORDON_REMOTE environment variable.`,
39	}
40
41	// Add persistent flags for remote targeting
42	rootCmd.PersistentFlags().StringVar(&remoteFlag, "remote", "", "Remote Gordon URL (e.g., https://gordon.mydomain.com)")
43	rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "Authentication token for remote")
44
45	// Server commands
46	rootCmd.AddCommand(newServeCmd())
47	rootCmd.AddCommand(newStartCmd()) // Deprecated alias for serve
48	rootCmd.AddCommand(newReloadCmd())
49	rootCmd.AddCommand(newDeployCmd())
50	rootCmd.AddCommand(newLogsCmd())
51	rootCmd.AddCommand(newVersionCmd())
52	rootCmd.AddCommand(newAuthCmd())
53
54	// Remote management commands
55	rootCmd.AddCommand(newRoutesCmd())
56	rootCmd.AddCommand(newSecretsCmd())
57	rootCmd.AddCommand(newRemotesCmd())
58	rootCmd.AddCommand(newStatusCmd())
59
60	return rootCmd
61}
62
63// GetRemoteClient returns a remote client if targeting a remote instance,
64// or nil if running locally.
65func GetRemoteClient() (*remote.Client, bool) {
66	url, token, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)
67	if !isRemote {
68		return nil, false
69	}
70
71	client := remote.NewClient(url, remote.WithToken(token))
72	return client, true
73}
74
75// IsRemoteMode returns true if CLI is targeting a remote Gordon instance.
76func IsRemoteMode() bool {
77	_, _, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)
78	return isRemote
79}
80
81// newReloadCmd creates the reload command.
82func newReloadCmd() *cobra.Command {
83	return &cobra.Command{
84		Use:   "reload",
85		Short: "Start containers for configured routes",
86		Long: `Starts containers for routes defined in config.toml that don't have
87a running container. Running containers are never restarted to ensure 100% uptime.
88
89Use this command after editing config.toml to add new routes, or after pushing
90images to the registry when the route was not yet configured.`,
91		RunE: func(cmd *cobra.Command, args []string) error {
92			client, isRemote := GetRemoteClient()
93			if isRemote {
94				return runReloadRemote(cmd.Context(), client)
95			}
96			return runReload()
97		},
98	}
99}
100
101// newVersionCmd creates the version command.
102func newVersionCmd() *cobra.Command {
103	return &cobra.Command{
104		Use:   "version",
105		Short: "Print version information",
106		Run: func(cmd *cobra.Command, args []string) {
107			cmd.Printf("Gordon %s\n", Version)
108			cmd.Printf("Commit: %s\n", Commit)
109			cmd.Printf("Build Date: %s\n", BuildDate)
110		},
111	}
112}
113
114// runReload sends SIGUSR1 to the running Gordon process.
115func runReload() error {
116	return app.SendReloadSignal()
117}
118
119// runReloadRemote triggers a reload on a remote Gordon instance.
120func runReloadRemote(ctx context.Context, client *remote.Client) error {
121	if err := client.Reload(ctx); err != nil {
122		return fmt.Errorf("failed to reload: %w", err)
123	}
124	fmt.Println("Configuration reloaded successfully")
125	return nil
126}
127
128// newLogsCmd creates the logs command.
129func newLogsCmd() *cobra.Command {
130	var follow bool
131	var lines int
132	var logsConfigPath string
133
134	cmd := &cobra.Command{
135		Use:   "logs [domain]",
136		Short: "Show logs (Gordon process or container)",
137		Long: `Shows logs from the Gordon process or a specific container.
138
139Without a domain argument, shows Gordon process logs.
140With a domain argument, shows container logs for that domain.
141
142Examples:
143  gordon logs                    # Gordon process logs
144  gordon logs -f                 # Follow process logs
145  gordon logs myapp.local        # Container logs for myapp.local
146  gordon logs myapp.local -f     # Follow container logs
147
148Remote mode:
149  gordon logs --remote https://gordon.mydomain.com --token $TOKEN
150  gordon logs myapp.local --remote https://gordon.mydomain.com --token $TOKEN`,
151		Args: cobra.MaximumNArgs(1),
152		RunE: func(cmd *cobra.Command, args []string) error {
153			logDomain := ""
154			if len(args) > 0 {
155				logDomain = args[0]
156			}
157			return runLogs(logsConfigPath, logDomain, follow, lines)
158		},
159	}
160
161	cmd.Flags().StringVarP(&logsConfigPath, "config", "c", "", "Path to config file")
162	cmd.Flags().BoolVarP(&follow, "follow", "f", false, "Follow log output")
163	cmd.Flags().IntVarP(&lines, "lines", "n", 50, "Number of lines to show")
164
165	return cmd
166}
167
168// runLogs handles the logs command logic.
169func runLogs(logsConfigPath, logDomain string, follow bool, lines int) error {
170	client, isRemote := GetRemoteClient()
171	if isRemote {
172		return runLogsRemote(client, logDomain, follow, lines)
173	}
174	return runLogsLocal(logsConfigPath, logDomain, follow, lines)
175}
176
177// runLogsRemote fetches logs from a remote Gordon instance.
178func runLogsRemote(client *remote.Client, logDomain string, follow bool, lines int) error {
179	ctx := context.Background()
180
181	if follow {
182		return streamLogsRemote(ctx, client, logDomain, lines)
183	}
184
185	if logDomain == "" {
186		// Process logs
187		logLines, err := client.GetProcessLogs(ctx, lines)
188		if err != nil {
189			return fmt.Errorf("failed to get process logs: %w", err)
190		}
191		for _, line := range logLines {
192			fmt.Println(line)
193		}
194	} else {
195		// Container logs
196		logLines, err := client.GetContainerLogs(ctx, logDomain, lines)
197		if err != nil {
198			return fmt.Errorf("failed to get container logs: %w", err)
199		}
200		for _, line := range logLines {
201			fmt.Println(line)
202		}
203	}
204	return nil
205}
206
207// streamLogsRemote streams logs from a remote Gordon instance.
208func streamLogsRemote(ctx context.Context, client *remote.Client, logDomain string, lines int) error {
209	var ch <-chan string
210	var err error
211
212	if logDomain == "" {
213		ch, err = client.StreamProcessLogs(ctx, lines)
214	} else {
215		ch, err = client.StreamContainerLogs(ctx, logDomain, lines)
216	}
217	if err != nil {
218		return fmt.Errorf("failed to stream logs: %w", err)
219	}
220
221	for line := range ch {
222		fmt.Println(line)
223	}
224	return nil
225}
226
227// runLogsLocal shows logs from a local Gordon instance.
228func runLogsLocal(logsConfigPath, logDomain string, follow bool, lines int) error {
229	if logDomain == "" {
230		// Process logs - use existing app.ShowLogs
231		return app.ShowLogs(logsConfigPath, follow, lines)
232	}
233
234	// Container logs - use local services
235	return showContainerLogsLocal(logsConfigPath, logDomain, follow, lines)
236}
237
238// showContainerLogsLocal shows container logs using local Docker access.
239func showContainerLogsLocal(logsConfigPath, logDomain string, follow bool, lines int) error {
240	// For local container logs, we need Docker access which requires
241	// the runtime to be initialized. For now, suggest using remote mode
242	// or direct docker logs command.
243	fmt.Printf("Container logs for %s\n", logDomain)
244	fmt.Println("To view container logs locally, use:")
245	fmt.Printf("  docker logs --tail %d %s\n", lines, logDomain)
246	if follow {
247		fmt.Printf("  docker logs -f --tail %d %s\n", lines, logDomain)
248	}
249	fmt.Println("\nOr use remote mode to access logs via the admin API.")
250	return nil
251}
252
253// SetVersionInfo sets the version information for the CLI.
254func SetVersionInfo(version, commit, date string) {
255	Version = version
256	Commit = commit
257	BuildDate = date
258}

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