From 87752492d0dc7df3cf78011d5ce315a3eb0cad51 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 23 Jan 2026 21:52:50 -0800 Subject: Restructure CLI with Cobra Replace custom switch-based routing with Cobra for cleaner command hierarchy. Reorganize commands into logical groups: - Root command handles deployment (--binary, --static, --domain, etc.) - App management at top level: list, logs, status, restart, remove - env subcommand group: list, set, unset - host subcommand group: init, status, update, ssh - Standalone: ui (renamed from webui), version Add version command with ldflags support for build info. --- cmd/deploy/deploy.go | 482 --------------------------------------------------- 1 file changed, 482 deletions(-) delete mode 100644 cmd/deploy/deploy.go (limited to 'cmd/deploy/deploy.go') diff --git a/cmd/deploy/deploy.go b/cmd/deploy/deploy.go deleted file mode 100644 index 31aabe5..0000000 --- a/cmd/deploy/deploy.go +++ /dev/null @@ -1,482 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/bdw/deploy/internal/ssh" - "github.com/bdw/deploy/internal/state" - "github.com/bdw/deploy/internal/templates" -) - -type envFlags []string - -func (e *envFlags) String() string { - return strings.Join(*e, ",") -} - -func (e *envFlags) Set(value string) error { - *e = append(*e, value) - return nil -} - -type fileFlags []string - -func (f *fileFlags) String() string { - return strings.Join(*f, ",") -} - -func (f *fileFlags) Set(value string) error { - *f = append(*f, value) - return nil -} - -func runDeploy(args []string) { - fs := flag.NewFlagSet("deploy", flag.ExitOnError) - host := fs.String("host", "", "VPS host (SSH config alias or user@host)") - domain := fs.String("domain", "", "Domain name (required)") - name := fs.String("name", "", "App name (default: inferred from binary or directory)") - binary := fs.String("binary", "", "Path to Go binary (for app deployment)") - static := fs.Bool("static", false, "Deploy as static site") - dir := fs.String("dir", ".", "Directory to deploy (for static sites)") - port := fs.Int("port", 0, "Port override (default: auto-allocate)") - var envVars envFlags - fs.Var(&envVars, "env", "Environment variable (KEY=VALUE, can be specified multiple times)") - envFile := fs.String("env-file", "", "Path to .env file") - binaryArgs := fs.String("args", "", "Arguments to pass to binary") - var files fileFlags - fs.Var(&files, "file", "Config file to upload to working directory (can be specified multiple times)") - - fs.Parse(args) - - // Get host from flag or state default - if *host == "" { - st, err := state.Load() - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) - os.Exit(1) - } - *host = st.GetDefaultHost() - } - - if *host == "" || *domain == "" { - fmt.Fprintf(os.Stderr, "Error: --host and --domain are required\n") - fs.Usage() - os.Exit(1) - } - - if *static { - deployStatic(*host, *domain, *name, *dir) - } else { - deployApp(*host, *domain, *name, *binary, *port, envVars, *envFile, *binaryArgs, files) - } -} - -func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string) { - // Require binary path - if binaryPath == "" { - fmt.Fprintf(os.Stderr, "Error: --binary is required\n") - os.Exit(1) - } - - // Determine app name from binary if not specified - if name == "" { - name = filepath.Base(binaryPath) - } - - // Verify binary exists - if _, err := os.Stat(binaryPath); err != nil { - fmt.Fprintf(os.Stderr, "Error: binary not found: %s\n", binaryPath) - os.Exit(1) - } - - fmt.Printf("Deploying app: %s\n", name) - fmt.Printf(" Domain: %s\n", domain) - fmt.Printf(" Binary: %s\n", binaryPath) - - // Load state - st, err := state.Load() - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) - os.Exit(1) - } - - // Check if app already exists (update) or new deployment - existingApp, _ := st.GetApp(host, name) - var port int - if existingApp != nil { - port = existingApp.Port - fmt.Printf(" Updating existing deployment (port %d)\n", port) - } else { - if portOverride > 0 { - port = portOverride - } else { - port = st.AllocatePort(host) - } - fmt.Printf(" Allocated port: %d\n", port) - } - - // Parse environment variables - env := make(map[string]string) - if existingApp != nil { - // Preserve existing env vars - for k, v := range existingApp.Env { - env[k] = v - } - // Preserve existing args if not provided - if args == "" && existingApp.Args != "" { - args = existingApp.Args - } - // Preserve existing files if not provided - if len(files) == 0 && len(existingApp.Files) > 0 { - files = existingApp.Files - } - } - - // Add/override from flags - for _, e := range envVars { - parts := strings.SplitN(e, "=", 2) - if len(parts) == 2 { - env[parts[0]] = parts[1] - } - } - - // Add/override from file - if envFile != "" { - fileEnv, err := parseEnvFile(envFile) - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading env file: %v\n", err) - os.Exit(1) - } - for k, v := range fileEnv { - env[k] = v - } - } - - // Always set PORT - env["PORT"] = strconv.Itoa(port) - - // Connect to VPS - client, err := ssh.Connect(host) - if err != nil { - fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) - os.Exit(1) - } - defer client.Close() - - // Upload binary - fmt.Println("→ Uploading binary...") - remoteTmpPath := fmt.Sprintf("/tmp/%s", name) - if err := client.Upload(binaryPath, remoteTmpPath); err != nil { - fmt.Fprintf(os.Stderr, "Error uploading binary: %v\n", err) - os.Exit(1) - } - - // Create user (ignore error if already exists) - fmt.Println("→ Creating system user...") - client.RunSudo(fmt.Sprintf("useradd -r -s /bin/false %s", name)) - - // Create working directory - fmt.Println("→ Setting up directories...") - workDir := fmt.Sprintf("/var/lib/%s", name) - if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", workDir)); err != nil { - fmt.Fprintf(os.Stderr, "Error creating work directory: %v\n", err) - os.Exit(1) - } - if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, workDir)); err != nil { - fmt.Fprintf(os.Stderr, "Error setting work directory ownership: %v\n", err) - os.Exit(1) - } - - // Move binary to /usr/local/bin - fmt.Println("→ Installing binary...") - binaryDest := fmt.Sprintf("/usr/local/bin/%s", name) - if _, err := client.RunSudo(fmt.Sprintf("mv %s %s", remoteTmpPath, binaryDest)); err != nil { - fmt.Fprintf(os.Stderr, "Error moving binary: %v\n", err) - os.Exit(1) - } - if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", binaryDest)); err != nil { - fmt.Fprintf(os.Stderr, "Error making binary executable: %v\n", err) - os.Exit(1) - } - - // Upload config files to working directory - if len(files) > 0 { - fmt.Println("→ Uploading config files...") - for _, file := range files { - // Verify file exists locally - if _, err := os.Stat(file); err != nil { - fmt.Fprintf(os.Stderr, "Error: config file not found: %s\n", file) - os.Exit(1) - } - - // Determine remote path (preserve filename) - remotePath := fmt.Sprintf("%s/%s", workDir, filepath.Base(file)) - remoteTmpPath := fmt.Sprintf("/tmp/%s_%s", name, filepath.Base(file)) - - // Upload to tmp first - if err := client.Upload(file, remoteTmpPath); err != nil { - fmt.Fprintf(os.Stderr, "Error uploading config file %s: %v\n", file, err) - os.Exit(1) - } - - // Move to working directory - if _, err := client.RunSudo(fmt.Sprintf("mv %s %s", remoteTmpPath, remotePath)); err != nil { - fmt.Fprintf(os.Stderr, "Error moving config file %s: %v\n", file, err) - os.Exit(1) - } - - // Set ownership - if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, remotePath)); err != nil { - fmt.Fprintf(os.Stderr, "Error setting config file ownership %s: %v\n", file, err) - os.Exit(1) - } - - fmt.Printf(" Uploaded: %s\n", file) - } - } - - // Create env file - fmt.Println("→ Creating environment file...") - envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", name) - envContent := "" - for k, v := range env { - envContent += fmt.Sprintf("%s=%s\n", k, v) - } - if err := client.WriteSudoFile(envFilePath, envContent); err != nil { - fmt.Fprintf(os.Stderr, "Error creating env file: %v\n", err) - os.Exit(1) - } - if _, err := client.RunSudo(fmt.Sprintf("chmod 600 %s", envFilePath)); err != nil { - fmt.Fprintf(os.Stderr, "Error setting env file permissions: %v\n", err) - os.Exit(1) - } - if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, envFilePath)); err != nil { - fmt.Fprintf(os.Stderr, "Error setting env file ownership: %v\n", err) - os.Exit(1) - } - - // Generate systemd unit - fmt.Println("→ Creating systemd service...") - serviceContent, err := templates.SystemdService(map[string]string{ - "Name": name, - "User": name, - "WorkDir": workDir, - "BinaryPath": binaryDest, - "Port": strconv.Itoa(port), - "EnvFile": envFilePath, - "Args": args, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "Error generating systemd unit: %v\n", err) - os.Exit(1) - } - - servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", name) - if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { - fmt.Fprintf(os.Stderr, "Error creating systemd unit: %v\n", err) - os.Exit(1) - } - - // Generate Caddy config - fmt.Println("→ Configuring Caddy...") - caddyContent, err := templates.AppCaddy(map[string]string{ - "Domain": domain, - "Port": strconv.Itoa(port), - }) - if err != nil { - fmt.Fprintf(os.Stderr, "Error generating Caddy config: %v\n", err) - os.Exit(1) - } - - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) - if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { - fmt.Fprintf(os.Stderr, "Error creating Caddy config: %v\n", err) - os.Exit(1) - } - - // Reload systemd - fmt.Println("→ Reloading systemd...") - if _, err := client.RunSudo("systemctl daemon-reload"); err != nil { - fmt.Fprintf(os.Stderr, "Error reloading systemd: %v\n", err) - os.Exit(1) - } - - // Enable and start service - fmt.Println("→ Starting service...") - if _, err := client.RunSudo(fmt.Sprintf("systemctl enable %s", name)); err != nil { - fmt.Fprintf(os.Stderr, "Error enabling service: %v\n", err) - os.Exit(1) - } - if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { - fmt.Fprintf(os.Stderr, "Error starting service: %v\n", err) - os.Exit(1) - } - - // Reload Caddy - fmt.Println("→ Reloading Caddy...") - if _, err := client.RunSudo("systemctl reload caddy"); err != nil { - fmt.Fprintf(os.Stderr, "Error reloading Caddy: %v\n", err) - os.Exit(1) - } - - // Update state - st.AddApp(host, name, &state.App{ - Type: "app", - Domain: domain, - Port: port, - Env: env, - Args: args, - Files: files, - }) - if err := st.Save(); err != nil { - fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err) - os.Exit(1) - } - - fmt.Printf("\n✓ App deployed successfully!\n") - fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", domain) -} - -func deployStatic(host, domain, name, dir string) { - // Determine site name (default to domain to avoid conflicts) - if name == "" { - name = domain - } - - // Verify directory exists - if _, err := os.Stat(dir); err != nil { - fmt.Fprintf(os.Stderr, "Error: directory not found: %s\n", dir) - os.Exit(1) - } - - fmt.Printf("Deploying static site: %s\n", name) - fmt.Printf(" Domain: %s\n", domain) - fmt.Printf(" Directory: %s\n", dir) - - // Load state - st, err := state.Load() - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) - os.Exit(1) - } - - // Connect to VPS - client, err := ssh.Connect(host) - if err != nil { - fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) - os.Exit(1) - } - defer client.Close() - - // Create remote directory - remoteDir := fmt.Sprintf("/var/www/%s", name) - fmt.Println("→ Creating remote directory...") - if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", remoteDir)); err != nil { - fmt.Fprintf(os.Stderr, "Error creating remote directory: %v\n", err) - os.Exit(1) - } - - // Get current user for temporary ownership during upload - currentUser, err := client.Run("whoami") - if err != nil { - fmt.Fprintf(os.Stderr, "Error getting current user: %v\n", err) - os.Exit(1) - } - currentUser = strings.TrimSpace(currentUser) - - // Set ownership to current user for upload - if _, err := client.RunSudo(fmt.Sprintf("chown -R %s:%s %s", currentUser, currentUser, remoteDir)); err != nil { - fmt.Fprintf(os.Stderr, "Error setting temporary ownership: %v\n", err) - os.Exit(1) - } - - // Upload files - fmt.Println("→ Uploading files...") - if err := client.UploadDir(dir, remoteDir); err != nil { - fmt.Fprintf(os.Stderr, "Error uploading files: %v\n", err) - os.Exit(1) - } - - // Set ownership and permissions - fmt.Println("→ Setting permissions...") - if _, err := client.RunSudo(fmt.Sprintf("chown -R www-data:www-data %s", remoteDir)); err != nil { - fmt.Fprintf(os.Stderr, "Error setting ownership: %v\n", err) - os.Exit(1) - } - // Make files readable by all (755 for dirs, 644 for files) - if _, err := client.RunSudo(fmt.Sprintf("chmod -R 755 %s", remoteDir)); err != nil { - fmt.Fprintf(os.Stderr, "Error setting directory permissions: %v\n", err) - os.Exit(1) - } - if _, err := client.RunSudo(fmt.Sprintf("find %s -type f -exec chmod 644 {} \\;", remoteDir)); err != nil { - fmt.Fprintf(os.Stderr, "Error setting file permissions: %v\n", err) - os.Exit(1) - } - - // Generate Caddy config - fmt.Println("→ Configuring Caddy...") - caddyContent, err := templates.StaticCaddy(map[string]string{ - "Domain": domain, - "RootDir": remoteDir, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "Error generating Caddy config: %v\n", err) - os.Exit(1) - } - - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) - if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { - fmt.Fprintf(os.Stderr, "Error creating Caddy config: %v\n", err) - os.Exit(1) - } - - // Reload Caddy - fmt.Println("→ Reloading Caddy...") - if _, err := client.RunSudo("systemctl reload caddy"); err != nil { - fmt.Fprintf(os.Stderr, "Error reloading Caddy: %v\n", err) - os.Exit(1) - } - - // Update state - st.AddApp(host, name, &state.App{ - Type: "static", - Domain: domain, - }) - if err := st.Save(); err != nil { - fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err) - os.Exit(1) - } - - fmt.Printf("\n✓ Static site deployed successfully!\n") - fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", domain) -} - -func parseEnvFile(path string) (map[string]string, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - env := make(map[string]string) - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - env[parts[0]] = parts[1] - } - } - - return env, scanner.Err() -} -- cgit v1.2.3