You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ro...@apache.org on 2025/10/13 09:03:35 UTC
(apisix-ingress-controller) branch fix/gatewayproxy_check created (now 3210a9d1)
This is an automated email from the ASF dual-hosted git repository.
ronething pushed a change to branch fix/gatewayproxy_check
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
at 3210a9d1 feat: add conflict detection for gateway proxy
This branch includes the following new commits:
new 3210a9d1 feat: add conflict detection for gateway proxy
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(apisix-ingress-controller) 01/01: feat: add conflict detection for gateway proxy
Posted by ro...@apache.org.
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 <ax...@gmail.com>
AuthorDate: Mon Oct 13 17:02:44 2025 +0800
feat: add conflict detection for gateway proxy
Signed-off-by: Ashing Zheng <ax...@gmail.com>
---
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)))
+ })
+ })
})