mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-06-05 22:43:51 +00:00
278 lines
8.6 KiB
Go
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
|
|
}
|