mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-06-25 04:34:38 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user