v2.4.2 7da87b2

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			return runReload()
93		},
94	}
95}
96
97// newVersionCmd creates the version command.
98func newVersionCmd() *cobra.Command {
99	return &cobra.Command{
100		Use:   "version",
101		Short: "Print version information",
102		Run: func(cmd *cobra.Command, args []string) {
103			cmd.Printf("Gordon %s\n", Version)
104			cmd.Printf("Commit: %s\n", Commit)
105			cmd.Printf("Build Date: %s\n", BuildDate)
106		},
107	}
108}
109
110// runReload sends SIGUSR1 to the running Gordon process.
111func runReload() error {
112	return app.SendReloadSignal()
113}
114
115// newLogsCmd creates the logs command.
116func newLogsCmd() *cobra.Command {
117	var follow bool
118	var lines int
119	var logsConfigPath string
120
121	cmd := &cobra.Command{
122		Use:   "logs [domain]",
123		Short: "Show logs (Gordon process or container)",
124		Long: `Shows logs from the Gordon process or a specific container.
125
126Without a domain argument, shows Gordon process logs.
127With a domain argument, shows container logs for that domain.
128
129Examples:
130  gordon logs                    # Gordon process logs
131  gordon logs -f                 # Follow process logs
132  gordon logs myapp.local        # Container logs for myapp.local
133  gordon logs myapp.local -f     # Follow container logs
134
135Remote mode:
136  gordon logs --remote https://gordon.mydomain.com --token $TOKEN
137  gordon logs myapp.local --remote https://gordon.mydomain.com --token $TOKEN`,
138		Args: cobra.MaximumNArgs(1),
139		RunE: func(cmd *cobra.Command, args []string) error {
140			logDomain := ""
141			if len(args) > 0 {
142				logDomain = args[0]
143			}
144			return runLogs(logsConfigPath, logDomain, follow, lines)
145		},
146	}
147
148	cmd.Flags().StringVarP(&logsConfigPath, "config", "c", "", "Path to config file")
149	cmd.Flags().BoolVarP(&follow, "follow", "f", false, "Follow log output")
150	cmd.Flags().IntVarP(&lines, "lines", "n", 50, "Number of lines to show")
151
152	return cmd
153}
154
155// runLogs handles the logs command logic.
156func runLogs(logsConfigPath, logDomain string, follow bool, lines int) error {
157	client, isRemote := GetRemoteClient()
158	if isRemote {
159		return runLogsRemote(client, logDomain, follow, lines)
160	}
161	return runLogsLocal(logsConfigPath, logDomain, follow, lines)
162}
163
164// runLogsRemote fetches logs from a remote Gordon instance.
165func runLogsRemote(client *remote.Client, logDomain string, follow bool, lines int) error {
166	ctx := context.Background()
167
168	if follow {
169		return streamLogsRemote(ctx, client, logDomain, lines)
170	}
171
172	if logDomain == "" {
173		// Process logs
174		logLines, err := client.GetProcessLogs(ctx, lines)
175		if err != nil {
176			return fmt.Errorf("failed to get process logs: %w", err)
177		}
178		for _, line := range logLines {
179			fmt.Println(line)
180		}
181	} else {
182		// Container logs
183		logLines, err := client.GetContainerLogs(ctx, logDomain, lines)
184		if err != nil {
185			return fmt.Errorf("failed to get container logs: %w", err)
186		}
187		for _, line := range logLines {
188			fmt.Println(line)
189		}
190	}
191	return nil
192}
193
194// streamLogsRemote streams logs from a remote Gordon instance.
195func streamLogsRemote(ctx context.Context, client *remote.Client, logDomain string, lines int) error {
196	var ch <-chan string
197	var err error
198
199	if logDomain == "" {
200		ch, err = client.StreamProcessLogs(ctx, lines)
201	} else {
202		ch, err = client.StreamContainerLogs(ctx, logDomain, lines)
203	}
204	if err != nil {
205		return fmt.Errorf("failed to stream logs: %w", err)
206	}
207
208	for line := range ch {
209		fmt.Println(line)
210	}
211	return nil
212}
213
214// runLogsLocal shows logs from a local Gordon instance.
215func runLogsLocal(logsConfigPath, logDomain string, follow bool, lines int) error {
216	if logDomain == "" {
217		// Process logs - use existing app.ShowLogs
218		return app.ShowLogs(logsConfigPath, follow, lines)
219	}
220
221	// Container logs - use local services
222	return showContainerLogsLocal(logsConfigPath, logDomain, follow, lines)
223}
224
225// showContainerLogsLocal shows container logs using local Docker access.
226func showContainerLogsLocal(logsConfigPath, logDomain string, follow bool, lines int) error {
227	// For local container logs, we need Docker access which requires
228	// the runtime to be initialized. For now, suggest using remote mode
229	// or direct docker logs command.
230	fmt.Printf("Container logs for %s\n", logDomain)
231	fmt.Println("To view container logs locally, use:")
232	fmt.Printf("  docker logs --tail %d %s\n", lines, logDomain)
233	if follow {
234		fmt.Printf("  docker logs -f --tail %d %s\n", lines, logDomain)
235	}
236	fmt.Println("\nOr use remote mode to access logs via the admin API.")
237	return nil
238}
239
240// SetVersionInfo sets the version information for the CLI.
241func SetVersionInfo(version, commit, date string) {
242	Version = version
243	Commit = commit
244	BuildDate = date
245}

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