Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions pkg/controllers/resources/storageclasses/host_syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -46,13 +54,25 @@ func (s *hostStorageClassSyncer) Resource() client.Object {
return &storagev1.StorageClass{}
}

func (s *hostStorageClassSyncer) HostToVirtual(ctx *synccontext.SyncContext, req types.NamespacedName, pObj client.Object) types.NamespacedName {
if isVClusterManagedHostStorageClass(pObj) {
return types.NamespacedName{}
}

return s.GenericTranslator.HostToVirtual(ctx, req, pObj)
}

var _ syncertypes.Syncer = &hostStorageClassSyncer{}

func (s *hostStorageClassSyncer) Syncer() syncertypes.Sync[client.Object] {
return syncer.ToGenericSyncer(s)
}

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)
Expand All @@ -75,6 +95,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)
Expand Down Expand Up @@ -110,3 +138,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
}
220 changes: 220 additions & 0 deletions pkg/controllers/resources/storageclasses/host_syncer_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +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"
Expand All @@ -12,11 +15,14 @@ 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"
ctrl "sigs.k8s.io/controller-runtime"
)

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{
Expand Down Expand Up @@ -60,8 +66,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()},
Expand Down Expand Up @@ -107,6 +181,152 @@ 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, 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) {
syncContext, object := syncertesting.FakeStartSyncer(t, ctx, NewHostStorageClassSyncer)
return syncContext, object.(*hostStorageClassSyncer)
Expand Down