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/framework/manifests/webhook.yaml b/test/e2e/framework/manifests/webhook.yaml index 432873c9..5e6b7517 100644 --- a/test/e2e/framework/manifests/webhook.yaml +++ b/test/e2e/framework/manifests/webhook.yaml @@ -20,33 +20,33 @@ kind: ValidatingWebhookConfiguration metadata: name: test-webhook-{{ .Namespace }} webhooks: -- name: vingress-v1.kb.io +- name: vapisixconsumer-v2.kb.io clientConfig: service: name: webhook-service namespace: {{ .Namespace }} - path: /validate-networking-k8s-io-v1-ingress + path: /validate-apisix-apache-org-v2-apisixconsumer caBundle: {{ .CABundle }} admissionReviewVersions: - v1 rules: - - operations: - - CREATE - - UPDATE - apiGroups: - - networking.k8s.io - apiVersions: - - v1 - resources: - - ingresses + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v2 + resources: + - apisixconsumers failurePolicy: Fail sideEffects: None -- name: vingressclass-v1.kb.io +- name: vapisixroute-v2.kb.io clientConfig: service: name: webhook-service namespace: {{ .Namespace }} - path: /validate-networking-k8s-io-v1-ingressclass + path: /validate-apisix-apache-org-v2-apisixroute caBundle: {{ .CABundle }} admissionReviewVersions: - v1 @@ -55,11 +55,53 @@ webhooks: - CREATE - UPDATE apiGroups: - - networking.k8s.io + - apisix.apache.org apiVersions: - - v1 + - v2 resources: - - ingressclasses + - apisixroutes + failurePolicy: Fail + sideEffects: None +- name: vapisixtls-v2.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-apisix-apache-org-v2-apisixtls + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v2 + resources: + - apisixtlses + failurePolicy: Fail + sideEffects: None +- name: vconsumer-v1alpha1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-apisix-apache-org-v1alpha1-consumer + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v1alpha1 + resources: + - consumers failurePolicy: Fail sideEffects: None - name: vgateway-v1.kb.io @@ -83,3 +125,66 @@ webhooks: - gateways failurePolicy: Fail sideEffects: None +- name: vgatewayproxy-v1alpha1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-apisix-apache-org-v1alpha1-gatewayproxy + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v1alpha1 + resources: + - gatewayproxies + failurePolicy: Fail + sideEffects: None +- name: vingress-v1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-networking-k8s-io-v1-ingress + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - networking.k8s.io + apiVersions: + - v1 + resources: + - ingresses + failurePolicy: Fail + sideEffects: None +- name: vingressclass-v1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-networking-k8s-io-v1-ingressclass + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - networking.k8s.io + apiVersions: + - v1 + resources: + - ingressclasses + failurePolicy: Fail + sideEffects: None diff --git a/test/e2e/gatewayapi/webhook.go b/test/e2e/gatewayapi/webhook.go index 92f65f93..799209f1 100644 --- a/test/e2e/gatewayapi/webhook.go +++ b/test/e2e/gatewayapi/webhook.go @@ -33,16 +33,17 @@ var _ = Describe("Test Gateway Webhook", Label("webhook"), func() { EnableWebhook: true, }) - It("should warn when referenced GatewayProxy does not exist on create and update", func() { - By("creating GatewayClass with controller name") - err := s.CreateResourceFromString(s.GetGatewayClassYaml()) - Expect(err).ShouldNot(HaveOccurred()) + Context("GatewayProxy reference validation warnings", func() { + It("should warn when referenced GatewayProxy does not exist on create and update", func() { + By("creating GatewayClass with controller name") + err := s.CreateResourceFromString(s.GetGatewayClassYaml()) + Expect(err).ShouldNot(HaveOccurred()) - time.Sleep(2 * time.Second) + time.Sleep(2 * time.Second) - By("creating Gateway referencing a missing GatewayProxy") - missingName := "missing-proxy" - gwYAML := ` + By("creating Gateway referencing a missing GatewayProxy") + missingName := "missing-proxy" + gwYAML := ` apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: @@ -60,35 +61,220 @@ spec: name: %s ` - output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), missingName)) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), missingName))) + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), missingName)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), missingName))) - time.Sleep(2 * time.Second) + time.Sleep(2 * time.Second) - By("updating Gateway to reference another missing GatewayProxy") - missingName2 := "missing-proxy-2" - output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), missingName2)) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), missingName2))) + By("updating Gateway to reference another missing GatewayProxy") + missingName2 := "missing-proxy-2" + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), missingName2)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), missingName2))) - By("create GatewayProxy") - err = s.CreateResourceFromString(s.GetGatewayProxySpec()) - Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") - time.Sleep(5 * time.Second) + By("create GatewayProxy") + err = s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) - By("updating Gateway to reference an existing GatewayProxy") - existingName := "apisix-proxy-config" - output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), existingName)) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), existingName))) + By("updating Gateway to reference an existing GatewayProxy") + existingName := "apisix-proxy-config" + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), existingName)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), existingName))) - By("delete Gateway") - err = s.DeleteResource("Gateway", s.Namespace()) - Expect(err).ShouldNot(HaveOccurred()) + By("delete Gateway") + err = s.DeleteResource("Gateway", s.Namespace()) + Expect(err).ShouldNot(HaveOccurred()) - By("delete GatewayClass") - err = s.DeleteResource("GatewayClass", s.Namespace()) - Expect(err).ShouldNot(HaveOccurred()) + By("delete GatewayClass") + err = s.DeleteResource("GatewayClass", s.Namespace()) + Expect(err).ShouldNot(HaveOccurred()) + }) + }) + + 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 +` + gatewayProxyTemplate := ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: %s +spec: + provider: + type: ControlPlane + controlPlane: + service: + name: %s + port: 9180 + auth: + type: AdminKey + adminKey: + valueFrom: + secretKeyRef: + name: %s + key: token +` + + 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 +` + gatewayProxyTemplate := ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: %s +spec: + provider: + type: ControlPlane + controlPlane: + service: + name: %s + port: 9180 + auth: + type: AdminKey + adminKey: + valueFrom: + secretKeyRef: + name: %s + key: token +` + + 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))) + }) }) })