From aaff9e25ea2650a4429e5d46ae04bb248e6023ca Mon Sep 17 00:00:00 2001 From: Gina Date: Sun, 12 Jul 2026 02:44:00 +0800 Subject: [PATCH 1/2] fix(storageclass): ignore vcluster-managed host mirrors --- .../resources/storageclasses/host_syncer.go | 29 ++++++ .../storageclasses/host_syncer_test.go | 96 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/pkg/controllers/resources/storageclasses/host_syncer.go b/pkg/controllers/resources/storageclasses/host_syncer.go index b8123849f4..e9783f8c85 100644 --- a/pkg/controllers/resources/storageclasses/host_syncer.go +++ b/pkg/controllers/resources/storageclasses/host_syncer.go @@ -46,6 +46,14 @@ func (s *hostStorageClassSyncer) Resource() client.Object { return &storagev1.StorageClass{} } +func (s *hostStorageClassSyncer) IsManaged(ctx *synccontext.SyncContext, pObj client.Object) (bool, error) { + if isVClusterManagedHostStorageClass(pObj) { + return false, nil + } + + return s.GenericTranslator.IsManaged(ctx, pObj) +} + var _ syncertypes.Syncer = &hostStorageClassSyncer{} func (s *hostStorageClassSyncer) Syncer() syncertypes.Sync[client.Object] { @@ -53,6 +61,10 @@ func (s *hostStorageClassSyncer) Syncer() syncertypes.Sync[client.Object] { } func (s *hostStorageClassSyncer) SyncToVirtual(ctx *synccontext.SyncContext, event *synccontext.SyncToVirtualEvent[*storagev1.StorageClass]) (ctrl.Result, error) { + if isVClusterManagedHostStorageClass(event.Host) { + return ctrl.Result{}, nil + } + matches, err := ctx.Config.Sync.FromHost.StorageClasses.Selector.Matches(event.Host) if err != nil { return ctrl.Result{}, fmt.Errorf("check storage class selector: %w", err) @@ -75,6 +87,14 @@ func (s *hostStorageClassSyncer) SyncToVirtual(ctx *synccontext.SyncContext, eve } func (s *hostStorageClassSyncer) Sync(ctx *synccontext.SyncContext, event *synccontext.SyncEvent[*storagev1.StorageClass]) (_ ctrl.Result, retErr error) { + if isVClusterManagedHostStorageClass(event.Host) { + if event.Virtual.Name == event.Host.Name { + return patcher.DeleteVirtualObject(ctx, event.Virtual, event.Host, fmt.Sprintf("storage class %q is managed by a vCluster and must not be mirrored from the host", event.Host.Name)) + } + + return ctrl.Result{}, nil + } + matches, err := ctx.Config.Sync.FromHost.StorageClasses.Selector.Matches(event.Host) if err != nil { return ctrl.Result{}, fmt.Errorf("check storage class selector: %w", err) @@ -110,3 +130,12 @@ func (s *hostStorageClassSyncer) SyncToHost(ctx *synccontext.SyncContext, event ctx.Log.Infof("delete virtual storage class %s, because physical object is missing", event.Virtual) return ctrl.Result{}, ctx.VirtualClient.Delete(ctx, event.Virtual) } + +func isVClusterManagedHostStorageClass(obj client.Object) bool { + if obj == nil { + return false + } + + _, managed := obj.GetLabels()[translate.MarkerLabel] + return managed +} diff --git a/pkg/controllers/resources/storageclasses/host_syncer_test.go b/pkg/controllers/resources/storageclasses/host_syncer_test.go index 418e4368bf..6328a48dcc 100644 --- a/pkg/controllers/resources/storageclasses/host_syncer_test.go +++ b/pkg/controllers/resources/storageclasses/host_syncer_test.go @@ -3,6 +3,7 @@ package storageclasses import ( "testing" + "github.com/loft-sh/vcluster/pkg/scheme" "github.com/loft-sh/vcluster/pkg/syncer/synccontext" syncertesting "github.com/loft-sh/vcluster/pkg/syncer/testing" testingutil "github.com/loft-sh/vcluster/pkg/util/testing" @@ -12,11 +13,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" ) func TestFromHostSync(t *testing.T) { translate.Default = translate.NewSingleNamespaceTranslator(testingutil.DefaultTestTargetNamespace) const storageClassName = "test-storageclass" + const staleStorageClassName = "test-storageclass-x-other-vcluster" pObject := &storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{ @@ -60,8 +63,76 @@ func TestFromHostSync(t *testing.T) { vObjectUpdated.Parameters = map[string]string{ "test": "value", } + managedHostObject := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: staleStorageClassName, + UID: types.UID("managed-host-storageclass-uid"), + ResourceVersion: syncertesting.FakeClientResourceVersion, + Labels: map[string]string{ + translate.MarkerLabel: "other-vcluster", + }, + Annotations: map[string]string{ + translate.NameAnnotation: storageClassName, + }, + }, + Provisioner: "stale-provisioner", + } + staleVirtualObject := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: staleStorageClassName, + ResourceVersion: syncertesting.FakeClientResourceVersion, + }, + Provisioner: "stale-provisioner", + } syncertesting.RunTests(t, []*syncertesting.SyncTest{ + { + Name: "Ignore managed host resource before creating a virtual resource", + InitialPhysicalState: []runtime.Object{managedHostObject.DeepCopy()}, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {managedHostObject}, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncerCtx, syncer := newFakeSyncer(t, ctx) + _, err := syncer.SyncToVirtual(syncerCtx, synccontext.NewSyncToVirtualEvent(managedHostObject.DeepCopy())) + assert.NilError(t, err) + }, + }, + { + Name: "Delete stale same-name virtual mirror without deleting managed host resource", + InitialPhysicalState: []runtime.Object{managedHostObject.DeepCopy()}, + InitialVirtualState: []runtime.Object{staleVirtualObject.DeepCopy()}, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {managedHostObject}, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncerCtx, syncer := newFakeSyncer(t, ctx) + _, err := syncer.Sync(syncerCtx, synccontext.NewSyncEvent(managedHostObject.DeepCopy(), staleVirtualObject.DeepCopy())) + assert.NilError(t, err) + }, + }, + { + Name: "Do not delete or overwrite annotation-mapped virtual collision", + InitialPhysicalState: []runtime.Object{managedHostObject.DeepCopy()}, + InitialVirtualState: []runtime.Object{vObject.DeepCopy()}, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {managedHostObject}, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {vObject}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncerCtx, syncer := newFakeSyncer(t, ctx) + _, err := syncer.Sync(syncerCtx, synccontext.NewSyncEvent(managedHostObject.DeepCopy(), vObject.DeepCopy())) + assert.NilError(t, err) + }, + }, { Name: "Sync new host resource to virtual", InitialPhysicalState: []runtime.Object{pObject.DeepCopy()}, @@ -107,6 +178,31 @@ func TestFromHostSync(t *testing.T) { }) } +func TestHostStorageClassAdmission(t *testing.T) { + translate.Default = translate.NewSingleNamespaceTranslator(testingutil.DefaultTestTargetNamespace) + ctx := syncertesting.NewFakeRegisterContext(testingutil.NewFakeConfig(), testingutil.NewFakeClient(scheme.Scheme), testingutil.NewFakeClient(scheme.Scheme)) + syncerCtx, syncer := newFakeSyncer(t, ctx) + + baseline := &storagev1.StorageClass{ObjectMeta: metav1.ObjectMeta{Name: "baseline"}} + managed := &storagev1.StorageClass{ObjectMeta: metav1.ObjectMeta{ + Name: "baseline-x-other-vcluster", + Labels: map[string]string{ + translate.MarkerLabel: "other-vcluster", + }, + Annotations: map[string]string{ + translate.NameAnnotation: "baseline", + }, + }} + + got, err := syncer.IsManaged(syncerCtx, baseline) + assert.NilError(t, err) + assert.Equal(t, got, true) + + got, err = syncer.IsManaged(syncerCtx, managed) + assert.NilError(t, err) + assert.Equal(t, got, false) +} + func newFakeSyncer(t *testing.T, ctx *synccontext.RegisterContext) (*synccontext.SyncContext, *hostStorageClassSyncer) { syncContext, object := syncertesting.FakeStartSyncer(t, ctx, NewHostStorageClassSyncer) return syncContext, object.(*hostStorageClassSyncer) From e829529cb1d62f82c737b64b27488333b222c99e Mon Sep 17 00:00:00 2001 From: Gina Date: Sun, 12 Jul 2026 02:56:16 +0800 Subject: [PATCH 2/2] fix(storageclass): own managed mirror UID cleanup --- .../resources/storageclasses/host_syncer.go | 14 +- .../storageclasses/host_syncer_test.go | 126 +++++++++++++++++- 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/pkg/controllers/resources/storageclasses/host_syncer.go b/pkg/controllers/resources/storageclasses/host_syncer.go index e9783f8c85..696cccd8b7 100644 --- a/pkg/controllers/resources/storageclasses/host_syncer.go +++ b/pkg/controllers/resources/storageclasses/host_syncer.go @@ -34,6 +34,14 @@ type hostStorageClassSyncer struct { syncertypes.GenericTranslator } +var _ syncertypes.OptionsProvider = &hostStorageClassSyncer{} + +func (s *hostStorageClassSyncer) Options() *syncertypes.Options { + return &syncertypes.Options{ + DisableUIDDeletion: true, + } +} + func (s *hostStorageClassSyncer) UseUncachedPhysicalClient() bool { return false } @@ -46,12 +54,12 @@ func (s *hostStorageClassSyncer) Resource() client.Object { return &storagev1.StorageClass{} } -func (s *hostStorageClassSyncer) IsManaged(ctx *synccontext.SyncContext, pObj client.Object) (bool, error) { +func (s *hostStorageClassSyncer) HostToVirtual(ctx *synccontext.SyncContext, req types.NamespacedName, pObj client.Object) types.NamespacedName { if isVClusterManagedHostStorageClass(pObj) { - return false, nil + return types.NamespacedName{} } - return s.GenericTranslator.IsManaged(ctx, pObj) + return s.GenericTranslator.HostToVirtual(ctx, req, pObj) } var _ syncertypes.Syncer = &hostStorageClassSyncer{} diff --git a/pkg/controllers/resources/storageclasses/host_syncer_test.go b/pkg/controllers/resources/storageclasses/host_syncer_test.go index 6328a48dcc..0e229fbc2e 100644 --- a/pkg/controllers/resources/storageclasses/host_syncer_test.go +++ b/pkg/controllers/resources/storageclasses/host_syncer_test.go @@ -1,9 +1,11 @@ package storageclasses import ( + "context" "testing" "github.com/loft-sh/vcluster/pkg/scheme" + syncercontroller "github.com/loft-sh/vcluster/pkg/syncer" "github.com/loft-sh/vcluster/pkg/syncer/synccontext" syncertesting "github.com/loft-sh/vcluster/pkg/syncer/testing" testingutil "github.com/loft-sh/vcluster/pkg/util/testing" @@ -14,6 +16,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" ) func TestFromHostSync(t *testing.T) { @@ -200,7 +203,128 @@ func TestHostStorageClassAdmission(t *testing.T) { got, err = syncer.IsManaged(syncerCtx, managed) assert.NilError(t, err) - assert.Equal(t, got, false) + assert.Equal(t, got, true) + + baselineName := syncer.HostToVirtual(syncerCtx, types.NamespacedName{Name: baseline.Name}, baseline) + assert.Equal(t, baselineName.Name, baseline.Name) + + managedName := syncer.HostToVirtual(syncerCtx, types.NamespacedName{Name: managed.Name}, managed) + assert.Equal(t, managedName.Name, "") +} + +func TestHostStorageClassReconcile(t *testing.T) { + translate.Default = translate.NewSingleNamespaceTranslator(testingutil.DefaultTestTargetNamespace) + const baselineName = "baseline" + const managedName = "baseline-x-other-vcluster" + + managedHost := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedName, + UID: types.UID("managed-host-uid"), + ResourceVersion: syncertesting.FakeClientResourceVersion, + Labels: map[string]string{ + translate.MarkerLabel: "other-vcluster", + }, + Annotations: map[string]string{ + translate.NameAnnotation: baselineName, + translate.UIDAnnotation: "old-guest-uid", + }, + }, + Provisioner: "stale-provisioner", + } + staleVirtual := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedName, + UID: types.UID("current-guest-uid"), + ResourceVersion: syncertesting.FakeClientResourceVersion, + }, + Provisioner: "stale-provisioner", + } + baselineHost := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: baselineName, + UID: types.UID("baseline-host-uid"), + ResourceVersion: syncertesting.FakeClientResourceVersion, + }, + Provisioner: "baseline-provisioner", + Parameters: map[string]string{"type": "baseline"}, + } + baselineVirtual := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: baselineName, + UID: types.UID("baseline-guest-uid"), + ResourceVersion: syncertesting.FakeClientResourceVersion, + }, + Provisioner: "old-provisioner", + } + baselineVirtualUpdated := baselineHost.DeepCopy() + baselineVirtualUpdated.UID = baselineVirtual.UID + + syncertesting.RunTests(t, []*syncertesting.SyncTest{ + { + Name: "Managed UID mismatch deletes stale virtual object instead of host object", + InitialPhysicalState: []runtime.Object{managedHost.DeepCopy()}, + InitialVirtualState: []runtime.Object{staleVirtual.DeepCopy()}, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {managedHost}, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + reconcileHostStorageClass(t, ctx, managedName) + }, + }, + { + Name: "Baseline host resource is created in virtual cluster", + InitialPhysicalState: []runtime.Object{baselineHost.DeepCopy()}, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {baselineHost}, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {baselineHost}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + reconcileHostStorageClass(t, ctx, baselineName) + }, + }, + { + Name: "Baseline host resource updates virtual cluster without deleting host", + InitialPhysicalState: []runtime.Object{baselineHost.DeepCopy()}, + InitialVirtualState: []runtime.Object{baselineVirtual.DeepCopy()}, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {baselineHost}, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {baselineVirtualUpdated}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + reconcileHostStorageClass(t, ctx, baselineName) + }, + }, + { + Name: "Missing baseline host resource deletes virtual mirror", + InitialVirtualState: []runtime.Object{baselineVirtual.DeepCopy()}, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {}, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + storagev1.SchemeGroupVersion.WithKind("StorageClass"): {}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + reconcileHostStorageClass(t, ctx, baselineName) + }, + }, + }) +} + +func reconcileHostStorageClass(t *testing.T, ctx *synccontext.RegisterContext, name string) { + _, object := newFakeSyncer(t, ctx) + controller, err := syncercontroller.NewSyncController(ctx, object) + assert.NilError(t, err) + + _, err = controller.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: name}}) + assert.NilError(t, err) } func newFakeSyncer(t *testing.T, ctx *synccontext.RegisterContext) (*synccontext.SyncContext, *hostStorageClassSyncer) {