Files
kubemirror/pkg/controller/mirror.go
T
2025-12-25 22:10:57 +00:00

278 lines
8.6 KiB
Go

// Package controller implements the kubemirror reconciliation logic.
package controller
import (
"fmt"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/lukaszraczylo/kubemirror/pkg/constants"
"github.com/lukaszraczylo/kubemirror/pkg/hash"
)
// CreateMirror creates a mirror resource in the target namespace.
// It copies the source resource's spec/data and adds ownership annotations.
func CreateMirror(source runtime.Object, targetNamespace string) (runtime.Object, error) {
// Compute content hash of source
sourceHash, err := hash.ComputeContentHash(source)
if err != nil {
return nil, fmt.Errorf("failed to compute source hash: %w", err)
}
// Handle typed resources
switch src := source.(type) {
case *corev1.Secret:
return createSecretMirror(src, targetNamespace, sourceHash)
case *corev1.ConfigMap:
return createConfigMapMirror(src, targetNamespace, sourceHash)
default:
// For unstructured/CRD resources
return createUnstructuredMirror(source, targetNamespace, sourceHash)
}
}
// createSecretMirror creates a mirror of a Secret.
func createSecretMirror(source *corev1.Secret, targetNamespace, sourceHash string) (*corev1.Secret, error) {
mirror := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: source.Name,
Namespace: targetNamespace,
Labels: map[string]string{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
Annotations: buildMirrorAnnotations(source, sourceHash),
},
Type: source.Type,
Data: source.Data,
// Note: Don't copy StringData as it's write-only and gets converted to Data
}
return mirror, nil
}
// createConfigMapMirror creates a mirror of a ConfigMap.
func createConfigMapMirror(source *corev1.ConfigMap, targetNamespace, sourceHash string) (*corev1.ConfigMap, error) {
mirror := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: source.Name,
Namespace: targetNamespace,
Labels: map[string]string{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
Annotations: buildMirrorAnnotations(source, sourceHash),
},
Data: source.Data,
BinaryData: source.BinaryData,
}
return mirror, nil
}
// filterKubeMirrorMetadata removes all kubemirror.raczylo.com/* keys from metadata.
// This prevents source kubemirror labels/annotations from being copied to mirrors.
func filterKubeMirrorMetadata(metadata map[string]string) map[string]string {
filtered := make(map[string]string)
for k, v := range metadata {
// Skip all kubemirror.raczylo.com keys
if !strings.HasPrefix(k, "kubemirror.raczylo.com/") {
filtered[k] = v
}
}
return filtered
}
// createUnstructuredMirror creates a mirror of an unstructured resource (CRD).
func createUnstructuredMirror(source runtime.Object, targetNamespace, sourceHash string) (*unstructured.Unstructured, error) {
// Convert to unstructured
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(source)
if err != nil {
return nil, fmt.Errorf("failed to convert to unstructured: %w", err)
}
u := &unstructured.Unstructured{Object: unstructuredObj}
// Create mirror
mirror := u.DeepCopy()
mirror.SetNamespace(targetNamespace)
// Remove kubemirror labels from source (don't propagate to mirrors)
labels := mirror.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
labels = filterKubeMirrorMetadata(labels)
labels[constants.LabelManagedBy] = constants.ControllerName
labels[constants.LabelMirror] = "true"
mirror.SetLabels(labels)
// Remove kubemirror annotations from source (don't propagate to mirrors)
existingAnnotations := mirror.GetAnnotations()
if existingAnnotations == nil {
existingAnnotations = make(map[string]string)
}
existingAnnotations = filterKubeMirrorMetadata(existingAnnotations)
// Add mirror-specific annotations
annotations := buildMirrorAnnotations(source, sourceHash)
for k, v := range annotations {
existingAnnotations[k] = v
}
mirror.SetAnnotations(existingAnnotations)
// Remove status (never mirror status)
unstructured.RemoveNestedField(mirror.Object, "status")
// Clear resource-specific metadata
mirror.SetResourceVersion("")
mirror.SetUID("")
mirror.SetGeneration(0)
mirror.SetCreationTimestamp(metav1.Time{})
mirror.SetFinalizers(nil) // Mirrors should not have finalizers
return mirror, nil
}
// buildMirrorAnnotations builds the ownership annotations for a mirror resource.
func buildMirrorAnnotations(source runtime.Object, sourceHash string) map[string]string {
sourceObj, _ := source.(metav1.Object)
annotations := map[string]string{
constants.AnnotationSourceNamespace: sourceObj.GetNamespace(),
constants.AnnotationSourceName: sourceObj.GetName(),
constants.AnnotationSourceUID: string(sourceObj.GetUID()),
constants.AnnotationSourceContentHash: sourceHash,
constants.AnnotationLastSyncTime: time.Now().UTC().Format(time.RFC3339),
}
// Add generation if available
if sourceObj.GetGeneration() > 0 {
annotations[constants.AnnotationSourceGeneration] = fmt.Sprintf("%d", sourceObj.GetGeneration())
}
// Add resource version for debugging
if sourceObj.GetResourceVersion() != "" {
annotations[constants.AnnotationSourceResourceVersion] = sourceObj.GetResourceVersion()
}
return annotations
}
// UpdateMirror updates an existing mirror with new source content.
func UpdateMirror(mirror, source runtime.Object) error {
// Compute new source hash
sourceHash, err := hash.ComputeContentHash(source)
if err != nil {
return fmt.Errorf("failed to compute source hash: %w", err)
}
// Update based on type
switch m := mirror.(type) {
case *corev1.Secret:
src := source.(*corev1.Secret)
m.Data = src.Data
m.Type = src.Type
updateMirrorAnnotations(m, source, sourceHash)
case *corev1.ConfigMap:
src := source.(*corev1.ConfigMap)
m.Data = src.Data
m.BinaryData = src.BinaryData
updateMirrorAnnotations(m, source, sourceHash)
default:
// Unstructured
return updateUnstructuredMirror(mirror, source, sourceHash)
}
return nil
}
// updateMirrorAnnotations updates the ownership annotations on a mirror.
func updateMirrorAnnotations(mirror metav1.Object, source runtime.Object, sourceHash string) {
sourceObj, _ := source.(metav1.Object)
annotations := mirror.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[constants.AnnotationSourceContentHash] = sourceHash
annotations[constants.AnnotationLastSyncTime] = time.Now().UTC().Format(time.RFC3339)
if sourceObj.GetGeneration() > 0 {
annotations[constants.AnnotationSourceGeneration] = fmt.Sprintf("%d", sourceObj.GetGeneration())
}
if sourceObj.GetResourceVersion() != "" {
annotations[constants.AnnotationSourceResourceVersion] = sourceObj.GetResourceVersion()
}
mirror.SetAnnotations(annotations)
}
// updateUnstructuredMirror updates an unstructured mirror.
func updateUnstructuredMirror(mirror, source runtime.Object, sourceHash string) error {
m := mirror.(*unstructured.Unstructured)
s := source.(*unstructured.Unstructured)
// Update spec
sourceSpec, found, err := unstructured.NestedMap(s.Object, "spec")
if err != nil {
return fmt.Errorf("failed to get source spec: %w", err)
}
if found {
if err := unstructured.SetNestedMap(m.Object, sourceSpec, "spec"); err != nil {
return fmt.Errorf("failed to set mirror spec: %w", err)
}
}
// Update annotations
updateMirrorAnnotations(m, source, sourceHash)
// Ensure mirrors never have finalizers (even if they were added before this fix)
m.SetFinalizers(nil)
return nil
}
// IsManagedByUs checks if a resource is managed by kubemirror.
func IsManagedByUs(obj metav1.Object) bool {
labels := obj.GetLabels()
if labels == nil {
return false
}
return labels[constants.LabelManagedBy] == constants.ControllerName
}
// IsMirrorResource checks if a resource is a mirror (not a source).
func IsMirrorResource(obj metav1.Object) bool {
labels := obj.GetLabels()
if labels == nil {
return false
}
return labels[constants.LabelMirror] == "true"
}
// GetSourceReference extracts the source reference from a mirror's annotations.
func GetSourceReference(mirror metav1.Object) (namespace, name, uid string, found bool) {
annotations := mirror.GetAnnotations()
if annotations == nil {
return "", "", "", false
}
namespace = annotations[constants.AnnotationSourceNamespace]
name = annotations[constants.AnnotationSourceName]
uid = annotations[constants.AnnotationSourceUID]
if namespace == "" || name == "" {
return "", "", "", false
}
return namespace, name, uid, true
}