box/internal/app/deploy_service.go

108 lines
3.5 KiB
Go

package app
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
)
func (a *App) handleDeployWebhook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/webhooks/deploy/")
parts := strings.Split(path, "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
http.Error(w, "bad path", http.StatusBadRequest)
return
}
projectID, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
http.Error(w, "invalid project id", http.StatusBadRequest)
return
}
p, err := a.getProjectForWebhook(r.Context(), projectID, parts[1])
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
var payload struct {
Ref string `json:"ref"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if payload.Ref != "refs/heads/main" {
writeJSON(w, http.StatusAccepted, map[string]string{"status": "ignored", "reason": "not main branch"})
return
}
if err := a.deployProjectFromMain(r.Context(), p); err != nil {
log.Printf("deploy failed project_id=%d err=%v", p.ID, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deployed"})
}
func (a *App) getProjectForWebhook(ctx context.Context, projectID int64, token string) (Project, error) {
var p Project
err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.created_at
FROM projects p JOIN workspaces w ON w.id=p.workspace_id WHERE p.id=$1 AND p.deploy_token=$2`, projectID, token).
Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.CreatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Project{}, errors.New("project not found")
}
return Project{}, err
}
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
return p, nil
}
func (a *App) deployProjectFromMain(_ context.Context, p Project) error {
if p.RepoURL == "" {
return errors.New("project has no repo_url to deploy from")
}
projectDir, err := a.projectDirFor(p)
if err != nil {
return err
}
repoDir := filepath.Join(projectDir, "repo")
binDir := filepath.Join(projectDir, "bin")
if err := os.MkdirAll(binDir, 0o755); err != nil {
return err
}
if _, err := os.Stat(repoDir); os.IsNotExist(err) {
if err := runCmd("git", "clone", "--branch", "main", "--single-branch", p.RepoURL, repoDir); err != nil {
return err
}
} else {
if err := runCmd("git", "-C", repoDir, "remote", "set-url", "origin", p.RepoURL); err != nil {
return err
}
if err := runCmd("git", "-C", repoDir, "fetch", "origin", "main", "--prune"); err != nil {
return err
}
if err := runCmd("git", "-C", repoDir, "reset", "--hard", "origin/main"); err != nil {
return err
}
}
binaryPath := filepath.Join(binDir, "app")
if err := runCmd("go", "-C", repoDir, "build", "-o", binaryPath, "."); err != nil {
return fmt.Errorf("build failed: %w", err)
}
if err := ensureOwnedByUnixUser(binaryPath, p.UnixUser, 0o755); err != nil {
return err
}
return runSystemctlUser(p.UnixUser, "restart", p.ServiceName)
}