internal/flux/detail.go
Rama main · réplica de lectura del repositorio original.
package flux
import (
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/sophotechlabs/spinoza/internal/api"
"github.com/sophotechlabs/spinoza/internal/faults"
"github.com/sophotechlabs/spinoza/internal/unstr"
)
const (
kustomizations = "kustomizations"
helmReleases = "helmreleases"
readyTrue = "True"
readyFalse = "False"
)
func IsSource(desc api.ResourceDescriptor) bool {
if desc.Group != "source.toolkit.fluxcd.io" {
return false
}
return sourceResources[desc.Resource]
}
func Applies(desc api.ResourceDescriptor) bool {
if desc.Group == "kustomize.toolkit.fluxcd.io" && desc.Resource == kustomizations {
return true
}
return desc.Group == helmGroup && desc.Resource == helmReleases
}
func Detail(obj *unstructured.Unstructured, desc api.ResourceDescriptor) api.GitopsApp {
return api.GitopsApp{
Controller: api.ControllerFlux,
Terminating: obj.GetDeletionTimestamp() != nil,
Kind: desc.Kind,
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
Source: sourceOfApplier(obj, desc),
State: stateOfApplier(obj, desc),
Issues: issuesOfApplier(obj),
Resources: resourcesOfApplier(obj),
History: historyOfApplier(obj),
Operation: operationOfApplier(obj),
}
}
func sourceOfApplier(obj *unstructured.Unstructured, desc api.ResourceDescriptor) api.GitopsSource {
return api.GitopsSource{
Repo: sourceOf(obj),
Path: pathOfApplier(obj, desc),
Destination: destinationOfApplier(obj),
SyncMode: syncModeOfApplier(obj),
Policy: policyOfApplier(obj),
}
}
func pathOfApplier(obj *unstructured.Unstructured, desc api.ResourceDescriptor) string {
if desc.Resource == kustomizations {
return unstr.String(obj, "spec", "path")
}
chart := unstr.String(obj, "spec", "chart", "spec", "chart")
if chart != "" {
return chart
}
return unstr.String(obj, "spec", "chartRef", "name")
}
func destinationOfApplier(obj *unstructured.Unstructured) string {
target := unstr.String(obj, "spec", "targetNamespace")
if target != "" {
return target
}
return obj.GetNamespace()
}
func syncModeOfApplier(obj *unstructured.Unstructured) string {
if unstr.Bool(obj, "spec", "suspend") {
return api.SyncModeSuspended
}
return api.SyncModeAuto
}
func policyOfApplier(obj *unstructured.Unstructured) string {
on := []string{}
if unstr.Bool(obj, "spec", "prune") {
on = append(on, "prune")
}
if unstr.Bool(obj, "spec", "force") {
on = append(on, "force")
}
if len(on) == 0 {
return ""
}
return strings.Join(on, ", ")
}
func stateOfApplier(obj *unstructured.Unstructured, desc api.ResourceDescriptor) api.GitopsState {
ready, message := unstr.Ready(obj)
return api.GitopsState{
Sync: syncWordOf(ready),
Health: healthWordOf(ready),
Revision: revisionOf(obj, desc),
CreatedAt: obj.GetCreationTimestamp().Time.UTC().Format(time.RFC3339),
SyncedAt: unstr.String(obj, "status", "lastHandledReconcileAt"),
Message: message,
}
}
func syncWordOf(ready string) string {
if ready == readyTrue {
return "Synced"
}
if ready == readyFalse {
return "OutOfSync"
}
return "Unknown"
}
func healthWordOf(ready string) string {
if ready == readyTrue {
return "Healthy"
}
if ready == readyFalse {
return "Degraded"
}
return "Progressing"
}
func resourcesOfApplier(obj *unstructured.Unstructured) []api.GitopsResource {
out := []api.GitopsResource{}
for _, raw := range unstr.Slice(obj, "status", "inventory", "entries") {
entry, ok := raw.(map[string]any)
if !ok {
continue
}
namespace, name, group, kind := splitInventoryID(unstr.At(entry, "id"))
if kind == "" || name == "" {
continue
}
out = append(out, api.GitopsResource{
Group: group,
Version: unstr.At(entry, "v"),
Kind: kind,
Name: name,
Namespace: namespace,
})
}
return out
}
func splitInventoryID(id string) (namespace, name, group, kind string) {
parts := strings.Split(id, "_")
if len(parts) != 4 {
return "", "", "", ""
}
group = parts[2]
if group == "''" {
group = ""
}
return parts[0], parts[1], group, parts[3]
}
func historyOfApplier(obj *unstructured.Unstructured) []api.GitopsDeployment {
out := []api.GitopsDeployment{}
for _, raw := range unstr.Slice(obj, "status", "history") {
entry, ok := raw.(map[string]any)
if !ok {
continue
}
version, carried := entry["version"].(int64)
if !carried {
continue
}
out = append(out, api.GitopsDeployment{
ID: version,
Revision: unstr.At(entry, "chartVersion"),
Source: unstr.At(entry, "chartName"),
StartedAt: unstr.At(entry, "firstDeployed"),
DeployedAt: unstr.At(entry, "lastDeployed"),
})
}
return out
}
func operationOfApplier(obj *unstructured.Unstructured) *api.GitopsOperation {
if reconciling(obj) {
return &api.GitopsOperation{
Phase: "Running",
Running: true,
Message: conditionMessage(obj, "Reconciling"),
Revision: unstr.String(obj, "status", "lastAttemptedRevision"),
StartedAt: unstr.String(obj, "status", "lastHandledReconcileAt"),
}
}
ready, message := unstr.Ready(obj)
if ready == "" {
return nil
}
phase := "Succeeded"
if ready == readyFalse {
phase = "Failed"
}
return &api.GitopsOperation{
Phase: phase,
Message: message,
Cause: faults.Cause(message),
Revision: revisionAttempted(obj),
FinishedAt: conditionUpdated(obj, "Ready"),
}
}
func revisionAttempted(obj *unstructured.Unstructured) string {
attempted := unstr.String(obj, "status", "lastAttemptedRevision")
if attempted != "" {
return attempted
}
return unstr.String(obj, "status", "lastAppliedRevision")
}
func reconciling(obj *unstructured.Unstructured) bool {
return conditionStatus(obj, "Reconciling") == readyTrue
}
func conditionStatus(obj *unstructured.Unstructured, kind string) string {
entry, found := conditionOf(obj, kind)
if !found {
return ""
}
return unstr.At(entry, "status")
}
func conditionMessage(obj *unstructured.Unstructured, kind string) string {
entry, found := conditionOf(obj, kind)
if !found {
return ""
}
return unstr.At(entry, "message")
}
func conditionUpdated(obj *unstructured.Unstructured, kind string) string {
entry, found := conditionOf(obj, kind)
if !found {
return ""
}
return unstr.At(entry, "lastTransitionTime")
}
func conditionOf(obj *unstructured.Unstructured, kind string) (map[string]any, bool) {
for _, raw := range unstr.Slice(obj, "status", "conditions") {
entry, ok := raw.(map[string]any)
if !ok {
continue
}
if unstr.At(entry, "type") != kind {
continue
}
return entry, true
}
return nil, false
}
func issuesOfApplier(obj *unstructured.Unstructured) []api.GitopsIssue {
out := []api.GitopsIssue{}
if obj.GetDeletionTimestamp() != nil {
out = append(out, api.GitopsIssue{
Severity: api.SeverityLow,
Title: "This object is being deleted",
Detail: heldByFinalizers(obj.GetFinalizers()),
})
}
if unstr.Bool(obj, "spec", "suspend") {
out = append(out, api.GitopsIssue{
Severity: api.SeverityLow,
Title: "Nothing will reconcile this",
Detail: "it is suspended; nothing changes until someone resumes it",
Subject: "drift",
})
}
out = append(out, failingConditions(obj)...)
if len(out) == 0 {
return nil
}
return out
}
func heldByFinalizers(finalizers []string) string {
if len(finalizers) == 0 {
return "no finalizers left"
}
return "held by " + strings.Join(finalizers, ", ")
}
func failingConditions(obj *unstructured.Unstructured) []api.GitopsIssue {
out := []api.GitopsIssue{}
for _, raw := range unstr.Slice(obj, "status", "conditions") {
entry, ok := raw.(map[string]any)
if !ok {
continue
}
kind := unstr.At(entry, "type")
if !failing(kind, unstr.At(entry, "status")) {
continue
}
message := unstr.At(entry, "message")
out = append(out, api.GitopsIssue{
Severity: api.SeverityHigh,
Title: unstr.At(entry, "reason"),
Detail: withCause(message),
Subject: kind,
})
}
return out
}
func failing(kind, status string) bool {
if kind == "Ready" || kind == "Healthy" {
return status == readyFalse
}
if kind == "Stalled" {
return status == readyTrue
}
return false
}
func withCause(message string) string {
cause := faults.Cause(message)
if cause == "" {
return message
}
return cause + " — " + message
}