use caddyfile instead of api
This commit is contained in:
parent
53a5e2e45a
commit
3c79a8c778
9 changed files with 206 additions and 248 deletions
|
|
@ -8,7 +8,10 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -57,90 +60,109 @@ func (a *App) ensureCaddyRoute(ctx context.Context, p Project) error {
|
|||
if !a.cfg.CaddyRouteEnabled {
|
||||
return nil
|
||||
}
|
||||
projectDir, err := a.projectDirFor(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
socketPath := filepath.Join(projectDir, "app.sock")
|
||||
route := map[string]any{
|
||||
"@id": "project-route-" + p.UserID + "-" + p.Slug,
|
||||
"match": []any{map[string]any{"host": []string{p.RouteHost}}},
|
||||
"handle": []any{map[string]any{"handler": "reverse_proxy", "upstreams": []any{map[string]any{"dial": "unix/" + socketPath}}}},
|
||||
}
|
||||
return a.syncCaddyConfigFragment(ctx)
|
||||
}
|
||||
|
||||
cfg, err := a.fetchCaddyConfig(ctx)
|
||||
func (a *App) syncCaddyConfigFragment(ctx context.Context) error {
|
||||
path := strings.TrimSpace(a.cfg.CaddyFragmentPath)
|
||||
if path == "" {
|
||||
return errors.New("CADDY_FRAGMENT_PATH is required when CADDY_ROUTE_ENABLED=true")
|
||||
}
|
||||
routes, err := a.listCaddyProjectRoutes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newRoutes, err := mergedDedupedRoutes(cfg, a.cfg.CaddyServerID, route)
|
||||
content, err := a.renderCaddyRoutesFragment(routes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, _ := json.Marshal(newRoutes)
|
||||
url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy routes rewrite failed: %w", err)
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, []byte(content), 0o640); err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("caddy routes rewrite error: status=%d body=%s", resp.StatusCode, string(respBody))
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.reloadCaddy()
|
||||
}
|
||||
|
||||
func (a *App) reloadCaddy() error {
|
||||
reloadCmd := strings.TrimSpace(a.cfg.CaddyReloadCommand)
|
||||
if reloadCmd == "" {
|
||||
return nil
|
||||
}
|
||||
cmd := exec.Command("bash", "-lc", reloadCmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy reload failed: %w output=%s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergedDedupedRoutes(cfg map[string]any, serverID string, desired map[string]any) ([]any, error) {
|
||||
apps, ok := cfg["apps"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("caddy config missing apps")
|
||||
func (a *App) listCaddyProjectRoutes(ctx context.Context) ([]Project, error) {
|
||||
rows, err := a.db.QueryContext(ctx, `
|
||||
SELECT p.id, p.user_id, p.name, p.slug, p.route_host, p.workspace_id, w.name, w.unix_user
|
||||
FROM projects p
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.route_host <> ''
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpApp, ok := apps["http"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("caddy config missing apps.http")
|
||||
}
|
||||
servers, ok := httpApp["servers"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("caddy config missing apps.http.servers")
|
||||
}
|
||||
server, ok := servers[serverID].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("caddy config missing server %q", serverID)
|
||||
}
|
||||
routes, ok := server["routes"].([]any)
|
||||
if !ok {
|
||||
routes = []any{}
|
||||
}
|
||||
desiredID, _ := desired["@id"].(string)
|
||||
var out []any
|
||||
seenDesired := false
|
||||
for i, r := range routes {
|
||||
_ = i
|
||||
rm, ok := r.(map[string]any)
|
||||
if !ok {
|
||||
out = append(out, r)
|
||||
continue
|
||||
defer rows.Close()
|
||||
var out []Project
|
||||
for rows.Next() {
|
||||
var p Project
|
||||
if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.RouteHost, &p.WorkspaceID, &p.Workspace, &p.UnixUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rid, ok := rm["@id"].(string); ok && rid == desiredID {
|
||||
if !seenDesired {
|
||||
out = append(out, desired)
|
||||
seenDesired = true
|
||||
}
|
||||
continue
|
||||
out = append(out, p)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].RouteHost == out[j].RouteHost {
|
||||
return out[i].ID < out[j].ID
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
if !seenDesired {
|
||||
out = append(out, desired)
|
||||
}
|
||||
return out[i].RouteHost < out[j].RouteHost
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *App) renderCaddyRoutesFragment(projects []Project) (string, error) {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Managed by project-manager. Do not edit manually.\n")
|
||||
for _, p := range projects {
|
||||
projectDir, err := a.projectDirFor(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
socketPath := filepath.Join(projectDir, "app.sock")
|
||||
b.WriteString("\n")
|
||||
b.WriteString(p.RouteHost)
|
||||
b.WriteString(" {\n")
|
||||
if a.cfg.CaddyAuthEnabled {
|
||||
importName := strings.TrimSpace(a.cfg.CaddyAuthImportName)
|
||||
if importName == "" {
|
||||
importName = "auth"
|
||||
}
|
||||
b.WriteString(" import ")
|
||||
b.WriteString(importName)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString(" log\n")
|
||||
b.WriteString(" reverse_proxy unix//")
|
||||
b.WriteString(socketPath)
|
||||
b.WriteString("\n")
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func (a *App) fetchCaddyConfig(ctx context.Context) (map[string]any, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.CaddyAdminURL+"/config/", nil)
|
||||
if err != nil {
|
||||
|
|
@ -162,151 +184,35 @@ func (a *App) fetchCaddyConfig(ctx context.Context) (map[string]any, error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (a *App) ensureCaddyTLSSubject(ctx context.Context, host string) error {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
func (a *App) persistCaddyConfigToDisk(ctx context.Context) error {
|
||||
path := strings.TrimSpace(a.cfg.CaddyPersistConfigPath)
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
cfg, err := a.fetchCaddyConfig(ctx)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.CaddyAdminURL+"/config/", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx, subjects, found, err := findTLSPolicyForHost(cfg, host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if found {
|
||||
return nil
|
||||
}
|
||||
if idx < 0 {
|
||||
return errors.New("no suitable Caddy TLS automation policy found")
|
||||
}
|
||||
|
||||
// Preferred: append one subject.
|
||||
appendURL := fmt.Sprintf("%s/config/apps/tls/automation/policies/%d/subjects", a.cfg.CaddyAdminURL, idx)
|
||||
body, _ := json.Marshal(host)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, appendURL, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 300 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: replace the subjects array with host appended.
|
||||
subjects = append(subjects, host)
|
||||
replaceBody, _ := json.Marshal(subjects)
|
||||
putReq, err := http.NewRequestWithContext(ctx, http.MethodPut, appendURL, strings.NewReader(string(replaceBody)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy config fetch for persist failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("caddy config fetch for persist error: status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, body, 0o640); err != nil {
|
||||
return err
|
||||
}
|
||||
putReq.Header.Set("Content-Type", "application/json")
|
||||
putResp, err := http.DefaultClient.Do(putReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy tls subjects update failed: %w", err)
|
||||
}
|
||||
defer putResp.Body.Close()
|
||||
if putResp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(putResp.Body)
|
||||
return fmt.Errorf("caddy tls subjects update error: status=%d body=%s", putResp.StatusCode, string(b))
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
func (a *App) ensureCaddyTLSSubject(ctx context.Context, host string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findTLSPolicyForHost(cfg map[string]any, host string) (policyIdx int, subjects []string, alreadyPresent bool, err error) {
|
||||
policyIdx = -1
|
||||
apps, ok := cfg["apps"].(map[string]any)
|
||||
if !ok {
|
||||
return -1, nil, false, errors.New("caddy config missing apps")
|
||||
}
|
||||
tlsObj, ok := apps["tls"].(map[string]any)
|
||||
if !ok {
|
||||
return -1, nil, false, errors.New("caddy config missing apps.tls")
|
||||
}
|
||||
automation, ok := tlsObj["automation"].(map[string]any)
|
||||
if !ok {
|
||||
return -1, nil, false, errors.New("caddy config missing apps.tls.automation")
|
||||
}
|
||||
policiesAny, ok := automation["policies"].([]any)
|
||||
if !ok || len(policiesAny) == 0 {
|
||||
return -1, nil, false, errors.New("caddy config missing tls automation policies")
|
||||
}
|
||||
|
||||
// Prefer a non-internal policy with subjects list.
|
||||
for i, pAny := range policiesAny {
|
||||
pol, ok := pAny.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
subjectsRaw, ok := pol["subjects"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cur := make([]string, 0, len(subjectsRaw))
|
||||
for _, s := range subjectsRaw {
|
||||
if sv, ok := s.(string); ok {
|
||||
cur = append(cur, sv)
|
||||
if strings.EqualFold(strings.TrimSpace(sv), host) {
|
||||
return i, cur, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if !policyHasInternalIssuer(pol) && policyIdx < 0 {
|
||||
policyIdx = i
|
||||
subjects = cur
|
||||
}
|
||||
}
|
||||
// Fallback to first policy that has subjects.
|
||||
if policyIdx < 0 {
|
||||
for i, pAny := range policiesAny {
|
||||
pol, ok := pAny.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
subjectsRaw, ok := pol["subjects"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cur := make([]string, 0, len(subjectsRaw))
|
||||
for _, s := range subjectsRaw {
|
||||
if sv, ok := s.(string); ok {
|
||||
cur = append(cur, sv)
|
||||
if strings.EqualFold(strings.TrimSpace(sv), host) {
|
||||
return i, cur, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
policyIdx = i
|
||||
subjects = cur
|
||||
break
|
||||
}
|
||||
}
|
||||
return policyIdx, subjects, false, nil
|
||||
}
|
||||
|
||||
func policyHasInternalIssuer(pol map[string]any) bool {
|
||||
issuersAny, ok := pol["issuers"].([]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, iAny := range issuersAny {
|
||||
iss, ok := iAny.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if mod, ok := iss["module"].(string); ok && strings.EqualFold(strings.TrimSpace(mod), "internal") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) ensureForgejoDeployWebhook(ctx context.Context, p Project) error {
|
||||
if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" || p.RepoURL == "" {
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue