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}