Files
kubemirror/pkg/controller/source_reconciler_test.go
T
lukaszraczylo 4277c8ac39 fix(controller): guard mirror deletion + enforce secret blacklist
C1: deleteAllMirrors used to issue a blind Delete on every namespace
matching the source name+GVK, which would destroy unrelated resources
(e.g. a 'default' SA, 'ca-bundle' ConfigMap) sharing the source name.
Now reads each candidate, verifies managed-by label and source-reference
annotation, and only deletes confirmed mirrors.

M1: BlacklistedSecretTypes was declared but never enforced. Enabling
mirroring on a service-account-token / bootstrap-token / helm release
Secret would mirror credentials cluster-wide. Now refused at Reconcile.

M3: deleteAllMirrors swallowed per-namespace errors and returned nil,
so callers removed the finalizer even on partial failure (orphans).
Errors are now joined and returned.
2026-05-02 22:35:40 +01:00

1106 lines
34 KiB
Go

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)
}
func (m *MockClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error {
args := m.Called(ctx, obj, opts)
return args.Error(0)
}
// 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 (m *MockNamespaceLister) ListOptOutNamespaces(ctx context.Context) ([]string, error) {
args := m.Called(ctx)
return args.Get(0).([]string), args.Error(1)
}
func (m *MockNamespaceLister) ListNamespacesWithLabels(ctx context.Context) (*NamespaceInfo, error) {
args := m.Called(ctx)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*NamespaceInfo), 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 {
nsInfo := &NamespaceInfo{
All: tt.allNamespaces,
AllowMirrors: tt.allowMirrorsNamespaces,
OptOut: []string{},
}
mockLister.On("ListNamespacesWithLabels", mock.Anything).Return(nsInfo, 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 := range 100 {
allNamespaces[i] = fmt.Sprintf("namespace-%d", i)
}
nsInfo := &NamespaceInfo{
All: allNamespaces,
AllowMirrors: allNamespaces[:50],
OptOut: []string{},
}
mockLister.On("ListNamespacesWithLabels", mock.Anything).Return(nsInfo, 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)
}
}
func TestSourceReconciler_cleanupOrphanedMirrors(t *testing.T) {
// Setup: Source in default namespace with mirrors in app-1, app-2, app-3
// Then target-namespaces changes to only app-1, app-2
// Expect: app-3 mirror is deleted (orphaned)
sourceObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": "default",
"uid": "source-uid-123",
},
},
}
// Mock existing mirror in app-3 (will be orphaned)
orphanedMirror := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": "app-3",
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "test-secret",
constants.AnnotationSourceUID: "source-uid-123",
},
},
},
}
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
// Setup: all namespaces in cluster
allNamespaces := []string{"default", "app-1", "app-2", "app-3", "prod-1"}
nsInfo := &NamespaceInfo{
All: allNamespaces,
AllowMirrors: []string{},
OptOut: []string{},
}
mockLister.On("ListNamespacesWithLabels", mock.Anything).Return(nsInfo, nil)
// Current target list (after annotation change): only app-1 and app-2
targetNamespaces := []string{"app-1", "app-2"}
// The function will iterate through all namespaces and:
// - Skip "default" (source namespace)
// - Skip "app-1" and "app-2" (in target list)
// - Check "app-3" (not in target list) → will find orphaned mirror
// - Check "prod-1" (not in target list) → no mirror exists
notFoundErr := errors.NewNotFound(schema.GroupResource{Group: "", Resource: "secrets"}, "test-secret")
// app-3: orphaned mirror exists
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-3", Name: "test-secret"}, mock.Anything).
Return(nil, orphanedMirror)
// prod-1: no mirror exists
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-1", Name: "test-secret"}, mock.Anything).
Return(notFoundErr, nil)
// Expect delete call for app-3 mirror
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
u, ok := obj.(*unstructured.Unstructured)
return ok && u.GetNamespace() == "app-3" && u.GetName() == "test-secret"
}), mock.Anything).Return(nil)
r := &SourceReconciler{
Client: mockClient,
NamespaceLister: mockLister,
}
ctx := context.Background()
deletedCount, err := r.cleanupOrphanedMirrors(ctx, sourceObj, targetNamespaces)
require.NoError(t, err)
assert.Equal(t, 1, deletedCount, "should have deleted 1 orphaned mirror")
mockClient.AssertExpectations(t)
mockLister.AssertExpectations(t)
}
func TestSourceReconciler_Reconcile_AnnotationChange_AllToAllLabeled(t *testing.T) {
// Scenario: annotation changes from "all" → "all-labeled"
// Before: mirrors in all 5 namespaces
// After: mirrors only in labeled namespaces (app-1, app-2)
// Expected: 3 orphaned mirrors deleted (app-3, prod-1, prod-2)
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": "default",
"uid": "source-uid-123",
"labels": map[string]interface{}{
constants.LabelEnabled: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSync: "true",
constants.AnnotationTargetNamespaces: "all-labeled", // Changed from "all"
},
"finalizers": []interface{}{
constants.FinalizerName,
},
},
"data": map[string]interface{}{
"password": "c2VjcmV0",
},
},
}
// Setup: 5 total namespaces, only 2 have allow-mirrors label
allNamespaces := []string{"default", "app-1", "app-2", "app-3", "prod-1", "prod-2"}
allowMirrorsNamespaces := []string{"app-1", "app-2"}
// Mock existing orphaned mirrors in app-3, prod-1, prod-2
createOrphanedMirror := func(ns string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": ns,
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "test-secret",
constants.AnnotationSourceUID: "source-uid-123",
},
},
"data": map[string]interface{}{
"password": "c2VjcmV0",
},
},
}
}
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
mockFilter := filter.NewNamespaceFilter(nil, nil)
nsInfo := &NamespaceInfo{
All: allNamespaces,
AllowMirrors: allowMirrorsNamespaces,
OptOut: []string{},
}
mockLister.On("ListNamespacesWithLabels", mock.Anything).Return(nsInfo, nil)
// Mock Get for source
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "default", Name: "test-secret"}, mock.Anything).
Return(nil, source)
// Helper to create a mock mirror for verification
createMirror := func(ns string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "test-secret",
"namespace": ns,
"labels": map[string]any{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
},
},
}
}
// Mock reconcileMirror calls for app-1 and app-2 (current targets)
notFoundErr := errors.NewNotFound(schema.GroupResource{Group: "", Resource: "secrets"}, "test-secret")
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-1", Name: "test-secret"}, mock.Anything).
Return(notFoundErr, nil).Once()
mockClient.On("Create", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "app-1"
}), mock.Anything).Return(nil).Once()
// Verification Get after Create
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-1", Name: "test-secret"}, mock.Anything).
Return(nil, createMirror("app-1")).Once()
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-2", Name: "test-secret"}, mock.Anything).
Return(notFoundErr, nil).Once()
mockClient.On("Create", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "app-2"
}), mock.Anything).Return(nil).Once()
// Verification Get after Create
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-2", Name: "test-secret"}, mock.Anything).
Return(nil, createMirror("app-2")).Once()
// Mock cleanup: check orphaned namespaces app-3, prod-1, prod-2
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-3", Name: "test-secret"}, mock.Anything).
Return(nil, createOrphanedMirror("app-3")).Once()
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "app-3"
}), mock.Anything).Return(nil).Once()
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-1", Name: "test-secret"}, mock.Anything).
Return(nil, createOrphanedMirror("prod-1")).Once()
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "prod-1"
}), mock.Anything).Return(nil).Once()
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-2", Name: "test-secret"}, mock.Anything).
Return(nil, createOrphanedMirror("prod-2")).Once()
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "prod-2"
}), mock.Anything).Return(nil).Once()
// Mock Update for status annotation
mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := &SourceReconciler{
Client: mockClient,
Scheme: runtime.NewScheme(),
Config: &config.Config{},
Filter: mockFilter,
NamespaceLister: mockLister,
GVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"},
}
ctx := context.Background()
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test-secret"}}
result, err := r.Reconcile(ctx, req)
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
mockClient.AssertExpectations(t)
mockLister.AssertExpectations(t)
}
func TestSourceReconciler_Reconcile_AnnotationChange_PatternChange(t *testing.T) {
// Scenario: annotation changes from "app-*" → "prod-*"
// Before: mirrors in app-1, app-2, app-3
// After: mirrors in prod-1, prod-2
// Expected: app-1, app-2, app-3 deleted; prod-1, prod-2 created
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "app-config",
"namespace": "default",
"uid": "config-uid-456",
"labels": map[string]interface{}{
constants.LabelEnabled: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSync: "true",
constants.AnnotationTargetNamespaces: "prod-*", // Changed from "app-*"
},
"finalizers": []interface{}{
constants.FinalizerName,
},
},
"data": map[string]interface{}{
"key": "value",
},
},
}
allNamespaces := []string{"default", "app-1", "app-2", "app-3", "prod-1", "prod-2"}
createOrphanedMirror := func(ns string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "app-config",
"namespace": ns,
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "app-config",
constants.AnnotationSourceUID: "config-uid-456",
},
},
"data": map[string]interface{}{
"key": "value",
},
},
}
}
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
mockFilter := filter.NewNamespaceFilter(nil, nil)
nsInfo := &NamespaceInfo{
All: allNamespaces,
AllowMirrors: []string{},
OptOut: []string{},
}
mockLister.On("ListNamespacesWithLabels", mock.Anything).Return(nsInfo, nil)
// Mock Get for source
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "default", Name: "app-config"}, mock.Anything).
Return(nil, source)
notFoundErr := errors.NewNotFound(schema.GroupResource{Group: "", Resource: "configmaps"}, "app-config")
// Helper to create a mock mirror for verification
createMirror := func(ns string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]any{
"name": "app-config",
"namespace": ns,
"labels": map[string]any{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
},
},
}
}
// Mock reconcileMirror for prod-1 and prod-2 (new targets)
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-1", Name: "app-config"}, mock.Anything).
Return(notFoundErr, nil).Once()
mockClient.On("Create", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "prod-1"
}), mock.Anything).Return(nil).Once()
// Verification Get after Create
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-1", Name: "app-config"}, mock.Anything).
Return(nil, createMirror("prod-1")).Once()
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-2", Name: "app-config"}, mock.Anything).
Return(notFoundErr, nil).Once()
mockClient.On("Create", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "prod-2"
}), mock.Anything).Return(nil).Once()
// Verification Get after Create
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-2", Name: "app-config"}, mock.Anything).
Return(nil, createMirror("prod-2")).Once()
// Mock cleanup: delete orphaned mirrors in app-1, app-2, app-3
for _, ns := range []string{"app-1", "app-2", "app-3"} {
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: ns, Name: "app-config"}, mock.Anything).
Return(nil, createOrphanedMirror(ns)).Once()
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == ns
}), mock.Anything).Return(nil).Once()
}
// Mock Update for status
mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := &SourceReconciler{
Client: mockClient,
Scheme: runtime.NewScheme(),
Config: &config.Config{},
Filter: mockFilter,
NamespaceLister: mockLister,
GVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"},
}
ctx := context.Background()
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "app-config"}}
result, err := r.Reconcile(ctx, req)
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
mockClient.AssertExpectations(t)
mockLister.AssertExpectations(t)
}
func TestSourceReconciler_deleteAllMirrors_skipsUnmanagedResources(t *testing.T) {
// Regression test: deleteAllMirrors must NOT delete a resource it does not own,
// even if the name and GVK happen to match the source.
sourceObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "shared-name",
"namespace": "default",
"uid": "source-uid",
},
},
}
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
mockLister.On("ListNamespaces", mock.Anything).Return([]string{"default", "ns-other"}, nil)
// In ns-other a resource with the same name/GVK exists but is NOT managed
// by kubemirror — pretend it's a regular Secret created by another operator.
otherSecret := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "shared-name",
"namespace": "ns-other",
},
},
}
mockClient.On("Get", mock.Anything,
types.NamespacedName{Namespace: "ns-other", Name: "shared-name"},
mock.AnythingOfType("*unstructured.Unstructured")).
Return(nil, otherSecret)
r := &SourceReconciler{Client: mockClient, NamespaceLister: mockLister}
err := r.deleteAllMirrors(context.Background(), sourceObj)
require.NoError(t, err)
// The critical assertion: Delete was NEVER called.
mockClient.AssertNotCalled(t, "Delete", mock.Anything, mock.Anything, mock.Anything)
mockLister.AssertExpectations(t)
}
func TestSourceReconciler_deleteAllMirrors_aggregatesErrors(t *testing.T) {
// Regression test: per-namespace deletion failures must be returned (joined),
// otherwise callers will remove the finalizer and orphan the failed mirrors.
sourceObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": "default",
"uid": "source-uid",
},
},
}
managedMirror := func(ns string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": ns,
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "test-secret",
constants.AnnotationSourceUID: "source-uid",
},
},
},
}
}
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
mockLister.On("ListNamespaces", mock.Anything).Return([]string{"default", "ns-ok", "ns-fail"}, nil)
mockClient.On("Get", mock.Anything,
types.NamespacedName{Namespace: "ns-ok", Name: "test-secret"},
mock.AnythingOfType("*unstructured.Unstructured")).
Return(nil, managedMirror("ns-ok"))
mockClient.On("Get", mock.Anything,
types.NamespacedName{Namespace: "ns-fail", Name: "test-secret"},
mock.AnythingOfType("*unstructured.Unstructured")).
Return(nil, managedMirror("ns-fail"))
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
u, ok := obj.(*unstructured.Unstructured)
return ok && u.GetNamespace() == "ns-ok"
}), mock.Anything).Return(nil)
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
u, ok := obj.(*unstructured.Unstructured)
return ok && u.GetNamespace() == "ns-fail"
}), mock.Anything).Return(fmt.Errorf("webhook denied"))
r := &SourceReconciler{Client: mockClient, NamespaceLister: mockLister}
err := r.deleteAllMirrors(context.Background(), sourceObj)
require.Error(t, err, "must surface deletion failure so finalizer is retained")
assert.Contains(t, err.Error(), "ns-fail")
}
func TestIsBlacklistedSecret(t *testing.T) {
cases := []struct {
obj *unstructured.Unstructured
name string
expected bool
}{
{
name: "service-account-token blacklisted",
obj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1", "kind": "Secret",
"type": "kubernetes.io/service-account-token",
}},
expected: true,
},
{
name: "bootstrap token blacklisted",
obj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1", "kind": "Secret",
"type": "bootstrap.kubernetes.io/token",
}},
expected: true,
},
{
name: "helm release blacklisted",
obj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1", "kind": "Secret",
"type": "helm.sh/release.v1",
}},
expected: true,
},
{
name: "opaque secret allowed",
obj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1", "kind": "Secret",
"type": "Opaque",
}},
expected: false,
},
{
name: "secret without type allowed",
obj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1", "kind": "Secret",
}},
expected: false,
},
{
name: "configmap with matching type ignored",
obj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1", "kind": "ConfigMap",
"type": "kubernetes.io/service-account-token",
}},
expected: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, isBlacklistedSecret(tc.obj))
})
}
}
func TestSourceReconciler_Reconcile_RefusesBlacklistedSecret(t *testing.T) {
// Regression test: enabling mirroring on a service-account-token Secret
// must NOT cause it to be mirrored anywhere.
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
tokenSecret := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "sa-token",
"namespace": "default",
"labels": map[string]interface{}{
constants.LabelEnabled: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSync: "true",
constants.AnnotationTargetNamespaces: "all",
},
},
"type": "kubernetes.io/service-account-token",
},
}
mockClient.On("Get", mock.Anything,
types.NamespacedName{Namespace: "default", Name: "sa-token"},
mock.AnythingOfType("*unstructured.Unstructured")).
Return(nil, tokenSecret)
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"},
}
result, err := r.Reconcile(context.Background(),
ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "sa-token"}})
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
// Critical: no namespace listing, no Create, no Update — the Secret was
// rejected before anything mirroring-related happened.
mockLister.AssertNotCalled(t, "ListNamespacesWithLabels", mock.Anything)
mockClient.AssertNotCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
}