aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/ship/commands_v2.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/ship/commands_v2.go')
-rw-r--r--cmd/ship/commands_v2.go365
1 files changed, 365 insertions, 0 deletions
diff --git a/cmd/ship/commands_v2.go b/cmd/ship/commands_v2.go
new file mode 100644
index 0000000..1b0d09c
--- /dev/null
+++ b/cmd/ship/commands_v2.go
@@ -0,0 +1,365 @@
1package main
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7 "time"
8
9 "github.com/bdw/ship/internal/output"
10 "github.com/bdw/ship/internal/ssh"
11 "github.com/bdw/ship/internal/state"
12 "github.com/spf13/cobra"
13)
14
15// listV2Cmd lists all deployments
16var listV2Cmd = &cobra.Command{
17 Use: "list",
18 Short: "List all deployments",
19 RunE: runListV2,
20}
21
22func runListV2(cmd *cobra.Command, args []string) error {
23 st, err := state.Load()
24 if err != nil {
25 output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error()))
26 }
27
28 hostName := hostFlag
29 if hostName == "" {
30 hostName = st.DefaultHost
31 }
32 if hostName == "" {
33 output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified"))
34 }
35
36 hostConfig := st.GetHost(hostName)
37
38 client, err := ssh.Connect(hostName)
39 if err != nil {
40 output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error()))
41 }
42 defer client.Close()
43
44 var deploys []output.DeployInfo
45
46 // Get all deployed services by checking /etc/ship/ports and /var/www
47 // Check ports (apps and docker)
48 portsOut, _ := client.Run("ls /etc/ship/ports/ 2>/dev/null || true")
49 for _, name := range strings.Fields(portsOut) {
50 if name == "" {
51 continue
52 }
53
54 // Get actual domain from Caddyfile (first word of first line)
55 domain := fmt.Sprintf("%s.%s", name, hostConfig.BaseDomain)
56 caddyOut, _ := client.Run(fmt.Sprintf("head -1 /etc/caddy/sites-enabled/%s.caddy 2>/dev/null | awk '{print $1}'", name))
57 if d := strings.TrimSpace(caddyOut); d != "" && d != "{" {
58 domain = d
59 }
60
61 info := output.DeployInfo{
62 Name: name,
63 URL: fmt.Sprintf("https://%s", domain),
64 }
65
66 // Check if it's docker or binary
67 dockerOut, _ := client.Run(fmt.Sprintf("docker inspect %s 2>/dev/null && echo docker", name))
68 if strings.Contains(dockerOut, "docker") {
69 info.Type = "docker"
70 } else {
71 info.Type = "binary"
72 }
73
74 // Check if running
75 statusOut, _ := client.RunSudo(fmt.Sprintf("systemctl is-active %s 2>/dev/null || echo inactive", name))
76 info.Running = strings.TrimSpace(statusOut) == "active"
77
78 // Check TTL
79 ttlOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ttl/%s 2>/dev/null || true", name))
80 if ttlOut != "" {
81 if expires, err := strconv.ParseInt(strings.TrimSpace(ttlOut), 10, 64); err == nil {
82 info.Expires = time.Unix(expires, 0).UTC().Format(time.RFC3339)
83 }
84 }
85
86 deploys = append(deploys, info)
87 }
88
89 // Check static sites in /var/www
90 wwwOut, _ := client.Run("ls -d /var/www/*/ 2>/dev/null | xargs -n1 basename 2>/dev/null || true")
91 for _, name := range strings.Fields(wwwOut) {
92 if name == "" || name == "html" {
93 continue
94 }
95
96 // Skip if already in ports (would be an app, not static)
97 found := false
98 for _, d := range deploys {
99 if d.Name == name {
100 found = true
101 break
102 }
103 }
104 if found {
105 continue
106 }
107
108 // Get actual domain from Caddyfile
109 domain := fmt.Sprintf("%s.%s", name, hostConfig.BaseDomain)
110 caddyOut, _ := client.Run(fmt.Sprintf("head -1 /etc/caddy/sites-enabled/%s.caddy 2>/dev/null | awk '{print $1}'", name))
111 if d := strings.TrimSpace(caddyOut); d != "" && d != "{" {
112 domain = d
113 }
114
115 info := output.DeployInfo{
116 Name: name,
117 URL: fmt.Sprintf("https://%s", domain),
118 Type: "static",
119 Running: true, // Static sites are always "running"
120 }
121
122 // Check TTL
123 ttlOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ttl/%s 2>/dev/null || true", name))
124 if ttlOut != "" {
125 if expires, err := strconv.ParseInt(strings.TrimSpace(ttlOut), 10, 64); err == nil {
126 info.Expires = time.Unix(expires, 0).UTC().Format(time.RFC3339)
127 }
128 }
129
130 deploys = append(deploys, info)
131 }
132
133 output.PrintAndExit(&output.ListResponse{
134 Status: "ok",
135 Deploys: deploys,
136 })
137 return nil
138}
139
140// statusV2Cmd shows status for a single deployment
141var statusV2Cmd = &cobra.Command{
142 Use: "status NAME",
143 Short: "Check status of a deployment",
144 Args: cobra.ExactArgs(1),
145 RunE: runStatusV2,
146}
147
148func runStatusV2(cmd *cobra.Command, args []string) error {
149 name := args[0]
150
151 st, err := state.Load()
152 if err != nil {
153 output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error()))
154 }
155
156 hostName := hostFlag
157 if hostName == "" {
158 hostName = st.DefaultHost
159 }
160 if hostName == "" {
161 output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified"))
162 }
163
164 hostConfig := st.GetHost(hostName)
165
166 client, err := ssh.Connect(hostName)
167 if err != nil {
168 output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error()))
169 }
170 defer client.Close()
171
172 // Check if deployment exists
173 portOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ports/%s 2>/dev/null || true", name))
174 wwwExists, _ := client.Run(fmt.Sprintf("test -d /var/www/%s && echo exists || true", name))
175
176 if strings.TrimSpace(portOut) == "" && strings.TrimSpace(wwwExists) == "" {
177 output.PrintAndExit(output.ErrWithName(output.ErrNotFound, "deployment not found", name))
178 }
179
180 // Get actual domain from Caddyfile
181 domain := fmt.Sprintf("%s.%s", name, hostConfig.BaseDomain)
182 caddyOut, _ := client.Run(fmt.Sprintf("head -1 /etc/caddy/sites-enabled/%s.caddy 2>/dev/null | awk '{print $1}'", name))
183 if d := strings.TrimSpace(caddyOut); d != "" && d != "{" {
184 domain = d
185 }
186
187 resp := &output.StatusResponse{
188 Status: "ok",
189 Name: name,
190 URL: fmt.Sprintf("https://%s", domain),
191 }
192
193 // Determine type and get details
194 if portOut != "" {
195 port, _ := strconv.Atoi(strings.TrimSpace(portOut))
196 resp.Port = port
197
198 // Check if docker
199 dockerOut, _ := client.Run(fmt.Sprintf("docker inspect %s 2>/dev/null && echo docker", name))
200 if strings.Contains(dockerOut, "docker") {
201 resp.Type = "docker"
202 } else {
203 resp.Type = "binary"
204 }
205
206 // Check if running
207 statusOut, _ := client.RunSudo(fmt.Sprintf("systemctl is-active %s 2>/dev/null || echo inactive", name))
208 resp.Running = strings.TrimSpace(statusOut) == "active"
209 } else {
210 resp.Type = "static"
211 resp.Running = true
212 }
213
214 // Check TTL
215 ttlOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ttl/%s 2>/dev/null || true", name))
216 if ttlOut != "" {
217 if expires, err := strconv.ParseInt(strings.TrimSpace(ttlOut), 10, 64); err == nil {
218 resp.Expires = time.Unix(expires, 0).UTC().Format(time.RFC3339)
219 }
220 }
221
222 output.PrintAndExit(resp)
223 return nil
224}
225
226// logsV2Cmd shows logs for a deployment
227var logsV2Cmd = &cobra.Command{
228 Use: "logs NAME",
229 Short: "View logs for a deployment",
230 Args: cobra.ExactArgs(1),
231 RunE: runLogsV2,
232}
233
234func init() {
235 logsV2Cmd.Flags().Int("lines", 50, "Number of log lines to show")
236}
237
238func runLogsV2(cmd *cobra.Command, args []string) error {
239 name := args[0]
240 lines, _ := cmd.Flags().GetInt("lines")
241
242 st, err := state.Load()
243 if err != nil {
244 output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error()))
245 }
246
247 hostName := hostFlag
248 if hostName == "" {
249 hostName = st.DefaultHost
250 }
251 if hostName == "" {
252 output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified"))
253 }
254
255 client, err := ssh.Connect(hostName)
256 if err != nil {
257 output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error()))
258 }
259 defer client.Close()
260
261 // Check if it's a static site (no logs)
262 portOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ports/%s 2>/dev/null || true", name))
263 if strings.TrimSpace(portOut) == "" {
264 // Check if static site exists
265 wwwExists, _ := client.Run(fmt.Sprintf("test -d /var/www/%s && echo exists || true", name))
266 if strings.TrimSpace(wwwExists) == "" {
267 output.PrintAndExit(output.ErrWithName(output.ErrNotFound, "deployment not found", name))
268 }
269 // Static site - check Caddy access logs
270 logsOut, err := client.RunSudo(fmt.Sprintf("journalctl -u caddy -n %d --no-pager 2>/dev/null | grep %s || echo 'No logs found'", lines*2, name))
271 if err != nil {
272 output.PrintAndExit(output.Err(output.ErrServiceFailed, err.Error()))
273 }
274 logLines := strings.Split(strings.TrimSpace(logsOut), "\n")
275 output.PrintAndExit(&output.LogsResponse{
276 Status: "ok",
277 Name: name,
278 Lines: logLines,
279 })
280 return nil
281 }
282
283 // Get journalctl logs
284 logsOut, err := client.RunSudo(fmt.Sprintf("journalctl -u %s -n %d --no-pager 2>/dev/null || echo 'No logs found'", name, lines))
285 if err != nil {
286 output.PrintAndExit(output.Err(output.ErrServiceFailed, err.Error()))
287 }
288
289 logLines := strings.Split(strings.TrimSpace(logsOut), "\n")
290
291 output.PrintAndExit(&output.LogsResponse{
292 Status: "ok",
293 Name: name,
294 Lines: logLines,
295 })
296 return nil
297}
298
299// removeV2Cmd removes a deployment
300var removeV2Cmd = &cobra.Command{
301 Use: "remove NAME",
302 Short: "Remove a deployment",
303 Args: cobra.ExactArgs(1),
304 RunE: runRemoveV2,
305}
306
307func runRemoveV2(cmd *cobra.Command, args []string) error {
308 name := args[0]
309
310 st, err := state.Load()
311 if err != nil {
312 output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error()))
313 }
314
315 hostName := hostFlag
316 if hostName == "" {
317 hostName = st.DefaultHost
318 }
319 if hostName == "" {
320 output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified"))
321 }
322
323 client, err := ssh.Connect(hostName)
324 if err != nil {
325 output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error()))
326 }
327 defer client.Close()
328
329 // Check if deployment exists
330 portOut, _ := client.Run(fmt.Sprintf("cat /etc/ship/ports/%s 2>/dev/null || true", name))
331 wwwExists, _ := client.Run(fmt.Sprintf("test -d /var/www/%s && echo exists || true", name))
332
333 if strings.TrimSpace(portOut) == "" && strings.TrimSpace(wwwExists) == "" {
334 output.PrintAndExit(output.ErrWithName(output.ErrNotFound, "deployment not found", name))
335 }
336
337 // Stop and disable service
338 client.RunSudo(fmt.Sprintf("systemctl stop %s 2>/dev/null || true", name))
339 client.RunSudo(fmt.Sprintf("systemctl disable %s 2>/dev/null || true", name))
340
341 // Remove files
342 client.RunSudo(fmt.Sprintf("rm -f /etc/systemd/system/%s.service", name))
343 client.RunSudo(fmt.Sprintf("rm -f /etc/caddy/sites-enabled/%s.caddy", name))
344 client.RunSudo(fmt.Sprintf("rm -rf /var/www/%s", name))
345 client.RunSudo(fmt.Sprintf("rm -rf /var/lib/%s", name))
346 client.RunSudo(fmt.Sprintf("rm -f /usr/local/bin/%s", name))
347 client.RunSudo(fmt.Sprintf("rm -f /etc/ship/env/%s.env", name))
348 client.RunSudo(fmt.Sprintf("rm -f /etc/ship/ports/%s", name))
349 client.RunSudo(fmt.Sprintf("rm -f /etc/ship/ttl/%s", name))
350
351 // Remove docker container and image
352 client.Run(fmt.Sprintf("docker rm -f %s 2>/dev/null || true", name))
353 client.Run(fmt.Sprintf("docker rmi %s 2>/dev/null || true", name))
354
355 // Reload services
356 client.RunSudo("systemctl daemon-reload")
357 client.RunSudo("systemctl reload caddy")
358
359 output.PrintAndExit(&output.RemoveResponse{
360 Status: "ok",
361 Name: name,
362 Removed: true,
363 })
364 return nil
365}