initial commit

This commit is contained in:
2025-12-25 22:10:57 +00:00
commit 8adb52608f
46 changed files with 7570 additions and 0 deletions
+277
View File
@@ -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
}
+622
View File
@@ -0,0 +1,622 @@
package controller
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/lukaszraczylo/kubemirror/pkg/constants"
)
func TestCreateMirror_Secret(t *testing.T) {
source := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
UID: "source-uid-123",
ResourceVersion: "100",
Generation: 5,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"password": []byte("secret123"),
},
}
mirror, err := CreateMirror(source, "app1")
require.NoError(t, err)
require.NotNil(t, mirror)
secretMirror, ok := mirror.(*corev1.Secret)
require.True(t, ok, "mirror should be a Secret")
// Verify mirror properties
assert.Equal(t, "test-secret", secretMirror.Name)
assert.Equal(t, "app1", secretMirror.Namespace)
assert.Equal(t, corev1.SecretTypeOpaque, secretMirror.Type)
assert.Equal(t, source.Data, secretMirror.Data)
// Verify ownership labels
assert.Equal(t, constants.ControllerName, secretMirror.Labels[constants.LabelManagedBy])
assert.Equal(t, "true", secretMirror.Labels[constants.LabelMirror])
// Verify ownership annotations
assert.Equal(t, "default", secretMirror.Annotations[constants.AnnotationSourceNamespace])
assert.Equal(t, "test-secret", secretMirror.Annotations[constants.AnnotationSourceName])
assert.Equal(t, "source-uid-123", secretMirror.Annotations[constants.AnnotationSourceUID])
assert.Equal(t, "5", secretMirror.Annotations[constants.AnnotationSourceGeneration])
assert.NotEmpty(t, secretMirror.Annotations[constants.AnnotationSourceContentHash])
assert.NotEmpty(t, secretMirror.Annotations[constants.AnnotationLastSyncTime])
}
func TestCreateMirror_ConfigMap(t *testing.T) {
source := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
UID: "config-uid-456",
ResourceVersion: "200",
},
Data: map[string]string{
"config.yaml": "setting: value",
},
BinaryData: map[string][]byte{
"binary": {0x00, 0x01, 0x02},
},
}
mirror, err := CreateMirror(source, "prod-ns")
require.NoError(t, err)
require.NotNil(t, mirror)
cmMirror, ok := mirror.(*corev1.ConfigMap)
require.True(t, ok, "mirror should be a ConfigMap")
// Verify mirror properties
assert.Equal(t, "test-config", cmMirror.Name)
assert.Equal(t, "prod-ns", cmMirror.Namespace)
assert.Equal(t, source.Data, cmMirror.Data)
assert.Equal(t, source.BinaryData, cmMirror.BinaryData)
// Verify ownership labels
assert.Equal(t, constants.ControllerName, cmMirror.Labels[constants.LabelManagedBy])
assert.Equal(t, "true", cmMirror.Labels[constants.LabelMirror])
// Verify ownership annotations
assert.Equal(t, "default", cmMirror.Annotations[constants.AnnotationSourceNamespace])
assert.Equal(t, "test-config", cmMirror.Annotations[constants.AnnotationSourceName])
assert.Equal(t, "config-uid-456", cmMirror.Annotations[constants.AnnotationSourceUID])
assert.NotEmpty(t, cmMirror.Annotations[constants.AnnotationSourceContentHash])
}
func TestCreateMirror_Unstructured(t *testing.T) {
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "traefik.io/v1alpha1",
"kind": "Middleware",
"metadata": map[string]interface{}{
"name": "test-middleware",
"namespace": "traefik",
"uid": "middleware-uid-789",
"resourceVersion": "300",
"generation": int64(3),
},
"spec": map[string]interface{}{
"basicAuth": map[string]interface{}{
"secret": "auth-secret",
},
},
"status": map[string]interface{}{
"condition": "Ready",
},
},
}
mirror, err := CreateMirror(source, "app-ns")
require.NoError(t, err)
require.NotNil(t, mirror)
uMirror, ok := mirror.(*unstructured.Unstructured)
require.True(t, ok, "mirror should be Unstructured")
// Verify mirror properties
assert.Equal(t, "test-middleware", uMirror.GetName())
assert.Equal(t, "app-ns", uMirror.GetNamespace())
// Verify spec is copied
spec, found, err := unstructured.NestedMap(uMirror.Object, "spec")
require.NoError(t, err)
require.True(t, found)
assert.NotNil(t, spec)
// Verify status is NOT copied
_, found, err = unstructured.NestedMap(uMirror.Object, "status")
require.NoError(t, err)
assert.False(t, found, "status should not be mirrored")
// Verify metadata is cleared
assert.Empty(t, uMirror.GetResourceVersion())
assert.Empty(t, uMirror.GetUID())
assert.Equal(t, int64(0), uMirror.GetGeneration())
// Verify ownership labels
assert.Equal(t, constants.ControllerName, uMirror.GetLabels()[constants.LabelManagedBy])
assert.Equal(t, "true", uMirror.GetLabels()[constants.LabelMirror])
// Verify ownership annotations
annotations := uMirror.GetAnnotations()
assert.Equal(t, "traefik", annotations[constants.AnnotationSourceNamespace])
assert.Equal(t, "test-middleware", annotations[constants.AnnotationSourceName])
assert.Equal(t, "middleware-uid-789", annotations[constants.AnnotationSourceUID])
assert.Equal(t, "3", annotations[constants.AnnotationSourceGeneration])
}
func TestUpdateMirror_Secret(t *testing.T) {
mirror := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "app1",
Labels: map[string]string{
constants.LabelManagedBy: constants.ControllerName,
},
Annotations: map[string]string{
constants.AnnotationSourceContentHash: "oldhash",
},
},
Data: map[string][]byte{
"password": []byte("old"),
},
Type: corev1.SecretTypeOpaque,
}
source := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
Generation: 10,
},
Data: map[string][]byte{
"password": []byte("new"),
},
Type: corev1.SecretTypeTLS,
}
err := UpdateMirror(mirror, source)
require.NoError(t, err)
// Verify data updated
assert.Equal(t, source.Data, mirror.Data)
assert.Equal(t, source.Type, mirror.Type)
// Verify hash updated
assert.NotEqual(t, "oldhash", mirror.Annotations[constants.AnnotationSourceContentHash])
assert.Equal(t, "10", mirror.Annotations[constants.AnnotationSourceGeneration])
assert.NotEmpty(t, mirror.Annotations[constants.AnnotationLastSyncTime])
}
func TestUpdateMirror_ConfigMap(t *testing.T) {
mirror := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "app1",
Annotations: map[string]string{
constants.AnnotationSourceContentHash: "oldhash",
},
},
Data: map[string]string{
"key": "old",
},
}
source := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
},
Data: map[string]string{
"key": "new",
},
BinaryData: map[string][]byte{
"binary": {0xFF},
},
}
err := UpdateMirror(mirror, source)
require.NoError(t, err)
// Verify data updated
assert.Equal(t, source.Data, mirror.Data)
assert.Equal(t, source.BinaryData, mirror.BinaryData)
assert.NotEqual(t, "oldhash", mirror.Annotations[constants.AnnotationSourceContentHash])
}
func TestIsManagedByUs(t *testing.T) {
tests := []struct {
obj metav1.Object
name string
want bool
}{
{
name: "managed by us",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelManagedBy: constants.ControllerName,
},
},
},
want: true,
},
{
name: "not managed by us",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelManagedBy: "other-controller",
},
},
},
want: false,
},
{
name: "no labels",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{},
},
want: false,
},
{
name: "nil labels",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: nil,
},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsManagedByUs(tt.obj)
assert.Equal(t, tt.want, got)
})
}
}
func TestIsMirrorResource(t *testing.T) {
tests := []struct {
obj metav1.Object
name string
want bool
}{
{
name: "is mirror",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelMirror: "true",
},
},
},
want: true,
},
{
name: "not mirror",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelMirror: "false",
},
},
},
want: false,
},
{
name: "no labels",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsMirrorResource(tt.obj)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetSourceReference(t *testing.T) {
tests := []struct {
name string
obj metav1.Object
wantNamespace string
wantName string
wantUID string
wantFound bool
}{
{
name: "valid source reference",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "my-secret",
constants.AnnotationSourceUID: "uid-123",
},
},
},
wantNamespace: "default",
wantName: "my-secret",
wantUID: "uid-123",
wantFound: true,
},
{
name: "missing annotations",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{},
},
wantFound: false,
},
{
name: "incomplete annotations - missing name",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
constants.AnnotationSourceNamespace: "default",
},
},
},
wantFound: false,
},
{
name: "incomplete annotations - missing namespace",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
constants.AnnotationSourceName: "my-secret",
},
},
},
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotNS, gotName, gotUID, gotFound := GetSourceReference(tt.obj)
assert.Equal(t, tt.wantFound, gotFound)
if tt.wantFound {
assert.Equal(t, tt.wantNamespace, gotNS)
assert.Equal(t, tt.wantName, gotName)
assert.Equal(t, tt.wantUID, gotUID)
}
})
}
}
// Test that mirrors don't include sync annotations (prevent infinite loop)
func TestCreateMirror_NoSyncAnnotations(t *testing.T) {
source := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Labels: map[string]string{
constants.LabelEnabled: "true",
},
Annotations: map[string]string{
constants.AnnotationSync: "true",
constants.AnnotationTargetNamespaces: "app1,app2",
constants.AnnotationExclude: "false",
constants.AnnotationRecreateOnImmutableChange: "true",
},
},
Data: map[string][]byte{"key": []byte("value")},
}
mirror, err := CreateMirror(source, "app1")
require.NoError(t, err)
secretMirror := mirror.(*corev1.Secret)
// Verify sync annotations are NOT copied
assert.NotContains(t, secretMirror.Annotations, constants.AnnotationSync)
assert.NotContains(t, secretMirror.Annotations, constants.AnnotationTargetNamespaces)
// Verify enabled label is NOT copied
assert.NotContains(t, secretMirror.Labels, constants.LabelEnabled)
// Verify ownership annotations ARE present
assert.Contains(t, secretMirror.Annotations, constants.AnnotationSourceNamespace)
}
// Benchmarks for critical paths
func BenchmarkCreateMirror_Secret(b *testing.B) {
source := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bench-secret",
Namespace: "default",
UID: "uid-123",
ResourceVersion: "100",
Generation: 1,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"password": []byte("secret123"),
"username": []byte("admin"),
"token": []byte("abcdef123456"),
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = CreateMirror(source, "target-ns")
}
}
func BenchmarkCreateMirror_ConfigMap(b *testing.B) {
source := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "bench-config",
Namespace: "default",
UID: "uid-456",
ResourceVersion: "200",
},
Data: map[string]string{
"config.yaml": "key1: value1\nkey2: value2\nkey3: value3",
"app.conf": "setting=value",
},
BinaryData: map[string][]byte{
"binary": {0x00, 0x01, 0x02, 0x03, 0x04},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = CreateMirror(source, "target-ns")
}
}
func BenchmarkCreateMirror_Unstructured(b *testing.B) {
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "traefik.io/v1alpha1",
"kind": "Middleware",
"metadata": map[string]interface{}{
"name": "bench-middleware",
"namespace": "traefik",
"uid": "uid-789",
"resourceVersion": "300",
"generation": int64(3),
},
"spec": map[string]interface{}{
"basicAuth": map[string]interface{}{
"secret": "auth-secret",
},
"headers": map[string]interface{}{
"customRequestHeaders": map[string]interface{}{
"X-Custom-Header": "value",
},
},
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = CreateMirror(source, "target-ns")
}
}
func BenchmarkUpdateMirror_Secret(b *testing.B) {
mirror := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "app1",
Labels: map[string]string{
constants.LabelManagedBy: constants.ControllerName,
},
Annotations: map[string]string{
constants.AnnotationSourceContentHash: "oldhash",
},
},
Data: map[string][]byte{
"password": []byte("old"),
},
Type: corev1.SecretTypeOpaque,
}
source := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
Generation: 10,
},
Data: map[string][]byte{
"password": []byte("new"),
},
Type: corev1.SecretTypeOpaque,
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = UpdateMirror(mirror, source)
}
}
func BenchmarkUpdateMirror_ConfigMap(b *testing.B) {
mirror := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "app1",
Annotations: map[string]string{
constants.AnnotationSourceContentHash: "oldhash",
},
},
Data: map[string]string{
"key": "old",
},
}
source := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
},
Data: map[string]string{
"key": "new",
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = UpdateMirror(mirror, source)
}
}
func BenchmarkIsManagedByUs(b *testing.B) {
obj := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelManagedBy: constants.ControllerName,
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = IsManagedByUs(obj)
}
}
func BenchmarkGetSourceReference(b *testing.B) {
obj := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "my-secret",
constants.AnnotationSourceUID: "uid-123",
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _, _, _ = GetSourceReference(obj)
}
}
+57
View File
@@ -0,0 +1,57 @@
// Package controller implements the kubemirror reconciliation logic.
package controller
import (
"context"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/lukaszraczylo/kubemirror/pkg/constants"
)
// KubernetesNamespaceLister implements NamespaceLister using the Kubernetes API.
type KubernetesNamespaceLister struct {
client client.Client
}
// NewKubernetesNamespaceLister creates a new KubernetesNamespaceLister.
func NewKubernetesNamespaceLister(client client.Client) *KubernetesNamespaceLister {
return &KubernetesNamespaceLister{
client: client,
}
}
// ListNamespaces returns all namespace names in the cluster.
func (k *KubernetesNamespaceLister) ListNamespaces(ctx context.Context) ([]string, error) {
namespaceList := &corev1.NamespaceList{}
if err := k.client.List(ctx, namespaceList); err != nil {
return nil, err
}
names := make([]string, 0, len(namespaceList.Items))
for _, ns := range namespaceList.Items {
names = append(names, ns.Name)
}
return names, nil
}
// ListAllowMirrorsNamespaces returns namespaces that have the allow-mirrors label.
func (k *KubernetesNamespaceLister) ListAllowMirrorsNamespaces(ctx context.Context) ([]string, error) {
namespaceList := &corev1.NamespaceList{}
// List namespaces with the allow-mirrors label
if err := k.client.List(ctx, namespaceList, client.MatchingLabels{
constants.LabelAllowMirrors: "true",
}); err != nil {
return nil, err
}
names := make([]string, 0, len(namespaceList.Items))
for _, ns := range namespaceList.Items {
names = append(names, ns.Name)
}
return names, nil
}
+446
View File
@@ -0,0 +1,446 @@
// Package controller implements the kubemirror reconciliation logic.
package controller
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/lukaszraczylo/kubemirror/pkg/config"
"github.com/lukaszraczylo/kubemirror/pkg/constants"
"github.com/lukaszraczylo/kubemirror/pkg/filter"
"github.com/lukaszraczylo/kubemirror/pkg/hash"
)
// SourceReconciler reconciles source resources that need mirroring.
type SourceReconciler struct {
client.Client
Scheme *runtime.Scheme
Config *config.Config
Filter *filter.NamespaceFilter
NamespaceLister NamespaceLister
GVK schema.GroupVersionKind // The resource type this reconciler handles
}
// NamespaceLister provides a list of all namespaces in the cluster.
// This interface allows for testing with mocks.
type NamespaceLister interface {
ListNamespaces(ctx context.Context) ([]string, error)
ListAllowMirrorsNamespaces(ctx context.Context) ([]string, error)
}
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
// Reconcile processes a single source resource.
func (r *SourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx).WithValues("namespace", req.Namespace, "name", req.Name)
// Fetch the source resource as unstructured (works for all resource types)
source := &unstructured.Unstructured{}
source.SetGroupVersionKind(r.GVK) // Set the GVK so the client knows what to fetch
if err := r.Get(ctx, req.NamespacedName, source); err != nil {
if errors.IsNotFound(err) {
// Resource deleted - nothing to do
return ctrl.Result{}, nil
}
logger.Error(err, "failed to get resource")
return ctrl.Result{}, err
}
sourceObj := source
// Check if this is a mirror resource (shouldn't reconcile mirrors as sources)
if IsMirrorResource(sourceObj) {
// Silently skip - mirrors reconcile via watch, not as sources
return ctrl.Result{}, nil
}
// Check if resource is enabled for mirroring
if !isEnabledForMirroring(sourceObj) {
// Silently skip - don't log as it would be too noisy
return r.handleDisabled(ctx, sourceObj)
}
// Handle deletion
if !sourceObj.GetDeletionTimestamp().IsZero() {
return r.handleDeletion(ctx, source, sourceObj)
}
// Add finalizer if not present
// source (*unstructured.Unstructured) already implements client.Object
if !controllerutil.ContainsFinalizer(source, constants.FinalizerName) {
controllerutil.AddFinalizer(source, constants.FinalizerName)
if err := r.Update(ctx, source); err != nil {
logger.Error(err, "failed to add finalizer")
return ctrl.Result{}, err
}
logger.V(1).Info("added finalizer")
}
// Get target namespaces
targetNamespaces, err := r.resolveTargetNamespaces(ctx, sourceObj)
if err != nil {
logger.Error(err, "failed to resolve target namespaces")
return ctrl.Result{}, err
}
if len(targetNamespaces) == 0 {
logger.V(1).Info("no target namespaces resolved")
return ctrl.Result{}, nil
}
logger.V(1).Info("reconciling mirrors", "targetCount", len(targetNamespaces))
// Reconcile each target namespace
var reconciledCount, errorCount int
for _, targetNs := range targetNamespaces {
if err := r.reconcileMirror(ctx, source, sourceObj, targetNs); err != nil {
logger.Error(err, "failed to reconcile mirror", "targetNamespace", targetNs)
errorCount++
} else {
reconciledCount++
}
}
// Update status annotation with last sync info
if err := r.updateLastSyncStatus(ctx, source, sourceObj, reconciledCount, errorCount); err != nil {
logger.Error(err, "failed to update sync status")
return ctrl.Result{}, err
}
logger.Info("reconciliation complete",
"reconciled", reconciledCount,
"errors", errorCount,
"total", len(targetNamespaces))
// Requeue if there were errors
if errorCount > 0 {
return ctrl.Result{Requeue: true}, fmt.Errorf("failed to reconcile %d/%d mirrors", errorCount, len(targetNamespaces))
}
return ctrl.Result{}, nil
}
// handleDeletion removes finalizer after cleaning up all mirrors.
func (r *SourceReconciler) handleDeletion(ctx context.Context, source runtime.Object, sourceObj metav1.Object) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// source (*unstructured.Unstructured) already implements client.Object
sourceUnstructured := source.(*unstructured.Unstructured)
if !controllerutil.ContainsFinalizer(sourceUnstructured, constants.FinalizerName) {
return ctrl.Result{}, nil
}
// Delete all mirrors
if err := r.deleteAllMirrors(ctx, sourceObj); err != nil {
logger.Error(err, "failed to delete mirrors")
return ctrl.Result{}, err
}
// Remove finalizer
controllerutil.RemoveFinalizer(sourceUnstructured, constants.FinalizerName)
if err := r.Update(ctx, sourceUnstructured); err != nil {
logger.Error(err, "failed to remove finalizer")
return ctrl.Result{}, err
}
logger.Info("finalizer removed, mirrors deleted")
return ctrl.Result{}, nil
}
// handleDisabled removes mirrors when a resource is disabled.
func (r *SourceReconciler) handleDisabled(ctx context.Context, sourceObj metav1.Object) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// Source is already a client.Object (unstructured implements it)
sourceClient := sourceObj.(client.Object)
// If resource has finalizer, clean up mirrors and remove it
if controllerutil.ContainsFinalizer(sourceClient, constants.FinalizerName) {
if err := r.deleteAllMirrors(ctx, sourceObj); err != nil {
logger.Error(err, "failed to delete mirrors for disabled resource")
return ctrl.Result{}, err
}
// Remove finalizer
controllerutil.RemoveFinalizer(sourceClient, constants.FinalizerName)
if err := r.Update(ctx, sourceClient); err != nil {
logger.Error(err, "failed to remove finalizer from disabled resource")
return ctrl.Result{}, err
}
logger.Info("mirrors deleted and finalizer removed for disabled resource")
}
return ctrl.Result{}, nil
}
// reconcileMirror creates or updates a mirror in the target namespace.
func (r *SourceReconciler) reconcileMirror(ctx context.Context, source runtime.Object, sourceObj metav1.Object, targetNs string) error {
logger := log.FromContext(ctx).WithValues("targetNamespace", targetNs)
// Try to get existing mirror as unstructured
sourceUnstructured := source.(*unstructured.Unstructured)
existing := &unstructured.Unstructured{}
existing.SetGroupVersionKind(sourceUnstructured.GroupVersionKind())
err := r.Get(ctx, client.ObjectKey{Namespace: targetNs, Name: sourceObj.GetName()}, existing)
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to get existing mirror: %w", err)
}
if err == nil {
// Mirror exists - check if it's managed by us
if !IsManagedByUs(existing) {
logger.Info("target resource exists but not managed by kubemirror, skipping")
return nil
}
// Check if update is needed
needsSync, err := hash.NeedsSync(source, existing, existing.GetAnnotations())
if err != nil {
return fmt.Errorf("failed to check if sync needed: %w", err)
}
if !needsSync {
logger.V(1).Info("mirror is up to date")
return nil
}
// Update mirror
if err := UpdateMirror(existing, source); err != nil {
return fmt.Errorf("failed to update mirror: %w", err)
}
if err := r.Update(ctx, existing); err != nil {
return fmt.Errorf("failed to update mirror in cluster: %w", err)
}
logger.Info("mirror updated")
return nil
}
// Create new mirror
mirror, err := CreateMirror(source, targetNs)
if err != nil {
return fmt.Errorf("failed to create mirror: %w", err)
}
if err := r.Create(ctx, mirror.(client.Object)); err != nil {
return fmt.Errorf("failed to create mirror in cluster: %w", err)
}
logger.Info("mirror created")
return nil
}
// deleteAllMirrors deletes all mirrors for a source resource.
func (r *SourceReconciler) deleteAllMirrors(ctx context.Context, sourceObj metav1.Object) error {
logger := log.FromContext(ctx)
// List all namespaces
allNamespaces, err := r.NamespaceLister.ListNamespaces(ctx)
if err != nil {
return fmt.Errorf("failed to list namespaces: %w", err)
}
// Get GVK from source object
sourceUnstructured, ok := sourceObj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("source object is not unstructured")
}
var deleteCount int
for _, ns := range allNamespaces {
// Skip source namespace
if ns == sourceObj.GetNamespace() {
continue
}
// Create mirror reference for deletion
mirror := &unstructured.Unstructured{}
mirror.SetGroupVersionKind(sourceUnstructured.GroupVersionKind())
mirror.SetNamespace(ns)
mirror.SetName(sourceObj.GetName())
err := r.Delete(ctx, mirror)
if err == nil {
deleteCount++
} else if !errors.IsNotFound(err) {
logger.Error(err, "failed to delete mirror", "namespace", ns)
}
}
logger.Info("deleted mirrors", "count", deleteCount)
return nil
}
// resolveTargetNamespaces determines which namespaces should receive mirrors.
func (r *SourceReconciler) resolveTargetNamespaces(ctx context.Context, sourceObj metav1.Object) ([]string, error) {
annotations := sourceObj.GetAnnotations()
if annotations == nil {
return nil, nil
}
targetNsAnnotation := annotations[constants.AnnotationTargetNamespaces]
if targetNsAnnotation == "" {
return nil, nil
}
// Parse patterns
patterns := filter.ParseTargetNamespaces(targetNsAnnotation)
if len(patterns) == 0 {
return nil, nil
}
// Get all namespaces
allNamespaces, err := r.NamespaceLister.ListNamespaces(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list namespaces: %w", err)
}
// Get namespaces with allow-mirrors label
allowMirrorsNamespaces, err := r.NamespaceLister.ListAllowMirrorsNamespaces(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list allow-mirrors namespaces: %w", err)
}
// Resolve target namespaces
targetNamespaces := filter.ResolveTargetNamespaces(
patterns,
allNamespaces,
allowMirrorsNamespaces,
sourceObj.GetNamespace(),
r.Filter,
)
// Enforce max targets limit
if r.Config != nil && r.Config.MaxTargetsPerResource > 0 && len(targetNamespaces) > r.Config.MaxTargetsPerResource {
targetNamespaces = targetNamespaces[:r.Config.MaxTargetsPerResource]
}
return targetNamespaces, nil
}
// updateLastSyncStatus updates the source resource's annotations with sync status.
func (r *SourceReconciler) updateLastSyncStatus(ctx context.Context, source runtime.Object, sourceObj metav1.Object, reconciledCount, errorCount int) error {
annotations := sourceObj.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[constants.AnnotationSyncStatus] = fmt.Sprintf("reconciled:%d,errors:%d", reconciledCount, errorCount)
sourceObj.SetAnnotations(annotations)
// source (*unstructured.Unstructured) already implements client.Object
return r.Update(ctx, source.(*unstructured.Unstructured))
}
// isEnabledForMirroring checks if a resource has both the label and annotation for mirroring.
func isEnabledForMirroring(obj metav1.Object) bool {
// Check label
labels := obj.GetLabels()
if labels == nil || labels[constants.LabelEnabled] != "true" {
return false
}
// Check annotation
annotations := obj.GetAnnotations()
if annotations == nil || annotations[constants.AnnotationSync] != "true" {
return false
}
return true
}
// SetupWithManager sets up the controller with the Manager.
func (r *SourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
// Build predicate to only watch resources with enabled label
// This reduces API server load by ~90%
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}).
Complete(r)
}
// SetupWithManagerForResourceType sets up a controller for a specific resource type.
// This allows dynamic controller registration for any discovered resource type.
func (r *SourceReconciler) SetupWithManagerForResourceType(
mgr ctrl.Manager,
gvk schema.GroupVersionKind,
) error {
// Create an unstructured object for this GVK
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(gvk)
// Create unique controller name including version to avoid collisions
// e.g., "HorizontalPodAutoscaler.v1.autoscaling"
controllerName := gvk.Kind + "." + gvk.Version
if gvk.Group != "" {
controllerName += "." + gvk.Group
}
// Create mirror object for watching
mirrorObj := &unstructured.Unstructured{}
mirrorObj.SetGroupVersionKind(gvk)
// Create predicates to only watch mirror deletions
mirrorDeletePredicate := predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool { return false },
UpdateFunc: func(e event.UpdateEvent) bool { return false },
DeleteFunc: func(e event.DeleteEvent) bool { return IsMirrorResource(e.Object) },
GenericFunc: func(e event.GenericEvent) bool { return false },
}
return ctrl.NewControllerManagedBy(mgr).
For(obj).
Named(controllerName).
// Watch mirror resources - when deleted, enqueue source for reconciliation
Watches(
mirrorObj,
handler.EnqueueRequestsFromMapFunc(r.mapMirrorToSource),
builder.WithPredicates(mirrorDeletePredicate),
).
Complete(r)
}
// mapMirrorToSource maps a mirror resource to its source for reconciliation.
func (r *SourceReconciler) mapMirrorToSource(ctx context.Context, obj client.Object) []reconcile.Request {
// Only process if this is a mirror
if !IsMirrorResource(obj) {
return nil
}
// Get source reference from annotations
sourceNs, sourceName, _, found := GetSourceReference(obj)
if !found {
return nil
}
// Enqueue reconciliation request for the source
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: sourceNs,
Name: sourceName,
},
},
}
}
+463
View File
@@ -0,0 +1,463 @@
package controller
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/lukaszraczylo/kubemirror/pkg/config"
"github.com/lukaszraczylo/kubemirror/pkg/constants"
"github.com/lukaszraczylo/kubemirror/pkg/filter"
)
// MockClient is a mock implementation of client.Client for testing.
type MockClient struct {
mock.Mock
}
func (m *MockClient) Get(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error {
args := m.Called(ctx, key, obj)
if args.Error(0) != nil {
return args.Error(0)
}
// Copy the mock object into obj
if mockObj := args.Get(1); mockObj != nil {
switch v := mockObj.(type) {
case *corev1.Secret:
*obj.(*corev1.Secret) = *v
case *corev1.ConfigMap:
*obj.(*corev1.ConfigMap) = *v
case *unstructured.Unstructured:
// Copy the unstructured object
*obj.(*unstructured.Unstructured) = *v
}
}
return nil
}
func (m *MockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
args := m.Called(ctx, list, opts)
return args.Error(0)
}
func (m *MockClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
args := m.Called(ctx, obj, opts)
return args.Error(0)
}
func (m *MockClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
args := m.Called(ctx, obj, opts)
return args.Error(0)
}
func (m *MockClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
args := m.Called(ctx, obj, opts)
return args.Error(0)
}
func (m *MockClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
args := m.Called(ctx, obj, patch, opts)
return args.Error(0)
}
func (m *MockClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
args := m.Called(ctx, obj, opts)
return args.Error(0)
}
func (m *MockClient) Status() client.StatusWriter {
args := m.Called()
return args.Get(0).(client.StatusWriter)
}
func (m *MockClient) Scheme() *runtime.Scheme {
args := m.Called()
return args.Get(0).(*runtime.Scheme)
}
func (m *MockClient) RESTMapper() meta.RESTMapper {
args := m.Called()
return args.Get(0).(meta.RESTMapper)
}
func (m *MockClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
args := m.Called(obj)
return args.Get(0).(schema.GroupVersionKind), args.Error(1)
}
func (m *MockClient) IsObjectNamespaced(obj runtime.Object) (bool, error) {
args := m.Called(obj)
return args.Bool(0), args.Error(1)
}
func (m *MockClient) SubResource(subResource string) client.SubResourceClient {
args := m.Called(subResource)
return args.Get(0).(client.SubResourceClient)
}
// MockNamespaceLister is a mock implementation of NamespaceLister for testing.
type MockNamespaceLister struct {
mock.Mock
}
func (m *MockNamespaceLister) ListNamespaces(ctx context.Context) ([]string, error) {
args := m.Called(ctx)
return args.Get(0).([]string), args.Error(1)
}
func (m *MockNamespaceLister) ListAllowMirrorsNamespaces(ctx context.Context) ([]string, error) {
args := m.Called(ctx)
return args.Get(0).([]string), args.Error(1)
}
func TestIsEnabledForMirroring(t *testing.T) {
tests := []struct {
obj metav1.Object
name string
want bool
}{
{
name: "enabled with both label and annotation",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelEnabled: "true",
},
Annotations: map[string]string{
constants.AnnotationSync: "true",
},
},
},
want: true,
},
{
name: "missing label",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
constants.AnnotationSync: "true",
},
},
},
want: false,
},
{
name: "missing annotation",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelEnabled: "true",
},
},
},
want: false,
},
{
name: "label set to false",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelEnabled: "false",
},
Annotations: map[string]string{
constants.AnnotationSync: "true",
},
},
},
want: false,
},
{
name: "no labels or annotations",
obj: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isEnabledForMirroring(tt.obj)
assert.Equal(t, tt.want, got)
})
}
}
func TestSourceReconciler_resolveTargetNamespaces(t *testing.T) {
tests := []struct {
name string
sourceAnnotations map[string]string
allNamespaces []string
allowMirrorsNamespaces []string
sourceNamespace string
wantContains []string
wantNotContains []string
wantError bool
expectListCalls bool
}{
{
name: "no target annotation",
sourceAnnotations: map[string]string{
constants.AnnotationSync: "true",
},
allNamespaces: []string{"app1", "app2"},
sourceNamespace: "default",
wantContains: nil,
expectListCalls: false,
},
{
name: "single target namespace",
sourceAnnotations: map[string]string{
constants.AnnotationTargetNamespaces: "app1",
},
allNamespaces: []string{"app1", "app2", "default"},
sourceNamespace: "default",
wantContains: []string{"app1"},
wantNotContains: []string{"app2", "default"},
expectListCalls: true,
},
{
name: "multiple target namespaces",
sourceAnnotations: map[string]string{
constants.AnnotationTargetNamespaces: "app1,app2",
},
allNamespaces: []string{"app1", "app2", "app3", "default"},
sourceNamespace: "default",
wantContains: []string{"app1", "app2"},
wantNotContains: []string{"app3", "default"},
expectListCalls: true,
},
{
name: "all keyword",
sourceAnnotations: map[string]string{
constants.AnnotationTargetNamespaces: "all",
},
allNamespaces: []string{"app1", "app2", "default"},
sourceNamespace: "default",
wantContains: []string{"app1", "app2"},
wantNotContains: []string{"default"}, // source excluded
expectListCalls: true,
},
{
name: "pattern matching",
sourceAnnotations: map[string]string{
constants.AnnotationTargetNamespaces: "app-*",
},
allNamespaces: []string{"app-frontend", "app-backend", "prod-api", "default"},
sourceNamespace: "default",
wantContains: []string{"app-frontend", "app-backend"},
wantNotContains: []string{"prod-api", "default"},
expectListCalls: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockLister := new(MockNamespaceLister)
if tt.expectListCalls {
mockLister.On("ListNamespaces", mock.Anything).Return(tt.allNamespaces, nil)
mockLister.On("ListAllowMirrorsNamespaces", mock.Anything).Return(tt.allowMirrorsNamespaces, nil)
}
r := &SourceReconciler{
Config: &config.Config{},
Filter: filter.NewNamespaceFilter([]string{}, []string{}),
NamespaceLister: mockLister,
}
sourceObj := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: tt.sourceNamespace,
Annotations: tt.sourceAnnotations,
},
}
got, err := r.resolveTargetNamespaces(context.Background(), sourceObj)
if tt.wantError {
require.Error(t, err)
return
}
require.NoError(t, err)
if tt.wantContains != nil {
for _, ns := range tt.wantContains {
assert.Contains(t, got, ns)
}
}
if tt.wantNotContains != nil {
for _, ns := range tt.wantNotContains {
assert.NotContains(t, got, ns)
}
}
if tt.expectListCalls {
mockLister.AssertExpectations(t)
}
})
}
}
func TestSourceReconciler_Reconcile_MirrorResource(t *testing.T) {
// Test that mirrors are not reconciled as sources
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
r := &SourceReconciler{
Client: mockClient,
Scheme: runtime.NewScheme(),
Config: &config.Config{},
Filter: filter.NewNamespaceFilter([]string{}, []string{}),
NamespaceLister: mockLister,
GVK: schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Secret",
},
}
// Create a mirror resource (has the mirror label) as unstructured
mirrorSecret := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": "app1",
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
},
},
}
mockClient.On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*unstructured.Unstructured")).
Return(nil, mirrorSecret)
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "app1",
Name: "test-secret",
},
}
result, err := r.Reconcile(context.Background(), req)
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
mockClient.AssertExpectations(t)
}
func TestSourceReconciler_Reconcile_NotFound(t *testing.T) {
// Test that deleted resources are handled gracefully
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
r := &SourceReconciler{
Client: mockClient,
Scheme: runtime.NewScheme(),
Config: &config.Config{},
Filter: filter.NewNamespaceFilter([]string{}, []string{}),
NamespaceLister: mockLister,
GVK: schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Secret",
},
}
notFoundErr := errors.NewNotFound(schema.GroupResource{
Group: "",
Resource: "secrets",
}, "test-secret")
mockClient.On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*unstructured.Unstructured")).
Return(notFoundErr, (*unstructured.Unstructured)(nil))
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-secret",
},
}
result, err := r.Reconcile(context.Background(), req)
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
mockClient.AssertExpectations(t)
}
// Benchmark tests for performance-critical paths
func BenchmarkIsEnabledForMirroring(b *testing.B) {
obj := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
constants.LabelEnabled: "true",
},
Annotations: map[string]string{
constants.AnnotationSync: "true",
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = isEnabledForMirroring(obj)
}
}
func BenchmarkResolveTargetNamespaces(b *testing.B) {
mockLister := new(MockNamespaceLister)
allNamespaces := make([]string, 100)
for i := 0; i < 100; i++ {
allNamespaces[i] = fmt.Sprintf("namespace-%d", i)
}
mockLister.On("ListNamespaces", mock.Anything).Return(allNamespaces, nil)
mockLister.On("ListAllowMirrorsNamespaces", mock.Anything).Return(allNamespaces[:50], nil)
r := &SourceReconciler{
Config: &config.Config{},
Filter: filter.NewNamespaceFilter([]string{}, []string{}),
NamespaceLister: mockLister,
}
sourceObj := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTargetNamespaces: "all",
},
},
}
ctx := context.Background()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = r.resolveTargetNamespaces(ctx, sourceObj)
}
}