136 lines
4.1 KiB
Go
136 lines
4.1 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
|
|
}
|
|
if err := a.syncProjectRepoFromMain(p, projectDir); err != nil {
|
|
return err
|
|
}
|
|
if err := a.buildProjectBinary(p); err != nil {
|
|
return err
|
|
}
|
|
return runSystemctlUser(p.UnixUser, "restart", p.ServiceName)
|
|
}
|
|
|
|
func (a *App) buildProjectBinary(p Project) error {
|
|
projectDir, err := a.projectDirFor(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if p.RepoURL == "" {
|
|
return errors.New("project has no repo_url to build from")
|
|
}
|
|
if err := a.syncProjectRepoFromMain(p, projectDir); 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 := ensureOwnedByUnixUser(binDir, p.UnixUser, 0o755); 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 nil
|
|
}
|
|
|
|
func (a *App) syncProjectRepoFromMain(p Project, projectDir string) error {
|
|
repoDir := filepath.Join(projectDir, "repo")
|
|
if _, err := os.Stat(repoDir); os.IsNotExist(err) {
|
|
if err := runCmd("git", "clone", "--branch", "main", "--single-branch", p.RepoURL, repoDir); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
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
|
|
}
|
|
return nil
|
|
}
|