summaryrefslogtreecommitdiffstats
path: root/cmd/deploy/webui.go
blob: f57400e4ebd60abaca68718e04a28ca6ae4c63a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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)
	}
}