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
2 changes: 1 addition & 1 deletion chart/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2586,7 +2586,7 @@
},
"gateways": {
"$ref": "#/$defs/EnableSwitchWithPatches",
"description": "Gateways configures tenant-created Gateway sync to the control plane cluster."
"description": "Gateways configures tenant-created Gateway sync to the control plane cluster.\nGateway spec.infrastructure.parametersRef names are translated for core ConfigMap and Secret refs.\nReferenced objects must also be synced to the host, for example with a pod reference or sync.toHost.configMaps.all / sync.toHost.secrets.all."
},
"tlsRoutes": {
"$ref": "#/$defs/EnableSwitchWithPatches",
Expand Down
2 changes: 2 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ sync:
# Enabled defines if this option should be enabled.
enabled: false
# Gateways configures tenant-created Gateway sync to the control plane cluster.
# Gateway spec.infrastructure.parametersRef names are translated for core ConfigMap and Secret refs.
# Referenced objects must also be synced to the host, for example with a pod reference or sync.toHost.configMaps.all / sync.toHost.secrets.all.
gateways:
# Enabled defines if this option should be enabled.
enabled: false
Expand Down
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,8 @@ type GatewayAPIEnableSwitchWithPatches struct {
HTTPRoutes EnableSwitchWithPatches `json:"httpRoutes,omitempty"`

// Gateways configures tenant-created Gateway sync to the control plane cluster.
// Gateway spec.infrastructure.parametersRef names are translated for core ConfigMap and Secret refs.
// Referenced objects must also be synced to the host, for example with a pod reference or sync.toHost.configMaps.all / sync.toHost.secrets.all.
Gateways EnableSwitchWithPatches `json:"gateways,omitempty"`

// TLSRoutes configures TLSRoute sync to the control plane cluster.
Expand Down
51 changes: 51 additions & 0 deletions e2e/test_gatewayapi/test_gatewayapi_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,17 +176,52 @@ func GatewayAPISyncSpec() {

service := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "backend-" + suffix, Namespace: ns.Name}, Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 80}}}}
Expect(vClusterClient.Create(ctx, service)).To(Succeed())
parameters := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "gw-params-" + suffix, Namespace: ns.Name}, Data: map[string]string{"profile": "edge"}}
Expect(vClusterClient.Create(ctx, parameters)).To(Succeed())
DeferCleanup(func(ctx context.Context) {
Expect(ctrlclient.IgnoreNotFound(vClusterClient.Delete(ctx, parameters))).To(Succeed())
})
clientCert := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "gw-client-cert-" + suffix, Namespace: ns.Name}, Type: corev1.SecretTypeTLS, Data: map[string][]byte{"tls.crt": []byte("cert"), "tls.key": []byte("key")}}
Expect(vClusterClient.Create(ctx, clientCert)).To(Succeed())
DeferCleanup(func(ctx context.Context) {
Expect(ctrlclient.IgnoreNotFound(vClusterClient.Delete(ctx, clientCert))).To(Succeed())
})
filterConfig := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "route-filter-" + suffix, Namespace: ns.Name}, Data: map[string]string{"mode": "rule"}}
Expect(vClusterClient.Create(ctx, filterConfig)).To(Succeed())
DeferCleanup(func(ctx context.Context) {
Expect(ctrlclient.IgnoreNotFound(vClusterClient.Delete(ctx, filterConfig))).To(Succeed())
})
filterSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "backend-filter-" + suffix, Namespace: ns.Name}, StringData: map[string]string{"mode": "backend"}}
Expect(vClusterClient.Create(ctx, filterSecret)).To(Succeed())
DeferCleanup(func(ctx context.Context) {
Expect(ctrlclient.IgnoreNotFound(vClusterClient.Delete(ctx, filterSecret))).To(Succeed())
})
gateway := tenantGateway(ns.Name, "gw-"+suffix, allowed.Name)
gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{ParametersRef: &gatewayv1.LocalParametersReference{Group: gatewayv1.Group(corev1.GroupName), Kind: gatewayv1.Kind("ConfigMap"), Name: parameters.Name}}
gateway.Spec.TLS = &gatewayv1.GatewayTLSConfig{Backend: &gatewayv1.GatewayBackendTLS{ClientCertificateRef: &gatewayv1.SecretObjectReference{Name: gatewayv1.ObjectName(clientCert.Name)}}}
Expect(vClusterClient.Create(ctx, gateway)).To(Succeed())
DeferCleanup(func(ctx context.Context) {
Expect(ctrlclient.IgnoreNotFound(vClusterClient.Delete(ctx, gateway))).To(Succeed())
})

hostGatewayName := translate.SafeConcatName(gateway.Name, "x", ns.Name, "x", vClusterName)
hostParametersName := translate.SafeConcatName(parameters.Name, "x", ns.Name, "x", vClusterName)
hostClientCertName := translate.SafeConcatName(clientCert.Name, "x", ns.Name, "x", vClusterName)
hostFilterConfigName := translate.SafeConcatName(filterConfig.Name, "x", ns.Name, "x", vClusterName)
hostFilterSecretName := translate.SafeConcatName(filterSecret.Name, "x", ns.Name, "x", vClusterName)
Eventually(func(g Gomega) {
g.Expect(hostClient.Get(ctx, types.NamespacedName{Namespace: vClusterHostNS, Name: hostParametersName}, &corev1.ConfigMap{})).To(Succeed())
g.Expect(hostClient.Get(ctx, types.NamespacedName{Namespace: vClusterHostNS, Name: hostClientCertName}, &corev1.Secret{})).To(Succeed())
got := &gatewayv1.Gateway{}
g.Expect(hostClient.Get(ctx, types.NamespacedName{Namespace: vClusterHostNS, Name: hostGatewayName}, got)).To(Succeed())
g.Expect(got.Spec.GatewayClassName).To(Equal(gatewayv1.ObjectName(allowed.Name)))
g.Expect(got.Spec.Infrastructure).NotTo(BeNil())
g.Expect(got.Spec.Infrastructure.ParametersRef).NotTo(BeNil())
g.Expect(got.Spec.Infrastructure.ParametersRef.Name).To(Equal(hostParametersName))
g.Expect(got.Spec.TLS).NotTo(BeNil())
g.Expect(got.Spec.TLS.Backend).NotTo(BeNil())
g.Expect(got.Spec.TLS.Backend.ClientCertificateRef).NotTo(BeNil())
g.Expect(got.Spec.TLS.Backend.ClientCertificateRef.Name).To(Equal(gatewayv1.ObjectName(hostClientCertName)))
g.Expect(got.Spec.Listeners).To(HaveLen(1))
listener := got.Spec.Listeners[0]
g.Expect(listener.Name).To(Equal(gatewayv1.SectionName("http")))
Expand All @@ -197,6 +232,14 @@ func GatewayAPISyncSpec() {
}).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeout).Should(Succeed())

route := tenantHTTPRoute(ns.Name, "route-"+suffix, gateway.Name, service.Name)
route.Spec.Rules[0].Filters = []gatewayv1.HTTPRouteFilter{{
Type: gatewayv1.HTTPRouteFilterExtensionRef,
ExtensionRef: &gatewayv1.LocalObjectReference{Group: gatewayv1.Group(corev1.GroupName), Kind: gatewayv1.Kind("ConfigMap"), Name: gatewayv1.ObjectName(filterConfig.Name)},
}}
route.Spec.Rules[0].BackendRefs[0].Filters = []gatewayv1.HTTPRouteFilter{{
Type: gatewayv1.HTTPRouteFilterExtensionRef,
ExtensionRef: &gatewayv1.LocalObjectReference{Group: gatewayv1.Group(corev1.GroupName), Kind: gatewayv1.Kind("Secret"), Name: gatewayv1.ObjectName(filterSecret.Name)},
}}
Expect(vClusterClient.Create(ctx, route)).To(Succeed())
DeferCleanup(func(ctx context.Context) {
Expect(ctrlclient.IgnoreNotFound(vClusterClient.Delete(ctx, route))).To(Succeed())
Expand All @@ -210,6 +253,14 @@ func GatewayAPISyncSpec() {
g.Expect(got.Spec.ParentRefs[0].Name).To(Equal(gatewayv1.ObjectName(hostGatewayName)))
g.Expect(got.Spec.ParentRefs[0].SectionName).NotTo(BeNil())
g.Expect(*got.Spec.ParentRefs[0].SectionName).To(Equal(gatewayv1.SectionName("http")))
g.Expect(got.Spec.Rules).To(HaveLen(1))
g.Expect(got.Spec.Rules[0].Filters).To(HaveLen(1))
g.Expect(got.Spec.Rules[0].Filters[0].ExtensionRef).NotTo(BeNil())
g.Expect(got.Spec.Rules[0].Filters[0].ExtensionRef.Name).To(Equal(gatewayv1.ObjectName(hostFilterConfigName)))
g.Expect(got.Spec.Rules[0].BackendRefs).To(HaveLen(1))
g.Expect(got.Spec.Rules[0].BackendRefs[0].Filters).To(HaveLen(1))
g.Expect(got.Spec.Rules[0].BackendRefs[0].Filters[0].ExtensionRef).NotTo(BeNil())
g.Expect(got.Spec.Rules[0].BackendRefs[0].Filters[0].ExtensionRef.Name).To(Equal(gatewayv1.ObjectName(hostFilterSecretName)))
}).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeout).Should(Succeed())

Expect(ctrlclient.IgnoreNotFound(vClusterClient.Delete(ctx, route))).To(Succeed())
Expand Down
6 changes: 6 additions & 0 deletions e2e/vcluster-gatewayapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ sync:
toHost:
services:
enabled: true
configMaps:
enabled: true
all: true
secrets:
enabled: true
all: true
gatewayApi:
gateways:
enabled: true
Expand Down
14 changes: 14 additions & 0 deletions pkg/controllers/resources/gatewayapi/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ func GatewayCertificate(ctx *synccontext.SyncContext, gatewayNamespace string, r
return referenceGrant(ctx, gatewayv1.GroupVersion.Group, "Gateway", gatewayNamespace, secretRefTarget(gatewayNamespace, ref))
}

// GatewayCACertificate checks whether a Gateway frontend CA certificateRef is allowed by virtual ReferenceGrants.
func GatewayCACertificate(ctx *synccontext.SyncContext, gatewayNamespace string, ref *gatewayv1.ObjectReference) error {
return referenceGrant(ctx, gatewayv1.GroupVersion.Group, "Gateway", gatewayNamespace, objectRefTarget(gatewayNamespace, ref))
}

type referenceTarget struct {
group string
kind string
Expand Down Expand Up @@ -120,6 +125,15 @@ func secretRefTarget(localNamespace string, ref *gatewayv1.SecretObjectReference
}
}

func objectRefTarget(localNamespace string, ref *gatewayv1.ObjectReference) referenceTarget {
return referenceTarget{
group: string(ref.Group),
kind: string(ref.Kind),
namespace: referenceNamespace(localNamespace, ref.Namespace),
name: string(ref.Name),
}
}

func referenceNamespace(localNamespace string, namespace *gatewayv1.Namespace) string {
if namespace == nil || *namespace == "" {
return localNamespace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func CreateToHost[O any, T interface {

pObj, err := toHost()
if err != nil {
if recordTerminalRefError(rec, event.Virtual, err) {
if RecordTerminalRefError(rec, event.Virtual, err) {
return ctrl.Result{}, nil
}

Expand All @@ -61,7 +61,7 @@ func Sync[T client.Object](
applyToHost func() error,
) (_ ctrl.Result, retErr error) {
if err := translateSpec(); err != nil {
if recordTerminalRefError(rec, event.Virtual, err) {
if RecordTerminalRefError(rec, event.Virtual, err) {
return patcher.DeleteHostObject(ctx, event.Host, event.Virtual, "virtual reference cannot be synced to the host")
}

Expand Down Expand Up @@ -117,8 +117,10 @@ func CreateToVirtual[O any, T interface {
return patcher.CreateVirtualObject(ctx, event.Host, vObj, rec, true)
}

// recordTerminalRefError records denied or unsupported reference errors.
func recordTerminalRefError(rec events.EventRecorder, obj client.Object, err error) bool {
// RecordTerminalRefError records denied or unsupported reference errors and reports
// whether err was terminal. Terminal ref errors cannot succeed on requeue, so callers
// surface a Warning event and skip-create / delete-host instead of returning a hard error.
func RecordTerminalRefError(rec events.EventRecorder, obj client.Object, err error) bool {
switch {
case gatewayauthz.IsNotPermitted(err):
RecordRefNotPermitted(rec, obj, err)
Expand Down
49 changes: 46 additions & 3 deletions pkg/controllers/resources/gatewayroutes/translate/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ func LocalObjectRefToHost(ctx *synccontext.SyncContext, localNamespace string, r
return localObjectRefToHost(ctx, localNamespace, ref, options.validateHostObject)
}

func ParametersRefToHost(ctx *synccontext.SyncContext, localNamespace string, ref *gatewayv1.LocalParametersReference, opts ...ToHostOption) error {
options := newToHostOptions(opts...)
return parametersRefToHost(ctx, localNamespace, ref, options.validateHostObject)
}

func ObjectRefToHost(ctx *synccontext.SyncContext, localNamespace string, ref *gatewayv1.ObjectReference, opts ...ToHostOption) error {
options := newToHostOptions(opts...)
return objectReferenceRefToHost(ctx, localNamespace, ref, options.validateHostObject)
}

func PolicyTargetRefToHost(ctx *synccontext.SyncContext, policyNamespace string, ref *gatewayv1.LocalPolicyTargetReferenceWithSectionName, opts ...ToHostOption) error {
options := newToHostOptions(opts...)
return policyTargetRefToHost(ctx, policyNamespace, ref, options.validateHostObject)
Expand Down Expand Up @@ -169,6 +179,30 @@ func localObjectRefToHost(ctx *synccontext.SyncContext, localNamespace string, r
return objectRefToHost(ctx, localNamespace, &ref.Name, nil, gvk, validateHostObject)
}

func parametersRefToHost(ctx *synccontext.SyncContext, localNamespace string, ref *gatewayv1.LocalParametersReference, validateHostObject bool) error {
gvk, err := parametersReferenceGVK(ref)
if err != nil {
return err
}

refName := gatewayv1.ObjectName(ref.Name)
if err := objectRefToHost(ctx, localNamespace, &refName, nil, gvk, validateHostObject); err != nil {
return err
}

ref.Name = string(refName)
return nil
}

func objectReferenceRefToHost(ctx *synccontext.SyncContext, localNamespace string, ref *gatewayv1.ObjectReference, validateHostObject bool) error {
gvk, err := objectReferenceGVK(ref)
if err != nil {
return err
}

return objectRefToHost(ctx, localNamespace, &ref.Name, &ref.Namespace, gvk, validateHostObject)
}

func policyTargetRefToHost(ctx *synccontext.SyncContext, policyNamespace string, ref *gatewayv1.LocalPolicyTargetReferenceWithSectionName, validateHostObject bool) error {
gvk, err := policyTargetReferenceGVK(ref)
if err != nil {
Expand Down Expand Up @@ -443,17 +477,26 @@ func secretObjectReferenceGVK(ref *gatewayv1.SecretObjectReference) (schema.Grou
}

func localObjectReferenceGVK(ref *gatewayv1.LocalObjectReference) (schema.GroupVersionKind, error) {
group := string(ref.Group)
kind := string(ref.Kind)
return objectReferenceGroupKindGVK(string(ref.Group), string(ref.Kind), "localObjectRef")
}

func parametersReferenceGVK(ref *gatewayv1.LocalParametersReference) (schema.GroupVersionKind, error) {
return objectReferenceGroupKindGVK(string(ref.Group), string(ref.Kind), "parametersRef")
}

func objectReferenceGVK(ref *gatewayv1.ObjectReference) (schema.GroupVersionKind, error) {
return objectReferenceGroupKindGVK(string(ref.Group), string(ref.Kind), "objectRef")
}

func objectReferenceGroupKindGVK(group, kind, field string) (schema.GroupVersionKind, error) {
if group == corev1.GroupName && kind == "ConfigMap" {
return mappings.ConfigMaps(), nil
}
if group == corev1.GroupName && kind == "Secret" {
return mappings.Secrets(), nil
}

return schema.GroupVersionKind{}, unsupportedReferencef("localObjectRef group %q kind %q is not supported", group, kind)
return schema.GroupVersionKind{}, unsupportedReferencef("%s group %q kind %q is not supported", field, group, kind)
}

func policyTargetReferenceGVK(ref *gatewayv1.LocalPolicyTargetReferenceWithSectionName) (schema.GroupVersionKind, error) {
Expand Down
26 changes: 21 additions & 5 deletions pkg/controllers/resources/gateways/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"fmt"

rootconfig "github.com/loft-sh/vcluster/config"
"github.com/loft-sh/vcluster/pkg/controllers/resources/gatewayapi/gatewaysync"
routetranslate "github.com/loft-sh/vcluster/pkg/controllers/resources/gatewayroutes/translate"
"github.com/loft-sh/vcluster/pkg/mappings"
"github.com/loft-sh/vcluster/pkg/mappings/resources"
"github.com/loft-sh/vcluster/pkg/patcher"
"github.com/loft-sh/vcluster/pkg/pro"
Expand All @@ -17,6 +20,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)
Expand Down Expand Up @@ -64,6 +68,7 @@ type tenantGatewaySyncer struct {

var _ syncertypes.Object = &tenantGatewaySyncer{}
var _ syncertypes.Syncer = &tenantGatewaySyncer{}
var _ syncertypes.ControllerModifier = &tenantGatewaySyncer{}

func NewToHostSyncer(ctx *synccontext.RegisterContext) (syncertypes.Object, error) {
mapper, err := ctx.Mappings.ByGVK(resources.NewImportedGatewayMapper().GroupVersionKind())
Expand All @@ -80,6 +85,10 @@ func (s *tenantGatewaySyncer) Syncer() syncertypes.Sync[client.Object] {
return syncer.ToGenericSyncer[*gatewayv1.Gateway](s)
}

func (s *tenantGatewaySyncer) ModifyController(ctx *synccontext.RegisterContext, builder *builder.Builder) (*builder.Builder, error) {
return routetranslate.RegisterReferencedWatches(ctx, builder, s.GroupVersionKind(), mappings.ConfigMaps(), mappings.Secrets())
}

func (s *tenantGatewaySyncer) SyncToHost(ctx *synccontext.SyncContext, event *synccontext.SyncToHostEvent[*gatewayv1.Gateway]) (ctrl.Result, error) {
if isImportedGateway(event.Virtual) {
return ctrl.Result{}, nil
Expand All @@ -99,6 +108,9 @@ func (s *tenantGatewaySyncer) SyncToHost(ctx *synccontext.SyncContext, event *sy
}
host, err := s.translate(ctx, event.Virtual)
if err != nil {
if gatewaysync.RecordTerminalRefError(s.EventRecorder(), event.Virtual, err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
host.Status = gatewayv1.GatewayStatus{}
Expand All @@ -119,6 +131,15 @@ func (s *tenantGatewaySyncer) Sync(ctx *synccontext.SyncContext, event *synccont
if !eligible {
return patcher.DeleteHostObject(ctx, event.Host, event.Virtual, "tenant Gateway is no longer eligible for host sync")
}

translated, err := s.translate(ctx, event.Virtual)
if err != nil {
if gatewaysync.RecordTerminalRefError(s.EventRecorder(), event.Virtual, err) {
return patcher.DeleteHostObject(ctx, event.Host, event.Virtual, "virtual reference cannot be synced to the host")
}
return ctrl.Result{}, err
}

patch, err := patcher.NewSyncerPatcher(ctx, event.Host, event.Virtual, patcher.TranslatePatches(ctx.Config.Sync.ToHost.GatewayAPI.Gateways.Patches, false))
if err != nil {
return ctrl.Result{}, fmt.Errorf("new syncer patcher: %w", err)
Expand All @@ -129,11 +150,6 @@ func (s *tenantGatewaySyncer) Sync(ctx *synccontext.SyncContext, event *synccont
}
}()

translated, err := s.translate(ctx, event.Virtual)
if err != nil {
return ctrl.Result{}, err
}

event.Host.Spec = translated.Spec
event.Virtual.Status = event.Host.Status
return ctrl.Result{}, nil
Expand Down
Loading
Loading