implemented gitops controller + drift detection
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/events"
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
type GitOpsHandler struct {
|
||||
db *xorm.Engine
|
||||
bus events.EventBus
|
||||
}
|
||||
|
||||
func NewGitOpsHandler(db *xorm.Engine, bus events.EventBus) *GitOpsHandler {
|
||||
return &GitOpsHandler{db: db, bus: bus}
|
||||
}
|
||||
|
||||
// GetConfig returns the GitOpsConfig for an environment, or 404 if not configured.
|
||||
func (h *GitOpsHandler) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
env, ok := h.resolveGitOpsEnv(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var cfg models.GitOpsConfig
|
||||
if found, _ := h.db.Where("env_id = ?", env.ID).Get(&cfg); !found {
|
||||
jsonError(w, "gitops not configured for this environment", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
jsonOK(w, cfg)
|
||||
}
|
||||
|
||||
// UpsertConfig creates or replaces the GitOpsConfig for an environment.
|
||||
func (h *GitOpsHandler) UpsertConfig(w http.ResponseWriter, r *http.Request) {
|
||||
env, ok := h.resolveGitOpsEnv(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Branch string `json:"branch"`
|
||||
AutoSync bool `json:"autoSync"`
|
||||
SyncInterval int `json:"syncInterval"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Branch == "" {
|
||||
jsonError(w, "branch is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg models.GitOpsConfig
|
||||
exists, _ := h.db.Where("env_id = ?", env.ID).Get(&cfg)
|
||||
|
||||
cfg.EnvID = env.ID
|
||||
cfg.RepoID = env.RepoID
|
||||
cfg.Branch = body.Branch
|
||||
cfg.AutoSync = body.AutoSync
|
||||
cfg.SyncInterval = body.SyncInterval
|
||||
if cfg.SyncStatus == "" {
|
||||
cfg.SyncStatus = "unknown"
|
||||
}
|
||||
|
||||
var err error
|
||||
if exists {
|
||||
_, err = h.db.ID(cfg.ID).Cols("branch", "auto_sync", "sync_interval").Update(&cfg)
|
||||
} else {
|
||||
_, err = h.db.Insert(&cfg)
|
||||
}
|
||||
if err != nil {
|
||||
jsonError(w, "could not save gitops config", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonOK(w, cfg)
|
||||
}
|
||||
|
||||
// DeleteConfig removes the GitOpsConfig for an environment without deleting deployments.
|
||||
func (h *GitOpsHandler) DeleteConfig(w http.ResponseWriter, r *http.Request) {
|
||||
env, ok := h.resolveGitOpsEnv(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := h.db.Where("env_id = ?", env.ID).Delete(&models.GitOpsConfig{}); err != nil {
|
||||
jsonError(w, "could not delete gitops config", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// TriggerSync manually initiates a reconciliation for the environment.
|
||||
func (h *GitOpsHandler) TriggerSync(w http.ResponseWriter, r *http.Request) {
|
||||
env, ok := h.resolveGitOpsEnv(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var cfg models.GitOpsConfig
|
||||
if found, _ := h.db.Where("env_id = ?", env.ID).Get(&cfg); !found {
|
||||
jsonError(w, "gitops not configured for this environment", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if cfg.DesiredSHA == "" {
|
||||
jsonError(w, "no desired SHA known yet — push to the configured branch first", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if cfg.SyncStatus == "syncing" {
|
||||
jsonError(w, "a sync is already in progress", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
deploy := &models.Deployment{
|
||||
EnvID: env.ID,
|
||||
RepoID: env.RepoID,
|
||||
SHA: cfg.DesiredSHA,
|
||||
Ref: "refs/heads/" + cfg.Branch,
|
||||
Status: models.DeployStatusPending,
|
||||
TriggeredBy: "gitops-manual",
|
||||
Description: "Manual GitOps sync",
|
||||
StartedAt: &now,
|
||||
}
|
||||
if _, err := h.db.Insert(deploy); err != nil {
|
||||
jsonError(w, "could not create deployment", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cfg.SyncStatus = "syncing"
|
||||
h.db.ID(cfg.ID).Cols("sync_status").Update(&cfg) //nolint:errcheck
|
||||
|
||||
h.bus.Publish(events.SubjectDeploymentStarted, events.DeploymentEvent{ //nolint:errcheck
|
||||
DeploymentID: deploy.ID,
|
||||
EnvID: env.ID,
|
||||
EnvName: env.Name,
|
||||
RepoID: deploy.RepoID,
|
||||
SHA: deploy.SHA,
|
||||
Ref: deploy.Ref,
|
||||
Status: string(deploy.Status),
|
||||
TriggeredBy: deploy.TriggeredBy,
|
||||
})
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
jsonOK(w, deploy)
|
||||
}
|
||||
|
||||
// GetDriftStatus returns the current sync status and SHA comparison for an environment.
|
||||
func (h *GitOpsHandler) GetDriftStatus(w http.ResponseWriter, r *http.Request) {
|
||||
env, ok := h.resolveGitOpsEnv(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var cfg models.GitOpsConfig
|
||||
if found, _ := h.db.Where("env_id = ?", env.ID).Get(&cfg); !found {
|
||||
jsonError(w, "gitops not configured for this environment", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
type driftStatus struct {
|
||||
SyncStatus string `json:"syncStatus"`
|
||||
DesiredSHA string `json:"desiredSha"`
|
||||
ActualSHA string `json:"actualSha"`
|
||||
Branch string `json:"branch"`
|
||||
IsDrifted bool `json:"isDrifted"`
|
||||
}
|
||||
jsonOK(w, driftStatus{
|
||||
SyncStatus: cfg.SyncStatus,
|
||||
DesiredSHA: cfg.DesiredSHA,
|
||||
ActualSHA: cfg.ActualSHA,
|
||||
Branch: cfg.Branch,
|
||||
IsDrifted: cfg.DesiredSHA != cfg.ActualSHA && cfg.DesiredSHA != "",
|
||||
})
|
||||
}
|
||||
|
||||
// ListDriftEvents returns the drift history for an environment, newest first.
|
||||
func (h *GitOpsHandler) ListDriftEvents(w http.ResponseWriter, r *http.Request) {
|
||||
env, ok := h.resolveGitOpsEnv(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
limit := 50
|
||||
if l, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && l > 0 && l <= 200 {
|
||||
limit = l
|
||||
}
|
||||
|
||||
var drifts []models.GitOpsDriftEvent
|
||||
if err := h.db.Where("env_id = ?", env.ID).Desc("id").Limit(limit).Find(&drifts); err != nil {
|
||||
jsonError(w, "could not list drift events", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if drifts == nil {
|
||||
drifts = []models.GitOpsDriftEvent{}
|
||||
}
|
||||
jsonOK(w, drifts)
|
||||
}
|
||||
|
||||
// AcknowledgeDrift marks a drift event as acknowledged without triggering a sync.
|
||||
func (h *GitOpsHandler) AcknowledgeDrift(w http.ResponseWriter, r *http.Request) {
|
||||
env, ok := h.resolveGitOpsEnv(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
driftID, err := strconv.ParseInt(chi.URLParam(r, "driftID"), 10, 64)
|
||||
if err != nil {
|
||||
jsonError(w, "invalid drift event ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var drift models.GitOpsDriftEvent
|
||||
if found, _ := h.db.Where("id = ? AND env_id = ?", driftID, env.ID).Get(&drift); !found {
|
||||
jsonError(w, "drift event not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if drift.ResolvedAt != nil {
|
||||
jsonError(w, "drift event is already resolved", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
drift.SyncStatus = "acknowledged"
|
||||
drift.ResolvedAt = &now
|
||||
if _, err := h.db.ID(drift.ID).Cols("sync_status", "resolved_at").Update(&drift); err != nil {
|
||||
jsonError(w, "could not acknowledge drift", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonOK(w, drift)
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *GitOpsHandler) resolveGitOpsEnv(w http.ResponseWriter, r *http.Request) (*models.Environment, bool) {
|
||||
repoID, ok := resolveRepoID(h.db, w, r)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
envName := chi.URLParam(r, "envName")
|
||||
var env models.Environment
|
||||
if found, _ := h.db.Where("repo_id = ? AND name = ?", repoID, envName).Get(&env); !found {
|
||||
jsonError(w, "environment not found", http.StatusNotFound)
|
||||
return nil, false
|
||||
}
|
||||
return &env, true
|
||||
}
|
||||
Reference in New Issue
Block a user