Added deployments overview
This commit is contained in:
@@ -2,6 +2,7 @@ package github
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -11,7 +12,67 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func FetchAndDeployBranch(repoOwner, repoName, branch, pat, destDir string) error {
|
||||
// DeployResult holds metadata about the deployed commit.
|
||||
type DeployResult struct {
|
||||
CommitHash string `json:"commit_hash"`
|
||||
CommitMessage string `json:"commit_message"`
|
||||
}
|
||||
|
||||
func FetchAndDeployBranch(repoOwner, repoName, branch, pat, destDir string) (*DeployResult, error) {
|
||||
result, err := fetchBranchHead(repoOwner, repoName, branch, pat)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching branch head: %w", err)
|
||||
}
|
||||
|
||||
if err = downloadAndExtract(repoOwner, repoName, branch, pat, destDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// fetchBranchHead returns the SHA and commit message for the tip of the given branch.
|
||||
func fetchBranchHead(owner, repo, branch, pat string) (*DeployResult, error) {
|
||||
url := fmt.Sprintf(
|
||||
"https://api.github.com/repos/%s/%s/commits/%s",
|
||||
owner, repo, branch,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+pat)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching commit: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("GitHub returned %s for %s", resp.Status, url)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
SHA string `json:"sha"`
|
||||
Commit struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"commit"`
|
||||
}
|
||||
if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
return nil, fmt.Errorf("decoding commit response: %w", err)
|
||||
}
|
||||
|
||||
return &DeployResult{
|
||||
CommitHash: payload.SHA,
|
||||
CommitMessage: strings.SplitN(payload.Commit.Message, "\n", 2)[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func downloadAndExtract(repoOwner, repoName, branch, pat, destDir string) error {
|
||||
archiveURL := fmt.Sprintf(
|
||||
"https://api.github.com/repos/%s/%s/zipball/%s",
|
||||
repoOwner, repoName, branch,
|
||||
@@ -59,7 +120,7 @@ func FetchAndDeployBranch(repoOwner, repoName, branch, pat, destDir string) erro
|
||||
return fmt.Errorf("unzipping: %w", err)
|
||||
}
|
||||
|
||||
// github wraps everything in one top-level folder ({owner}-{repo}-{sha}/).
|
||||
// GitHub wraps everything in one top-level folder ({owner}-{repo}-{sha}/).
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading temp dir: %w", err)
|
||||
|
||||
@@ -11,20 +11,22 @@ import (
|
||||
"quay/internal/envconfig"
|
||||
"quay/internal/security"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type UpdateSiteHandler struct {
|
||||
EnvCfg *envconfig.EnvConfig
|
||||
SiteRepo repository.SiteRepository
|
||||
type DeploySiteHandler struct {
|
||||
EnvCfg *envconfig.EnvConfig
|
||||
SiteRepo repository.SiteRepository
|
||||
DeploymentRepo repository.DeploymentRepository
|
||||
}
|
||||
|
||||
func NewUpdateSiteHandler(envCfg *envconfig.EnvConfig, siteRepo repository.SiteRepository) *UpdateSiteHandler {
|
||||
return &UpdateSiteHandler{EnvCfg: envCfg, SiteRepo: siteRepo}
|
||||
func NewDeploySiteHandler(envCfg *envconfig.EnvConfig, siteRepo repository.SiteRepository, deploymentRepo repository.DeploymentRepository) *DeploySiteHandler {
|
||||
return &DeploySiteHandler{EnvCfg: envCfg, SiteRepo: siteRepo, DeploymentRepo: deploymentRepo}
|
||||
}
|
||||
|
||||
func (h *UpdateSiteHandler) PostDeploy(c fiber.Ctx) error {
|
||||
func (h *DeploySiteHandler) PostDeploy(c fiber.Ctx) error {
|
||||
siteId := c.Query("site")
|
||||
if siteId == "" {
|
||||
return c.Status(400).JSON(models.APIError{
|
||||
@@ -40,7 +42,7 @@ func (h *UpdateSiteHandler) PostDeploy(c fiber.Ctx) error {
|
||||
Message: "Site not found",
|
||||
})
|
||||
}
|
||||
log.Println("Message getting site: ", err)
|
||||
log.Println("Error getting site: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while getting site",
|
||||
})
|
||||
@@ -54,6 +56,7 @@ func (h *UpdateSiteHandler) PostDeploy(c fiber.Ctx) error {
|
||||
|
||||
deployToken := site.DeployToken
|
||||
if deployToken == "" {
|
||||
log.Println("No deploy token configured for site " + siteId)
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Message: "Deploy token not configured for this site",
|
||||
})
|
||||
@@ -77,12 +80,26 @@ func (h *UpdateSiteHandler) PostDeploy(c fiber.Ctx) error {
|
||||
|
||||
sitePath := filepath.Join(h.EnvCfg.StoragePath, site.ID)
|
||||
if _, err := filepath.Abs(sitePath); err != nil {
|
||||
log.Println("Error getting absolute path of site: ", err)
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Message: "Failed to resolve site path",
|
||||
})
|
||||
}
|
||||
|
||||
err = github.FetchAndDeployBranch(
|
||||
deployId, err := h.DeploymentRepo.CreateDeployment(&models.Deployment{
|
||||
SiteId: siteId,
|
||||
Status: models.DeploymentStatusRunning,
|
||||
StartTime: time.Now().UTC().Format(time.DateTime),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error creating deployment: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Failed to create deployment",
|
||||
})
|
||||
}
|
||||
|
||||
res, err := github.FetchAndDeployBranch(
|
||||
site.Owner,
|
||||
site.Repository,
|
||||
site.Branch,
|
||||
@@ -90,11 +107,38 @@ func (h *UpdateSiteHandler) PostDeploy(c fiber.Ctx) error {
|
||||
sitePath,
|
||||
)
|
||||
|
||||
if res != nil {
|
||||
deployUpdateErr := h.DeploymentRepo.UpdateDeploymentGitInfo(deployId, res.CommitHash, res.CommitMessage)
|
||||
if deployUpdateErr != nil {
|
||||
log.Println("Error updating deployment git info: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error deploying site: ", err)
|
||||
|
||||
deployUpdateErr := h.DeploymentRepo.UpdateDeploymentStatus(deployId, models.DeploymentStatusFailed)
|
||||
if deployUpdateErr != nil {
|
||||
log.Println("Error updating deployment status to failed: ", err)
|
||||
}
|
||||
deployUpdateErr = h.DeploymentRepo.UpdateDeploymentFinishTime(deployId, time.Now().UTC())
|
||||
if deployUpdateErr != nil {
|
||||
log.Println("Error updating deployment finish time: ", err)
|
||||
}
|
||||
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Message: "Failed to deploy site: " + err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
deployUpdateErr := h.DeploymentRepo.UpdateDeploymentStatus(deployId, models.DeploymentStatusSuccess)
|
||||
if deployUpdateErr != nil {
|
||||
log.Println("Error updating deployment status to success: ", err)
|
||||
}
|
||||
deployUpdateErr = h.DeploymentRepo.UpdateDeploymentFinishTime(deployId, time.Now().UTC())
|
||||
if deployUpdateErr != nil {
|
||||
log.Println("Error updating deployment finish time: ", err)
|
||||
}
|
||||
|
||||
return c.SendStatus(201)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"quay/app/models"
|
||||
"quay/app/repository"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type DeploymentHandler struct {
|
||||
Repo repository.DeploymentRepository
|
||||
}
|
||||
|
||||
func NewDeploymentHandler(repo repository.DeploymentRepository) *DeploymentHandler {
|
||||
return &DeploymentHandler{Repo: repo}
|
||||
}
|
||||
|
||||
// GetDeployment godoc
|
||||
// @Summary Get deployment by ID
|
||||
// @Description Get a single deployment by its ID
|
||||
// @Tags Deployments
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Deployment ID"
|
||||
// @Success 200 {object} models.Deployment
|
||||
// @Failure 404 {object} models.APIError
|
||||
// @Failure 500 {object} models.APIError
|
||||
// @Router /deployments/{id} [get]
|
||||
func (h *DeploymentHandler) GetDeployment(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
|
||||
deployment, err := h.Repo.GetDeploymentByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
|
||||
Message: "Deployment not found",
|
||||
})
|
||||
}
|
||||
log.Println("Error getting deployment by id: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while getting deployment by id",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(&deployment)
|
||||
}
|
||||
|
||||
// GetDeploymentsBySite godoc
|
||||
// @Summary Get deployments for a site
|
||||
// @Description Get a list of deployments for a specific site
|
||||
// @Tags Deployments
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param siteId path string true "Site ID"
|
||||
// @Param limit query int false "Maximum number of deployments to return" default(100)
|
||||
// @Success 200 {object} models.GetDeploymentsBySiteResponse
|
||||
// @Failure 500 {object} models.APIError
|
||||
// @Router /sites/{siteId}/deployments [get]
|
||||
func (h *DeploymentHandler) GetDeploymentsBySite(c fiber.Ctx) error {
|
||||
siteId := c.Params("id")
|
||||
limit, err := strconv.Atoi(c.Query("limit", "100"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
|
||||
Message: "Invalid limit",
|
||||
})
|
||||
}
|
||||
|
||||
deployments, err := h.Repo.GetDeploymentsForSite(siteId, limit)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return c.JSON(models.GetDeploymentsBySiteResponse{
|
||||
Deployments: []models.Deployment{},
|
||||
Total: 0,
|
||||
})
|
||||
}
|
||||
|
||||
log.Println("Error getting deployments: ", err)
|
||||
return c.JSON(models.APIError{
|
||||
Message: "Unexpected error while getting deployments: ",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(models.GetDeploymentsBySiteResponse{
|
||||
Deployments: deployments,
|
||||
Total: len(deployments),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
type DeploymentStatus string
|
||||
|
||||
const (
|
||||
DeploymentStatusPending DeploymentStatus = "pending"
|
||||
DeploymentStatusRunning DeploymentStatus = "running"
|
||||
DeploymentStatusSuccess DeploymentStatus = "success"
|
||||
DeploymentStatusFailed DeploymentStatus = "failed"
|
||||
)
|
||||
|
||||
func (s DeploymentStatus) IsValid() bool {
|
||||
switch s {
|
||||
case DeploymentStatusPending, DeploymentStatusRunning,
|
||||
DeploymentStatusSuccess, DeploymentStatusFailed:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Deployment struct {
|
||||
Id string `json:"id"`
|
||||
SiteId string `json:"site_id"`
|
||||
CommitHash string `json:"commit_hash"`
|
||||
Message string `json:"message"`
|
||||
Status DeploymentStatus `json:"status"`
|
||||
StartTime string `json:"start_time"`
|
||||
FinishTime string `json:"finish_time"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type GetDeploymentsBySiteResponse struct {
|
||||
Deployments []Deployment `json:"deployments"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"quay/app/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeploymentRepository interface {
|
||||
GetDeploymentByID(id string) (*models.Deployment, error)
|
||||
GetDeploymentsForSite(deploymentId string, limit int) ([]models.Deployment, error)
|
||||
CreateDeployment(deployment *models.Deployment) (string, error)
|
||||
UpdateDeployment(deployment *models.Deployment) error
|
||||
UpdateDeploymentStatus(deploymentId string, status models.DeploymentStatus) error
|
||||
UpdateDeploymentFinishTime(deploymentId string, finishTime time.Time) error
|
||||
UpdateDeploymentGitInfo(deploymentId, commitHash, message string) error
|
||||
DeleteDeployment(id string) error
|
||||
}
|
||||
@@ -17,15 +17,18 @@ import (
|
||||
|
||||
func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, db *sql.DB) {
|
||||
siteRepository := cachedrepo.NewCachedSiteRepository(database.NewSQLiteSiteRepository(db))
|
||||
deploymentRepository := database.NewSQLiteDeploymentRepository(db)
|
||||
|
||||
updateSiteHandler := handlers.NewUpdateSiteHandler(envCfg, siteRepository)
|
||||
deploySiteHandler := handlers.NewDeploySiteHandler(envCfg, siteRepository, deploymentRepository)
|
||||
siteHandler := handlers.NewSiteHandler(siteRepository)
|
||||
deploymentsHandler := handlers.NewDeploymentHandler(deploymentRepository)
|
||||
|
||||
api := app.Group("/api/v1", middleware.APIHostGuard(envCfg.DashboardHost))
|
||||
api.Get("/health", handlers.HealthCheck)
|
||||
|
||||
api.Post("/deploy", updateSiteHandler.PostDeploy)
|
||||
api.Post("/deploy", deploySiteHandler.PostDeploy)
|
||||
|
||||
// Sites
|
||||
api.Get("/sites", siteHandler.GetSites)
|
||||
api.Get("/sites/:id", siteHandler.GetSite)
|
||||
api.Post("/sites", siteHandler.PostSite)
|
||||
@@ -33,24 +36,31 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d
|
||||
api.Delete("/sites/:id", siteHandler.DeleteSite)
|
||||
api.Patch("/sites/:id/enabled", siteHandler.ToggleEnabled)
|
||||
|
||||
// Forward rules
|
||||
api.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules)
|
||||
api.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule)
|
||||
api.Get("/forward-rules/:id", siteHandler.GetForwardRule)
|
||||
api.Put("/forward-rules/:id", siteHandler.PutForwardRule)
|
||||
api.Delete("/forward-rules/:id", siteHandler.DeleteForwardRule)
|
||||
|
||||
// Custom headers (header rules)
|
||||
api.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders)
|
||||
api.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders)
|
||||
api.Get("/custom-headers/:id", siteHandler.GetCustomHeaders)
|
||||
api.Put("/custom-headers/:id", siteHandler.PutCustomHeaders)
|
||||
api.Delete("/custom-headers/:id", siteHandler.DeleteCustomHeaders)
|
||||
|
||||
// Headers
|
||||
api.Get("/custom-headers/:id/headers", siteHandler.GetCustomHeaderHeaders)
|
||||
api.Post("/custom-headers/:id/headers", siteHandler.PostHeader)
|
||||
api.Get("/headers/:id", siteHandler.GetHeader)
|
||||
api.Put("/headers/:id", siteHandler.PutHeader)
|
||||
api.Delete("/headers/:id", siteHandler.DeleteHeader)
|
||||
|
||||
// Deployments
|
||||
api.Get("/deployments/:id", deploymentsHandler.GetDeployment)
|
||||
api.Get("/sites/:id/deployments", deploymentsHandler.GetDeploymentsBySite)
|
||||
|
||||
api.Use(func(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
|
||||
Message: "Endpoint not found",
|
||||
|
||||
Reference in New Issue
Block a user