diff --git a/chart/templates/_rbac.tpl b/chart/templates/_rbac.tpl index 91ab3c4816..1b578f7060 100644 --- a/chart/templates/_rbac.tpl +++ b/chart/templates/_rbac.tpl @@ -27,6 +27,7 @@ .Values.sync.toHost.volumeSnapshots.enabled .Values.controlPlane.advanced.virtualScheduler.enabled .Values.sync.fromHost.ingressClasses.enabled + .Values.sync.fromHost.runtimeClasses.enabled (eq (toString .Values.sync.fromHost.storageClasses.enabled) "true") (eq (toString .Values.sync.fromHost.csiNodes.enabled) "true") (eq (toString .Values.sync.fromHost.csiDrivers.enabled) "true") diff --git a/chart/templates/clusterrole.yaml b/chart/templates/clusterrole.yaml index 05ab2c6c00..6fd817851b 100644 --- a/chart/templates/clusterrole.yaml +++ b/chart/templates/clusterrole.yaml @@ -74,6 +74,11 @@ rules: resources: ["ingressclasses"] verbs: ["get", "watch", "list"] {{- end }} + {{- if .Values.sync.fromHost.runtimeClasses.enabled }} + - apiGroups: ["nodes.k8s.io"] + resources: ["runtimeclasses"] + verbs: ["get", "watch", "list"] + {{- end }} {{- if .Values.sync.toHost.storageClasses.enabled }} - apiGroups: ["storage.k8s.io"] resources: ["storageclasses"] diff --git a/chart/values.schema.json b/chart/values.schema.json index e89ebb4e8d..ec92aeff19 100755 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -2701,6 +2701,10 @@ "$ref": "#/$defs/EnableSwitch", "description": "IngressClasses defines if ingress classes should get synced from the host cluster to the virtual cluster, but not back." }, + "runtimeClasses": { + "$ref": "#/$defs/EnableSwitch", + "description": "RuntimeClasses defines if runtime classes should get synced from the host cluster to the virtual cluster, but not back." + }, "storageClasses": { "$ref": "#/$defs/EnableAutoSwitch", "description": "StorageClasses defines if storage classes should get synced from the host cluster to the virtual cluster, but not back. If auto, is automatically enabled when the virtual scheduler is enabled." diff --git a/chart/values.yaml b/chart/values.yaml index edb959b6b0..d6b7abee5d 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -102,6 +102,9 @@ sync: # IngressClasses defines if ingress classes should get synced from the host cluster to the virtual cluster, but not back. ingressClasses: enabled: false + # RuntimeClasses defines if runtime classes should get synced from the host cluster to the virtual cluster, but not back. + runtimeClasses: + enabled: false # Nodes defines if nodes should get synced from the host cluster to the virtual cluster, but not back. nodes: # Enabled specifies if syncing real nodes should be enabled. If this is disabled, vCluster will create fake nodes instead. diff --git a/config/config.go b/config/config.go index 0c3f37ba64..31ed0661c7 100644 --- a/config/config.go +++ b/config/config.go @@ -405,6 +405,9 @@ type SyncFromHost struct { // IngressClasses defines if ingress classes should get synced from the host cluster to the virtual cluster, but not back. IngressClasses EnableSwitch `json:"ingressClasses,omitempty"` + // RuntimeClasses defines if runtime classes should get synced from the host cluster to the virtual cluster, but not back. + RuntimeClasses EnableSwitch `json:"runtimeClasses,omitempty"` + // StorageClasses defines if storage classes should get synced from the host cluster to the virtual cluster, but not back. If auto, is automatically enabled when the virtual scheduler is enabled. StorageClasses EnableAutoSwitch `json:"storageClasses,omitempty"` diff --git a/config/values.yaml b/config/values.yaml index 66847a9b75..1a3895e038 100644 --- a/config/values.yaml +++ b/config/values.yaml @@ -61,6 +61,8 @@ sync: enabled: auto ingressClasses: enabled: false + runtimeClasses: + enabled: false nodes: enabled: false syncBackChanges: false diff --git a/pkg/controllers/resources/register.go b/pkg/controllers/resources/register.go index 5d03a9419e..4dd9f7f2c0 100644 --- a/pkg/controllers/resources/register.go +++ b/pkg/controllers/resources/register.go @@ -19,6 +19,7 @@ import ( "github.com/loft-sh/vcluster/pkg/controllers/resources/poddisruptionbudgets" "github.com/loft-sh/vcluster/pkg/controllers/resources/pods" "github.com/loft-sh/vcluster/pkg/controllers/resources/priorityclasses" + "github.com/loft-sh/vcluster/pkg/controllers/resources/runtimeclasses" "github.com/loft-sh/vcluster/pkg/controllers/resources/secrets" "github.com/loft-sh/vcluster/pkg/controllers/resources/serviceaccounts" "github.com/loft-sh/vcluster/pkg/controllers/resources/services" @@ -50,6 +51,7 @@ func getSyncers(ctx *synccontext.RegisterContext) []BuildController { isEnabled(ctx.Config.Sync.ToHost.PersistentVolumeClaims.Enabled, persistentvolumeclaims.New), isEnabled(ctx.Config.Sync.ToHost.Ingresses.Enabled, ingresses.New), isEnabled(ctx.Config.Sync.FromHost.IngressClasses.Enabled, ingressclasses.New), + isEnabled(ctx.Config.Sync.FromHost.RuntimeClasses.Enabled, runtimeclasses.New), isEnabled(ctx.Config.Sync.ToHost.StorageClasses.Enabled, storageclasses.New), isEnabled(ctx.Config.Sync.FromHost.StorageClasses.Enabled == "true", storageclasses.NewHostStorageClassSyncer), isEnabled(ctx.Config.Sync.ToHost.PriorityClasses.Enabled, priorityclasses.New), diff --git a/pkg/controllers/resources/runtimeclasses/syncer.go b/pkg/controllers/resources/runtimeclasses/syncer.go new file mode 100644 index 0000000000..516005e40c --- /dev/null +++ b/pkg/controllers/resources/runtimeclasses/syncer.go @@ -0,0 +1,76 @@ +package runtimeclasses + +import ( + "fmt" + + "github.com/loft-sh/vcluster/pkg/mappings/generic" + "github.com/loft-sh/vcluster/pkg/patcher" + "github.com/loft-sh/vcluster/pkg/syncer" + "github.com/loft-sh/vcluster/pkg/syncer/synccontext" + syncertypes "github.com/loft-sh/vcluster/pkg/syncer/types" + "github.com/loft-sh/vcluster/pkg/util/translate" + nodev1 "k8s.io/api/node/v1" + "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func New(_ *synccontext.RegisterContext) (syncertypes.Object, error) { + mapper, err := generic.NewMirrorMapper(&nodev1.RuntimeClass{}) + if err != nil { + return nil, err + } + + return &runtimeClassSyncer{ + Mapper: mapper, + }, nil +} + +type runtimeClassSyncer struct { + synccontext.Mapper +} + +func (i *runtimeClassSyncer) Name() string { + return "runtimeclass" +} + +func (i *runtimeClassSyncer) Resource() client.Object { + return &nodev1.RuntimeClass{} +} + +var _ syncertypes.Syncer = &runtimeClassSyncer{} + +func (i *runtimeClassSyncer) Syncer() syncertypes.Sync[client.Object] { + return syncer.ToGenericSyncer[*nodev1.RuntimeClass](i) +} + +func (i *runtimeClassSyncer) SyncToVirtual(ctx *synccontext.SyncContext, event *synccontext.SyncToVirtualEvent[*nodev1.RuntimeClass]) (ctrl.Result, error) { + vObj := translate.CopyObjectWithName(event.Host, types.NamespacedName{Name: event.Host.Name, Namespace: event.Host.Namespace}, false) + ctx.Log.Infof("create runtime class %s, because it does not exist in virtual cluster", vObj.Name) + return ctrl.Result{}, ctx.VirtualClient.Create(ctx, vObj) +} + +func (i *runtimeClassSyncer) Sync(ctx *synccontext.SyncContext, event *synccontext.SyncEvent[*nodev1.RuntimeClass]) (_ ctrl.Result, retErr error) { + patch, err := patcher.NewSyncerPatcher(ctx, event.Host, event.Virtual) + if err != nil { + return ctrl.Result{}, fmt.Errorf("new syncer patcher: %w", err) + } + defer func() { + if err := patch.Patch(ctx, event.Host, event.Virtual); err != nil { + retErr = utilerrors.NewAggregate([]error{retErr, err}) + } + }() + + event.Virtual.Annotations = event.Host.Annotations + event.Virtual.Labels = event.Host.Labels + event.Virtual.Handler = event.Host.Handler + event.Virtual.Overhead = event.Host.Overhead + event.Virtual.Scheduling = event.Host.Scheduling + return ctrl.Result{}, nil +} + +func (i *runtimeClassSyncer) SyncToHost(ctx *synccontext.SyncContext, event *synccontext.SyncToHostEvent[*nodev1.RuntimeClass]) (ctrl.Result, error) { + ctx.Log.Infof("delete virtual runtime class %s, because physical object is missing", event.Virtual.Name) + return ctrl.Result{}, ctx.VirtualClient.Delete(ctx, event.Virtual) +} diff --git a/pkg/controllers/resources/runtimeclasses/syncer_test.go b/pkg/controllers/resources/runtimeclasses/syncer_test.go new file mode 100644 index 0000000000..d385d961fd --- /dev/null +++ b/pkg/controllers/resources/runtimeclasses/syncer_test.go @@ -0,0 +1,137 @@ +package runtimeclasses + +import ( + "testing" + + "github.com/loft-sh/vcluster/pkg/syncer/synccontext" + syncertesting "github.com/loft-sh/vcluster/pkg/syncer/testing" + "github.com/loft-sh/vcluster/pkg/util/translate" + "gotest.tools/assert" + corev1 "k8s.io/api/core/v1" + nodev1 "k8s.io/api/node/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestSync(t *testing.T) { + vObjectMeta := metav1.ObjectMeta{ + Name: "test-ingc", + Annotations: map[string]string{ + translate.NameAnnotation: "test-runtimec", + translate.UIDAnnotation: "", + translate.KindAnnotation: nodev1.SchemeGroupVersion.WithKind("RuntimeClass").String(), + }, + } + + vObj := &nodev1.RuntimeClass{ + ObjectMeta: vObjectMeta, + Scheduling: &nodev1.Scheduling{ + NodeSelector: map[string]string{"stuff": "stuff"}, + }, + Handler: "somehandler", + Overhead: &nodev1.Overhead{ + PodFixed: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")}, + }, + } + + pObj := &nodev1.RuntimeClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: vObjectMeta.Name, + Labels: map[string]string{ + translate.MarkerLabel: translate.VClusterName, + }, + Annotations: map[string]string{ + translate.NameAnnotation: "test-runtimec", + translate.UIDAnnotation: "", + translate.KindAnnotation: nodev1.SchemeGroupVersion.WithKind("RuntimeClass").String(), + }, + }, + Scheduling: &nodev1.Scheduling{ + NodeSelector: map[string]string{"stuff": "stuff"}, + }, + Handler: "somehandler", + Overhead: &nodev1.Overhead{ + PodFixed: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")}, + }, + } + + vObjUpdated := &nodev1.RuntimeClass{ + ObjectMeta: vObjectMeta, + Scheduling: &nodev1.Scheduling{ + NodeSelector: map[string]string{"stuff": "stuff2"}, + }, + Handler: "somehandler", + Overhead: &nodev1.Overhead{ + PodFixed: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")}, + }, + } + + pObjUpdated := &nodev1.RuntimeClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: translate.Default.HostNameCluster(vObjectMeta.Name), + Labels: map[string]string{ + translate.MarkerLabel: translate.VClusterName, + }, + Annotations: map[string]string{ + translate.NameAnnotation: "test-runtimec", + translate.UIDAnnotation: "", + translate.KindAnnotation: nodev1.SchemeGroupVersion.WithKind("RuntimeClass").String(), + }, + }, + Scheduling: &nodev1.Scheduling{ + NodeSelector: map[string]string{"stuff": "stuff2"}, + }, + Handler: "somehandler", + Overhead: &nodev1.Overhead{ + PodFixed: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")}, + }, + } + + syncertesting.RunTests(t, []*syncertesting.SyncTest{ + { + Name: "Import", + InitialVirtualState: []runtime.Object{}, + InitialPhysicalState: []runtime.Object{pObj}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + nodev1.SchemeGroupVersion.WithKind("RuntimeClass"): {vObj}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + nodev1.SchemeGroupVersion.WithKind("RuntimeClass"): {pObj}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + _, err := syncer.(*runtimeClassSyncer).SyncToVirtual(syncCtx, synccontext.NewSyncToVirtualEvent(pObj)) + assert.NilError(t, err) + }, + }, + { + Name: "Delete virtual", + InitialVirtualState: []runtime.Object{vObj}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{}, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{}, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + _, err := syncer.(*runtimeClassSyncer).SyncToHost(syncCtx, synccontext.NewSyncToHostEvent(vObj)) + assert.NilError(t, err) + }, + }, + { + Name: "Sync", + InitialVirtualState: []runtime.Object{vObj}, + InitialPhysicalState: []runtime.Object{pObjUpdated}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + nodev1.SchemeGroupVersion.WithKind("RuntimeClass"): {vObjUpdated}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + nodev1.SchemeGroupVersion.WithKind("RuntimeClass"): {pObjUpdated}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + _, err := syncer.(*runtimeClassSyncer).Sync(syncCtx, synccontext.NewSyncEvent(pObjUpdated, vObj)) + assert.NilError(t, err) + }, + }, + }) +}