Ctrl K
ComunidadGuías…

internal/compare/compare.go

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

Archivo original ↗Preguntar a KiBot
package compare

import (
	"fmt"
	"slices"
	"strings"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/yaml"

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

const lastApplied = "kubectl.kubernetes.io/last-applied-configuration"

const headless = "None"

const specField = "spec"

const (
	serviceKind    = "Service"
	claimKind      = "PersistentVolumeClaim"
	volumeKind     = "PersistentVolume"
	crdKind        = "CustomResourceDefinition"
	validatingKind = "ValidatingWebhookConfiguration"
	mutatingKind   = "MutatingWebhookConfiguration"
)

var allocatedByTheServer = map[string][][]string{
	claimKind:  {{specField, "volumeName"}},
	volumeKind: {{specField, "claimRef", "uid"}, {specField, "claimRef", "resourceVersion"}},
	serviceKind: {
		{specField, "healthCheckNodePort"},
	},
	crdKind: {{specField, "conversion", "webhook", "clientConfig", "caBundle"}},
}

var assignedByTheServer = []string{
	"uid",
	"resourceVersion",
	"generation",
	"creationTimestamp",
	"managedFields",
	"selfLink",
}

func Normalise(item *unstructured.Unstructured) *unstructured.Unstructured {
	clean, _ := Strip(item)
	return clean
}

func Strip(item *unstructured.Unstructured) (*unstructured.Unstructured, []string) {
	clean := authored(item)
	return clean, dropAllocations(clean)
}

func authored(item *unstructured.Unstructured) *unstructured.Unstructured {
	clean := item.DeepCopy()
	unstructured.RemoveNestedField(clean.Object, "status")
	for _, field := range assignedByTheServer {
		unstructured.RemoveNestedField(clean.Object, "metadata", field)
	}
	dropLastApplied(clean)
	dropOwnerUIDs(clean)
	return clean
}

func dropAllocations(clean *unstructured.Unstructured) []string {
	kind := clean.GetKind()
	stripped := []string{}
	for _, path := range allocatedByTheServer[kind] {
		_, found, _ := unstructured.NestedFieldNoCopy(clean.Object, path...)
		if found {
			stripped = append(stripped, strings.Join(path, "."))
		}
		unstructured.RemoveNestedField(clean.Object, path...)
	}
	if dropClusterIPs(clean, kind) {
		stripped = append(stripped, "spec.clusterIP")
	}
	if dropNodePorts(clean, kind) {
		stripped = append(stripped, "spec.ports[].nodePort")
	}
	if dropCABundles(clean, kind) {
		stripped = append(stripped, "webhooks[].clientConfig.caBundle")
	}
	slices.Sort(stripped)
	return stripped
}

func dropClusterIPs(clean *unstructured.Unstructured, kind string) bool {
	if kind != serviceKind {
		return false
	}
	dropped := false
	one, found, err := unstructured.NestedString(clean.Object, specField, "clusterIP")
	if found && err == nil && one != headless {
		unstructured.RemoveNestedField(clean.Object, specField, "clusterIP")
		dropped = true
	}
	many, found, err := unstructured.NestedStringSlice(clean.Object, specField, "clusterIPs")
	if !found || err != nil {
		return dropped
	}
	if slices.Contains(many, headless) {
		return dropped
	}
	unstructured.RemoveNestedField(clean.Object, specField, "clusterIPs")
	return true
}

func dropNodePorts(clean *unstructured.Unstructured, kind string) bool {
	if kind != serviceKind {
		return false
	}
	ports, found, err := unstructured.NestedSlice(clean.Object, specField, "ports")
	if !found || err != nil {
		return false
	}
	dropped := false
	for _, entry := range ports {
		port, ok := entry.(map[string]any)
		if !ok {
			continue
		}
		if _, carried := port["nodePort"]; carried {
			dropped = true
		}
		delete(port, "nodePort")
	}
	setErr := unstructured.SetNestedSlice(clean.Object, ports, specField, "ports")
	if setErr != nil {
		unstructured.RemoveNestedField(clean.Object, specField, "ports")
	}
	return dropped
}

func dropCABundles(clean *unstructured.Unstructured, kind string) bool {
	if kind != validatingKind && kind != mutatingKind {
		return false
	}
	hooks, found, err := unstructured.NestedSlice(clean.Object, "webhooks")
	if !found || err != nil {
		return false
	}
	dropped := false
	for _, entry := range hooks {
		hook, ok := entry.(map[string]any)
		if !ok {
			continue
		}
		config, ok := hook["clientConfig"].(map[string]any)
		if !ok {
			continue
		}
		if _, carried := config["caBundle"]; carried {
			dropped = true
		}
		delete(config, "caBundle")
	}
	setErr := unstructured.SetNestedSlice(clean.Object, hooks, "webhooks")
	if setErr != nil {
		unstructured.RemoveNestedField(clean.Object, "webhooks")
	}
	return dropped
}

func dropLastApplied(clean *unstructured.Unstructured) {
	annotations := clean.GetAnnotations()
	if annotations == nil {
		return
	}
	_, carried := annotations[lastApplied]
	if !carried {
		return
	}
	delete(annotations, lastApplied)
	if len(annotations) == 0 {
		clean.SetAnnotations(nil)
		return
	}
	clean.SetAnnotations(annotations)
}

func dropOwnerUIDs(clean *unstructured.Unstructured) {
	owners, found, err := unstructured.NestedSlice(clean.Object, "metadata", "ownerReferences")
	if !found || err != nil {
		return
	}
	for _, entry := range owners {
		owner, ok := entry.(map[string]any)
		if !ok {
			continue
		}
		delete(owner, "uid")
	}
	setErr := unstructured.SetNestedSlice(clean.Object, owners, "metadata", "ownerReferences")
	if setErr != nil {
		unstructured.RemoveNestedField(clean.Object, "metadata", "ownerReferences")
	}
}

func Parse(raw string) (*unstructured.Unstructured, error) {
	object := map[string]any{}
	err := yaml.Unmarshal([]byte(raw), &object)
	if err != nil {
		return nil, fmt.Errorf("%w: that object could not be read as yaml: %w", api.ErrInternal, err)
	}
	return &unstructured.Unstructured{Object: object}, nil
}

func YAML(item *unstructured.Unstructured) (string, error) {
	raw, err := yaml.Marshal(item.Object)
	if err != nil {
		return "", fmt.Errorf("%w: that object could not be written as yaml: %w", api.ErrInternal, err)
	}
	return string(raw), nil
}

type Rendering struct {
	Text     string
	Authored string
	Stripped []string
}

func Render(raw string, keep bool) (Rendering, error) {
	if keep {
		return Rendering{Text: raw, Authored: raw}, nil
	}
	parsed, err := Parse(raw)
	if err != nil {
		return Rendering{}, err
	}
	clean, stripped := Strip(parsed)
	text, textErr := YAML(clean)
	if textErr != nil {
		return Rendering{}, textErr
	}
	wrote, wroteErr := YAML(authored(parsed))
	if wroteErr != nil {
		return Rendering{}, wroteErr
	}
	return Rendering{Text: text, Authored: wrote, Stripped: stripped}, nil
}

func Rendered(raw string, keep bool) (string, error) {
	rendering, err := Render(raw, keep)
	if err != nil {
		return "", err
	}
	return rendering.Text, nil
}

func Union(left, right []string) []string {
	out := slices.Clone(left)
	for _, one := range right {
		if !slices.Contains(out, one) {
			out = append(out, one)
		}
	}
	slices.Sort(out)
	return out
}