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}