internal/compare/compare.go
Rama main · réplica de lectura del repositorio original.
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
}