Skip to content
Draft
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
4 changes: 4 additions & 0 deletions chart/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3705,6 +3705,10 @@
"$ref": "#/$defs/EnableSwitchWithPatches",
"description": "Endpoints defines if endpoints created within the virtual cluster should get synced to the host cluster."
},
"endpointSlices": {
"$ref": "#/$defs/EnableSwitchWithPatches",
"description": "EndpointSlices defines if endpointslices created within the virtual cluster should get synced to the host cluster."
},
"networkPolicies": {
"$ref": "#/$defs/EnableSwitchWithPatches",
"description": "NetworkPolicies defines if network policies created within the virtual cluster should get synced to the host cluster."
Expand Down
4 changes: 4 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ sync:
endpoints:
# Enabled defines if this option should be enabled.
enabled: true
# EndpointSlices defines if endpointslices created within the virtual cluster should get synced to the host cluster.
endpointSlices:
# Enabled defines if this option should be enabled.
enabled: true
# PersistentVolumeClaims defines if persistent volume claims created within the virtual cluster should get synced to the host cluster.
persistentVolumeClaims:
# Enabled defines if this option should be enabled.
Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,9 @@ type SyncToHost struct {
// Endpoints defines if endpoints created within the virtual cluster should get synced to the host cluster.
Endpoints EnableSwitchWithPatches `json:"endpoints,omitempty"`

// EndpointSlices defines if endpointslices created within the virtual cluster should get synced to the host cluster.
EndpointSlices EnableSwitchWithPatches `json:"endpointSlices,omitempty"`

// NetworkPolicies defines if network policies created within the virtual cluster should get synced to the host cluster.
NetworkPolicies EnableSwitchWithPatches `json:"networkPolicies,omitempty"`

Expand Down
2 changes: 2 additions & 0 deletions config/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ sync:
enabled: true
endpoints:
enabled: true
endpointSlices:
enabled: true
persistentVolumeClaims:
enabled: true
configMaps:
Expand Down
174 changes: 174 additions & 0 deletions pkg/controllers/resources/endpointSlices/syncer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package endpoints

import (
"errors"
"fmt"

"github.com/loft-sh/vcluster/pkg/mappings"
"github.com/loft-sh/vcluster/pkg/patcher"
"github.com/loft-sh/vcluster/pkg/pro"
"github.com/loft-sh/vcluster/pkg/specialservices"
"github.com/loft-sh/vcluster/pkg/syncer"
"github.com/loft-sh/vcluster/pkg/syncer/synccontext"
"github.com/loft-sh/vcluster/pkg/syncer/translator"
syncertypes "github.com/loft-sh/vcluster/pkg/syncer/types"
"github.com/loft-sh/vcluster/pkg/util/translate"
corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func New(ctx *synccontext.RegisterContext) (syncertypes.Object, error) {
mapper, err := ctx.Mappings.ByGVK(mappings.Endpoints())
if err != nil {
return nil, err
}

return &endpointSliceSyncer{
GenericTranslator: translator.NewGenericTranslator(ctx, "endpointslice", &discoveryv1.EndpointSlice{}, mapper),

excludedAnnotations: []string{
"control-plane.alpha.kubernetes.io/leader",
},
}, nil
}

type endpointSliceSyncer struct {
syncertypes.GenericTranslator

excludedAnnotations []string
}

var _ syncertypes.OptionsProvider = &endpointSliceSyncer{}

func (s *endpointSliceSyncer) Options() *syncertypes.Options {
return &syncertypes.Options{
ObjectCaching: true,
}
}

var _ syncertypes.Syncer = &endpointSliceSyncer{}

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

func (s *endpointSliceSyncer) SyncToHost(ctx *synccontext.SyncContext, event *synccontext.SyncToHostEvent[*discoveryv1.EndpointSlice]) (ctrl.Result, error) {
if event.HostOld != nil {
return patcher.DeleteVirtualObject(ctx, event.Virtual, event.HostOld, "host object was deleted")
}

pObj := s.translate(ctx, event.Virtual)
err := pro.ApplyPatchesHostObject(ctx, nil, pObj, event.Virtual, ctx.Config.Sync.ToHost.EndpointSlices.Patches, false)
if err != nil {
return ctrl.Result{}, err
}

return patcher.CreateHostObject(ctx, event.Virtual, pObj, s.EventRecorder(), false)
}

func (s *endpointSliceSyncer) Sync(ctx *synccontext.SyncContext, event *synccontext.SyncEvent[*discoveryv1.EndpointSlice]) (_ ctrl.Result, retErr error) {
patch, err := patcher.NewSyncerPatcher(ctx, event.Host, event.Virtual, patcher.TranslatePatches(ctx.Config.Sync.ToHost.EndpointSlices.Patches, false))
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 = errors.Join(retErr, err)
}

if retErr != nil {
s.EventRecorder().Eventf(event.Virtual, "Warning", "SyncError", "Error syncing: %v", retErr)
}
}()

err = s.translateUpdate(ctx, event.Host, event.Virtual)
if err != nil {
return ctrl.Result{}, err
}

// bi-directional sync of annotations and labels
event.Virtual.Annotations, event.Host.Annotations = translate.AnnotationsBidirectionalUpdate(event, s.excludedAnnotations...)
event.Virtual.Labels, event.Host.Labels = translate.LabelsBidirectionalUpdate(event)

return ctrl.Result{}, nil
}

func (s *endpointSliceSyncer) SyncToVirtual(ctx *synccontext.SyncContext, event *synccontext.SyncToVirtualEvent[*discoveryv1.EndpointSlice]) (_ ctrl.Result, retErr error) {
// virtual object is not here anymore, so we delete
return patcher.DeleteHostObject(ctx, event.Host, event.VirtualOld, "virtual object was deleted")
}

var _ syncertypes.Starter = &endpointSliceSyncer{}

func (s *endpointSliceSyncer) ReconcileStart(ctx *synccontext.SyncContext, req ctrl.Request) (bool, error) {
if req.NamespacedName == specialservices.DefaultKubernetesSvcKey {
return true, nil
}
if specialservices.Default != nil {
if _, ok := specialservices.Default.SpecialServicesToSync()[req.NamespacedName]; ok {
return true, nil
}
}

svc := &corev1.Service{}
err := ctx.VirtualClient.Get(ctx, types.NamespacedName{
Namespace: req.Namespace,
Name: req.Name,
}, svc)
if err != nil {
if kerrors.IsNotFound(err) {
return true, nil
}

return true, err
} else if svc.Spec.Selector != nil {
// check if it was a managed endpointSlice object before and delete it
endpointSlice := &discoveryv1.EndpointSlice{}
err = ctx.PhysicalClient.Get(ctx, s.VirtualToHost(ctx, req.NamespacedName, nil), endpointSlice)
if err != nil {
if !kerrors.IsNotFound(err) {
klog.Infof("Error retrieving endpointSliceList: %v", err)
}

return true, nil
}

// check if endpoints were created by us
if endpointSlice.Annotations != nil && endpointSlice.Annotations[translate.NameAnnotation] != "" {
// Deleting the endpointSlice is necessary here as some clusters would not correctly maintain
// the endpointSlices if they were managed by us previously and now should be managed by Kubernetes.
// In the worst case we would end up in a state where we have multiple endpoint slices pointing
// to the same endpoints resulting in wrong DNS and cluster networking. Hence, deleting the previously
// managed endpointSlices signals the Kubernetes controller to recreate the endpointSlices from the selector.
klog.Infof("Refresh endpointSlice in physical cluster because they shouldn't be managed by vcluster anymore")
err = ctx.PhysicalClient.Delete(ctx, endpointSlice)
if err != nil {
klog.Infof("Error deleting endpoints %s/%s: %v", endpointSlice.Namespace, endpointSlice.Name, err)
return true, err
}
}

return true, nil
}

// check if it was a Kubernetes managed endpointSlice object before and delete it
endpointSlice := &discoveryv1.EndpointSlice{}
err = ctx.PhysicalClient.Get(ctx, s.VirtualToHost(ctx, req.NamespacedName, nil), endpointSlice)
if err == nil && (endpointSlice.Annotations == nil || endpointSlice.Annotations[translate.NameAnnotation] == "") {
klog.Infof("Refresh endpointSlice in physical cluster because they should be managed by vCluster now")
err = ctx.PhysicalClient.Delete(ctx, endpointSlice)
if err != nil {
klog.Infof("Error deleting endpointSlice %s/%s: %v", endpointSlice.Namespace, endpointSlice.Name, err)
return true, err
}
}

return false, nil
}

func (s *endpointSliceSyncer) ReconcileEnd() {}
35 changes: 35 additions & 0 deletions pkg/controllers/resources/endpointSlices/translate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package endpoints

import (
"github.com/loft-sh/vcluster/pkg/mappings"
"github.com/loft-sh/vcluster/pkg/syncer/synccontext"
"github.com/loft-sh/vcluster/pkg/util/translate"
discoveryv1 "k8s.io/api/discovery/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func (s *endpointSliceSyncer) translate(ctx *synccontext.SyncContext, vObj client.Object) *discoveryv1.EndpointSlice {
endpointSlice := translate.HostMetadata(vObj.(*discoveryv1.EndpointSlice), s.VirtualToHost(ctx, types.NamespacedName{Name: vObj.GetName(), Namespace: vObj.GetNamespace()}, vObj), s.excludedAnnotations...)
s.translateSpec(ctx, endpointSlice)
return endpointSlice
}

func (s *endpointSliceSyncer) translateSpec(ctx *synccontext.SyncContext, endpointSlice *discoveryv1.EndpointSlice) {
// translate the endpoints
for i, ep := range endpointSlice.Endpoints {
if ep.TargetRef != nil && ep.TargetRef.Kind == "Pod" {
nameAndNamespace := mappings.VirtualToHost(ctx, ep.TargetRef.Name, ep.TargetRef.Namespace, mappings.Pods())
endpointSlice.Endpoints[i].TargetRef.Name = nameAndNamespace.Name
endpointSlice.Endpoints[i].TargetRef.Namespace = nameAndNamespace.Namespace
}
}
}

func (s *endpointSliceSyncer) translateUpdate(ctx *synccontext.SyncContext, pObj, vObj *discoveryv1.EndpointSlice) error {
// check endpointSlice.Endpoints
translated := vObj.DeepCopy()
s.translateSpec(ctx, translated)
pObj.Endpoints = translated.Endpoints
return nil
}
1 change: 1 addition & 0 deletions pkg/controllers/resources/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func getSyncers(ctx *synccontext.RegisterContext) []BuildController {
isEnabled(ctx.Config.Sync.FromHost.Secrets.Enabled, secrets.NewFromHost),
isEnabled(ctx.Config.Sync.ToHost.Secrets.Enabled, secrets.New),
isEnabled(ctx.Config.Sync.ToHost.Endpoints.Enabled, endpoints.New),
isEnabled(ctx.Config.Sync.ToHost.EndpointSlices.Enabled, endpoints.New),
isEnabled(ctx.Config.Sync.ToHost.Pods.Enabled, pods.New),
isEnabled(ctx.Config.Sync.FromHost.Events.Enabled, events.New),
isEnabled(ctx.Config.Sync.ToHost.PersistentVolumeClaims.Enabled, persistentvolumeclaims.New),
Expand Down
Loading