From 98b9af372025595e8a4255538e2836e019311474 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 23 Jan 2026 20:54:46 -0800 Subject: Add deploy command and fix static site naming Static sites now default to using the domain as the name instead of the source directory basename, preventing conflicts when multiple sites use the same directory name (e.g., dist). Also fixes .gitignore to not exclude cmd/deploy/ directory. --- cmd/deploy/webui.go | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 cmd/deploy/webui.go (limited to 'cmd/deploy/webui.go') diff --git a/cmd/deploy/webui.go b/cmd/deploy/webui.go new file mode 100644 index 0000000..f57400e --- /dev/null +++ b/cmd/deploy/webui.go @@ -0,0 +1,228 @@ +package main + +import ( + "embed" + "encoding/json" + "flag" + "fmt" + "html/template" + "net/http" + "os" + "sort" + "strconv" + + "github.com/bdw/deploy/internal/state" + "github.com/bdw/deploy/internal/templates" +) + +//go:embed templates/*.html +var templatesFS embed.FS + +func runWebUI(args []string) { + fs := flag.NewFlagSet("webui", flag.ExitOnError) + port := fs.String("port", "8080", "Port to run the web UI on") + help := fs.Bool("h", false, "Show help") + + fs.Parse(args) + + if *help { + fmt.Fprintf(os.Stderr, `Usage: deploy webui [flags] + +Launch a web interface to view and manage deployments. + +FLAGS: + -port string + Port to run the web UI on (default "8080") + -h Show this help message + +EXAMPLE: + # Launch web UI on default port (8080) + deploy webui + + # Launch on custom port + deploy webui -port 3000 +`) + os.Exit(0) + } + + // Parse template + tmpl, err := template.ParseFS(templatesFS, "templates/webui.html") + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing template: %v\n", err) + os.Exit(1) + } + + // Handler for the main page + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Reload state on each request to show latest changes + st, err := state.Load() + if err != nil { + http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) + return + } + + // Prepare data for template + type AppData struct { + Name string + Type string + Domain string + Port int + Env map[string]string + Host string + } + + type HostData struct { + Host string + Apps []AppData + } + + var hosts []HostData + for hostName, host := range st.Hosts { + var apps []AppData + for appName, app := range host.Apps { + apps = append(apps, AppData{ + Name: appName, + Type: app.Type, + Domain: app.Domain, + Port: app.Port, + Env: app.Env, + Host: hostName, + }) + } + + // Sort apps by name + sort.Slice(apps, func(i, j int) bool { + return apps[i].Name < apps[j].Name + }) + + hosts = append(hosts, HostData{ + Host: hostName, + Apps: apps, + }) + } + + // Sort hosts by name + sort.Slice(hosts, func(i, j int) bool { + return hosts[i].Host < hosts[j].Host + }) + + data := struct { + Hosts []HostData + }{ + Hosts: hosts, + } + + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError) + return + } + }) + + // API endpoint to get state as JSON + http.HandleFunc("/api/state", func(w http.ResponseWriter, r *http.Request) { + st, err := state.Load() + if err != nil { + http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(st) + }) + + // API endpoint to get rendered configs for an app + http.HandleFunc("/api/configs", func(w http.ResponseWriter, r *http.Request) { + host := r.URL.Query().Get("host") + appName := r.URL.Query().Get("app") + + if host == "" || appName == "" { + http.Error(w, "Missing host or app parameter", http.StatusBadRequest) + return + } + + st, err := state.Load() + if err != nil { + http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) + return + } + + app, err := st.GetApp(host, appName) + if err != nil { + http.Error(w, fmt.Sprintf("App not found: %v", err), http.StatusNotFound) + return + } + + configs := make(map[string]string) + + // Render environment file + if app.Env != nil && len(app.Env) > 0 { + envContent := "" + for k, v := range app.Env { + envContent += fmt.Sprintf("%s=%s\n", k, v) + } + configs["env"] = envContent + configs["envPath"] = fmt.Sprintf("/etc/deploy/env/%s.env", appName) + } + + // Render configs based on app type + if app.Type == "app" { + // Render systemd service + workDir := fmt.Sprintf("/var/lib/%s", appName) + binaryPath := fmt.Sprintf("/usr/local/bin/%s", appName) + envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", appName) + + serviceContent, err := templates.SystemdService(map[string]string{ + "Name": appName, + "User": appName, + "WorkDir": workDir, + "BinaryPath": binaryPath, + "Port": strconv.Itoa(app.Port), + "EnvFile": envFilePath, + "Args": app.Args, + }) + if err != nil { + http.Error(w, fmt.Sprintf("Error rendering systemd service: %v", err), http.StatusInternalServerError) + return + } + configs["systemd"] = serviceContent + configs["systemdPath"] = fmt.Sprintf("/etc/systemd/system/%s.service", appName) + + // Render Caddy config + caddyContent, err := templates.AppCaddy(map[string]string{ + "Domain": app.Domain, + "Port": strconv.Itoa(app.Port), + }) + if err != nil { + http.Error(w, fmt.Sprintf("Error rendering Caddy config: %v", err), http.StatusInternalServerError) + return + } + configs["caddy"] = caddyContent + configs["caddyPath"] = fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", appName) + } else if app.Type == "static" { + // Render Caddy config for static site + remoteDir := fmt.Sprintf("/var/www/%s", appName) + caddyContent, err := templates.StaticCaddy(map[string]string{ + "Domain": app.Domain, + "RootDir": remoteDir, + }) + if err != nil { + http.Error(w, fmt.Sprintf("Error rendering Caddy config: %v", err), http.StatusInternalServerError) + return + } + configs["caddy"] = caddyContent + configs["caddyPath"] = fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", appName) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(configs) + }) + + addr := fmt.Sprintf("localhost:%s", *port) + fmt.Printf("Starting web UI on http://%s\n", addr) + fmt.Printf("Press Ctrl+C to stop\n") + + if err := http.ListenAndServe(addr, nil); err != nil { + fmt.Fprintf(os.Stderr, "Error starting server: %v\n", err) + os.Exit(1) + } +} -- cgit v1.2.3