Added deployments overview

This commit is contained in:
2026-04-06 22:04:50 +02:00
parent a95c76ce7e
commit 1978a31cbf
21 changed files with 1249 additions and 169 deletions
+63 -2
View File
@@ -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)
+52 -8
View File
@@ -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)
}
+91
View File
@@ -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),
})
}
+35
View File
@@ -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
}
+12 -2
View File
@@ -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",
+214
View File
@@ -250,6 +250,50 @@ const docTemplate = `{
}
}
},
"/deployments/{id}": {
"get": {
"description": "Get a single deployment by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Deployments"
],
"summary": "Get deployment by ID",
"parameters": [
{
"type": "string",
"description": "Deployment ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Deployment"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.APIError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
},
"/forward-rules/{id}": {
"get": {
"description": "Returns a single forward rule by ID",
@@ -832,6 +876,50 @@ const docTemplate = `{
}
}
},
"/sites/{id}/enabled": {
"patch": {
"description": "Enable or disable a site by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Sites"
],
"summary": "Toggle site enabled status",
"parameters": [
{
"type": "string",
"description": "Site ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.ToggleEnabledResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.APIError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
},
"/sites/{id}/forward-rules": {
"get": {
"description": "Returns all forward rules associated with the given site",
@@ -932,6 +1020,51 @@ const docTemplate = `{
}
}
}
},
"/sites/{siteId}/deployments": {
"get": {
"description": "Get a list of deployments for a specific site",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Deployments"
],
"summary": "Get deployments for a site",
"parameters": [
{
"type": "string",
"description": "Site ID",
"name": "siteId",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 100,
"description": "Maximum number of deployments to return",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.GetDeploymentsBySiteResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
}
},
"definitions": {
@@ -966,11 +1099,58 @@ const docTemplate = `{
"id": {
"type": "string"
},
"regex": {
"type": "boolean"
},
"source": {
"type": "string"
}
}
},
"models.Deployment": {
"type": "object",
"properties": {
"commit_hash": {
"type": "string"
},
"created_at": {
"type": "string"
},
"finish_time": {
"type": "string"
},
"id": {
"type": "string"
},
"message": {
"type": "string"
},
"site_id": {
"type": "string"
},
"start_time": {
"type": "string"
},
"status": {
"$ref": "#/definitions/models.DeploymentStatus"
}
}
},
"models.DeploymentStatus": {
"type": "string",
"enum": [
"pending",
"running",
"success",
"failed"
],
"x-enum-varnames": [
"DeploymentStatusPending",
"DeploymentStatusRunning",
"DeploymentStatusSuccess",
"DeploymentStatusFailed"
]
},
"models.ForwardRule": {
"type": "object",
"properties": {
@@ -1005,6 +1185,20 @@ const docTemplate = `{
}
}
},
"models.GetDeploymentsBySiteResponse": {
"type": "object",
"properties": {
"deployments": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Deployment"
}
},
"total": {
"type": "integer"
}
}
},
"models.Header": {
"type": "object",
"properties": {
@@ -1025,6 +1219,9 @@ const docTemplate = `{
"branch": {
"type": "string"
},
"created_at": {
"type": "string"
},
"custom_headers": {
"type": "array",
"items": {
@@ -1052,6 +1249,12 @@ const docTemplate = `{
"id": {
"type": "string"
},
"index_file": {
"type": "string"
},
"name": {
"type": "string"
},
"not_found_file": {
"type": "string"
},
@@ -1063,6 +1266,17 @@ const docTemplate = `{
},
"spa": {
"type": "boolean"
},
"trailing_slash": {
"type": "boolean"
}
}
},
"models.ToggleEnabledResponse": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
}
+214
View File
@@ -244,6 +244,50 @@
}
}
},
"/deployments/{id}": {
"get": {
"description": "Get a single deployment by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Deployments"
],
"summary": "Get deployment by ID",
"parameters": [
{
"type": "string",
"description": "Deployment ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Deployment"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.APIError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
},
"/forward-rules/{id}": {
"get": {
"description": "Returns a single forward rule by ID",
@@ -826,6 +870,50 @@
}
}
},
"/sites/{id}/enabled": {
"patch": {
"description": "Enable or disable a site by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Sites"
],
"summary": "Toggle site enabled status",
"parameters": [
{
"type": "string",
"description": "Site ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.ToggleEnabledResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.APIError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
},
"/sites/{id}/forward-rules": {
"get": {
"description": "Returns all forward rules associated with the given site",
@@ -926,6 +1014,51 @@
}
}
}
},
"/sites/{siteId}/deployments": {
"get": {
"description": "Get a list of deployments for a specific site",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Deployments"
],
"summary": "Get deployments for a site",
"parameters": [
{
"type": "string",
"description": "Site ID",
"name": "siteId",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 100,
"description": "Maximum number of deployments to return",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.GetDeploymentsBySiteResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
}
},
"definitions": {
@@ -960,11 +1093,58 @@
"id": {
"type": "string"
},
"regex": {
"type": "boolean"
},
"source": {
"type": "string"
}
}
},
"models.Deployment": {
"type": "object",
"properties": {
"commit_hash": {
"type": "string"
},
"created_at": {
"type": "string"
},
"finish_time": {
"type": "string"
},
"id": {
"type": "string"
},
"message": {
"type": "string"
},
"site_id": {
"type": "string"
},
"start_time": {
"type": "string"
},
"status": {
"$ref": "#/definitions/models.DeploymentStatus"
}
}
},
"models.DeploymentStatus": {
"type": "string",
"enum": [
"pending",
"running",
"success",
"failed"
],
"x-enum-varnames": [
"DeploymentStatusPending",
"DeploymentStatusRunning",
"DeploymentStatusSuccess",
"DeploymentStatusFailed"
]
},
"models.ForwardRule": {
"type": "object",
"properties": {
@@ -999,6 +1179,20 @@
}
}
},
"models.GetDeploymentsBySiteResponse": {
"type": "object",
"properties": {
"deployments": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Deployment"
}
},
"total": {
"type": "integer"
}
}
},
"models.Header": {
"type": "object",
"properties": {
@@ -1019,6 +1213,9 @@
"branch": {
"type": "string"
},
"created_at": {
"type": "string"
},
"custom_headers": {
"type": "array",
"items": {
@@ -1046,6 +1243,12 @@
"id": {
"type": "string"
},
"index_file": {
"type": "string"
},
"name": {
"type": "string"
},
"not_found_file": {
"type": "string"
},
@@ -1057,6 +1260,17 @@
},
"spa": {
"type": "boolean"
},
"trailing_slash": {
"type": "boolean"
}
}
},
"models.ToggleEnabledResponse": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
}
+143
View File
@@ -20,9 +20,42 @@ definitions:
type: array
id:
type: string
regex:
type: boolean
source:
type: string
type: object
models.Deployment:
properties:
commit_hash:
type: string
created_at:
type: string
finish_time:
type: string
id:
type: string
message:
type: string
site_id:
type: string
start_time:
type: string
status:
$ref: '#/definitions/models.DeploymentStatus'
type: object
models.DeploymentStatus:
enum:
- pending
- running
- success
- failed
type: string
x-enum-varnames:
- DeploymentStatusPending
- DeploymentStatusRunning
- DeploymentStatusSuccess
- DeploymentStatusFailed
models.ForwardRule:
properties:
destination:
@@ -45,6 +78,15 @@ definitions:
total:
type: integer
type: object
models.GetDeploymentsBySiteResponse:
properties:
deployments:
items:
$ref: '#/definitions/models.Deployment'
type: array
total:
type: integer
type: object
models.Header:
properties:
id:
@@ -58,6 +100,8 @@ definitions:
properties:
branch:
type: string
created_at:
type: string
custom_headers:
items:
$ref: '#/definitions/models.CustomHeaders'
@@ -76,6 +120,10 @@ definitions:
type: string
id:
type: string
index_file:
type: string
name:
type: string
not_found_file:
type: string
owner:
@@ -84,6 +132,13 @@ definitions:
type: string
spa:
type: boolean
trailing_slash:
type: boolean
type: object
models.ToggleEnabledResponse:
properties:
enabled:
type: boolean
type: object
host: localhost:4321
info:
@@ -249,6 +304,35 @@ paths:
summary: Create a header
tags:
- Headers
/deployments/{id}:
get:
consumes:
- application/json
description: Get a single deployment by its ID
parameters:
- description: Deployment ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Deployment'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.APIError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Get deployment by ID
tags:
- Deployments
/forward-rules/{id}:
delete:
description: Deletes a forward rule by ID
@@ -635,6 +719,35 @@ paths:
summary: Create a custom header group
tags:
- Custom-Headers
/sites/{id}/enabled:
patch:
consumes:
- application/json
description: Enable or disable a site by its ID
parameters:
- description: Site ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.ToggleEnabledResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.APIError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Toggle site enabled status
tags:
- Sites
/sites/{id}/forward-rules:
get:
description: Returns all forward rules associated with the given site
@@ -702,6 +815,36 @@ paths:
summary: Create a forward rule
tags:
- Forward-Rules
/sites/{siteId}/deployments:
get:
consumes:
- application/json
description: Get a list of deployments for a specific site
parameters:
- description: Site ID
in: path
name: siteId
required: true
type: string
- default: 100
description: Maximum number of deployments to return
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.GetDeploymentsBySiteResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Get deployments for a site
tags:
- Deployments
swagger: "2.0"
tags:
- description: Manage sites
@@ -0,0 +1,149 @@
package database
import (
"database/sql"
"fmt"
"quay/app/models"
"quay/app/repository"
"time"
"github.com/google/uuid"
)
type SQLiteDeploymentRepository struct {
db *sql.DB
}
func NewSQLiteDeploymentRepository(db *sql.DB) *SQLiteDeploymentRepository {
return &SQLiteDeploymentRepository{db: db}
}
var _ repository.DeploymentRepository = (*SQLiteDeploymentRepository)(nil)
func (r *SQLiteDeploymentRepository) GetDeploymentByID(id string) (*models.Deployment, error) {
row := r.db.QueryRow(`
SELECT id, site_id, commit_hash, message, status, start_time, finish_time, created_at
FROM deployments WHERE id = ?`, id)
d, err := scanDeployment(row)
if err != nil {
return nil, fmt.Errorf("get deployment: %w", err)
}
return d, nil
}
func (r *SQLiteDeploymentRepository) GetDeploymentsForSite(siteId string, limit int) ([]models.Deployment, error) {
rows, err := r.db.Query(`
SELECT id, site_id, commit_hash, message, status, start_time, finish_time, created_at
FROM deployments WHERE site_id = ?
ORDER BY created_at DESC
LIMIT ?`, siteId, limit)
if err != nil {
return nil, fmt.Errorf("get deployments: %w", err)
}
var deployments []models.Deployment
for rows.Next() {
d, err := scanDeployment(rows)
if err != nil {
return nil, fmt.Errorf("scan deployment: %w", err)
}
deployments = append(deployments, *d)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate deployments: %w", err)
}
return deployments, nil
}
func (r *SQLiteDeploymentRepository) CreateDeployment(d *models.Deployment) (string, error) {
d.Id = uuid.NewString()
if d.Status == "" || !d.Status.IsValid() {
d.Status = models.DeploymentStatusPending
}
_, err := r.db.Exec(`
INSERT INTO deployments (id, site_id, commit_hash, message, status, start_time, finish_time)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
d.Id, d.SiteId, d.CommitHash, d.Message, d.Status, d.StartTime, d.FinishTime,
)
if err != nil {
return "", fmt.Errorf("create deployment: %w", err)
}
return d.Id, nil
}
func (r *SQLiteDeploymentRepository) UpdateDeployment(d *models.Deployment) error {
_, err := r.db.Exec(`
UPDATE deployments SET site_id=?, commit_hash=?, message=?, status=?, start_time=?, finish_time=?
WHERE id=?`,
d.SiteId, d.CommitHash, d.Message, d.Status, d.StartTime, d.FinishTime, d.Id,
)
if err != nil {
return fmt.Errorf("update deployment: %w", err)
}
return nil
}
func (r *SQLiteDeploymentRepository) UpdateDeploymentStatus(deploymentId string, status models.DeploymentStatus) error {
if !status.IsValid() {
return fmt.Errorf("invalid deployment status")
}
_, err := r.db.Exec(`
UPDATE deployments SET status=? WHERE id=?`,
status, deploymentId,
)
if err != nil {
return fmt.Errorf("update deployment status: %w", err)
}
return nil
}
func (r *SQLiteDeploymentRepository) UpdateDeploymentFinishTime(deploymentId string, finishTime time.Time) error {
_, err := r.db.Exec(`
UPDATE deployments SET finish_time=? WHERE id=?`,
finishTime.Format(time.DateTime), deploymentId,
)
if err != nil {
return fmt.Errorf("update deployment finish time: %w", err)
}
return nil
}
func (r *SQLiteDeploymentRepository) UpdateDeploymentGitInfo(deploymentId, commitHash, message string) error {
_, err := r.db.Exec(`
UPDATE deployments SET commit_hash=?, message=? WHERE id=?`,
commitHash, message, deploymentId,
)
if err != nil {
return fmt.Errorf("update deployment git info: %w", err)
}
return nil
}
func (r *SQLiteDeploymentRepository) DeleteDeployment(id string) error {
_, err := r.db.Exec(`DELETE FROM deployments WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete deployment: %w", err)
}
return nil
}
func scanDeployment(s scanner) (*models.Deployment, error) {
var d models.Deployment
err := s.Scan(
&d.Id, &d.SiteId, &d.CommitHash, &d.Message,
&d.Status, &d.StartTime, &d.FinishTime, &d.CreatedAt,
)
if err != nil {
return nil, err
}
return &d, nil
}
+11
View File
@@ -49,6 +49,17 @@ CREATE TABLE IF NOT EXISTS headers (
value TEXT NOT NULL,
FOREIGN KEY (custom_header_id) REFERENCES custom_headers(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS deployments (
id TEXT PRIMARY KEY,
site_id TEXT NOT NULL,
commit_hash TEXT,
message TEXT,
status TEXT NOT NULL,
start_time TIMESTAMP NOT NULL DEFAULT 0,
finish_time TIMESTAMP NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`)
if err == nil {