diff options
Diffstat (limited to 'cmd/ship')
| -rw-r--r-- | cmd/ship/init.go | 113 | ||||
| -rw-r--r-- | cmd/ship/list.go | 11 |
2 files changed, 76 insertions, 48 deletions
diff --git a/cmd/ship/init.go b/cmd/ship/init.go index 167347d..b495702 100644 --- a/cmd/ship/init.go +++ b/cmd/ship/init.go | |||
| @@ -3,6 +3,7 @@ package main | |||
| 3 | import ( | 3 | import ( |
| 4 | "fmt" | 4 | "fmt" |
| 5 | "os" | 5 | "os" |
| 6 | "os/exec" | ||
| 6 | "path/filepath" | 7 | "path/filepath" |
| 7 | "strconv" | 8 | "strconv" |
| 8 | 9 | ||
| @@ -18,7 +19,8 @@ var initCmd = &cobra.Command{ | |||
| 18 | Long: `Create a bare git repo on the VPS and generate local .ship/ config files. | 19 | Long: `Create a bare git repo on the VPS and generate local .ship/ config files. |
| 19 | 20 | ||
| 20 | Pushing to the remote triggers an automatic docker build and deploy (for apps) | 21 | Pushing to the remote triggers an automatic docker build and deploy (for apps) |
| 21 | or a static file checkout (for static sites). | 22 | or a static file checkout (for static sites). If no Dockerfile is present in an |
| 23 | app repo, pushes are accepted without triggering a deploy. | ||
| 22 | 24 | ||
| 23 | Examples: | 25 | Examples: |
| 24 | # Initialize an app (Docker-based) | 26 | # Initialize an app (Docker-based) |
| @@ -28,19 +30,27 @@ Examples: | |||
| 28 | ship init myapp --domain custom.example.com | 30 | ship init myapp --domain custom.example.com |
| 29 | 31 | ||
| 30 | # Initialize a static site | 32 | # Initialize a static site |
| 31 | ship init mysite --static`, | 33 | ship init mysite --static |
| 34 | |||
| 35 | # Initialize a public repo (cloneable via go get / git clone over HTTPS) | ||
| 36 | ship init mylib --public`, | ||
| 32 | Args: cobra.ExactArgs(1), | 37 | Args: cobra.ExactArgs(1), |
| 33 | RunE: runInit, | 38 | RunE: runInit, |
| 34 | } | 39 | } |
| 35 | 40 | ||
| 36 | func init() { | 41 | func init() { |
| 37 | initCmd.Flags().Bool("static", false, "Initialize as static site") | 42 | initCmd.Flags().Bool("static", false, "Initialize as static site") |
| 43 | initCmd.Flags().Bool("public", false, "Make repo publicly cloneable over HTTPS (for go get)") | ||
| 38 | initCmd.Flags().String("domain", "", "Custom domain (default: name.basedomain)") | 44 | initCmd.Flags().String("domain", "", "Custom domain (default: name.basedomain)") |
| 39 | } | 45 | } |
| 40 | 46 | ||
| 41 | func runInit(cmd *cobra.Command, args []string) error { | 47 | func runInit(cmd *cobra.Command, args []string) error { |
| 42 | name := args[0] | 48 | name := args[0] |
| 49 | if err := validateName(name); err != nil { | ||
| 50 | return err | ||
| 51 | } | ||
| 43 | static, _ := cmd.Flags().GetBool("static") | 52 | static, _ := cmd.Flags().GetBool("static") |
| 53 | public, _ := cmd.Flags().GetBool("public") | ||
| 44 | domain, _ := cmd.Flags().GetString("domain") | 54 | domain, _ := cmd.Flags().GetString("domain") |
| 45 | 55 | ||
| 46 | st, err := state.Load() | 56 | st, err := state.Load() |
| @@ -66,6 +76,11 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 66 | return fmt.Errorf("app %s already exists", name) | 76 | return fmt.Errorf("app %s already exists", name) |
| 67 | } | 77 | } |
| 68 | 78 | ||
| 79 | appType := "git-app" | ||
| 80 | if static { | ||
| 81 | appType = "git-static" | ||
| 82 | } | ||
| 83 | |||
| 69 | // Resolve domain | 84 | // Resolve domain |
| 70 | if domain == "" && hostState.BaseDomain != "" { | 85 | if domain == "" && hostState.BaseDomain != "" { |
| 71 | domain = name + "." + hostState.BaseDomain | 86 | domain = name + "." + hostState.BaseDomain |
| @@ -74,12 +89,7 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 74 | return fmt.Errorf("--domain required (or configure base domain)") | 89 | return fmt.Errorf("--domain required (or configure base domain)") |
| 75 | } | 90 | } |
| 76 | 91 | ||
| 77 | appType := "git-app" | 92 | // Allocate port for apps only |
| 78 | if static { | ||
| 79 | appType = "git-static" | ||
| 80 | } | ||
| 81 | |||
| 82 | // Allocate port for apps | ||
| 83 | port := 0 | 93 | port := 0 |
| 84 | if !static { | 94 | if !static { |
| 85 | port = st.AllocatePort(host) | 95 | port = st.AllocatePort(host) |
| @@ -96,10 +106,16 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 96 | // Create bare repo | 106 | // Create bare repo |
| 97 | fmt.Println("-> Creating bare git repo...") | 107 | fmt.Println("-> Creating bare git repo...") |
| 98 | repo := fmt.Sprintf("/srv/git/%s.git", name) | 108 | repo := fmt.Sprintf("/srv/git/%s.git", name) |
| 99 | if _, err := client.RunSudo(fmt.Sprintf("sudo -u git git init --bare %s", repo)); err != nil { | 109 | if _, err := client.RunSudo(fmt.Sprintf("sudo -u git git init --bare -b main %s", repo)); err != nil { |
| 100 | return fmt.Errorf("error creating bare repo: %w", err) | 110 | return fmt.Errorf("error creating bare repo: %w", err) |
| 101 | } | 111 | } |
| 102 | 112 | ||
| 113 | if public { | ||
| 114 | if _, err := client.RunSudo(fmt.Sprintf("sudo -u git touch %s/git-daemon-export-ok", repo)); err != nil { | ||
| 115 | return fmt.Errorf("error setting repo public: %w", err) | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 103 | if static { | 119 | if static { |
| 104 | // Create web root | 120 | // Create web root |
| 105 | fmt.Println("-> Creating web root...") | 121 | fmt.Println("-> Creating web root...") |
| @@ -118,32 +134,10 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 118 | if err != nil { | 134 | if err != nil { |
| 119 | return fmt.Errorf("error generating hook: %w", err) | 135 | return fmt.Errorf("error generating hook: %w", err) |
| 120 | } | 136 | } |
| 121 | hookPath := fmt.Sprintf("%s/hooks/post-receive", repo) | 137 | if err := writeHook(client, repo, hookContent); err != nil { |
| 122 | if err := client.WriteSudoFile(hookPath, hookContent); err != nil { | 138 | return err |
| 123 | return fmt.Errorf("error writing hook: %w", err) | ||
| 124 | } | ||
| 125 | if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", hookPath)); err != nil { | ||
| 126 | return fmt.Errorf("error making hook executable: %w", err) | ||
| 127 | } | ||
| 128 | if _, err := client.RunSudo(fmt.Sprintf("chown git:git %s", hookPath)); err != nil { | ||
| 129 | return fmt.Errorf("error setting hook ownership: %w", err) | ||
| 130 | } | 139 | } |
| 131 | } else { | 140 | } else { |
| 132 | // Create directories for app | ||
| 133 | fmt.Println("-> Creating app directories...") | ||
| 134 | dirs := []string{ | ||
| 135 | fmt.Sprintf("/var/lib/%s/data", name), | ||
| 136 | fmt.Sprintf("/var/lib/%s/src", name), | ||
| 137 | } | ||
| 138 | for _, dir := range dirs { | ||
| 139 | if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", dir)); err != nil { | ||
| 140 | return fmt.Errorf("error creating directory %s: %w", dir, err) | ||
| 141 | } | ||
| 142 | } | ||
| 143 | if _, err := client.RunSudo(fmt.Sprintf("chown -R git:git /var/lib/%s", name)); err != nil { | ||
| 144 | return fmt.Errorf("error setting directory ownership: %w", err) | ||
| 145 | } | ||
| 146 | |||
| 147 | // Create env file | 141 | // Create env file |
| 148 | fmt.Println("-> Creating environment file...") | 142 | fmt.Println("-> Creating environment file...") |
| 149 | envContent := fmt.Sprintf("PORT=%d\nDATA_DIR=/data\n", port) | 143 | envContent := fmt.Sprintf("PORT=%d\nDATA_DIR=/data\n", port) |
| @@ -152,7 +146,7 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 152 | return fmt.Errorf("error creating env file: %w", err) | 146 | return fmt.Errorf("error creating env file: %w", err) |
| 153 | } | 147 | } |
| 154 | 148 | ||
| 155 | // Write post-receive hook | 149 | // Write post-receive hook (handles dir creation on first push) |
| 156 | fmt.Println("-> Writing post-receive hook...") | 150 | fmt.Println("-> Writing post-receive hook...") |
| 157 | hookContent, err := templates.PostReceiveHook(map[string]string{ | 151 | hookContent, err := templates.PostReceiveHook(map[string]string{ |
| 158 | "Name": name, | 152 | "Name": name, |
| @@ -160,15 +154,8 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 160 | if err != nil { | 154 | if err != nil { |
| 161 | return fmt.Errorf("error generating hook: %w", err) | 155 | return fmt.Errorf("error generating hook: %w", err) |
| 162 | } | 156 | } |
| 163 | hookPath := fmt.Sprintf("%s/hooks/post-receive", repo) | 157 | if err := writeHook(client, repo, hookContent); err != nil { |
| 164 | if err := client.WriteSudoFile(hookPath, hookContent); err != nil { | 158 | return err |
| 165 | return fmt.Errorf("error writing hook: %w", err) | ||
| 166 | } | ||
| 167 | if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", hookPath)); err != nil { | ||
| 168 | return fmt.Errorf("error making hook executable: %w", err) | ||
| 169 | } | ||
| 170 | if _, err := client.RunSudo(fmt.Sprintf("chown git:git %s", hookPath)); err != nil { | ||
| 171 | return fmt.Errorf("error setting hook ownership: %w", err) | ||
| 172 | } | 159 | } |
| 173 | } | 160 | } |
| 174 | 161 | ||
| @@ -178,6 +165,7 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 178 | Domain: domain, | 165 | Domain: domain, |
| 179 | Port: port, | 166 | Port: port, |
| 180 | Repo: repo, | 167 | Repo: repo, |
| 168 | Public: public, | ||
| 181 | }) | 169 | }) |
| 182 | if err := st.Save(); err != nil { | 170 | if err := st.Save(); err != nil { |
| 183 | return fmt.Errorf("error saving state: %w", err) | 171 | return fmt.Errorf("error saving state: %w", err) |
| @@ -224,8 +212,25 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 224 | } | 212 | } |
| 225 | } | 213 | } |
| 226 | 214 | ||
| 227 | // Resolve SSH hostname for git remote URL | 215 | // Initialize local git repo if needed |
| 216 | if _, err := os.Stat(".git"); os.IsNotExist(err) { | ||
| 217 | fmt.Println("-> Initializing git repo...") | ||
| 218 | gitInit := exec.Command("git", "init") | ||
| 219 | gitInit.Stdout = os.Stdout | ||
| 220 | gitInit.Stderr = os.Stderr | ||
| 221 | if err := gitInit.Run(); err != nil { | ||
| 222 | return fmt.Errorf("error initializing git repo: %w", err) | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | // Add origin remote (replace if it already exists) | ||
| 228 | sshHost := host | 227 | sshHost := host |
| 228 | remoteURL := fmt.Sprintf("git@%s:%s", sshHost, repo) | ||
| 229 | exec.Command("git", "remote", "remove", "origin").Run() // ignore error if not exists | ||
| 230 | addRemote := exec.Command("git", "remote", "add", "origin", remoteURL) | ||
| 231 | if err := addRemote.Run(); err != nil { | ||
| 232 | return fmt.Errorf("error adding git remote: %w", err) | ||
| 233 | } | ||
| 229 | 234 | ||
| 230 | fmt.Printf("\nProject initialized: %s\n", name) | 235 | fmt.Printf("\nProject initialized: %s\n", name) |
| 231 | fmt.Println("\nGenerated:") | 236 | fmt.Println("\nGenerated:") |
| @@ -240,8 +245,24 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 240 | fmt.Println(" git add .ship/ Dockerfile") | 245 | fmt.Println(" git add .ship/ Dockerfile") |
| 241 | } | 246 | } |
| 242 | fmt.Println(" git commit -m \"initial deploy\"") | 247 | fmt.Println(" git commit -m \"initial deploy\"") |
| 243 | fmt.Printf(" git remote add ship git@%s:%s\n", sshHost, repo) | 248 | fmt.Println(" git push origin main") |
| 244 | fmt.Println(" git push ship main") | 249 | if !static { |
| 250 | fmt.Println("\n (No Dockerfile? Just push — deploy is skipped until one is added.)") | ||
| 251 | } | ||
| 245 | 252 | ||
| 246 | return nil | 253 | return nil |
| 247 | } | 254 | } |
| 255 | |||
| 256 | func writeHook(client *ssh.Client, repo, content string) error { | ||
| 257 | hookPath := fmt.Sprintf("%s/hooks/post-receive", repo) | ||
| 258 | if err := client.WriteSudoFile(hookPath, content); err != nil { | ||
| 259 | return fmt.Errorf("error writing hook: %w", err) | ||
| 260 | } | ||
| 261 | if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", hookPath)); err != nil { | ||
| 262 | return fmt.Errorf("error making hook executable: %w", err) | ||
| 263 | } | ||
| 264 | if _, err := client.RunSudo(fmt.Sprintf("chown git:git %s", hookPath)); err != nil { | ||
| 265 | return fmt.Errorf("error setting hook ownership: %w", err) | ||
| 266 | } | ||
| 267 | return nil | ||
| 268 | } | ||
diff --git a/cmd/ship/list.go b/cmd/ship/list.go index a10b2ca..af5baf8 100644 --- a/cmd/ship/list.go +++ b/cmd/ship/list.go | |||
| @@ -37,7 +37,7 @@ func runList(cmd *cobra.Command, args []string) error { | |||
| 37 | } | 37 | } |
| 38 | 38 | ||
| 39 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) | 39 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) |
| 40 | fmt.Fprintln(w, "NAME\tTYPE\tDOMAIN\tPORT") | 40 | fmt.Fprintln(w, "NAME\tTYPE\tVISIBILITY\tDOMAIN\tPORT") |
| 41 | for name, app := range apps { | 41 | for name, app := range apps { |
| 42 | port := "" | 42 | port := "" |
| 43 | if app.Type == "app" || app.Type == "git-app" { | 43 | if app.Type == "app" || app.Type == "git-app" { |
| @@ -47,7 +47,14 @@ func runList(cmd *cobra.Command, args []string) error { | |||
| 47 | if domain == "" { | 47 | if domain == "" { |
| 48 | domain = "-" | 48 | domain = "-" |
| 49 | } | 49 | } |
| 50 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, app.Type, domain, port) | 50 | visibility := "" |
| 51 | if app.Repo != "" { | ||
| 52 | visibility = "private" | ||
| 53 | if app.Public { | ||
| 54 | visibility = "public" | ||
| 55 | } | ||
| 56 | } | ||
| 57 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, app.Type, visibility, domain, port) | ||
| 51 | } | 58 | } |
| 52 | w.Flush() | 59 | w.Flush() |
| 53 | return nil | 60 | return nil |
