Ctrl K
ComunidadGuías…

internal/flux/dashboard.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package flux

import (
	"context"
	"slices"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"github.com/sophotechlabs/spinoza/internal/api"
	"github.com/sophotechlabs/spinoza/internal/charts"
	"github.com/sophotechlabs/spinoza/internal/listerr"
	"github.com/sophotechlabs/spinoza/internal/unstr"
)

var groupOrder = []string{
	"Kustomizations",
	"Helm Releases",
	"Sources",
	"Image Automation",
	"Notifications",
}

var sourceResources = map[string]bool{
	"gitrepositories":  true,
	"helmrepositories": true,
	"ocirepositories":  true,
	"helmcharts":       true,
	"buckets":          true,
}

var imageResources = map[string]bool{
	"imagerepositories":      true,
	"imagepolicies":          true,
	"imageupdateautomations": true,
}

var notificationResources = map[string]bool{
	"alerts":    true,
	"providers": true,
	"receivers": true,
}

type Charts interface {
	Latest(repo charts.Repo, chart string) string
	Warm(repo charts.Repo, chart string)
}

const helmGroup = "helm.toolkit.fluxcd.io"

type Lister interface {
	List(ctx context.Context, desc api.ResourceDescriptor) ([]*unstructured.Unstructured, error)
	Warm(ctx context.Context, descs []api.ResourceDescriptor)
}

func Build(ctx context.Context, lister Lister, descs map[string]api.ResourceDescriptor, index Charts) api.FluxDashboard {
	byGroup := map[string][]api.FluxResource{}
	items := map[string][]*unstructured.Unstructured{}
	failures := listerr.New()
	lister.Warm(ctx, needed(descs, index))
	for _, desc := range descs {
		group := categoryOf(desc)
		if group == "" {
			continue
		}
		gvr := schema.GroupVersionResource{Group: desc.Group, Version: desc.Version, Resource: desc.Resource}
		found, err := lister.List(ctx, desc)
		failures.Record(gvr.GroupResource().String(), err)
		if err != nil {
			continue
		}
		for _, item := range found {
			byGroup[group] = append(byGroup[group], resourceOf(item, desc))
			items[group] = append(items[group], item)
		}
	}
	applyLatest(byGroup, items, repos(ctx, lister, descs, index), index)
	dashboard := assemble(byGroup)
	dashboard.Error = failures.Message()
	return dashboard
}

func needed(descs map[string]api.ResourceDescriptor, index Charts) []api.ResourceDescriptor {
	out := []api.ResourceDescriptor{}
	for _, desc := range descs {
		if categoryOf(desc) != "" {
			out = append(out, desc)
			continue
		}
		if index != nil && isHelmRepository(desc) {
			out = append(out, desc)
		}
	}
	return out
}

func isHelmRepository(desc api.ResourceDescriptor) bool {
	if desc.Group != "source.toolkit.fluxcd.io" {
		return false
	}
	return desc.Resource == "helmrepositories"
}

func repos(ctx context.Context, lister Lister, descs map[string]api.ResourceDescriptor, index Charts) map[string]charts.Repo {
	if index == nil {
		return nil
	}
	return repoIndex(ctx, lister, descs)
}

func repoIndex(ctx context.Context, lister Lister, descs map[string]api.ResourceDescriptor) map[string]charts.Repo {
	out := map[string]charts.Repo{}
	for _, desc := range descs {
		if !isHelmRepository(desc) {
			continue
		}
		found, err := lister.List(ctx, desc)
		if err != nil {
			continue
		}
		for _, repo := range found {
			source := unstr.String(repo, "spec", "url")
			if charts.CheckRepoURL(source) != nil {
				continue
			}
			out[repo.GetNamespace()+"/"+repo.GetName()] = charts.Repo{
				URL: source,
				OCI: unstr.String(repo, "spec", "type") == "oci",
			}
		}
	}
	return out
}

func applyLatest(byGroup map[string][]api.FluxResource, items map[string][]*unstructured.Unstructured, repos map[string]charts.Repo, index Charts) {
	if index == nil {
		return
	}
	rows := byGroup["Helm Releases"]
	objects := items["Helm Releases"]
	for i := range rows {
		if i >= len(objects) {
			continue
		}
		repo, chart, ok := chartSource(objects[i], repos)
		if !ok {
			continue
		}
		index.Warm(repo, chart)
		latest := index.Latest(repo, chart)
		rows[i].Latest = latest
		rows[i].Outdated = charts.Newer(rows[i].Revision, latest)
	}
}

func chartSource(obj *unstructured.Unstructured, repos map[string]charts.Repo) (charts.Repo, string, bool) {
	chart := unstr.String(obj, "spec", "chart", "spec", "chart")
	if chart == "" {
		return charts.Repo{}, "", false
	}
	if unstr.String(obj, "spec", "chart", "spec", "sourceRef", "kind") != "HelmRepository" {
		return charts.Repo{}, "", false
	}
	name := unstr.String(obj, "spec", "chart", "spec", "sourceRef", "name")
	if name == "" {
		return charts.Repo{}, "", false
	}
	namespace := unstr.String(obj, "spec", "chart", "spec", "sourceRef", "namespace")
	if namespace == "" {
		namespace = obj.GetNamespace()
	}
	repo, ok := repos[namespace+"/"+name]
	if !ok {
		return charts.Repo{}, "", false
	}
	if repo.URL == "" {
		return charts.Repo{}, "", false
	}
	return repo, chart, true
}

func categoryOf(desc api.ResourceDescriptor) string {
	switch desc.Group {
	case "kustomize.toolkit.fluxcd.io":
		if desc.Resource == "kustomizations" {
			return "Kustomizations"
		}
	case helmGroup:
		if desc.Resource == "helmreleases" {
			return "Helm Releases"
		}
	case "source.toolkit.fluxcd.io":
		if sourceResources[desc.Resource] {
			return "Sources"
		}
	case "image.toolkit.fluxcd.io":
		if imageResources[desc.Resource] {
			return "Image Automation"
		}
	case "notification.toolkit.fluxcd.io":
		if notificationResources[desc.Resource] {
			return "Notifications"
		}
	default:
	}
	return ""
}

func assemble(byGroup map[string][]api.FluxResource) api.FluxDashboard {
	groups := make([]api.FluxGroup, 0, len(byGroup))
	for _, name := range groupOrder {
		items := byGroup[name]
		if len(items) == 0 {
			continue
		}
		sortResources(items)
		groups = append(groups, api.FluxGroup{
			Name:      name,
			Ready:     readyCount(items),
			Reporting: reportingCount(items),
			Total:     len(items),
			Resources: items,
		})
	}
	return api.FluxDashboard{Groups: groups}
}

func sortResources(items []api.FluxResource) {
	slices.SortFunc(items, func(left, right api.FluxResource) int {
		if left.Kind != right.Kind {
			return strings.Compare(left.Kind, right.Kind)
		}
		if left.Namespace != right.Namespace {
			return strings.Compare(left.Namespace, right.Namespace)
		}
		return strings.Compare(left.Name, right.Name)
	})
}

func readyCount(items []api.FluxResource) int {
	count := 0
	for _, it := range items {
		if it.Ready == "True" {
			count++
		}
	}
	return count
}

func reportingCount(items []api.FluxResource) int {
	count := 0
	for _, it := range items {
		if it.Ready != "" {
			count++
		}
	}
	return count
}

func resourceOf(obj *unstructured.Unstructured, desc api.ResourceDescriptor) api.FluxResource {
	ready, message := unstr.Ready(obj)
	return api.FluxResource{
		Kind:      desc.Kind,
		Group:     desc.Group,
		Version:   desc.Version,
		Resource:  desc.Resource,
		Name:      obj.GetName(),
		Namespace: obj.GetNamespace(),
		Ready:     ready,
		Suspended: unstr.Bool(obj, "spec", "suspend"),
		Revision:  revisionOf(obj, desc),
		Source:    sourceOf(obj),
		Message:   message,
		CreatedAt: obj.GetCreationTimestamp().Time.UTC().Format(time.RFC3339),
	}
}

func revisionOf(obj *unstructured.Unstructured, desc api.ResourceDescriptor) string {
	if desc.Group == helmGroup && desc.Resource == "helmreleases" {
		version := historyChartVersion(obj)
		if version != "" {
			return version
		}
	}
	paths := [][]string{
		{"status", "lastAppliedRevision"},
		{"status", "lastAttemptedRevision"},
		{"status", "artifact", "revision"},
		{"status", "latestImage"},
	}
	for _, p := range paths {
		v := unstr.String(obj, p...)
		if v != "" {
			return v
		}
	}
	return ""
}

func historyChartVersion(obj *unstructured.Unstructured) string {
	entries := unstr.Slice(obj, "status", "history")
	if len(entries) == 0 {
		return ""
	}
	entry, ok := entries[0].(map[string]any)
	if !ok {
		return ""
	}
	return unstr.At(entry, "chartVersion")
}

func sourceOf(obj *unstructured.Unstructured) string {
	kind, name := refAt(obj, "spec", "chartRef")
	if kind == "" || name == "" {
		kind, name = refAt(obj, "spec", "sourceRef")
	}
	if kind == "" || name == "" {
		kind, name = refAt(obj, "spec", "chart", "spec", "sourceRef")
	}
	if kind == "" || name == "" {
		return ""
	}
	return kind + "/" + name
}

func refAt(obj *unstructured.Unstructured, fields ...string) (kind, name string) {
	kind = unstr.String(obj, append(fields, "kind")...)
	name = unstr.String(obj, append(fields, "name")...)
	return kind, name
}