No One Can Stop Me Now
This commit is contained in:
453
services/setec-manager/internal/handlers/sites.go
Normal file
453
services/setec-manager/internal/handlers/sites.go
Normal file
@@ -0,0 +1,453 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"setec-manager/internal/db"
|
||||
"setec-manager/internal/deploy"
|
||||
"setec-manager/internal/nginx"
|
||||
)
|
||||
|
||||
var validDomainRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
|
||||
|
||||
func isValidDomain(domain string) bool {
|
||||
if len(domain) > 253 {
|
||||
return false
|
||||
}
|
||||
if net.ParseIP(domain) != nil {
|
||||
return true
|
||||
}
|
||||
return validDomainRegex.MatchString(domain)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteList(w http.ResponseWriter, r *http.Request) {
|
||||
sites, err := h.DB.ListSites()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Enrich with running status
|
||||
type siteView struct {
|
||||
db.Site
|
||||
Running bool `json:"running"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var views []siteView
|
||||
for _, s := range sites {
|
||||
sv := siteView{Site: s}
|
||||
if s.AppType != "static" && s.AppPort > 0 {
|
||||
unitName := fmt.Sprintf("app-%s", s.Domain)
|
||||
active, _ := deploy.IsActive(unitName)
|
||||
sv.Running = active
|
||||
if active {
|
||||
sv.Status = "active"
|
||||
} else {
|
||||
sv.Status = "inactive"
|
||||
}
|
||||
} else {
|
||||
sv.Status = "static"
|
||||
sv.Running = s.Enabled
|
||||
}
|
||||
views = append(views, sv)
|
||||
}
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, views)
|
||||
return
|
||||
}
|
||||
h.render(w, "sites.html", views)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteNewForm(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "site_new.html", nil)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteCreate(w http.ResponseWriter, r *http.Request) {
|
||||
var site db.Site
|
||||
if err := json.NewDecoder(r.Body).Decode(&site); err != nil {
|
||||
// Try form values
|
||||
site.Domain = r.FormValue("domain")
|
||||
site.Aliases = r.FormValue("aliases")
|
||||
site.AppType = r.FormValue("app_type")
|
||||
site.AppRoot = r.FormValue("app_root")
|
||||
site.GitRepo = r.FormValue("git_repo")
|
||||
site.GitBranch = r.FormValue("git_branch")
|
||||
site.AppEntry = r.FormValue("app_entry")
|
||||
}
|
||||
|
||||
if site.Domain == "" {
|
||||
writeError(w, http.StatusBadRequest, "domain is required")
|
||||
return
|
||||
}
|
||||
if !isValidDomain(site.Domain) {
|
||||
writeError(w, http.StatusBadRequest, "invalid domain name")
|
||||
return
|
||||
}
|
||||
if site.AppType == "" {
|
||||
site.AppType = "static"
|
||||
}
|
||||
if site.AppRoot == "" {
|
||||
site.AppRoot = filepath.Join(h.Config.Nginx.Webroot, site.Domain)
|
||||
}
|
||||
if site.GitBranch == "" {
|
||||
site.GitBranch = "main"
|
||||
}
|
||||
site.Enabled = true
|
||||
|
||||
// Check for duplicate
|
||||
existing, _ := h.DB.GetSiteByDomain(site.Domain)
|
||||
if existing != nil {
|
||||
writeError(w, http.StatusConflict, "domain already exists")
|
||||
return
|
||||
}
|
||||
|
||||
// Create directory
|
||||
os.MkdirAll(site.AppRoot, 0755)
|
||||
|
||||
// Clone repo if provided
|
||||
if site.GitRepo != "" {
|
||||
depID, _ := h.DB.CreateDeployment(nil, "clone")
|
||||
out, err := deploy.Clone(site.GitRepo, site.GitBranch, site.AppRoot)
|
||||
if err != nil {
|
||||
h.DB.FinishDeployment(depID, "failed", out)
|
||||
writeError(w, http.StatusInternalServerError, "git clone failed: "+out)
|
||||
return
|
||||
}
|
||||
h.DB.FinishDeployment(depID, "success", out)
|
||||
}
|
||||
|
||||
// Save to DB
|
||||
id, err := h.DB.CreateSite(&site)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
site.ID = id
|
||||
|
||||
// Generate nginx config
|
||||
if err := nginx.GenerateConfig(h.Config, &site); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "nginx config: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Enable site
|
||||
nginx.EnableSite(h.Config, site.Domain)
|
||||
nginx.Reload()
|
||||
|
||||
// Generate systemd unit for non-static apps
|
||||
if site.AppType != "static" && site.AppEntry != "" {
|
||||
h.generateAppUnit(&site)
|
||||
}
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusCreated, site)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, fmt.Sprintf("/sites/%d", id), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteDetail(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Get deployment history
|
||||
deps, _ := h.DB.ListDeployments(&id, 10)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Site": site,
|
||||
"Deployments": deps,
|
||||
}
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, data)
|
||||
return
|
||||
}
|
||||
h.render(w, "site_detail.html", data)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
var update db.Site
|
||||
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
if update.Domain != "" {
|
||||
site.Domain = update.Domain
|
||||
}
|
||||
site.Aliases = update.Aliases
|
||||
if update.AppType != "" {
|
||||
site.AppType = update.AppType
|
||||
}
|
||||
if update.AppPort > 0 {
|
||||
site.AppPort = update.AppPort
|
||||
}
|
||||
site.AppEntry = update.AppEntry
|
||||
site.GitRepo = update.GitRepo
|
||||
site.GitBranch = update.GitBranch
|
||||
site.SSLEnabled = update.SSLEnabled
|
||||
site.Enabled = update.Enabled
|
||||
|
||||
if err := h.DB.UpdateSite(site); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate nginx config
|
||||
nginx.GenerateConfig(h.Config, site)
|
||||
nginx.Reload()
|
||||
|
||||
writeJSON(w, http.StatusOK, site)
|
||||
}
|
||||
|
||||
func (h *Handler) SiteDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Disable nginx
|
||||
nginx.DisableSite(h.Config, site.Domain)
|
||||
nginx.Reload()
|
||||
|
||||
// Stop, disable, and remove the systemd unit
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.RemoveUnit(unitName)
|
||||
|
||||
if err := h.DB.DeleteSite(id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteDeploy(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramInt(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
site, err := h.DB.GetSite(id)
|
||||
if err != nil || site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
depID, _ := h.DB.CreateDeployment(&id, "deploy")
|
||||
|
||||
var output strings.Builder
|
||||
|
||||
// Git pull
|
||||
if site.GitRepo != "" {
|
||||
out, err := deploy.Pull(site.AppRoot)
|
||||
output.WriteString(out)
|
||||
if err != nil {
|
||||
h.DB.FinishDeployment(depID, "failed", output.String())
|
||||
writeError(w, http.StatusInternalServerError, "git pull failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Reinstall deps based on app type
|
||||
switch site.AppType {
|
||||
case "python", "autarch":
|
||||
venvDir := filepath.Join(site.AppRoot, "venv")
|
||||
reqFile := filepath.Join(site.AppRoot, "requirements.txt")
|
||||
if _, err := os.Stat(reqFile); err == nil {
|
||||
out, _ := deploy.InstallRequirements(venvDir, reqFile)
|
||||
output.WriteString(out)
|
||||
}
|
||||
case "node":
|
||||
out, _ := deploy.NpmInstall(site.AppRoot)
|
||||
output.WriteString(out)
|
||||
}
|
||||
|
||||
// Restart service
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.Restart(unitName)
|
||||
|
||||
h.DB.FinishDeployment(depID, "success", output.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deployed"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteRestart(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.Restart(unitName)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "restarted"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteStop(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.Stop(unitName)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "stopped"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteStart(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
deploy.Start(unitName)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "started"})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteLogs(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
out, _ := deploy.Logs(unitName, 100)
|
||||
|
||||
if acceptsJSON(r) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"logs": out})
|
||||
return
|
||||
}
|
||||
h.render(w, "site_detail.html", map[string]interface{}{
|
||||
"Site": site,
|
||||
"Logs": out,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) SiteLogStream(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := paramInt(r, "id")
|
||||
site, _ := h.DB.GetSite(id)
|
||||
if site == nil {
|
||||
writeError(w, http.StatusNotFound, "site not found")
|
||||
return
|
||||
}
|
||||
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
streamJournalctl(w, r, unitName)
|
||||
}
|
||||
|
||||
func (h *Handler) generateAppUnit(site *db.Site) {
|
||||
var execStart string
|
||||
|
||||
switch site.AppType {
|
||||
case "python":
|
||||
venvPython := filepath.Join(site.AppRoot, "venv", "bin", "python3")
|
||||
execStart = fmt.Sprintf("%s %s", venvPython, filepath.Join(site.AppRoot, site.AppEntry))
|
||||
case "node":
|
||||
execStart = fmt.Sprintf("/usr/bin/node %s", filepath.Join(site.AppRoot, site.AppEntry))
|
||||
case "autarch":
|
||||
venvPython := filepath.Join(site.AppRoot, "venv", "bin", "python3")
|
||||
execStart = fmt.Sprintf("%s %s", venvPython, filepath.Join(site.AppRoot, "autarch_web.py"))
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
unitName := fmt.Sprintf("app-%s", site.Domain)
|
||||
unitContent := deploy.GenerateUnit(deploy.UnitConfig{
|
||||
Name: unitName,
|
||||
Description: fmt.Sprintf("%s (%s)", site.Domain, site.AppType),
|
||||
ExecStart: execStart,
|
||||
WorkingDirectory: site.AppRoot,
|
||||
User: "root",
|
||||
Environment: map[string]string{"PYTHONUNBUFFERED": "1"},
|
||||
})
|
||||
|
||||
deploy.InstallUnit(unitName, unitContent)
|
||||
deploy.Enable(unitName)
|
||||
}
|
||||
|
||||
func acceptsJSON(r *http.Request) bool {
|
||||
accept := r.Header.Get("Accept")
|
||||
return strings.Contains(accept, "application/json")
|
||||
}
|
||||
|
||||
func streamJournalctl(w http.ResponseWriter, r *http.Request, unit string) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "streaming not supported")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
cmd := exec.Command("journalctl", "-u", unit, "-f", "-n", "50", "--no-pager", "-o", "short-iso")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd.Start()
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
default:
|
||||
n, err := stdout.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
fmt.Fprintf(w, "data: %s\n\n", strings.TrimSpace(string(buf[:n])))
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user