pkg/k8score/transform.go
Rama main · réplica de lectura del repositorio original.
package k8score
import (
"bytes"
"crypto/sha256"
"encoding/hex"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
policyv1 "k8s.io/api/policy/v1"
storagev1 "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const lastAppliedAnnotation = "kubectl.kubernetes.io/last-applied-configuration"
// HelmReleaseSecretType is the type Helm gives the Secrets that store
// releases, one Secret per revision.
const HelmReleaseSecretType corev1.SecretType = "helm.sh/release.v1"
// DropManagedFields is a SharedInformer transform that reduces memory usage
// by removing managedFields and heavy annotations from cached objects.
// This is the union of transforms used by both Radar and skyhook-connector.
//
// It also replaces each value of a Helm release Secret (see
// IsHelmReleaseSecret) with a digest of it. Each value is a full encoded
// release, typically 30-400KB, and Helm keeps up to 10 revisions per release
// by default, so on a Helm-heavy cluster they are most of the cache. Keys stay,
// so key counts and key names are unchanged, and a changed value still
// compares as changed. The digests are not Secret data: anything that serves
// a cached Helm release Secret must use WithoutHelmReleaseValues, and
// anything that needs a value must read the Secret from the API server.
func DropManagedFields(obj any) (any, error) {
if meta, ok := obj.(metav1.Object); ok {
meta.SetManagedFields(nil)
}
if secret, ok := obj.(*corev1.Secret); ok && IsHelmReleaseSecret(secret) {
for key, value := range secret.Data {
// Watch-list informers run the transform twice on objects present
// at startup, so a value may already be a digest.
if !isHelmReleaseValueDigest(value) {
secret.Data[key] = helmReleaseValueDigest(value)
}
}
}
// Special handling for Events — aggressively strip to essentials
if event, ok := obj.(*corev1.Event); ok {
return &corev1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: event.Name,
Namespace: event.Namespace,
UID: event.UID,
ResourceVersion: event.ResourceVersion,
CreationTimestamp: event.CreationTimestamp,
},
InvolvedObject: event.InvolvedObject,
Related: event.Related,
Reason: event.Reason,
Message: event.Message,
Type: event.Type,
Count: event.Count,
FirstTimestamp: event.FirstTimestamp,
LastTimestamp: event.LastTimestamp,
// Events recorded through events.k8s.io carry their times here
// instead of in First/LastTimestamp.
EventTime: event.EventTime,
Series: event.Series,
}, nil
}
// Drop heavy annotations from common resources
switch obj.(type) {
case *corev1.Pod, *corev1.Service, *corev1.Node, *corev1.Namespace,
*corev1.PersistentVolumeClaim, *corev1.PersistentVolume,
*corev1.ConfigMap, *corev1.Secret, *corev1.ServiceAccount,
*appsv1.Deployment, *appsv1.DaemonSet, *appsv1.StatefulSet, *appsv1.ReplicaSet,
*networkingv1.Ingress, *networkingv1.IngressClass,
*batchv1.Job, *batchv1.CronJob,
*autoscalingv2.HorizontalPodAutoscaler,
*policyv1.PodDisruptionBudget, *storagev1.StorageClass:
if meta, ok := obj.(metav1.Object); ok && meta.GetAnnotations() != nil {
delete(meta.GetAnnotations(), lastAppliedAnnotation)
}
}
return obj, nil
}
// IsHelmReleaseSecret reports whether secret is a Helm release Secret. The
// informer cache holds these with digests in place of their values (see
// DropManagedFields).
func IsHelmReleaseSecret(secret *corev1.Secret) bool {
return secret != nil && secret.Type == HelmReleaseSecretType
}
// WithoutHelmReleaseValues returns secret as it may be served from the
// cache: for a Helm release Secret, a copy whose keys have no values; any
// other Secret unchanged. It never modifies secret.
func WithoutHelmReleaseValues(secret *corev1.Secret) *corev1.Secret {
if !IsHelmReleaseSecret(secret) || len(secret.Data) == 0 {
return secret
}
out := *secret
out.Data = make(map[string][]byte, len(secret.Data))
for key := range secret.Data {
out.Data[key] = nil
}
return &out
}
const helmReleaseDigestPrefix = "radar-cache-digest:sha256:"
func helmReleaseValueDigest(value []byte) []byte {
sum := sha256.Sum256(value)
return []byte(helmReleaseDigestPrefix + hex.EncodeToString(sum[:]))
}
func isHelmReleaseValueDigest(value []byte) bool {
return len(value) == len(helmReleaseDigestPrefix)+2*sha256.Size && bytes.HasPrefix(value, []byte(helmReleaseDigestPrefix))
}
// DropUnstructuredManagedFields is the dynamic-cache counterpart of
// DropManagedFields. It's the SharedInformer transform for dynamic
// informers — which carry *unstructured.Unstructured — and mirrors the
// typed-cache transform's intent: shrink cached objects before they hit
// the store.
//
// Mutates in-place. This is correct here: SharedInformer transforms run
// exactly once per object before the cache stores it, and the object is
// not visible to any other reader until after the transform returns. Do
// NOT call this from places that hand you an object off the cache —
// those need StripUnstructuredFields which deep-copies.
//
// Always strips:
// - metadata.managedFields
// - kubectl.kubernetes.io/last-applied-configuration. GitOps drift
// detection needs this annotation, but it pulls live objects via a
// dedicated direct-GET path (DynamicResourceCache.GetDirectPreserveLastApplied)
// so the informer cache never has to retain it — which matters
// because the dynamic cache can host core kinds (apps/Deployment,
// /v1/Service, etc.) referenced from an Argo Application's
// status.resources, where retaining last-applied across every object
// cluster-wide would be a meaningful memory regression to power a
// per-page diagnostic.
//
// For CustomResourceDefinitions specifically, also strips:
// - spec.versions[].schema — the OpenAPI v3 schema. On operator-heavy
// clusters a single CRD's schema is 50-100KB (ArgoCD, cert-manager,
// Istio) and 100+ CRDs easily produce a multi-MB list response.
// Radar doesn't render CRD schemas; a future "inspect CRD schema"
// feature should fetch fresh from the API server.
// - spec.conversion — webhook config. Not rendered.
//
// Explicitly preserves:
// - spec.versions[].name and served/storage/deprecated flags
// - spec.versions[].additionalPrinterColumns — drives column hints
// - spec.group, spec.names, spec.scope, status.*
func DropUnstructuredManagedFields(obj any) (any, error) {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
// Dynamic informer should never hand us anything else, but be
// defensive — return the object unchanged rather than erroring,
// because a transform error is fatal for the informer.
return obj, nil
}
unstructured.RemoveNestedField(u.Object, "metadata", "managedFields")
unstructured.RemoveNestedField(u.Object, "metadata", "annotations", lastAppliedAnnotation)
if u.GetKind() == "CustomResourceDefinition" {
// Strip .schema from each version entry. Preserve everything
// else (name, served, storage, deprecated, additionalPrinterColumns).
if versions, found, _ := unstructured.NestedSlice(u.Object, "spec", "versions"); found {
for i, v := range versions {
vm, ok := v.(map[string]any)
if !ok {
continue
}
delete(vm, "schema")
versions[i] = vm
}
// SetNestedSlice can only fail on non-serializable types; slices
// of map[string]any are always fine. Ignoring the error is safe.
_ = unstructured.SetNestedSlice(u.Object, versions, "spec", "versions")
}
// spec.conversion holds the webhook clientConfig (caBundle + URL)
// for conversion webhooks. Not rendered by Radar.
unstructured.RemoveNestedField(u.Object, "spec", "conversion")
}
return u, nil
}
// StripUnstructuredFields removes managedFields and heavy internal annotations
// from a deep copy of an unstructured object, so outward readers never leak a
// full desired manifest through last-applied. The informer transform already
// drops it from cached objects; GitOps drift reads it through
// GetDirectPreserveLastApplied instead.
func StripUnstructuredFields(u *unstructured.Unstructured) *unstructured.Unstructured {
return stripUnstructuredFields(u, false)
}
// StripUnstructuredFieldsPreserveLastApplied removes managedFields from a deep
// copy while preserving kubectl last-applied. Use only for internal drift
// computation paths; API/UI/MCP payloads should call StripUnstructuredFields.
func StripUnstructuredFieldsPreserveLastApplied(u *unstructured.Unstructured) *unstructured.Unstructured {
return stripUnstructuredFields(u, true)
}
func stripUnstructuredFields(u *unstructured.Unstructured, preserveLastApplied bool) *unstructured.Unstructured {
if u == nil {
return nil
}
cp := u.DeepCopy()
unstructured.RemoveNestedField(cp.Object, "metadata", "managedFields")
if !preserveLastApplied {
if annotations := cp.GetAnnotations(); annotations != nil {
delete(annotations, lastAppliedAnnotation)
if len(annotations) == 0 {
cp.SetAnnotations(nil)
} else {
cp.SetAnnotations(annotations)
}
}
}
return cp
}