summaryrefslogtreecommitdiffstats
path: root/cmd/ship/env
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-01-24 09:48:34 -0800
committerbndw <ben@bdw.to>2026-01-24 09:48:34 -0800
commit5861e465a2ccf31d87ea25ac145770786f9cc96e (patch)
tree4ac6b57a06b46d8492717b235909f9e0db0b4f22 /cmd/ship/env
parentef37850c7090493cf2b26d2e565511fe23cc9bfc (diff)
Rename project from deploy to ship
- Rename module to github.com/bdw/ship - Rename cmd/deploy to cmd/ship - Update all import paths - Update config path from ~/.config/deploy to ~/.config/ship - Update VPS env path from /etc/deploy to /etc/ship - Update README, Makefile, and docs
Diffstat (limited to 'cmd/ship/env')
-rw-r--r--cmd/ship/env/env.go17
-rw-r--r--cmd/ship/env/list.go69
-rw-r--r--cmd/ship/env/set.go132
-rw-r--r--cmd/ship/env/unset.go92
4 files changed, 310 insertions, 0 deletions
diff --git a/cmd/ship/env/env.go b/cmd/ship/env/env.go
new file mode 100644
index 0000000..489353a
--- /dev/null
+++ b/cmd/ship/env/env.go
@@ -0,0 +1,17 @@
1package env
2
3import (
4 "github.com/spf13/cobra"
5)
6
7var Cmd = &cobra.Command{
8 Use: "env",
9 Short: "Manage environment variables",
10 Long: "Manage environment variables for deployed applications",
11}
12
13func init() {
14 Cmd.AddCommand(listCmd)
15 Cmd.AddCommand(setCmd)
16 Cmd.AddCommand(unsetCmd)
17}
diff --git a/cmd/ship/env/list.go b/cmd/ship/env/list.go
new file mode 100644
index 0000000..ad76eb6
--- /dev/null
+++ b/cmd/ship/env/list.go
@@ -0,0 +1,69 @@
1package env
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/bdw/ship/internal/state"
8 "github.com/spf13/cobra"
9)
10
11var listCmd = &cobra.Command{
12 Use: "list <app>",
13 Short: "List environment variables for an app",
14 Args: cobra.ExactArgs(1),
15 RunE: runList,
16}
17
18func runList(cmd *cobra.Command, args []string) error {
19 name := args[0]
20
21 st, err := state.Load()
22 if err != nil {
23 return fmt.Errorf("error loading state: %w", err)
24 }
25
26 host, _ := cmd.Flags().GetString("host")
27 if host == "" {
28 host = st.GetDefaultHost()
29 }
30
31 if host == "" {
32 return fmt.Errorf("--host is required")
33 }
34
35 app, err := st.GetApp(host, name)
36 if err != nil {
37 return err
38 }
39
40 if app.Type != "app" {
41 return fmt.Errorf("env is only available for apps, not static sites")
42 }
43
44 fmt.Printf("Environment variables for %s:\n\n", name)
45 if len(app.Env) == 0 {
46 fmt.Println(" (none)")
47 } else {
48 for k, v := range app.Env {
49 display := v
50 if isSensitive(k) {
51 display = "***"
52 }
53 fmt.Printf(" %s=%s\n", k, display)
54 }
55 }
56
57 return nil
58}
59
60func isSensitive(key string) bool {
61 key = strings.ToLower(key)
62 sensitiveWords := []string{"key", "secret", "password", "token", "api"}
63 for _, word := range sensitiveWords {
64 if strings.Contains(key, word) {
65 return true
66 }
67 }
68 return false
69}
diff --git a/cmd/ship/env/set.go b/cmd/ship/env/set.go
new file mode 100644
index 0000000..e11d2c9
--- /dev/null
+++ b/cmd/ship/env/set.go
@@ -0,0 +1,132 @@
1package env
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "strings"
8
9 "github.com/bdw/ship/internal/ssh"
10 "github.com/bdw/ship/internal/state"
11 "github.com/spf13/cobra"
12)
13
14var setCmd = &cobra.Command{
15 Use: "set <app> KEY=VALUE...",
16 Short: "Set environment variable(s)",
17 Long: "Set one or more environment variables for an app. Variables are specified as KEY=VALUE pairs.",
18 Args: cobra.MinimumNArgs(2),
19 RunE: runSet,
20}
21
22func init() {
23 setCmd.Flags().StringP("file", "f", "", "Load environment from file")
24}
25
26func runSet(cmd *cobra.Command, args []string) error {
27 name := args[0]
28 envVars := args[1:]
29
30 st, err := state.Load()
31 if err != nil {
32 return fmt.Errorf("error loading state: %w", err)
33 }
34
35 host, _ := cmd.Flags().GetString("host")
36 if host == "" {
37 host = st.GetDefaultHost()
38 }
39
40 if host == "" {
41 return fmt.Errorf("--host is required")
42 }
43
44 app, err := st.GetApp(host, name)
45 if err != nil {
46 return err
47 }
48
49 if app.Type != "app" {
50 return fmt.Errorf("env is only available for apps, not static sites")
51 }
52
53 if app.Env == nil {
54 app.Env = make(map[string]string)
55 }
56
57 // Set variables from args
58 for _, e := range envVars {
59 parts := strings.SplitN(e, "=", 2)
60 if len(parts) == 2 {
61 app.Env[parts[0]] = parts[1]
62 fmt.Printf("Set %s\n", parts[0])
63 } else {
64 return fmt.Errorf("invalid format: %s (expected KEY=VALUE)", e)
65 }
66 }
67
68 // Set variables from file if provided
69 envFile, _ := cmd.Flags().GetString("file")
70 if envFile != "" {
71 fileEnv, err := parseEnvFile(envFile)
72 if err != nil {
73 return fmt.Errorf("error reading env file: %w", err)
74 }
75 for k, v := range fileEnv {
76 app.Env[k] = v
77 fmt.Printf("Set %s\n", k)
78 }
79 }
80
81 if err := st.Save(); err != nil {
82 return fmt.Errorf("error saving state: %w", err)
83 }
84
85 client, err := ssh.Connect(host)
86 if err != nil {
87 return fmt.Errorf("error connecting to VPS: %w", err)
88 }
89 defer client.Close()
90
91 fmt.Println("-> Updating environment file on VPS...")
92 envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", name)
93 envContent := ""
94 for k, v := range app.Env {
95 envContent += fmt.Sprintf("%s=%s\n", k, v)
96 }
97 if err := client.WriteSudoFile(envFilePath, envContent); err != nil {
98 return fmt.Errorf("error updating env file: %w", err)
99 }
100
101 fmt.Println("-> Restarting service...")
102 if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil {
103 return fmt.Errorf("error restarting service: %w", err)
104 }
105
106 fmt.Println("Environment variables updated successfully")
107 return nil
108}
109
110func parseEnvFile(path string) (map[string]string, error) {
111 file, err := os.Open(path)
112 if err != nil {
113 return nil, err
114 }
115 defer file.Close()
116
117 env := make(map[string]string)
118 scanner := bufio.NewScanner(file)
119 for scanner.Scan() {
120 line := strings.TrimSpace(scanner.Text())
121 if line == "" || strings.HasPrefix(line, "#") {
122 continue
123 }
124
125 parts := strings.SplitN(line, "=", 2)
126 if len(parts) == 2 {
127 env[parts[0]] = parts[1]
128 }
129 }
130
131 return env, scanner.Err()
132}
diff --git a/cmd/ship/env/unset.go b/cmd/ship/env/unset.go
new file mode 100644
index 0000000..7d9a141
--- /dev/null
+++ b/cmd/ship/env/unset.go
@@ -0,0 +1,92 @@
1package env
2
3import (
4 "fmt"
5
6 "github.com/bdw/ship/internal/ssh"
7 "github.com/bdw/ship/internal/state"
8 "github.com/spf13/cobra"
9)
10
11var unsetCmd = &cobra.Command{
12 Use: "unset <app> KEY...",
13 Short: "Unset environment variable(s)",
14 Long: "Remove one or more environment variables from an app.",
15 Args: cobra.MinimumNArgs(2),
16 RunE: runUnset,
17}
18
19func runUnset(cmd *cobra.Command, args []string) error {
20 name := args[0]
21 keys := args[1:]
22
23 st, err := state.Load()
24 if err != nil {
25 return fmt.Errorf("error loading state: %w", err)
26 }
27
28 host, _ := cmd.Flags().GetString("host")
29 if host == "" {
30 host = st.GetDefaultHost()
31 }
32
33 if host == "" {
34 return fmt.Errorf("--host is required")
35 }
36
37 app, err := st.GetApp(host, name)
38 if err != nil {
39 return err
40 }
41
42 if app.Type != "app" {
43 return fmt.Errorf("env is only available for apps, not static sites")
44 }
45
46 if app.Env == nil {
47 return fmt.Errorf("no environment variables set")
48 }
49
50 changed := false
51 for _, key := range keys {
52 if _, exists := app.Env[key]; exists {
53 delete(app.Env, key)
54 changed = true
55 fmt.Printf("Unset %s\n", key)
56 } else {
57 fmt.Printf("Warning: %s not found\n", key)
58 }
59 }
60
61 if !changed {
62 return nil
63 }
64
65 if err := st.Save(); err != nil {
66 return fmt.Errorf("error saving state: %w", err)
67 }
68
69 client, err := ssh.Connect(host)
70 if err != nil {
71 return fmt.Errorf("error connecting to VPS: %w", err)
72 }
73 defer client.Close()
74
75 fmt.Println("-> Updating environment file on VPS...")
76 envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", name)
77 envContent := ""
78 for k, v := range app.Env {
79 envContent += fmt.Sprintf("%s=%s\n", k, v)
80 }
81 if err := client.WriteSudoFile(envFilePath, envContent); err != nil {
82 return fmt.Errorf("error updating env file: %w", err)
83 }
84
85 fmt.Println("-> Restarting service...")
86 if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil {
87 return fmt.Errorf("error restarting service: %w", err)
88 }
89
90 fmt.Println("Environment variables updated successfully")
91 return nil
92}