Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions pkg/api/handlers/gitops/gitops_drift.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package gitops

import (
"fmt"
"time"

"github.com/gofiber/fiber/v2"
)

// driftCacheTTL bounds how long a DetectDrift result stays in the shared
// cache feeding ListDrifts. Long enough to be useful across a dashboard
// render cycle, short enough that manual refreshes (#5952) actually see
// fresh data rather than a stale repeat.
const driftCacheTTL = 30 * time.Second

// driftCacheEntry is a single cached drift-detection result keyed by
// repo/path/cluster/namespace.
type driftCacheEntry struct {
drifts []GitOpsDrift
detected time.Time
}

// ListDrifts returns a list of detected drifts (for GET endpoint).
//
// #5950 β€” Previously this always returned an empty slice, so the UI drift
// card never showed anything. We now expose drift results cached from recent
// DetectDrift calls (see rememberDrift) filtered by the optional query
// params. Entries older than driftCacheTTL are evicted on read.
func (h *GitOpsHandlers) ListDrifts(c *fiber.Ctx) error {
cluster := c.Query("cluster")
namespace := c.Query("namespace")

drifts := h.snapshotDrifts(cluster, namespace)
return c.JSON(fiber.Map{
"drifts": drifts,
})
}

// rememberDrift stores a drift-detection result in the in-memory cache keyed
// by repo URL / path / cluster / namespace. Safe for concurrent use.
func (h *GitOpsHandlers) rememberDrift(req DetectDriftRequest, result *DetectDriftResponse) {
if result == nil {
return
}
key := fmt.Sprintf("%s|%s|%s|%s", req.RepoURL, req.Path, req.Cluster, req.Namespace)
drifts := make([]GitOpsDrift, 0, len(result.Resources))
if result.Drifted {
for _, r := range result.Resources {
drifts = append(drifts, GitOpsDrift{
Resource: r.Name,
Namespace: r.Namespace,
Cluster: req.Cluster,
Kind: r.Kind,
DriftType: "modified",
Details: fmt.Sprintf("%s: %s", r.Field, r.DiffOutput),
Severity: "medium",
})
}
}
h.driftCacheMu.Lock()
defer h.driftCacheMu.Unlock()
h.driftCache[key] = driftCacheEntry{drifts: drifts, detected: time.Now()}
}

// snapshotDrifts returns all cached drifts matching the optional
// cluster/namespace filter, dropping entries older than driftCacheTTL.
func (h *GitOpsHandlers) snapshotDrifts(cluster, namespace string) []GitOpsDrift {
now := time.Now()
h.driftCacheMu.Lock()
defer h.driftCacheMu.Unlock()
out := make([]GitOpsDrift, 0)
for k, entry := range h.driftCache {
if now.Sub(entry.detected) > driftCacheTTL {
delete(h.driftCache, k)
continue
}
for _, d := range entry.drifts {
if cluster != "" && d.Cluster != cluster {
continue
}
if namespace != "" && d.Namespace != namespace {
continue
}
out = append(out, d)
}
}
return out
}
80 changes: 0 additions & 80 deletions pkg/api/handlers/gitops/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,6 @@ type GitOpsDrift struct {
Severity string `json:"severity"` // low, medium, high
}

// driftCacheTTL bounds how long a DetectDrift result stays in the shared
// cache feeding ListDrifts. Long enough to be useful across a dashboard
// render cycle, short enough that manual refreshes (#5952) actually see
// fresh data rather than a stale repeat.
const driftCacheTTL = 30 * time.Second

// driftCacheEntry is a single cached drift-detection result keyed by
// repo/path/cluster/namespace.
type driftCacheEntry struct {
drifts []GitOpsDrift
detected time.Time
}

// GitOpsHandlers handles GitOps-related API endpoints
type GitOpsHandlers struct {
bridge *mcppkg.Bridge
Expand Down Expand Up @@ -109,57 +96,6 @@ func NewGitOpsHandlers(bridge *mcppkg.Bridge, k8sClient *k8s.MultiClusterClient,
// admin-only helper was removed in #6022 when the policy was loosened to
// editor-or-admin for mutations and viewer-or-above for drift detection.

// rememberDrift stores a drift-detection result in the in-memory cache keyed
// by repo URL / path / cluster / namespace. Safe for concurrent use.
func (h *GitOpsHandlers) rememberDrift(req DetectDriftRequest, result *DetectDriftResponse) {
if result == nil {
return
}
key := fmt.Sprintf("%s|%s|%s|%s", req.RepoURL, req.Path, req.Cluster, req.Namespace)
drifts := make([]GitOpsDrift, 0, len(result.Resources))
if result.Drifted {
for _, r := range result.Resources {
drifts = append(drifts, GitOpsDrift{
Resource: r.Name,
Namespace: r.Namespace,
Cluster: req.Cluster,
Kind: r.Kind,
DriftType: "modified",
Details: fmt.Sprintf("%s: %s", r.Field, r.DiffOutput),
Severity: "medium",
})
}
}
h.driftCacheMu.Lock()
defer h.driftCacheMu.Unlock()
h.driftCache[key] = driftCacheEntry{drifts: drifts, detected: time.Now()}
}

// snapshotDrifts returns all cached drifts matching the optional
// cluster/namespace filter, dropping entries older than driftCacheTTL.
func (h *GitOpsHandlers) snapshotDrifts(cluster, namespace string) []GitOpsDrift {
now := time.Now()
h.driftCacheMu.Lock()
defer h.driftCacheMu.Unlock()
out := make([]GitOpsDrift, 0)
for k, entry := range h.driftCache {
if now.Sub(entry.detected) > driftCacheTTL {
delete(h.driftCache, k)
continue
}
for _, d := range entry.drifts {
if cluster != "" && d.Cluster != cluster {
continue
}
if namespace != "" && d.Namespace != namespace {
continue
}
out = append(out, d)
}
}
return out
}

// DriftedResource represents a resource that has drifted from git
type DriftedResource struct {
Kind string `json:"kind"`
Expand Down Expand Up @@ -256,22 +192,6 @@ type Operator struct {
Cluster string `json:"cluster,omitempty"`
}

// ListDrifts returns a list of detected drifts (for GET endpoint).
//
// #5950 β€” Previously this always returned an empty slice, so the UI drift
// card never showed anything. We now expose drift results cached from recent
// DetectDrift calls (see rememberDrift) filtered by the optional query
// params. Entries older than driftCacheTTL are evicted on read.
func (h *GitOpsHandlers) ListDrifts(c *fiber.Ctx) error {
cluster := c.Query("cluster")
namespace := c.Query("namespace")

drifts := h.snapshotDrifts(cluster, namespace)
return c.JSON(fiber.Map{
"drifts": drifts,
})
}

// ListHelmReleases returns all Helm releases across all namespaces
func (h *GitOpsHandlers) ListHelmReleases(c *fiber.Ctx) error {
cluster := c.Query("cluster")
Expand Down
Loading