Ctrl K
ComunidadGuías…

internal/rollout/rollout.go

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

Archivo original ↗Preguntar a KiBot
package rollout

import (
	"cmp"
	"context"
	"errors"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"

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

const (
	appsGroup           = "apps"
	replicaSets         = "replicasets"
	controllerRevisions = "controllerrevisions"
	deploymentKind      = "Deployment"
	statefulSetKind     = "StatefulSet"
	daemonSetKind       = "DaemonSet"
	revisionAnnotation  = "deployment.kubernetes.io/revision"
	causeAnnotation     = "kubernetes.io/change-cause"
	templateHashLabel   = "pod-template-hash"
	revisionHashLabel   = "controller-revision-hash"
	specField           = "spec"
	templateField       = "template"
)

type Source interface {
	Get(ctx context.Context, ref api.ObjectRef) (*unstructured.Unstructured, error)
	List(ctx context.Context, ref api.ObjectRef) ([]*unstructured.Unstructured, error)
}

func FromDynamic(dyn dynamic.Interface) Source {
	return clusterSource{dyn: dyn}
}

type clusterSource struct {
	dyn dynamic.Interface
}

func (source clusterSource) Get(ctx context.Context, ref api.ObjectRef) (*unstructured.Unstructured, error) {
	return source.objects(ref).Get(ctx, ref.Name, metav1.GetOptions{})
}

func (source clusterSource) List(ctx context.Context, ref api.ObjectRef) ([]*unstructured.Unstructured, error) {
	found, err := source.objects(ref).List(ctx, metav1.ListOptions{})
	if err != nil {
		return nil, err
	}
	out := make([]*unstructured.Unstructured, 0, len(found.Items))
	for i := range found.Items {
		out = append(out, &found.Items[i])
	}
	return out, nil
}

func (source clusterSource) objects(ref api.ObjectRef) dynamic.ResourceInterface {
	gvr := schema.GroupVersionResource{Group: ref.Group, Version: ref.Version, Resource: ref.Resource}
	if ref.Namespace == "" {
		return source.dyn.Resource(gvr)
	}
	return source.dyn.Resource(gvr).Namespace(ref.Namespace)
}

type entry struct {
	api.Revision

	template map[string]any
}

func List(ctx context.Context, src Source, ref api.ObjectRef) (api.Revisions, error) {
	found, reason, err := history(ctx, src, ref)
	if err != nil {
		return api.Revisions{}, err
	}
	if reason != "" {
		return api.Revisions{Supported: false, Reason: reason}, nil
	}
	out := make([]api.Revision, 0, len(found))
	for _, one := range found {
		out = append(out, one.Revision)
	}
	return api.Revisions{Revisions: out, Supported: true}, nil
}

func Template(ctx context.Context, src Source, ref api.ObjectRef, number int64) (map[string]any, error) {
	found, reason, err := history(ctx, src, ref)
	if err != nil {
		return nil, err
	}
	if reason != "" {
		return nil, errors.New(reason)
	}
	return pick(found, ref, number)
}

func pick(found []entry, ref api.ObjectRef, number int64) (map[string]any, error) {
	for _, one := range found {
		if one.Number != number {
			continue
		}
		if one.template == nil {
			return nil, fmt.Errorf("revision %d of %s holds no pod template", number, ref.Name)
		}
		return one.template, nil
	}
	return nil, fmt.Errorf("%s has no revision %d", ref.Name, number)
}

func history(ctx context.Context, src Source, ref api.ObjectRef) ([]entry, string, error) {
	kind := kindOf(ref)
	switch kind {
	case deploymentKind:
		found, err := replicaSetHistory(ctx, src, ref)
		return found, "", err
	case statefulSetKind, daemonSetKind:
		found, err := controllerRevisionHistory(ctx, src, ref, kind)
		return found, "", err
	default:
		return nil, describe(ref) + " keep no rollout revisions", nil
	}
}

var kinds = map[string]string{
	"deployments":  deploymentKind,
	"statefulsets": statefulSetKind,
	"daemonsets":   daemonSetKind,
}

func kindOf(ref api.ObjectRef) string {
	if ref.Group != appsGroup {
		return ""
	}
	return kinds[ref.Resource]
}

func describe(ref api.ObjectRef) string {
	if ref.Group == "" {
		return ref.Resource
	}
	return ref.Group + "/" + ref.Resource
}

func replicaSetHistory(ctx context.Context, src Source, ref api.ObjectRef) ([]entry, error) {
	if _, err := src.Get(ctx, ref); err != nil {
		return nil, err
	}
	sets, err := src.List(ctx, beside(ref, replicaSets))
	if err != nil {
		return nil, err
	}
	found := make([]entry, 0, len(sets))
	for _, set := range sets {
		if !ownedBy(set, ref.Name, deploymentKind) {
			continue
		}
		number, numbered := annotatedRevision(set)
		if !numbered {
			continue
		}
		one := entryOf(set, number, templateOf(set, specField, templateField))
		one.Replicas = int(unstr.Int(set, specField, "replicas"))
		one.Ready = int(unstr.Int(set, "status", "readyReplicas"))
		found = append(found, one)
	}
	return ordered(found), nil
}

func controllerRevisionHistory(ctx context.Context, src Source, ref api.ObjectRef, kind string) ([]entry, error) {
	if _, err := src.Get(ctx, ref); err != nil {
		return nil, err
	}
	held, err := src.List(ctx, beside(ref, controllerRevisions))
	if err != nil {
		return nil, err
	}
	found := make([]entry, 0, len(held))
	for _, revision := range held {
		if !ownedBy(revision, ref.Name, kind) {
			continue
		}
		number := unstr.Int(revision, "revision")
		if number == 0 {
			continue
		}
		one := entryOf(revision, number, templateOf(revision, "data", specField, templateField))
		one.Replicas = int(unstr.Int(revision, "data", specField, "replicas"))
		found = append(found, one)
	}
	return ordered(found), nil
}

func entryOf(item *unstructured.Unstructured, number int64, template map[string]any) entry {
	return entry{
		Number:    number,
		Name:      item.GetName(),
		CreatedAt: createdAt(item),
		Cause:     item.GetAnnotations()[causeAnnotation],
		Images:    imagesOf(template),
		template:  template,
	}
}

func beside(ref api.ObjectRef, resource string) api.ObjectRef {
	return api.ObjectRef{
		Group:     appsGroup,
		Version:   ref.Version,
		Resource:  resource,
		Namespace: ref.Namespace,
	}
}

func ownedBy(item *unstructured.Unstructured, name, kind string) bool {
	for _, owner := range item.GetOwnerReferences() {
		if owner.Kind == kind && owner.Name == name {
			return true
		}
	}
	return false
}

func annotatedRevision(item *unstructured.Unstructured) (int64, bool) {
	raw := item.GetAnnotations()[revisionAnnotation]
	if raw == "" {
		return 0, false
	}
	number, err := strconv.ParseInt(raw, 10, 64)
	if err != nil {
		return 0, false
	}
	return number, true
}

func templateOf(item *unstructured.Unstructured, fields ...string) map[string]any {
	template, found := unstr.Map(item, fields...)
	if !found {
		return nil
	}
	return withoutHashes(template)
}

func withoutHashes(template map[string]any) map[string]any {
	labels, found, err := unstructured.NestedStringMap(template, "metadata", "labels")
	if !found || err != nil {
		return template
	}
	delete(labels, templateHashLabel)
	delete(labels, revisionHashLabel)
	if len(labels) == 0 {
		unstructured.RemoveNestedField(template, "metadata", "labels")
		return template
	}
	setErr := unstructured.SetNestedStringMap(template, labels, "metadata", "labels")
	if setErr != nil {
		unstructured.RemoveNestedField(template, "metadata", "labels")
	}
	return template
}

func createdAt(item *unstructured.Unstructured) string {
	stamp := item.GetCreationTimestamp()
	if stamp.IsZero() {
		return ""
	}
	return stamp.Time.UTC().Format(time.RFC3339)
}

func imagesOf(template map[string]any) []string {
	containers, found, err := unstructured.NestedSlice(template, specField, "containers")
	if !found || err != nil {
		return nil
	}
	var out []string
	for _, raw := range containers {
		container, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		image := unstr.At(container, "image")
		if image == "" {
			continue
		}
		out = append(out, image)
	}
	return out
}

func ordered(found []entry) []entry {
	slices.SortFunc(found, func(left, right entry) int {
		if left.Number != right.Number {
			return cmp.Compare(right.Number, left.Number)
		}
		return strings.Compare(left.Name, right.Name)
	})
	return onlyOneCurrent(found)
}

func onlyOneCurrent(found []entry) []entry {
	for at := range found {
		found[at].Current = at == 0
	}
	return found
}