This is an automated email from the ASF dual-hosted git repository. ronething pushed a commit to branch fix/gatewayproxy_check in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
commit 3210a9d109e4af9f96399dcfda7e816f32d33af7 Author: Ashing Zheng <[email protected]> AuthorDate: Mon Oct 13 17:02:44 2025 +0800 feat: add conflict detection for gateway proxy Signed-off-by: Ashing Zheng <[email protected]> --- internal/controller/indexer/tlsroute.go | 3 +- internal/provider/init/init.go | 3 +- internal/provider/register.go | 3 +- internal/webhook/v1/gatewayproxy_webhook.go | 139 ++++++++++++++- internal/webhook/v1/gatewayproxy_webhook_test.go | 217 ++++++++++++++++++++++- test/e2e/webhook/gatewayproxy.go | 159 ++++++++++++++++- 6 files changed, 510 insertions(+), 14 deletions(-) diff --git a/internal/controller/indexer/tlsroute.go b/internal/controller/indexer/tlsroute.go index 567131c4..acef5317 100644 --- a/internal/controller/indexer/tlsroute.go +++ b/internal/controller/indexer/tlsroute.go @@ -20,10 +20,11 @@ package indexer import ( "context" - internaltypes "github.com/apache/apisix-ingress-controller/internal/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + internaltypes "github.com/apache/apisix-ingress-controller/internal/types" ) func setupTLSRouteIndexer(mgr ctrl.Manager) error { diff --git a/internal/provider/init/init.go b/internal/provider/init/init.go index b6ed9e99..be21c07d 100644 --- a/internal/provider/init/init.go +++ b/internal/provider/init/init.go @@ -18,11 +18,12 @@ package init import ( + "github.com/go-logr/logr" + "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/manager/readiness" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/provider/apisix" - "github.com/go-logr/logr" ) func init() { diff --git a/internal/provider/register.go b/internal/provider/register.go index fddb1af5..a9feb032 100644 --- a/internal/provider/register.go +++ b/internal/provider/register.go @@ -21,9 +21,10 @@ import ( "fmt" "net/http" + "github.com/go-logr/logr" + "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/manager/readiness" - "github.com/go-logr/logr" ) type RegisterHandler interface { diff --git a/internal/webhook/v1/gatewayproxy_webhook.go b/internal/webhook/v1/gatewayproxy_webhook.go index 75bccea3..b76a8cf7 100644 --- a/internal/webhook/v1/gatewayproxy_webhook.go +++ b/internal/webhook/v1/gatewayproxy_webhook.go @@ -18,6 +18,8 @@ package v1 import ( "context" "fmt" + "sort" + "strings" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -63,7 +65,12 @@ func (v *GatewayProxyCustomValidator) ValidateCreate(ctx context.Context, obj ru } gatewayProxyLog.Info("Validation for GatewayProxy upon creation", "name", gp.GetName(), "namespace", gp.GetNamespace()) - return v.collectWarnings(ctx, gp), nil + warnings := v.collectWarnings(ctx, gp) + if err := v.validateGatewayProxyConflict(ctx, gp); err != nil { + return nil, err + } + + return warnings, nil } func (v *GatewayProxyCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { @@ -73,7 +80,12 @@ func (v *GatewayProxyCustomValidator) ValidateUpdate(ctx context.Context, oldObj } gatewayProxyLog.Info("Validation for GatewayProxy upon update", "name", gp.GetName(), "namespace", gp.GetNamespace()) - return v.collectWarnings(ctx, gp), nil + warnings := v.collectWarnings(ctx, gp) + if err := v.validateGatewayProxyConflict(ctx, gp); err != nil { + return nil, err + } + + return warnings, nil } func (v *GatewayProxyCustomValidator) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) { @@ -111,3 +123,126 @@ func (v *GatewayProxyCustomValidator) collectWarnings(ctx context.Context, gp *v return warnings } + +func (v *GatewayProxyCustomValidator) validateGatewayProxyConflict(ctx context.Context, gp *v1alpha1.GatewayProxy) error { + current := buildGatewayProxyConfig(gp) + if !current.readyForConflict() { + return nil + } + + var list v1alpha1.GatewayProxyList + if err := v.Client.List(ctx, &list); err != nil { + gatewayProxyLog.Error(err, "failed to list GatewayProxy objects for conflict detection") + return fmt.Errorf("failed to list existing GatewayProxy resources: %w", err) + } + + for _, other := range list.Items { + if other.GetNamespace() == gp.GetNamespace() && other.GetName() == gp.GetName() { + // skip self + continue + } + otherConfig := buildGatewayProxyConfig(&other) + if !otherConfig.readyForConflict() { + continue + } + if !current.sharesAdminKeyWith(otherConfig) { + continue + } + if current.serviceKey != "" && current.serviceKey == otherConfig.serviceKey { + return fmt.Errorf("gateway proxy configuration conflict: GatewayProxy %s/%s and %s/%s both target %s while sharing %s", + gp.GetNamespace(), gp.GetName(), + other.GetNamespace(), other.GetName(), + current.serviceDescription, + current.adminKeyDetail(), + ) + } + if len(current.endpoints) > 0 && len(otherConfig.endpoints) > 0 { + if overlap := current.endpointOverlap(otherConfig); len(overlap) > 0 { + return fmt.Errorf("gateway proxy configuration conflict: GatewayProxy %s/%s and %s/%s both target control plane endpoints [%s] while sharing %s", + gp.GetNamespace(), gp.GetName(), + other.GetNamespace(), other.GetName(), + strings.Join(overlap, ", "), + current.adminKeyDetail(), + ) + } + } + } + + return nil +} + +type gatewayProxyConfig struct { + inlineAdminKey string + secretKey string + serviceKey string + serviceDescription string + endpoints map[string]struct{} +} + +func buildGatewayProxyConfig(gp *v1alpha1.GatewayProxy) gatewayProxyConfig { + var cfg gatewayProxyConfig + + if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.Type != v1alpha1.ProviderTypeControlPlane || gp.Spec.Provider.ControlPlane == nil { + return cfg + } + + cp := gp.Spec.Provider.ControlPlane + + if cp.Auth.AdminKey != nil { + if value := strings.TrimSpace(cp.Auth.AdminKey.Value); value != "" { + cfg.inlineAdminKey = value + } else if cp.Auth.AdminKey.ValueFrom != nil && cp.Auth.AdminKey.ValueFrom.SecretKeyRef != nil { + ref := cp.Auth.AdminKey.ValueFrom.SecretKeyRef + cfg.secretKey = fmt.Sprintf("%s/%s:%s", gp.GetNamespace(), ref.Name, ref.Key) + } + } + + if cp.Service != nil && cp.Service.Name != "" { + cfg.serviceKey = fmt.Sprintf("service:%s/%s:%d", gp.GetNamespace(), cp.Service.Name, cp.Service.Port) + cfg.serviceDescription = fmt.Sprintf("Service %s/%s port %d", gp.GetNamespace(), cp.Service.Name, cp.Service.Port) + } + + if len(cp.Endpoints) > 0 { + cfg.endpoints = make(map[string]struct{}, len(cp.Endpoints)) + for _, endpoint := range cp.Endpoints { + cfg.endpoints[endpoint] = struct{}{} + } + } + + return cfg +} + +func (c gatewayProxyConfig) adminKeyDetail() string { + if c.secretKey != "" { + return fmt.Sprintf("AdminKey secret %s", c.secretKey) + } + return "the same inline AdminKey value" +} + +func (c gatewayProxyConfig) sharesAdminKeyWith(other gatewayProxyConfig) bool { + if c.inlineAdminKey != "" && other.inlineAdminKey != "" { + return c.inlineAdminKey == other.inlineAdminKey + } + if c.secretKey != "" && other.secretKey != "" { + return c.secretKey == other.secretKey + } + return false +} + +func (c gatewayProxyConfig) readyForConflict() bool { + if c.inlineAdminKey == "" && c.secretKey == "" { + return false + } + return c.serviceKey != "" || len(c.endpoints) > 0 +} + +func (c gatewayProxyConfig) endpointOverlap(other gatewayProxyConfig) []string { + var overlap []string + for endpoint := range c.endpoints { + if _, ok := other.endpoints[endpoint]; ok { + overlap = append(overlap, endpoint) + } + } + sort.Strings(overlap) + return overlap +} diff --git a/internal/webhook/v1/gatewayproxy_webhook_test.go b/internal/webhook/v1/gatewayproxy_webhook_test.go index c43253c1..2768ac7a 100644 --- a/internal/webhook/v1/gatewayproxy_webhook_test.go +++ b/internal/webhook/v1/gatewayproxy_webhook_test.go @@ -29,6 +29,10 @@ import ( v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1" ) +const ( + candidateName = "candidate" +) + func buildGatewayProxyValidator(t *testing.T, objects ...runtime.Object) *GatewayProxyCustomValidator { t.Helper() @@ -54,7 +58,7 @@ func newGatewayProxy() *v1alpha1.GatewayProxy { Provider: &v1alpha1.GatewayProxyProvider{ Type: v1alpha1.ProviderTypeControlPlane, ControlPlane: &v1alpha1.ControlPlaneProvider{ - Service: &v1alpha1.ProviderService{Name: "control-plane"}, + Service: &v1alpha1.ProviderService{Name: "control-plane", Port: 9180}, Auth: v1alpha1.ControlPlaneAuth{ Type: v1alpha1.AuthTypeAdminKey, AdminKey: &v1alpha1.AdminKeyAuth{ @@ -72,6 +76,41 @@ func newGatewayProxy() *v1alpha1.GatewayProxy { } } +func newGatewayProxyWithEndpoints(name string, endpoints []string) *v1alpha1.GatewayProxy { + gp := newGatewayProxy() + gp.Name = name + gp.Spec.Provider.ControlPlane.Service = nil + gp.Spec.Provider.ControlPlane.Endpoints = endpoints + return gp +} + +func setInlineAdminKey(gp *v1alpha1.GatewayProxy, value string) { + if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil { + return + } + if gp.Spec.Provider.ControlPlane.Auth.AdminKey == nil { + gp.Spec.Provider.ControlPlane.Auth.AdminKey = &v1alpha1.AdminKeyAuth{} + } + gp.Spec.Provider.ControlPlane.Auth.AdminKey.Value = value + gp.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom = nil +} + +func setSecretAdminKey(gp *v1alpha1.GatewayProxy, name, key string) { + if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil { + return + } + if gp.Spec.Provider.ControlPlane.Auth.AdminKey == nil { + gp.Spec.Provider.ControlPlane.Auth.AdminKey = &v1alpha1.AdminKeyAuth{} + } + gp.Spec.Provider.ControlPlane.Auth.AdminKey.Value = "" + gp.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom = &v1alpha1.AdminKeyValueFrom{ + SecretKeyRef: &v1alpha1.SecretKeySelector{ + Name: name, + Key: key, + }, + } +} + func TestGatewayProxyValidator_MissingService(t *testing.T) { gp := newGatewayProxy() gp.Spec.Provider.ControlPlane.Auth.AdminKey = nil @@ -150,3 +189,179 @@ func TestGatewayProxyValidator_NoWarnings(t *testing.T) { require.NoError(t, err) require.Empty(t, warnings) } + +func TestGatewayProxyValidator_DetectsServiceConflict(t *testing.T) { + existing := newGatewayProxy() + existing.Name = "existing" + + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "control-plane", + Namespace: "default", + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-key", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("value"), + }, + } + + validator := buildGatewayProxyValidator(t, existing, service, secret) + + candidate := newGatewayProxy() + candidate.Name = candidateName + + warnings, err := validator.ValidateCreate(context.Background(), candidate) + require.Error(t, err) + require.Len(t, warnings, 0) + require.Contains(t, err.Error(), "gateway proxy configuration conflict") + require.Contains(t, err.Error(), "Service default/control-plane port 9180") + require.Contains(t, err.Error(), "AdminKey secret default/admin-key:token") +} + +func TestGatewayProxyValidator_DetectsEndpointConflict(t *testing.T) { + existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"}) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-key", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("value"), + }, + } + validator := buildGatewayProxyValidator(t, existing, secret) + + candidate := newGatewayProxyWithEndpoints(candidateName, []string{"https://10.0.0.1:9443", "https://127.0.0.1:9443"}) + + warnings, err := validator.ValidateCreate(context.Background(), candidate) + require.Error(t, err) + require.Len(t, warnings, 0) + require.Contains(t, err.Error(), "gateway proxy configuration conflict") + require.Contains(t, err.Error(), "endpoints [https://10.0.0.1:9443, https://127.0.0.1:9443]") + require.Contains(t, err.Error(), "AdminKey secret default/admin-key:token") +} + +func TestGatewayProxyValidator_AllowsDistinctGatewayGroups(t *testing.T) { + existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443"}) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-key", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("value"), + }, + } + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "control-plane", + Namespace: "default", + }, + } + validator := buildGatewayProxyValidator(t, existing, secret, service) + + candidate := newGatewayProxy() + candidate.Name = candidateName + candidate.Spec.Provider.ControlPlane.Service = &v1alpha1.ProviderService{ + Name: "control-plane", + Port: 9180, + } + + warnings, err := validator.ValidateCreate(context.Background(), candidate) + require.NoError(t, err) + require.Empty(t, warnings) +} + +func TestGatewayProxyValidator_AllowsServiceConflictWithDifferentAdminSecret(t *testing.T) { + existing := newGatewayProxy() + existing.Name = "existing" + + candidate := newGatewayProxy() + candidate.Name = candidateName + setSecretAdminKey(candidate, "admin-key-alt", "token") + + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "control-plane", + Namespace: "default", + }, + } + existingSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-key", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("value"), + }, + } + altSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-key-alt", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("value"), + }, + } + + validator := buildGatewayProxyValidator(t, existing, service, existingSecret, altSecret) + + warnings, err := validator.ValidateCreate(context.Background(), candidate) + require.NoError(t, err) + require.Empty(t, warnings) +} + +func TestGatewayProxyValidator_DetectsInlineAdminKeyConflict(t *testing.T) { + existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"}) + setInlineAdminKey(existing, "inline-cred") + + candidate := newGatewayProxyWithEndpoints(candidateName, []string{"https://10.0.0.1:9443"}) + setInlineAdminKey(candidate, "inline-cred") + + validator := buildGatewayProxyValidator(t, existing) + + warnings, err := validator.ValidateCreate(context.Background(), candidate) + require.Error(t, err) + require.Len(t, warnings, 0) + require.Contains(t, err.Error(), "gateway proxy configuration conflict") + require.Contains(t, err.Error(), "control plane endpoints [https://10.0.0.1:9443]") + require.Contains(t, err.Error(), "inline AdminKey value") +} + +func TestGatewayProxyValidator_AllowsEndpointOverlapWithDifferentAdminKey(t *testing.T) { + existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"}) + + candidate := newGatewayProxyWithEndpoints(candidateName, []string{"https://10.0.0.1:9443", "https://192.168.0.1:9443"}) + setSecretAdminKey(candidate, "admin-key-alt", "token") + + existingSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-key", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("value"), + }, + } + altSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-key-alt", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("value"), + }, + } + + validator := buildGatewayProxyValidator(t, existing, existingSecret, altSecret) + + warnings, err := validator.ValidateCreate(context.Background(), candidate) + require.NoError(t, err) + require.Empty(t, warnings) +} diff --git a/test/e2e/webhook/gatewayproxy.go b/test/e2e/webhook/gatewayproxy.go index 6b1f6189..4f2d12e4 100644 --- a/test/e2e/webhook/gatewayproxy.go +++ b/test/e2e/webhook/gatewayproxy.go @@ -33,11 +33,7 @@ var _ = Describe("Test GatewayProxy Webhook", Label("webhook"), func() { EnableWebhook: true, }) - It("should warn on missing service or secret references", func() { - missingService := "missing-control-plane" - missingSecret := "missing-admin-secret" - gpName := "webhook-gateway-proxy" - gpWithSecrets := ` + gatewayProxyTemplate := ` apiVersion: apisix.apache.org/v1alpha1 kind: GatewayProxy metadata: @@ -58,7 +54,12 @@ spec: key: token ` - output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret)) + It("should warn on missing service or secret references", func() { + missingService := "missing-control-plane" + missingSecret := "missing-admin-secret" + gpName := "webhook-gateway-proxy" + + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret)) Expect(err).ShouldNot(HaveOccurred()) Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret))) @@ -98,7 +99,7 @@ stringData: err = s.DeleteResource("GatewayProxy", gpName) Expect(err).ShouldNot(HaveOccurred()) - output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret)) + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret)) Expect(err).ShouldNot(HaveOccurred()) Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Secret key 'token' not found in Secret '%s/%s'", s.Namespace(), missingSecret))) @@ -121,9 +122,151 @@ stringData: err = s.DeleteResource("GatewayProxy", gpName) Expect(err).ShouldNot(HaveOccurred()) - output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret)) + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret)) Expect(err).ShouldNot(HaveOccurred()) Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Secret key 'token' not found in Secret '%s/%s'", s.Namespace(), missingSecret))) }) + + Context("GatewayProxy configuration conflicts", func() { + It("should reject GatewayProxy that reuses the same Service and AdminKey Secret as an existing one on create and update", func() { + serviceTemplate := ` +apiVersion: v1 +kind: Service +metadata: + name: %s +spec: + selector: + app: dummy-control-plane + ports: + - name: admin + port: 9180 + targetPort: 9180 +` + secretTemplate := ` +apiVersion: v1 +kind: Secret +metadata: + name: %s +type: Opaque +stringData: + %s: %s +` + serviceName := "gatewayproxy-shared-service" + secretName := "gatewayproxy-shared-secret" + initialProxy := "gatewayproxy-shared-primary" + conflictingProxy := "gatewayproxy-shared-conflict" + + Expect(s.CreateResourceFromString(fmt.Sprintf(serviceTemplate, serviceName))).ShouldNot(HaveOccurred(), "creating shared Service") + Expect(s.CreateResourceFromString(fmt.Sprintf(secretTemplate, secretName, "token", "value"))).ShouldNot(HaveOccurred(), "creating shared Secret") + + err := s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, initialProxy, serviceName, secretName)) + Expect(err).ShouldNot(HaveOccurred(), "creating initial GatewayProxy") + + time.Sleep(2 * time.Second) + + err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, conflictingProxy, serviceName, secretName)) + Expect(err).Should(HaveOccurred(), "expecting conflict for duplicated GatewayProxy") + Expect(err.Error()).To(ContainSubstring("gateway proxy configuration conflict")) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), conflictingProxy))) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), initialProxy))) + Expect(err.Error()).To(ContainSubstring("Service")) + Expect(err.Error()).To(ContainSubstring("AdminKey secret")) + + Expect(s.DeleteResource("GatewayProxy", initialProxy)).ShouldNot(HaveOccurred()) + Expect(s.DeleteResource("Service", serviceName)).ShouldNot(HaveOccurred()) + Expect(s.DeleteResource("Secret", secretName)).ShouldNot(HaveOccurred()) + }) + + It("should reject GatewayProxy that overlaps endpoints when sharing inline AdminKey value", func() { + gatewayProxyTemplate := ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: %s +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + + existingProxy := "gatewayproxy-inline-primary" + conflictingProxy := "gatewayproxy-inline-conflict" + endpointA := "https://127.0.0.1:9443" + endpointB := "https://10.0.0.1:9443" + endpointC := "https://192.168.0.1:9443" + inlineKey := "inline-credential" + + err := s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, existingProxy, endpointA, endpointB, inlineKey)) + Expect(err).ShouldNot(HaveOccurred(), "creating GatewayProxy with inline AdminKey") + + time.Sleep(2 * time.Second) + + err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, conflictingProxy, endpointB, endpointC, inlineKey)) + Expect(err).Should(HaveOccurred(), "expecting conflict for overlapping endpoints with shared AdminKey") + Expect(err.Error()).To(ContainSubstring("gateway proxy configuration conflict")) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), conflictingProxy))) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), existingProxy))) + Expect(err.Error()).To(ContainSubstring("control plane endpoints")) + Expect(err.Error()).To(ContainSubstring("inline AdminKey value")) + }) + + It("should reject GatewayProxy update that creates conflict with another GatewayProxy", func() { + serviceTemplate := ` +apiVersion: v1 +kind: Service +metadata: + name: %s +spec: + selector: + app: dummy-control-plane + ports: + - name: admin + port: 9180 + targetPort: 9180 +` + secretTemplate := ` +apiVersion: v1 +kind: Secret +metadata: + name: %s +type: Opaque +stringData: + %s: %s +` + sharedServiceName := "gatewayproxy-update-shared-service" + sharedSecretName := "gatewayproxy-update-shared-secret" + uniqueServiceName := "gatewayproxy-update-unique-service" + proxyA := "gatewayproxy-update-a" + proxyB := "gatewayproxy-update-b" + + Expect(s.CreateResourceFromString(fmt.Sprintf(serviceTemplate, sharedServiceName))).ShouldNot(HaveOccurred(), "creating shared Service") + Expect(s.CreateResourceFromString(fmt.Sprintf(serviceTemplate, uniqueServiceName))).ShouldNot(HaveOccurred(), "creating unique Service") + Expect(s.CreateResourceFromString(fmt.Sprintf(secretTemplate, sharedSecretName, "token", "value"))).ShouldNot(HaveOccurred(), "creating shared Secret") + + err := s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, proxyA, sharedServiceName, sharedSecretName)) + Expect(err).ShouldNot(HaveOccurred(), "creating GatewayProxy A with shared Service and Secret") + + time.Sleep(2 * time.Second) + + err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, proxyB, uniqueServiceName, sharedSecretName)) + Expect(err).ShouldNot(HaveOccurred(), "creating GatewayProxy B with unique Service but same Secret") + + time.Sleep(2 * time.Second) + + By("updating GatewayProxy B to use the same Service as GatewayProxy A, causing conflict") + err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, proxyB, sharedServiceName, sharedSecretName)) + Expect(err).Should(HaveOccurred(), "expecting conflict when updating to same Service") + Expect(err.Error()).To(ContainSubstring("gateway proxy configuration conflict")) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), proxyA))) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), proxyB))) + }) + }) })
