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)
}
}
|