mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-07-12 03:10:58 +00:00
refactor(controller): extract shared mirror-deletion and naming helpers
Replace the triplicated ownership-verified delete guard with deleteOwnedMirror, and hand-built controller-name strings with gvkControllerName. Use constants.Domain/ControllerName over magic strings and maps.Copy/Clone for plain copy loops. Behaviour-preserving; tests green.
This commit is contained in:
+10
-23
@@ -3,6 +3,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ func filterKubeMirrorMetadata(metadata map[string]string) map[string]string {
|
|||||||
filtered := make(map[string]string)
|
filtered := make(map[string]string)
|
||||||
for k, v := range metadata {
|
for k, v := range metadata {
|
||||||
// Skip all kubemirror.raczylo.com keys
|
// Skip all kubemirror.raczylo.com keys
|
||||||
if !strings.HasPrefix(k, "kubemirror.raczylo.com/") {
|
if !strings.HasPrefix(k, constants.Domain+"/") {
|
||||||
filtered[k] = v
|
filtered[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,9 +137,7 @@ func createUnstructuredMirror(source runtime.Object, targetNamespace, sourceHash
|
|||||||
|
|
||||||
// Add mirror-specific annotations
|
// Add mirror-specific annotations
|
||||||
annotations := buildMirrorAnnotations(source, sourceHash)
|
annotations := buildMirrorAnnotations(source, sourceHash)
|
||||||
for k, v := range annotations {
|
maps.Copy(existingAnnotations, annotations)
|
||||||
existingAnnotations[k] = v
|
|
||||||
}
|
|
||||||
mirror.SetAnnotations(existingAnnotations)
|
mirror.SetAnnotations(existingAnnotations)
|
||||||
|
|
||||||
// Remove status (never mirror status)
|
// Remove status (never mirror status)
|
||||||
@@ -272,8 +271,8 @@ func UpdateMirror(mirror, source runtime.Object) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertToStringMap converts map[string]interface{} to map[string]string.
|
// convertToStringMap converts map[string]any to map[string]string.
|
||||||
func convertToStringMap(data map[string]interface{}) map[string]string {
|
func convertToStringMap(data map[string]any) map[string]string {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
for k, v := range data {
|
for k, v := range data {
|
||||||
if s, ok := v.(string); ok {
|
if s, ok := v.(string); ok {
|
||||||
@@ -283,8 +282,8 @@ func convertToStringMap(data map[string]interface{}) map[string]string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertToByteMap converts map[string]interface{} to map[string][]byte.
|
// convertToByteMap converts map[string]any to map[string][]byte.
|
||||||
func convertToByteMap(data map[string]interface{}) map[string][]byte {
|
func convertToByteMap(data map[string]any) map[string][]byte {
|
||||||
result := make(map[string][]byte)
|
result := make(map[string][]byte)
|
||||||
for k, v := range data {
|
for k, v := range data {
|
||||||
switch val := v.(type) {
|
switch val := v.(type) {
|
||||||
@@ -445,13 +444,7 @@ func applyTransformations(source, mirror runtime.Object, targetNamespace string)
|
|||||||
|
|
||||||
// Save original annotations to restore on failure
|
// Save original annotations to restore on failure
|
||||||
originalAnnotations := mirrorObj.GetAnnotations()
|
originalAnnotations := mirrorObj.GetAnnotations()
|
||||||
var savedAnnotations map[string]string
|
savedAnnotations := maps.Clone(originalAnnotations)
|
||||||
if originalAnnotations != nil {
|
|
||||||
savedAnnotations = make(map[string]string, len(originalAnnotations))
|
|
||||||
for k, v := range originalAnnotations {
|
|
||||||
savedAnnotations[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mirrorAnnotations := mirrorObj.GetAnnotations()
|
mirrorAnnotations := mirrorObj.GetAnnotations()
|
||||||
if mirrorAnnotations == nil {
|
if mirrorAnnotations == nil {
|
||||||
@@ -504,18 +497,12 @@ func buildTransformContext(source, mirror runtime.Object, targetNamespace string
|
|||||||
|
|
||||||
// Copy labels (if any)
|
// Copy labels (if any)
|
||||||
if labels := sourceObj.GetLabels(); labels != nil {
|
if labels := sourceObj.GetLabels(); labels != nil {
|
||||||
ctx.Labels = make(map[string]string)
|
ctx.Labels = maps.Clone(labels)
|
||||||
for k, v := range labels {
|
|
||||||
ctx.Labels[k] = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy annotations (if any)
|
// Copy annotations (if any)
|
||||||
if annotations := sourceObj.GetAnnotations(); annotations != nil {
|
if annotations := sourceObj.GetAnnotations(); annotations != nil {
|
||||||
ctx.Annotations = make(map[string]string)
|
ctx.Annotations = maps.Clone(annotations)
|
||||||
for k, v := range annotations {
|
|
||||||
ctx.Annotations[k] = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Package controller implements the kubemirror reconciliation logic.
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mirrorDeletionOutcome describes the result of an ownership-verified mirror
|
||||||
|
// deletion attempt.
|
||||||
|
type mirrorDeletionOutcome int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// mirrorAbsent means no resource exists at the requested namespace/name.
|
||||||
|
mirrorAbsent mirrorDeletionOutcome = iota
|
||||||
|
// mirrorNotOwned means a resource exists but is not a kubemirror-managed
|
||||||
|
// mirror of the given source, so it was left untouched.
|
||||||
|
mirrorNotOwned
|
||||||
|
// mirrorDeleted means an owned mirror was found and deleted.
|
||||||
|
mirrorDeleted
|
||||||
|
)
|
||||||
|
|
||||||
|
// deleteOwnedMirror deletes the mirror at gvk/ns/name only if it is managed by
|
||||||
|
// kubemirror AND its source reference matches srcNs/srcName. This ownership
|
||||||
|
// guard is the single safety gate that prevents deleting an unrelated resource
|
||||||
|
// that merely shares the same name. All mirror-cleanup paths route through here
|
||||||
|
// so the guard cannot be forgotten in one place and drift in another.
|
||||||
|
//
|
||||||
|
// A NotFound on delete is treated as success (the mirror is already gone).
|
||||||
|
func deleteOwnedMirror(
|
||||||
|
ctx context.Context,
|
||||||
|
c client.Client,
|
||||||
|
gvk schema.GroupVersionKind,
|
||||||
|
ns, name, srcNs, srcName string,
|
||||||
|
) (mirrorDeletionOutcome, error) {
|
||||||
|
mirror := &unstructured.Unstructured{}
|
||||||
|
mirror.SetGroupVersionKind(gvk)
|
||||||
|
|
||||||
|
if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: name}, mirror); err != nil {
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
return mirrorAbsent, nil
|
||||||
|
}
|
||||||
|
return mirrorAbsent, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify this is actually our mirror (not someone else's resource with the same name).
|
||||||
|
if !IsManagedByUs(mirror) {
|
||||||
|
return mirrorNotOwned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify this mirror points to our source.
|
||||||
|
refNs, refName, _, found := GetSourceReference(mirror)
|
||||||
|
if !found || refNs != srcNs || refName != srcName {
|
||||||
|
return mirrorNotOwned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Delete(ctx, mirror); err != nil && !errors.IsNotFound(err) {
|
||||||
|
return mirrorNotOwned, err
|
||||||
|
}
|
||||||
|
return mirrorDeleted, nil
|
||||||
|
}
|
||||||
@@ -149,7 +149,7 @@ func (r *MirrorReconciler) SetupWithManager(mgr ctrl.Manager, gvk schema.GroupVe
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
managedBy, exists := labels[constants.LabelManagedBy]
|
managedBy, exists := labels[constants.LabelManagedBy]
|
||||||
return exists && managedBy == "kubemirror"
|
return exists && managedBy == constants.ControllerName
|
||||||
})
|
})
|
||||||
|
|
||||||
// Convert GVK to resource object for watching
|
// Convert GVK to resource object for watching
|
||||||
@@ -157,9 +157,7 @@ func (r *MirrorReconciler) SetupWithManager(mgr ctrl.Manager, gvk schema.GroupVe
|
|||||||
gv := schema.GroupVersion{Group: gvk.Group, Version: gvk.Version}
|
gv := schema.GroupVersion{Group: gvk.Group, Version: gvk.Version}
|
||||||
obj.SetGroupVersionKind(gv.WithKind(gvk.Kind))
|
obj.SetGroupVersionKind(gv.WithKind(gvk.Kind))
|
||||||
|
|
||||||
// Set custom controller name to avoid conflicts with source reconciler and multiple API versions
|
controllerName := gvkControllerName(gvk, true)
|
||||||
// Include group and version to make it truly unique
|
|
||||||
controllerName := gvk.Kind + "." + gvk.Version + "." + gvk.Group + "-mirror"
|
|
||||||
|
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(obj).
|
For(obj).
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Package controller implements the kubemirror reconciliation logic.
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import "k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
|
// gvkControllerName builds the unique controller-runtime controller name for a
|
||||||
|
// GVK. Including version and group avoids collisions across API versions, e.g.
|
||||||
|
// "HorizontalPodAutoscaler.v1.autoscaling" or "Secret.v1." (empty group for
|
||||||
|
// core resources). Source and mirror reconcilers for the same GVK must differ,
|
||||||
|
// so mirror controllers get a "-mirror" suffix. This is the single source of
|
||||||
|
// truth for the convention shared by both reconcilers.
|
||||||
|
func gvkControllerName(gvk schema.GroupVersionKind, mirror bool) string {
|
||||||
|
name := gvk.Kind + "." + gvk.Version + "." + gvk.Group
|
||||||
|
if mirror {
|
||||||
|
name += "-mirror"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user