This is an automated email from the ASF dual-hosted git repository.
ronething pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new ec819175 feat: add secret/service resource checker for webhook (#2583)
ec819175 is described below
commit ec819175cff42354a55525efc9cd28f1a9e52c18
Author: Ashing Zheng <[email protected]>
AuthorDate: Mon Sep 29 14:15:44 2025 +0800
feat: add secret/service resource checker for webhook (#2583)
Signed-off-by: Ashing Zheng <[email protected]>
---
config/webhook/manifests.yaml | 60 ++++++++
internal/controller/grpcroute_controller.go | 1 -
internal/manager/webhooks.go | 9 ++
internal/webhook/v1/apisixconsumer_webhook.go | 8 +
internal/webhook/v1/apisixconsumer_webhook_test.go | 23 ++-
internal/webhook/v1/apisixroute_webhook.go | 7 +
internal/webhook/v1/apisixroute_webhook_test.go | 24 ++-
internal/webhook/v1/apisixtls_webhook.go | 7 +
internal/webhook/v1/apisixtls_webhook_test.go | 23 ++-
internal/webhook/v1/consumer_webhook.go | 7 +
internal/webhook/v1/consumer_webhook_test.go | 26 +++-
internal/webhook/v1/gateway_webhook.go | 98 +++++++++++--
internal/webhook/v1/gateway_webhook_test.go | 113 ++++++++++++++
internal/webhook/v1/grpcroute_webhook.go | 162 +++++++++++++++++++++
internal/webhook/v1/grpcroute_webhook_test.go | 116 +++++++++++++++
internal/webhook/v1/httproute_webhook.go | 162 +++++++++++++++++++++
internal/webhook/v1/httproute_webhook_test.go | 138 ++++++++++++++++++
internal/webhook/v1/ingress_webhook.go | 96 ++++++++++--
internal/webhook/v1/ingress_webhook_test.go | 114 +++++++++++++--
internal/webhook/v1/ownership.go | 103 +++++++++++++
internal/webhook/v1/tcproute_webhook.go | 146 +++++++++++++++++++
internal/webhook/v1/tcproute_webhook_test.go | 122 ++++++++++++++++
22 files changed, 1513 insertions(+), 52 deletions(-)
diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index b5b33201..e2ad06a3 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -124,6 +124,46 @@ webhooks:
resources:
- gatewayproxies
sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /validate-gateway-networking-k8s-io-v1-grpcroute
+ failurePolicy: Fail
+ name: vgrpcroute-v1.kb.io
+ rules:
+ - apiGroups:
+ - gateway.networking.k8s.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - grpcroutes
+ sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /validate-gateway-networking-k8s-io-v1-httproute
+ failurePolicy: Fail
+ name: vhttproute-v1.kb.io
+ rules:
+ - apiGroups:
+ - gateway.networking.k8s.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - httproutes
+ sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
@@ -164,3 +204,23 @@ webhooks:
resources:
- ingressclasses
sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /validate-gateway-networking-k8s-io-v1alpha2-tcproute
+ failurePolicy: Fail
+ name: vtcproute-v1alpha2.kb.io
+ rules:
+ - apiGroups:
+ - gateway.networking.k8s.io
+ apiVersions:
+ - v1alpha2
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - tcproutes
+ sideEffects: None
diff --git a/internal/controller/grpcroute_controller.go
b/internal/controller/grpcroute_controller.go
index 138011a9..782f98f8 100644
--- a/internal/controller/grpcroute_controller.go
+++ b/internal/controller/grpcroute_controller.go
@@ -297,7 +297,6 @@ func (r *GRPCRouteReconciler)
listGRPCRoutesForBackendTrafficPolicy(ctx context.
r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to BackendTrafficPolicy")
return nil
}
-
grpcRouteList := []gatewayv1.GRPCRoute{}
for _, targetRef := range policy.Spec.TargetRefs {
service := &corev1.Service{}
diff --git a/internal/manager/webhooks.go b/internal/manager/webhooks.go
index 6907d762..a1bc1dae 100644
--- a/internal/manager/webhooks.go
+++ b/internal/manager/webhooks.go
@@ -38,6 +38,15 @@ func setupWebhooks(_ context.Context, mgr manager.Manager)
error {
if err := webhookv1.SetupGatewayProxyWebhookWithManager(mgr); err !=
nil {
return err
}
+ if err := webhookv1.SetupHTTPRouteWebhookWithManager(mgr); err != nil {
+ return err
+ }
+ if err := webhookv1.SetupGRPCRouteWebhookWithManager(mgr); err != nil {
+ return err
+ }
+ if err := webhookv1.SetupTCPRouteWebhookWithManager(mgr); err != nil {
+ return err
+ }
if err := webhookv1.SetupApisixConsumerWebhookWithManager(mgr); err !=
nil {
return err
}
diff --git a/internal/webhook/v1/apisixconsumer_webhook.go
b/internal/webhook/v1/apisixconsumer_webhook.go
index 83ffb699..ad22d4b2 100644
--- a/internal/webhook/v1/apisixconsumer_webhook.go
+++ b/internal/webhook/v1/apisixconsumer_webhook.go
@@ -29,6 +29,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/internal/controller"
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
)
@@ -64,6 +65,10 @@ func (v *ApisixConsumerCustomValidator) ValidateCreate(ctx
context.Context, obj
}
apisixConsumerLog.Info("Validation for ApisixConsumer upon creation",
"name", consumer.GetName(), "namespace", consumer.GetNamespace())
+ if !controller.MatchesIngressClass(v.Client, apisixConsumerLog,
consumer) {
+ return nil, nil
+ }
+
return v.collectWarnings(ctx, consumer), nil
}
@@ -73,6 +78,9 @@ func (v *ApisixConsumerCustomValidator) ValidateUpdate(ctx
context.Context, oldO
return nil, fmt.Errorf("expected an ApisixConsumer object for
the newObj but got %T", newObj)
}
apisixConsumerLog.Info("Validation for ApisixConsumer upon update",
"name", consumer.GetName(), "namespace", consumer.GetNamespace())
+ if !controller.MatchesIngressClass(v.Client, apisixConsumerLog,
consumer) {
+ return nil, nil
+ }
return v.collectWarnings(ctx, consumer), nil
}
diff --git a/internal/webhook/v1/apisixconsumer_webhook_test.go
b/internal/webhook/v1/apisixconsumer_webhook_test.go
index 1fe9d9fb..8c31768c 100644
--- a/internal/webhook/v1/apisixconsumer_webhook_test.go
+++ b/internal/webhook/v1/apisixconsumer_webhook_test.go
@@ -21,12 +21,14 @@ import (
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
+ networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
)
func buildApisixConsumerValidator(t *testing.T, objects ...runtime.Object)
*ApisixConsumerCustomValidator {
@@ -34,12 +36,24 @@ func buildApisixConsumerValidator(t *testing.T, objects
...runtime.Object) *Apis
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
+ require.NoError(t, networkingv1.AddToScheme(scheme))
require.NoError(t, apisixv2.AddToScheme(scheme))
- builder := fake.NewClientBuilder().WithScheme(scheme)
- if len(objects) > 0 {
- builder = builder.WithRuntimeObjects(objects...)
+ managed := []runtime.Object{
+ &networkingv1.IngressClass{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "apisix",
+ Annotations: map[string]string{
+
"ingressclass.kubernetes.io/is-default-class": "true",
+ },
+ },
+ Spec: networkingv1.IngressClassSpec{
+ Controller:
config.ControllerConfig.ControllerName,
+ },
+ },
}
+ allObjects := append(managed, objects...)
+ builder :=
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
return NewApisixConsumerCustomValidator(builder.Build())
}
@@ -51,6 +65,7 @@ func TestApisixConsumerValidator_MissingBasicAuthSecret(t
*testing.T) {
Namespace: "default",
},
Spec: apisixv2.ApisixConsumerSpec{
+ IngressClassName: "apisix",
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
SecretRef:
&corev1.LocalObjectReference{Name: "basic-auth"},
@@ -74,6 +89,7 @@ func TestApisixConsumerValidator_MultipleSecretWarnings(t
*testing.T) {
Namespace: "default",
},
Spec: apisixv2.ApisixConsumerSpec{
+ IngressClassName: "apisix",
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
SecretRef:
&corev1.LocalObjectReference{Name: "basic-auth"},
@@ -113,6 +129,7 @@ func
TestApisixConsumerValidator_NoWarningsWhenSecretsExist(t *testing.T) {
Namespace: "default",
},
Spec: apisixv2.ApisixConsumerSpec{
+ IngressClassName: "apisix",
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
SecretRef:
&corev1.LocalObjectReference{Name: "key-auth"},
diff --git a/internal/webhook/v1/apisixroute_webhook.go
b/internal/webhook/v1/apisixroute_webhook.go
index 6ad83c6b..aeb801ec 100644
--- a/internal/webhook/v1/apisixroute_webhook.go
+++ b/internal/webhook/v1/apisixroute_webhook.go
@@ -28,6 +28,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/internal/controller"
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
)
@@ -62,6 +63,9 @@ func (v *ApisixRouteCustomValidator) ValidateCreate(ctx
context.Context, obj run
return nil, fmt.Errorf("expected an ApisixRoute object but got
%T", obj)
}
apisixRouteLog.Info("Validation for ApisixRoute upon creation", "name",
route.GetName(), "namespace", route.GetNamespace())
+ if !controller.MatchesIngressClass(v.Client, apisixRouteLog, route) {
+ return nil, nil
+ }
return v.collectWarnings(ctx, route), nil
}
@@ -72,6 +76,9 @@ func (v *ApisixRouteCustomValidator) ValidateUpdate(ctx
context.Context, oldObj,
return nil, fmt.Errorf("expected an ApisixRoute object for the
newObj but got %T", newObj)
}
apisixRouteLog.Info("Validation for ApisixRoute upon update", "name",
route.GetName(), "namespace", route.GetNamespace())
+ if !controller.MatchesIngressClass(v.Client, apisixRouteLog, route) {
+ return nil, nil
+ }
return v.collectWarnings(ctx, route), nil
}
diff --git a/internal/webhook/v1/apisixroute_webhook_test.go
b/internal/webhook/v1/apisixroute_webhook_test.go
index cd60fb99..b8ca3aa2 100644
--- a/internal/webhook/v1/apisixroute_webhook_test.go
+++ b/internal/webhook/v1/apisixroute_webhook_test.go
@@ -21,12 +21,14 @@ import (
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
+ networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
)
func buildApisixRouteValidator(t *testing.T, objects ...runtime.Object)
*ApisixRouteCustomValidator {
@@ -34,12 +36,24 @@ func buildApisixRouteValidator(t *testing.T, objects
...runtime.Object) *ApisixR
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
+ require.NoError(t, networkingv1.AddToScheme(scheme))
require.NoError(t, apisixv2.AddToScheme(scheme))
- builder := fake.NewClientBuilder().WithScheme(scheme)
- if len(objects) > 0 {
- builder = builder.WithRuntimeObjects(objects...)
+ managed := []runtime.Object{
+ &networkingv1.IngressClass{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "apisix",
+ Annotations: map[string]string{
+
"ingressclass.kubernetes.io/is-default-class": "true",
+ },
+ },
+ Spec: networkingv1.IngressClassSpec{
+ Controller:
config.ControllerConfig.ControllerName,
+ },
+ },
}
+ allObjects := append(managed, objects...)
+ builder :=
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
return NewApisixRouteCustomValidator(builder.Build())
}
@@ -51,6 +65,7 @@ func TestApisixRouteValidator_MissingHTTPService(t
*testing.T) {
Namespace: "default",
},
Spec: apisixv2.ApisixRouteSpec{
+ IngressClassName: "apisix",
HTTP: []apisixv2.ApisixRouteHTTP{{
Name: "rule",
Backends: []apisixv2.ApisixRouteHTTPBackend{{
@@ -75,6 +90,7 @@ func TestApisixRouteValidator_MissingPluginSecret(t
*testing.T) {
Namespace: "default",
},
Spec: apisixv2.ApisixRouteSpec{
+ IngressClassName: "apisix",
HTTP: []apisixv2.ApisixRouteHTTP{{
Name: "rule",
Backends: []apisixv2.ApisixRouteHTTPBackend{{
@@ -106,6 +122,7 @@ func TestApisixRouteValidator_MissingStreamService(t
*testing.T) {
Namespace: "default",
},
Spec: apisixv2.ApisixRouteSpec{
+ IngressClassName: "apisix",
Stream: []apisixv2.ApisixRouteStream{{
Name: "stream",
Protocol: "TCP",
@@ -131,6 +148,7 @@ func TestApisixRouteValidator_NoWarnings(t *testing.T) {
Namespace: "default",
},
Spec: apisixv2.ApisixRouteSpec{
+ IngressClassName: "apisix",
HTTP: []apisixv2.ApisixRouteHTTP{{
Name: "rule",
Backends: []apisixv2.ApisixRouteHTTPBackend{{
diff --git a/internal/webhook/v1/apisixtls_webhook.go
b/internal/webhook/v1/apisixtls_webhook.go
index fe3faf7d..16bcf88f 100644
--- a/internal/webhook/v1/apisixtls_webhook.go
+++ b/internal/webhook/v1/apisixtls_webhook.go
@@ -28,6 +28,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/internal/controller"
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
)
@@ -62,6 +63,9 @@ func (v *ApisixTlsCustomValidator) ValidateCreate(ctx
context.Context, obj runti
return nil, fmt.Errorf("expected an ApisixTls object but got
%T", obj)
}
apisixTlsLog.Info("Validation for ApisixTls upon creation", "name",
tls.GetName(), "namespace", tls.GetNamespace())
+ if !controller.MatchesIngressClass(v.Client, apisixTlsLog, tls) {
+ return nil, nil
+ }
return v.collectWarnings(ctx, tls), nil
}
@@ -72,6 +76,9 @@ func (v *ApisixTlsCustomValidator) ValidateUpdate(ctx
context.Context, oldObj, n
return nil, fmt.Errorf("expected an ApisixTls object for the
newObj but got %T", newObj)
}
apisixTlsLog.Info("Validation for ApisixTls upon update", "name",
tls.GetName(), "namespace", tls.GetNamespace())
+ if !controller.MatchesIngressClass(v.Client, apisixTlsLog, tls) {
+ return nil, nil
+ }
return v.collectWarnings(ctx, tls), nil
}
diff --git a/internal/webhook/v1/apisixtls_webhook_test.go
b/internal/webhook/v1/apisixtls_webhook_test.go
index 45a764a5..205236f6 100644
--- a/internal/webhook/v1/apisixtls_webhook_test.go
+++ b/internal/webhook/v1/apisixtls_webhook_test.go
@@ -21,12 +21,14 @@ import (
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
+ networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
)
func buildApisixTlsValidator(t *testing.T, objects ...runtime.Object)
*ApisixTlsCustomValidator {
@@ -34,12 +36,24 @@ func buildApisixTlsValidator(t *testing.T, objects
...runtime.Object) *ApisixTls
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
+ require.NoError(t, networkingv1.AddToScheme(scheme))
require.NoError(t, apisixv2.AddToScheme(scheme))
- builder := fake.NewClientBuilder().WithScheme(scheme)
- if len(objects) > 0 {
- builder = builder.WithRuntimeObjects(objects...)
+ managed := []runtime.Object{
+ &networkingv1.IngressClass{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "apisix",
+ Annotations: map[string]string{
+
"ingressclass.kubernetes.io/is-default-class": "true",
+ },
+ },
+ Spec: networkingv1.IngressClassSpec{
+ Controller:
config.ControllerConfig.ControllerName,
+ },
+ },
}
+ allObjects := append(managed, objects...)
+ builder :=
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
return NewApisixTlsCustomValidator(builder.Build())
}
@@ -51,7 +65,8 @@ func newApisixTls() *apisixv2.ApisixTls {
Namespace: "default",
},
Spec: apisixv2.ApisixTlsSpec{
- Hosts: []apisixv2.HostType{"example.com"},
+ IngressClassName: "apisix",
+ Hosts: []apisixv2.HostType{"example.com"},
Secret: apisixv2.ApisixSecret{
Name: "server-cert",
Namespace: "default",
diff --git a/internal/webhook/v1/consumer_webhook.go
b/internal/webhook/v1/consumer_webhook.go
index 401550ef..0b74c714 100644
--- a/internal/webhook/v1/consumer_webhook.go
+++ b/internal/webhook/v1/consumer_webhook.go
@@ -28,6 +28,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
apisixv1alpha1
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ "github.com/apache/apisix-ingress-controller/internal/controller"
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
)
@@ -62,6 +63,9 @@ func (v *ConsumerCustomValidator) ValidateCreate(ctx
context.Context, obj runtim
return nil, fmt.Errorf("expected a Consumer object but got %T",
obj)
}
consumerLog.Info("Validation for Consumer upon creation", "name",
consumer.GetName(), "namespace", consumer.GetNamespace())
+ if !controller.MatchConsumerGatewayRef(ctx, v.Client, consumerLog,
consumer) {
+ return nil, nil
+ }
return v.collectWarnings(ctx, consumer), nil
}
@@ -72,6 +76,9 @@ func (v *ConsumerCustomValidator) ValidateUpdate(ctx
context.Context, oldObj, ne
return nil, fmt.Errorf("expected a Consumer object for the
newObj but got %T", newObj)
}
consumerLog.Info("Validation for Consumer upon update", "name",
consumer.GetName(), "namespace", consumer.GetNamespace())
+ if !controller.MatchConsumerGatewayRef(ctx, v.Client, consumerLog,
consumer) {
+ return nil, nil
+ }
return v.collectWarnings(ctx, consumer), nil
}
diff --git a/internal/webhook/v1/consumer_webhook_test.go
b/internal/webhook/v1/consumer_webhook_test.go
index ed6489ff..045bc12b 100644
--- a/internal/webhook/v1/consumer_webhook_test.go
+++ b/internal/webhook/v1/consumer_webhook_test.go
@@ -25,8 +25,10 @@ import (
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
apisixv1alpha1
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
)
func buildConsumerValidator(t *testing.T, objects ...runtime.Object)
*ConsumerCustomValidator {
@@ -35,11 +37,24 @@ func buildConsumerValidator(t *testing.T, objects
...runtime.Object) *ConsumerCu
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
require.NoError(t, apisixv1alpha1.AddToScheme(scheme))
-
- builder := fake.NewClientBuilder().WithScheme(scheme)
- if len(objects) > 0 {
- builder = builder.WithRuntimeObjects(objects...)
+ require.NoError(t, gatewayv1.Install(scheme))
+
+ managed := []runtime.Object{
+ &gatewayv1.GatewayClass{
+ ObjectMeta: metav1.ObjectMeta{Name:
"apisix-gateway-class"},
+ Spec: gatewayv1.GatewayClassSpec{
+ ControllerName:
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+ },
+ },
+ &gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-gateway",
Namespace: "default"},
+ Spec: gatewayv1.GatewaySpec{
+ GatewayClassName:
gatewayv1.ObjectName("apisix-gateway-class"),
+ },
+ },
}
+ allObjects := append(managed, objects...)
+ builder :=
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
return NewConsumerCustomValidator(builder.Build())
}
@@ -51,6 +66,7 @@ func TestConsumerValidator_MissingSecretDefaultNamespace(t
*testing.T) {
Namespace: "default",
},
Spec: apisixv1alpha1.ConsumerSpec{
+ GatewayRef: apisixv1alpha1.GatewayRef{Name:
"test-gateway"},
Credentials: []apisixv1alpha1.Credential{{
Type: "jwt-auth",
SecretRef: &apisixv1alpha1.SecretReference{
@@ -76,6 +92,7 @@ func TestConsumerValidator_MissingSecretCustomNamespace(t
*testing.T) {
Namespace: "default",
},
Spec: apisixv1alpha1.ConsumerSpec{
+ GatewayRef: apisixv1alpha1.GatewayRef{Name:
"test-gateway"},
Credentials: []apisixv1alpha1.Credential{{
Type: "jwt-auth",
SecretRef: &apisixv1alpha1.SecretReference{
@@ -102,6 +119,7 @@ func TestConsumerValidator_NoWarnings(t *testing.T) {
Namespace: "default",
},
Spec: apisixv1alpha1.ConsumerSpec{
+ GatewayRef: apisixv1alpha1.GatewayRef{Name:
"test-gateway"},
Credentials: []apisixv1alpha1.Credential{{
Type: "jwt-auth",
SecretRef: &apisixv1alpha1.SecretReference{
diff --git a/internal/webhook/v1/gateway_webhook.go
b/internal/webhook/v1/gateway_webhook.go
index e2c11ff4..bb21b236 100644
--- a/internal/webhook/v1/gateway_webhook.go
+++ b/internal/webhook/v1/gateway_webhook.go
@@ -19,18 +19,20 @@ import (
"context"
"fmt"
+ corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
- gatewaynetworkingk8siov1 "sigs.k8s.io/gateway-api/apis/v1"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
- "github.com/apache/apisix-ingress-controller/internal/controller/config"
internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
)
// nolint:unused
@@ -39,8 +41,8 @@ var gatewaylog = logf.Log.WithName("gateway-resource")
// SetupGatewayWebhookWithManager registers the webhook for Gateway in the
manager.
func SetupGatewayWebhookWithManager(mgr ctrl.Manager) error {
- return
ctrl.NewWebhookManagedBy(mgr).For(&gatewaynetworkingk8siov1.Gateway{}).
- WithValidator(&GatewayCustomValidator{Client: mgr.GetClient()}).
+ return ctrl.NewWebhookManagedBy(mgr).For(&gatewayv1.Gateway{}).
+ WithValidator(NewGatewayCustomValidator(mgr.GetClient())).
Complete()
}
@@ -54,33 +56,61 @@ func SetupGatewayWebhookWithManager(mgr ctrl.Manager) error
{
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen
from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to
be deeply copied.
type GatewayCustomValidator struct {
- Client client.Client
+ Client client.Client
+ checker reference.Checker
}
var _ webhook.CustomValidator = &GatewayCustomValidator{}
+func NewGatewayCustomValidator(c client.Client) *GatewayCustomValidator {
+ return &GatewayCustomValidator{
+ Client: c,
+ checker: reference.NewChecker(c, gatewaylog),
+ }
+}
+
// ValidateCreate implements webhook.CustomValidator so a webhook will be
registered for the type Gateway.
func (v *GatewayCustomValidator) ValidateCreate(ctx context.Context, obj
runtime.Object) (admission.Warnings, error) {
- gateway, ok := obj.(*gatewaynetworkingk8siov1.Gateway)
+ gateway, ok := obj.(*gatewayv1.Gateway)
if !ok {
return nil, fmt.Errorf("expected a Gateway object but got %T",
obj)
}
gatewaylog.Info("Validation for Gateway upon creation", "name",
gateway.GetName())
+ managed, err := isGatewayManaged(ctx, v.Client, gateway)
+ if err != nil {
+ gatewaylog.Error(err, "failed to decide controller ownership",
"name", gateway.GetName(), "namespace", gateway.GetNamespace())
+ return nil, nil
+ }
+ if !managed {
+ return nil, nil
+ }
+
warnings := v.warnIfMissingGatewayProxyForGateway(ctx, gateway)
+ warnings = append(warnings, v.collectReferenceWarnings(ctx, gateway)...)
return warnings, nil
}
// ValidateUpdate implements webhook.CustomValidator so a webhook will be
registered for the type Gateway.
func (v *GatewayCustomValidator) ValidateUpdate(ctx context.Context, oldObj,
newObj runtime.Object) (admission.Warnings, error) {
- gateway, ok := newObj.(*gatewaynetworkingk8siov1.Gateway)
+ gateway, ok := newObj.(*gatewayv1.Gateway)
if !ok {
return nil, fmt.Errorf("expected a Gateway object for the
newObj but got %T", newObj)
}
gatewaylog.Info("Validation for Gateway upon update", "name",
gateway.GetName())
+ managed, err := isGatewayManaged(ctx, v.Client, gateway)
+ if err != nil {
+ gatewaylog.Error(err, "failed to decide controller ownership",
"name", gateway.GetName(), "namespace", gateway.GetNamespace())
+ return nil, nil
+ }
+ if !managed {
+ return nil, nil
+ }
+
warnings := v.warnIfMissingGatewayProxyForGateway(ctx, gateway)
+ warnings = append(warnings, v.collectReferenceWarnings(ctx, gateway)...)
return warnings, nil
}
@@ -90,20 +120,56 @@ func (v *GatewayCustomValidator) ValidateDelete(_
context.Context, obj runtime.O
return nil, nil
}
-func (v *GatewayCustomValidator) warnIfMissingGatewayProxyForGateway(ctx
context.Context, gateway *gatewaynetworkingk8siov1.Gateway) admission.Warnings {
+func (v *GatewayCustomValidator) collectReferenceWarnings(ctx context.Context,
gateway *gatewayv1.Gateway) admission.Warnings {
+ if gateway == nil {
+ return nil
+ }
+
var warnings admission.Warnings
+ secretVisited := make(map[types.NamespacedName]struct{})
- // get gateway class
- gatewayClass := &gatewaynetworkingk8siov1.GatewayClass{}
- if err := v.Client.Get(ctx, client.ObjectKey{Name:
string(gateway.Spec.GatewayClassName)}, gatewayClass); err != nil {
- gatewaylog.Error(err, "failed to get gateway class", "gateway",
gateway.GetName(), "gatewayclass", gateway.Spec.GatewayClassName)
- return nil
+ addSecretWarning := func(nn types.NamespacedName) {
+ if nn.Name == "" || nn.Namespace == "" {
+ return
+ }
+ if _, seen := secretVisited[nn]; seen {
+ return
+ }
+ secretVisited[nn] = struct{}{}
+ warnings = append(warnings, v.checker.Secret(ctx,
reference.SecretRef{
+ Object: gateway,
+ NamespacedName: nn,
+ })...)
}
- // match controller
- if string(gatewayClass.Spec.ControllerName) !=
config.ControllerConfig.ControllerName {
- return nil
+
+ for _, listener := range gateway.Spec.Listeners {
+ if listener.TLS == nil {
+ continue
+ }
+ for _, ref := range listener.TLS.CertificateRefs {
+ if ref.Kind != nil && *ref.Kind !=
internaltypes.KindSecret {
+ continue
+ }
+ if ref.Group != nil && string(*ref.Group) !=
corev1.GroupName {
+ continue
+ }
+ nn := types.NamespacedName{
+ Namespace: gateway.GetNamespace(),
+ Name: string(ref.Name),
+ }
+ if ref.Namespace != nil && *ref.Namespace != "" {
+ nn.Namespace = string(*ref.Namespace)
+ }
+ addSecretWarning(nn)
+ }
}
+ return warnings
+}
+
+func (v *GatewayCustomValidator) warnIfMissingGatewayProxyForGateway(ctx
context.Context, gateway *gatewayv1.Gateway) admission.Warnings {
+ var warnings admission.Warnings
+
infra := gateway.Spec.Infrastructure
if infra == nil || infra.ParametersRef == nil {
return nil
diff --git a/internal/webhook/v1/gateway_webhook_test.go
b/internal/webhook/v1/gateway_webhook_test.go
new file mode 100644
index 00000000..c47be6bc
--- /dev/null
+++ b/internal/webhook/v1/gateway_webhook_test.go
@@ -0,0 +1,113 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func buildGatewayValidator(t *testing.T, objects ...runtime.Object)
*GatewayCustomValidator {
+ t.Helper()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, clientgoscheme.AddToScheme(scheme))
+ require.NoError(t, gatewayv1.Install(scheme))
+
+ builder := fake.NewClientBuilder().WithScheme(scheme)
+ if len(objects) > 0 {
+ builder = builder.WithRuntimeObjects(objects...)
+ }
+
+ return NewGatewayCustomValidator(builder.Build())
+}
+
+func TestGatewayCustomValidator_WarnsWhenTLSSecretMissing(t *testing.T) {
+ className := gatewayv1.ObjectName("apisix")
+ gatewayClass := &gatewayv1.GatewayClass{
+ ObjectMeta: metav1.ObjectMeta{Name: string(className)},
+ Spec: gatewayv1.GatewayClassSpec{
+ ControllerName:
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+ },
+ }
+ validator := buildGatewayValidator(t, gatewayClass)
+
+ gateway := &gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace:
"default"},
+ Spec: gatewayv1.GatewaySpec{
+ GatewayClassName: className,
+ Listeners: []gatewayv1.Listener{{
+ Name: "https",
+ Port: 443,
+ Protocol: gatewayv1.HTTPSProtocolType,
+ TLS: &gatewayv1.GatewayTLSConfig{
+ CertificateRefs:
[]gatewayv1.SecretObjectReference{{
+ Name: "missing-cert",
+ }},
+ },
+ }},
+ },
+ }
+
+ warnings, err := validator.ValidateCreate(context.Background(), gateway)
+ require.NoError(t, err)
+ require.Len(t, warnings, 1)
+ assert.Equal(t, warnings[0], "Referenced Secret 'default/missing-cert'
not found")
+}
+
+func TestGatewayCustomValidator_NoWarningsWhenSecretExists(t *testing.T) {
+ className := gatewayv1.ObjectName("apisix")
+ gatewayClass := &gatewayv1.GatewayClass{
+ ObjectMeta: metav1.ObjectMeta{Name: string(className)},
+ Spec: gatewayv1.GatewayClassSpec{
+ ControllerName:
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+ },
+ }
+ secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name:
"tls-cert", Namespace: "default"}}
+ validator := buildGatewayValidator(t, gatewayClass, secret)
+
+ gateway := &gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace:
"default"},
+ Spec: gatewayv1.GatewaySpec{
+ GatewayClassName: className,
+ Listeners: []gatewayv1.Listener{{
+ Name: "https",
+ Port: 443,
+ Protocol: gatewayv1.HTTPSProtocolType,
+ TLS: &gatewayv1.GatewayTLSConfig{
+ CertificateRefs:
[]gatewayv1.SecretObjectReference{{
+ Name: "tls-cert",
+ }},
+ },
+ }},
+ },
+ }
+
+ warnings, err := validator.ValidateCreate(context.Background(), gateway)
+ require.NoError(t, err)
+ assert.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/grpcroute_webhook.go
b/internal/webhook/v1/grpcroute_webhook.go
new file mode 100644
index 00000000..26d0e9f3
--- /dev/null
+++ b/internal/webhook/v1/grpcroute_webhook.go
@@ -0,0 +1,162 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "context"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+)
+
+var grpcRouteLog = logf.Log.WithName("grpcroute-resource")
+
+func SetupGRPCRouteWebhookWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewWebhookManagedBy(mgr).
+ For(&gatewayv1.GRPCRoute{}).
+ WithValidator(NewGRPCRouteCustomValidator(mgr.GetClient())).
+ Complete()
+}
+
+//
+kubebuilder:webhook:path=/validate-gateway-networking-k8s-io-v1-grpcroute,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.networking.k8s.io,resources=grpcroutes,verbs=create;update,versions=v1,name=vgrpcroute-v1.kb.io,admissionReviewVersions=v1
+
+type GRPCRouteCustomValidator struct {
+ Client client.Client
+ checker reference.Checker
+}
+
+var _ webhook.CustomValidator = &GRPCRouteCustomValidator{}
+
+func NewGRPCRouteCustomValidator(c client.Client) *GRPCRouteCustomValidator {
+ return &GRPCRouteCustomValidator{
+ Client: c,
+ checker: reference.NewChecker(c, grpcRouteLog),
+ }
+}
+
+func (v *GRPCRouteCustomValidator) ValidateCreate(ctx context.Context, obj
runtime.Object) (admission.Warnings, error) {
+ route, ok := obj.(*gatewayv1.GRPCRoute)
+ if !ok {
+ return nil, fmt.Errorf("expected a GRPCRoute object but got
%T", obj)
+ }
+ grpcRouteLog.Info("Validation for GRPCRoute upon creation", "name",
route.GetName(), "namespace", route.GetNamespace())
+ managed, err := isGRPCRouteManaged(ctx, v.Client, route)
+ if err != nil {
+ grpcRouteLog.Error(err, "failed to decide controller
ownership", "name", route.GetName(), "namespace", route.GetNamespace())
+ return nil, nil
+ }
+ if !managed {
+ return nil, nil
+ }
+
+ return v.collectWarnings(ctx, route), nil
+}
+
+func (v *GRPCRouteCustomValidator) ValidateUpdate(ctx context.Context, oldObj,
newObj runtime.Object) (admission.Warnings, error) {
+ route, ok := newObj.(*gatewayv1.GRPCRoute)
+ if !ok {
+ return nil, fmt.Errorf("expected a GRPCRoute object for the
newObj but got %T", newObj)
+ }
+ grpcRouteLog.Info("Validation for GRPCRoute upon update", "name",
route.GetName(), "namespace", route.GetNamespace())
+ managed, err := isGRPCRouteManaged(ctx, v.Client, route)
+ if err != nil {
+ grpcRouteLog.Error(err, "failed to decide controller
ownership", "name", route.GetName(), "namespace", route.GetNamespace())
+ return nil, nil
+ }
+ if !managed {
+ return nil, nil
+ }
+
+ return v.collectWarnings(ctx, route), nil
+}
+
+func (*GRPCRouteCustomValidator) ValidateDelete(context.Context,
runtime.Object) (admission.Warnings, error) {
+ return nil, nil
+}
+
+func (v *GRPCRouteCustomValidator) collectWarnings(ctx context.Context, route
*gatewayv1.GRPCRoute) admission.Warnings {
+ serviceVisited := make(map[types.NamespacedName]struct{})
+ namespace := route.GetNamespace()
+
+ var warnings admission.Warnings
+
+ addServiceWarning := func(nn types.NamespacedName) {
+ if nn.Name == "" || nn.Namespace == "" {
+ return
+ }
+ if _, seen := serviceVisited[nn]; seen {
+ return
+ }
+ serviceVisited[nn] = struct{}{}
+ warnings = append(warnings, v.checker.Service(ctx,
reference.ServiceRef{
+ Object: route,
+ NamespacedName: nn,
+ })...)
+ }
+
+ addBackendRef := func(ns string, name string, group *gatewayv1.Group,
kind *gatewayv1.Kind) {
+ if name == "" {
+ return
+ }
+ if group != nil && string(*group) != corev1.GroupName {
+ return
+ }
+ if kind != nil && *kind != internaltypes.KindService {
+ return
+ }
+ nn := types.NamespacedName{Namespace: ns, Name: name}
+ addServiceWarning(nn)
+ }
+
+ processFilters := func(filters []gatewayv1.GRPCRouteFilter) {
+ for _, filter := range filters {
+ if filter.RequestMirror != nil {
+ targetNamespace := namespace
+ if filter.RequestMirror.BackendRef.Namespace !=
nil && *filter.RequestMirror.BackendRef.Namespace != "" {
+ targetNamespace =
string(*filter.RequestMirror.BackendRef.Namespace)
+ }
+ addBackendRef(targetNamespace,
string(filter.RequestMirror.BackendRef.Name),
+ filter.RequestMirror.BackendRef.Group,
filter.RequestMirror.BackendRef.Kind)
+ }
+ }
+ }
+
+ for _, rule := range route.Spec.Rules {
+ for _, backend := range rule.BackendRefs {
+ targetNamespace := namespace
+ if backend.Namespace != nil && *backend.Namespace != ""
{
+ targetNamespace = string(*backend.Namespace)
+ }
+ addBackendRef(targetNamespace, string(backend.Name),
backend.Group, backend.Kind)
+ processFilters(backend.Filters)
+ }
+
+ processFilters(rule.Filters)
+ }
+
+ return warnings
+}
diff --git a/internal/webhook/v1/grpcroute_webhook_test.go
b/internal/webhook/v1/grpcroute_webhook_test.go
new file mode 100644
index 00000000..4bf3a686
--- /dev/null
+++ b/internal/webhook/v1/grpcroute_webhook_test.go
@@ -0,0 +1,116 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func buildGRPCRouteValidator(t *testing.T, objects ...runtime.Object)
*GRPCRouteCustomValidator {
+ t.Helper()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, clientgoscheme.AddToScheme(scheme))
+ require.NoError(t, gatewayv1.Install(scheme))
+
+ managed := []runtime.Object{
+ &gatewayv1.GatewayClass{
+ ObjectMeta: metav1.ObjectMeta{Name:
"apisix-gateway-class"},
+ Spec: gatewayv1.GatewayClassSpec{
+ ControllerName:
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+ },
+ },
+ &gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-gateway",
Namespace: "default"},
+ Spec: gatewayv1.GatewaySpec{
+ GatewayClassName:
gatewayv1.ObjectName("apisix-gateway-class"),
+ },
+ },
+ }
+ allObjects := append(managed, objects...)
+ builder :=
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
+
+ return NewGRPCRouteCustomValidator(builder.Build())
+}
+
+func TestGRPCRouteCustomValidator_WarnsForMissingService(t *testing.T) {
+ route := &gatewayv1.GRPCRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace:
"default"},
+ Spec: gatewayv1.GRPCRouteSpec{
+ CommonRouteSpec: gatewayv1.CommonRouteSpec{
+ ParentRefs: []gatewayv1.ParentReference{{
+ Name:
gatewayv1.ObjectName("test-gateway"),
+ }},
+ },
+ Rules: []gatewayv1.GRPCRouteRule{{
+ BackendRefs: []gatewayv1.GRPCBackendRef{{
+ BackendRef: gatewayv1.BackendRef{
+ BackendObjectReference:
gatewayv1.BackendObjectReference{
+ Name:
gatewayv1.ObjectName("missing"),
+ },
+ },
+ }},
+ }},
+ },
+ }
+
+ validator := buildGRPCRouteValidator(t)
+ warnings, err := validator.ValidateCreate(context.Background(), route)
+ require.NoError(t, err)
+ require.Len(t, warnings, 1)
+ assert.Equal(t, warnings[0], "Referenced Service 'default/missing' not
found")
+}
+
+func TestGRPCRouteCustomValidator_NoWarningsWhenServiceExists(t *testing.T) {
+ service := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name:
"backend", Namespace: "default"}}
+ validator := buildGRPCRouteValidator(t, service)
+
+ route := &gatewayv1.GRPCRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace:
"default"},
+ Spec: gatewayv1.GRPCRouteSpec{
+ CommonRouteSpec: gatewayv1.CommonRouteSpec{
+ ParentRefs: []gatewayv1.ParentReference{{
+ Name:
gatewayv1.ObjectName("test-gateway"),
+ }},
+ },
+ Rules: []gatewayv1.GRPCRouteRule{{
+ BackendRefs: []gatewayv1.GRPCBackendRef{{
+ BackendRef: gatewayv1.BackendRef{
+ BackendObjectReference:
gatewayv1.BackendObjectReference{
+ Name:
gatewayv1.ObjectName("backend"),
+ },
+ },
+ }},
+ }},
+ },
+ }
+
+ warnings, err := validator.ValidateCreate(context.Background(), route)
+ require.NoError(t, err)
+ assert.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/httproute_webhook.go
b/internal/webhook/v1/httproute_webhook.go
new file mode 100644
index 00000000..191cd9b6
--- /dev/null
+++ b/internal/webhook/v1/httproute_webhook.go
@@ -0,0 +1,162 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "context"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+)
+
+var httpRouteLog = logf.Log.WithName("httproute-resource")
+
+func SetupHTTPRouteWebhookWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewWebhookManagedBy(mgr).
+ For(&gatewayv1.HTTPRoute{}).
+ WithValidator(NewHTTPRouteCustomValidator(mgr.GetClient())).
+ Complete()
+}
+
+//
+kubebuilder:webhook:path=/validate-gateway-networking-k8s-io-v1-httproute,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.networking.k8s.io,resources=httproutes,verbs=create;update,versions=v1,name=vhttproute-v1.kb.io,admissionReviewVersions=v1
+
+type HTTPRouteCustomValidator struct {
+ Client client.Client
+ checker reference.Checker
+}
+
+var _ webhook.CustomValidator = &HTTPRouteCustomValidator{}
+
+func NewHTTPRouteCustomValidator(c client.Client) *HTTPRouteCustomValidator {
+ return &HTTPRouteCustomValidator{
+ Client: c,
+ checker: reference.NewChecker(c, httpRouteLog),
+ }
+}
+
+func (v *HTTPRouteCustomValidator) ValidateCreate(ctx context.Context, obj
runtime.Object) (admission.Warnings, error) {
+ route, ok := obj.(*gatewayv1.HTTPRoute)
+ if !ok {
+ return nil, fmt.Errorf("expected a HTTPRoute object but got
%T", obj)
+ }
+ httpRouteLog.Info("Validation for HTTPRoute upon creation", "name",
route.GetName(), "namespace", route.GetNamespace())
+ managed, err := isHTTPRouteManaged(ctx, v.Client, route)
+ if err != nil {
+ httpRouteLog.Error(err, "failed to decide controller
ownership", "name", route.GetName(), "namespace", route.GetNamespace())
+ return nil, nil
+ }
+ if !managed {
+ return nil, nil
+ }
+
+ return v.collectWarnings(ctx, route), nil
+}
+
+func (v *HTTPRouteCustomValidator) ValidateUpdate(ctx context.Context, oldObj,
newObj runtime.Object) (admission.Warnings, error) {
+ route, ok := newObj.(*gatewayv1.HTTPRoute)
+ if !ok {
+ return nil, fmt.Errorf("expected a HTTPRoute object for the
newObj but got %T", newObj)
+ }
+ httpRouteLog.Info("Validation for HTTPRoute upon update", "name",
route.GetName(), "namespace", route.GetNamespace())
+ managed, err := isHTTPRouteManaged(ctx, v.Client, route)
+ if err != nil {
+ httpRouteLog.Error(err, "failed to decide controller
ownership", "name", route.GetName(), "namespace", route.GetNamespace())
+ return nil, nil
+ }
+ if !managed {
+ return nil, nil
+ }
+
+ return v.collectWarnings(ctx, route), nil
+}
+
+func (*HTTPRouteCustomValidator) ValidateDelete(context.Context,
runtime.Object) (admission.Warnings, error) {
+ return nil, nil
+}
+
+func (v *HTTPRouteCustomValidator) collectWarnings(ctx context.Context, route
*gatewayv1.HTTPRoute) admission.Warnings {
+ serviceVisited := make(map[types.NamespacedName]struct{})
+ namespace := route.GetNamespace()
+
+ var warnings admission.Warnings
+
+ addServiceWarning := func(nn types.NamespacedName) {
+ if nn.Name == "" || nn.Namespace == "" {
+ return
+ }
+ if _, seen := serviceVisited[nn]; seen {
+ return
+ }
+ serviceVisited[nn] = struct{}{}
+ warnings = append(warnings, v.checker.Service(ctx,
reference.ServiceRef{
+ Object: route,
+ NamespacedName: nn,
+ })...)
+ }
+
+ addBackendRef := func(ns string, name string, group *gatewayv1.Group,
kind *gatewayv1.Kind) {
+ if name == "" {
+ return
+ }
+ if group != nil && string(*group) != corev1.GroupName {
+ return
+ }
+ if kind != nil && *kind != internaltypes.KindService {
+ return
+ }
+ nn := types.NamespacedName{Namespace: ns, Name: name}
+ addServiceWarning(nn)
+ }
+
+ processFilters := func(filters []gatewayv1.HTTPRouteFilter) {
+ for _, filter := range filters {
+ if filter.RequestMirror != nil {
+ targetNamespace := namespace
+ if filter.RequestMirror.BackendRef.Namespace !=
nil && *filter.RequestMirror.BackendRef.Namespace != "" {
+ targetNamespace =
string(*filter.RequestMirror.BackendRef.Namespace)
+ }
+ addBackendRef(targetNamespace,
string(filter.RequestMirror.BackendRef.Name),
+ filter.RequestMirror.BackendRef.Group,
filter.RequestMirror.BackendRef.Kind)
+ }
+ }
+ }
+
+ for _, rule := range route.Spec.Rules {
+ for _, backend := range rule.BackendRefs {
+ targetNamespace := namespace
+ if backend.Namespace != nil && *backend.Namespace != ""
{
+ targetNamespace = string(*backend.Namespace)
+ }
+ addBackendRef(targetNamespace, string(backend.Name),
backend.Group, backend.Kind)
+ processFilters(backend.Filters)
+ }
+
+ processFilters(rule.Filters)
+ }
+
+ return warnings
+}
diff --git a/internal/webhook/v1/httproute_webhook_test.go
b/internal/webhook/v1/httproute_webhook_test.go
new file mode 100644
index 00000000..1dd4fe0c
--- /dev/null
+++ b/internal/webhook/v1/httproute_webhook_test.go
@@ -0,0 +1,138 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func buildHTTPRouteValidator(t *testing.T, objects ...runtime.Object)
*HTTPRouteCustomValidator {
+ t.Helper()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, clientgoscheme.AddToScheme(scheme))
+ require.NoError(t, gatewayv1.Install(scheme))
+
+ managed := []runtime.Object{
+ &gatewayv1.GatewayClass{
+ ObjectMeta: metav1.ObjectMeta{Name:
"apisix-gateway-class"},
+ Spec: gatewayv1.GatewayClassSpec{
+ ControllerName:
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+ },
+ },
+ &gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-gateway",
Namespace: "default"},
+ Spec: gatewayv1.GatewaySpec{
+ GatewayClassName:
gatewayv1.ObjectName("apisix-gateway-class"),
+ },
+ },
+ }
+ allObjects := append(managed, objects...)
+ builder :=
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
+
+ return NewHTTPRouteCustomValidator(builder.Build())
+}
+
+func TestHTTPRouteCustomValidator_WarnsForMissingReferences(t *testing.T) {
+ route := &gatewayv1.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace:
"default"},
+ Spec: gatewayv1.HTTPRouteSpec{
+ CommonRouteSpec: gatewayv1.CommonRouteSpec{
+ ParentRefs: []gatewayv1.ParentReference{{
+ Name:
gatewayv1.ObjectName("test-gateway"),
+ }},
+ },
+ Rules: []gatewayv1.HTTPRouteRule{{
+ BackendRefs: []gatewayv1.HTTPBackendRef{{
+ BackendRef: gatewayv1.BackendRef{
+ BackendObjectReference:
gatewayv1.BackendObjectReference{
+ Name:
gatewayv1.ObjectName("missing-svc"),
+ },
+ },
+ }},
+ Filters: []gatewayv1.HTTPRouteFilter{{
+ Type:
gatewayv1.HTTPRouteFilterRequestMirror,
+ RequestMirror:
&gatewayv1.HTTPRequestMirrorFilter{
+ BackendRef:
gatewayv1.BackendObjectReference{
+ Name:
gatewayv1.ObjectName("mirror-svc"),
+ },
+ },
+ }},
+ }},
+ },
+ }
+
+ validator := buildHTTPRouteValidator(t)
+ warnings, err := validator.ValidateCreate(context.Background(), route)
+ require.NoError(t, err)
+ assert.ElementsMatch(t, []string{
+ "Referenced Service 'default/mirror-svc' not found",
+ "Referenced Service 'default/missing-svc' not found",
+ }, warnings)
+}
+
+func TestHTTPRouteCustomValidator_NoWarningsWhenResourcesExist(t *testing.T) {
+ objects := []runtime.Object{
+ &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "primary",
Namespace: "default"}},
+ &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "mirror",
Namespace: "default"}},
+ }
+
+ validator := buildHTTPRouteValidator(t, objects...)
+
+ route := &gatewayv1.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace:
"default"},
+ Spec: gatewayv1.HTTPRouteSpec{
+ CommonRouteSpec: gatewayv1.CommonRouteSpec{
+ ParentRefs: []gatewayv1.ParentReference{{
+ Name:
gatewayv1.ObjectName("test-gateway"),
+ }},
+ },
+ Rules: []gatewayv1.HTTPRouteRule{{
+ BackendRefs: []gatewayv1.HTTPBackendRef{{
+ BackendRef: gatewayv1.BackendRef{
+ BackendObjectReference:
gatewayv1.BackendObjectReference{
+ Name:
gatewayv1.ObjectName("primary"),
+ },
+ },
+ }},
+ Filters: []gatewayv1.HTTPRouteFilter{{
+ Type:
gatewayv1.HTTPRouteFilterRequestMirror,
+ RequestMirror:
&gatewayv1.HTTPRequestMirrorFilter{
+ BackendRef:
gatewayv1.BackendObjectReference{
+ Name:
gatewayv1.ObjectName("mirror"),
+ },
+ },
+ }},
+ }},
+ },
+ }
+
+ warnings, err := validator.ValidateCreate(context.Background(), route)
+ require.NoError(t, err)
+ assert.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/ingress_webhook.go
b/internal/webhook/v1/ingress_webhook.go
index 777d1e1b..10e18ab4 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -20,12 +20,17 @@ import (
"fmt"
"slices"
- networkingk8siov1 "k8s.io/api/networking/v1"
+ networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+ "github.com/apache/apisix-ingress-controller/internal/controller"
+
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
)
var ingresslog = logf.Log.WithName("ingress-resource")
@@ -75,7 +80,7 @@ var unsupportedAnnotations = []string{
// checkUnsupportedAnnotations checks if the Ingress contains any unsupported
annotations
// and returns appropriate warnings
-func checkUnsupportedAnnotations(ingress *networkingk8siov1.Ingress)
admission.Warnings {
+func checkUnsupportedAnnotations(ingress *networkingv1.Ingress)
admission.Warnings {
var warnings admission.Warnings
if len(ingress.Annotations) == 0 {
@@ -98,8 +103,8 @@ func checkUnsupportedAnnotations(ingress
*networkingk8siov1.Ingress) admission.W
// SetupIngressWebhookWithManager registers the webhook for Ingress in the
manager.
func SetupIngressWebhookWithManager(mgr ctrl.Manager) error {
- return ctrl.NewWebhookManagedBy(mgr).For(&networkingk8siov1.Ingress{}).
- WithValidator(&IngressCustomValidator{}).
+ return ctrl.NewWebhookManagedBy(mgr).For(&networkingv1.Ingress{}).
+ WithValidator(NewIngressCustomValidator(mgr.GetClient())).
Complete()
}
@@ -112,34 +117,52 @@ func SetupIngressWebhookWithManager(mgr ctrl.Manager)
error {
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen
from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to
be deeply copied.
-type IngressCustomValidator struct{}
+type IngressCustomValidator struct {
+ Client client.Client
+ checker reference.Checker
+}
var _ webhook.CustomValidator = &IngressCustomValidator{}
+func NewIngressCustomValidator(c client.Client) *IngressCustomValidator {
+ return &IngressCustomValidator{
+ Client: c,
+ checker: reference.NewChecker(c, ingresslog),
+ }
+}
+
// ValidateCreate implements webhook.CustomValidator so a webhook will be
registered for the type Ingress.
-func (v *IngressCustomValidator) ValidateCreate(_ context.Context, obj
runtime.Object) (admission.Warnings, error) {
- ingress, ok := obj.(*networkingk8siov1.Ingress)
+func (v *IngressCustomValidator) ValidateCreate(ctx context.Context, obj
runtime.Object) (admission.Warnings, error) {
+ ingress, ok := obj.(*networkingv1.Ingress)
if !ok {
return nil, fmt.Errorf("expected a Ingress object but got %T",
obj)
}
ingresslog.Info("Validation for Ingress upon creation", "name",
ingress.GetName(), "namespace", ingress.GetNamespace())
+ if !controller.MatchesIngressClass(v.Client, ingresslog, ingress) {
+ return nil, nil
+ }
// Check for unsupported annotations and generate warnings
warnings := checkUnsupportedAnnotations(ingress)
+ warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...)
return warnings, nil
}
// ValidateUpdate implements webhook.CustomValidator so a webhook will be
registered for the type Ingress.
-func (v *IngressCustomValidator) ValidateUpdate(_ context.Context, oldObj,
newObj runtime.Object) (admission.Warnings, error) {
- ingress, ok := newObj.(*networkingk8siov1.Ingress)
+func (v *IngressCustomValidator) ValidateUpdate(ctx context.Context, oldObj,
newObj runtime.Object) (admission.Warnings, error) {
+ ingress, ok := newObj.(*networkingv1.Ingress)
if !ok {
return nil, fmt.Errorf("expected a Ingress object for the
newObj but got %T", newObj)
}
ingresslog.Info("Validation for Ingress upon update", "name",
ingress.GetName(), "namespace", ingress.GetNamespace())
+ if !controller.MatchesIngressClass(v.Client, ingresslog, ingress) {
+ return nil, nil
+ }
// Check for unsupported annotations and generate warnings
warnings := checkUnsupportedAnnotations(ingress)
+ warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...)
return warnings, nil
}
@@ -148,3 +171,58 @@ func (v *IngressCustomValidator) ValidateUpdate(_
context.Context, oldObj, newOb
func (v *IngressCustomValidator) ValidateDelete(ctx context.Context, obj
runtime.Object) (admission.Warnings, error) {
return nil, nil
}
+
+func (v *IngressCustomValidator) collectReferenceWarnings(ctx context.Context,
ingress *networkingv1.Ingress) admission.Warnings {
+ serviceVisited := make(map[types.NamespacedName]struct{})
+ secretVisited := make(map[types.NamespacedName]struct{})
+ namespace := ingress.GetNamespace()
+
+ var warnings admission.Warnings
+
+ addServiceWarning := func(name string) {
+ if name == "" {
+ return
+ }
+ nn := types.NamespacedName{Namespace: namespace, Name: name}
+ if _, seen := serviceVisited[nn]; seen {
+ return
+ }
+ serviceVisited[nn] = struct{}{}
+ warnings = append(warnings, v.checker.Service(ctx,
reference.ServiceRef{
+ Object: ingress,
+ NamespacedName: nn,
+ })...)
+ }
+
+ addSecretWarning := func(name string) {
+ if name == "" {
+ return
+ }
+ nn := types.NamespacedName{Namespace: namespace, Name: name}
+ if _, seen := secretVisited[nn]; seen {
+ return
+ }
+ secretVisited[nn] = struct{}{}
+ warnings = append(warnings, v.checker.Secret(ctx,
reference.SecretRef{
+ Object: ingress,
+ NamespacedName: nn,
+ })...)
+ }
+
+ for _, rule := range ingress.Spec.Rules {
+ if rule.HTTP == nil {
+ continue
+ }
+ for _, path := range rule.HTTP.Paths {
+ if path.Backend.Service != nil {
+ addServiceWarning(path.Backend.Service.Name)
+ }
+ }
+ }
+
+ for _, tls := range ingress.Spec.TLS {
+ addSecretWarning(tls.SecretName)
+ }
+
+ return warnings
+}
diff --git a/internal/webhook/v1/ingress_webhook_test.go
b/internal/webhook/v1/ingress_webhook_test.go
index 5ae9bd2a..89f3fa6d 100644
--- a/internal/webhook/v1/ingress_webhook_test.go
+++ b/internal/webhook/v1/ingress_webhook_test.go
@@ -21,13 +21,52 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- networkingk8siov1 "k8s.io/api/networking/v1"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
+
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
)
+func buildIngressValidator(t *testing.T, objects ...runtime.Object)
*IngressCustomValidator {
+ t.Helper()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, clientgoscheme.AddToScheme(scheme))
+ require.NoError(t, networkingv1.AddToScheme(scheme))
+ require.NoError(t, apisixv2.AddToScheme(scheme))
+
+ managed := []runtime.Object{
+ &networkingv1.IngressClass{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "apisix",
+ Annotations: map[string]string{
+
"ingressclass.kubernetes.io/is-default-class": "true",
+ },
+ },
+ Spec: networkingv1.IngressClassSpec{
+ Controller:
config.ControllerConfig.ControllerName,
+ },
+ },
+ }
+ allObjects := append(managed, objects...)
+ builder := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithIndex(&networkingv1.IngressClass{}, indexer.IngressClass,
indexer.IngressClassIndexFunc).
+ WithRuntimeObjects(allObjects...)
+
+ return NewIngressCustomValidator(builder.Build())
+}
+
func TestIngressCustomValidator_ValidateCreate_UnsupportedAnnotations(t
*testing.T) {
- validator := IngressCustomValidator{}
- obj := &networkingk8siov1.Ingress{
+ validator := buildIngressValidator(t)
+ obj := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
@@ -49,8 +88,8 @@ func
TestIngressCustomValidator_ValidateCreate_UnsupportedAnnotations(t *testing
}
func TestIngressCustomValidator_ValidateCreate_SupportedAnnotations(t
*testing.T) {
- validator := IngressCustomValidator{}
- obj := &networkingk8siov1.Ingress{
+ validator := buildIngressValidator(t)
+ obj := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
@@ -66,9 +105,9 @@ func
TestIngressCustomValidator_ValidateCreate_SupportedAnnotations(t *testing.T
}
func TestIngressCustomValidator_ValidateUpdate_UnsupportedAnnotations(t
*testing.T) {
- validator := IngressCustomValidator{}
- oldObj := &networkingk8siov1.Ingress{}
- obj := &networkingk8siov1.Ingress{
+ validator := buildIngressValidator(t)
+ oldObj := &networkingv1.Ingress{}
+ obj := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
@@ -90,8 +129,8 @@ func
TestIngressCustomValidator_ValidateUpdate_UnsupportedAnnotations(t *testing
}
func TestIngressCustomValidator_ValidateDelete_NoWarnings(t *testing.T) {
- validator := IngressCustomValidator{}
- obj := &networkingk8siov1.Ingress{
+ validator := buildIngressValidator(t)
+ obj := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
@@ -107,8 +146,8 @@ func TestIngressCustomValidator_ValidateDelete_NoWarnings(t
*testing.T) {
}
func TestIngressCustomValidator_ValidateCreate_NoAnnotations(t *testing.T) {
- validator := IngressCustomValidator{}
- obj := &networkingk8siov1.Ingress{
+ validator := buildIngressValidator(t)
+ obj := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
@@ -119,3 +158,54 @@ func
TestIngressCustomValidator_ValidateCreate_NoAnnotations(t *testing.T) {
assert.NoError(t, err)
assert.Empty(t, warnings)
}
+
+func TestIngressCustomValidator_WarnsForMissingServiceAndSecret(t *testing.T) {
+ validator := buildIngressValidator(t)
+ obj := &networkingv1.Ingress{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ingress", Namespace:
"default"},
+ Spec: networkingv1.IngressSpec{
+ Rules: []networkingv1.IngressRule{{
+ IngressRuleValue:
networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{
+ Paths: []networkingv1.HTTPIngressPath{{
+ Backend:
networkingv1.IngressBackend{
+ Service:
&networkingv1.IngressServiceBackend{Name: "default-svc"},
+ },
+ }},
+ }},
+ }},
+ TLS: []networkingv1.IngressTLS{{SecretName:
"missing-cert"}},
+ },
+ }
+
+ warnings, err := validator.ValidateCreate(context.Background(), obj)
+ require.NoError(t, err)
+ require.Len(t, warnings, 2)
+ require.Contains(t, warnings, "Referenced Service 'default/default-svc'
not found")
+ require.Contains(t, warnings, "Referenced Secret 'default/missing-cert'
not found")
+}
+
+func TestIngressCustomValidator_NoWarningsWhenReferencesExist(t *testing.T) {
+ service := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name:
"default-svc", Namespace: "default"}}
+ secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name:
"tls-cert", Namespace: "default"}}
+ validator := buildIngressValidator(t, service, secret)
+
+ obj := &networkingv1.Ingress{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ingress", Namespace:
"default"},
+ Spec: networkingv1.IngressSpec{
+ Rules: []networkingv1.IngressRule{{
+ IngressRuleValue:
networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{
+ Paths: []networkingv1.HTTPIngressPath{{
+ Backend:
networkingv1.IngressBackend{
+ Service:
&networkingv1.IngressServiceBackend{Name: "default-svc"},
+ },
+ }},
+ }},
+ }},
+ TLS: []networkingv1.IngressTLS{{SecretName:
"tls-cert"}},
+ },
+ }
+
+ warnings, err := validator.ValidateCreate(context.Background(), obj)
+ require.NoError(t, err)
+ assert.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/ownership.go b/internal/webhook/v1/ownership.go
new file mode 100644
index 00000000..d2a72eae
--- /dev/null
+++ b/internal/webhook/v1/ownership.go
@@ -0,0 +1,103 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "context"
+
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func isGatewayManaged(ctx context.Context, c client.Client, gateway
*gatewayv1.Gateway) (bool, error) {
+ if gateway == nil {
+ return false, nil
+ }
+
+ className := string(gateway.Spec.GatewayClassName)
+ if className == "" {
+ return false, nil
+ }
+
+ var gatewayClass gatewayv1.GatewayClass
+ if err := c.Get(ctx, client.ObjectKey{Name: className}, &gatewayClass);
err != nil {
+ if client.IgnoreNotFound(err) == nil {
+ return false, nil
+ }
+ return false, err
+ }
+
+ return string(gatewayClass.Spec.ControllerName) ==
config.ControllerConfig.ControllerName, nil
+}
+
+func isHTTPRouteManaged(ctx context.Context, c client.Client, route
*gatewayv1.HTTPRoute) (bool, error) {
+ if route == nil {
+ return false, nil
+ }
+ return routeReferencesManagedGateway(ctx, c, route.Spec.ParentRefs,
route.Namespace)
+}
+
+func isGRPCRouteManaged(ctx context.Context, c client.Client, route
*gatewayv1.GRPCRoute) (bool, error) {
+ if route == nil {
+ return false, nil
+ }
+ return routeReferencesManagedGateway(ctx, c, route.Spec.ParentRefs,
route.Namespace)
+}
+
+func isTCPRouteManaged(ctx context.Context, c client.Client, route
*gatewayv1alpha2.TCPRoute) (bool, error) {
+ if route == nil {
+ return false, nil
+ }
+ return routeReferencesManagedGateway(ctx, c, route.Spec.ParentRefs,
route.Namespace)
+}
+
+func routeReferencesManagedGateway(ctx context.Context, c client.Client,
parents []gatewayv1.ParentReference, defaultNamespace string) (bool, error) {
+ for _, parent := range parents {
+ if parent.Name == "" {
+ continue
+ }
+ if parent.Kind != nil && string(*parent.Kind) !=
internaltypes.KindGateway {
+ continue
+ }
+
+ namespace := defaultNamespace
+ if parent.Namespace != nil && *parent.Namespace != "" {
+ namespace = string(*parent.Namespace)
+ }
+
+ var gateway gatewayv1.Gateway
+ if err := c.Get(ctx, client.ObjectKey{Namespace: namespace,
Name: string(parent.Name)}, &gateway); err != nil {
+ if client.IgnoreNotFound(err) == nil {
+ continue
+ }
+ return false, err
+ }
+
+ managed, err := isGatewayManaged(ctx, c, &gateway)
+ if err != nil {
+ return false, err
+ }
+ if managed {
+ return true, nil
+ }
+ }
+
+ return false, nil
+}
diff --git a/internal/webhook/v1/tcproute_webhook.go
b/internal/webhook/v1/tcproute_webhook.go
new file mode 100644
index 00000000..7edc6a0c
--- /dev/null
+++ b/internal/webhook/v1/tcproute_webhook.go
@@ -0,0 +1,146 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "context"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+)
+
+var tcpRouteLog = logf.Log.WithName("tcproute-resource")
+
+func SetupTCPRouteWebhookWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewWebhookManagedBy(mgr).
+ For(&gatewayv1alpha2.TCPRoute{}).
+ WithValidator(NewTCPRouteCustomValidator(mgr.GetClient())).
+ Complete()
+}
+
+//
+kubebuilder:webhook:path=/validate-gateway-networking-k8s-io-v1alpha2-tcproute,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.networking.k8s.io,resources=tcproutes,verbs=create;update,versions=v1alpha2,name=vtcproute-v1alpha2.kb.io,admissionReviewVersions=v1
+
+type TCPRouteCustomValidator struct {
+ Client client.Client
+ checker reference.Checker
+}
+
+var _ webhook.CustomValidator = &TCPRouteCustomValidator{}
+
+func NewTCPRouteCustomValidator(c client.Client) *TCPRouteCustomValidator {
+ return &TCPRouteCustomValidator{
+ Client: c,
+ checker: reference.NewChecker(c, tcpRouteLog),
+ }
+}
+
+func (v *TCPRouteCustomValidator) ValidateCreate(ctx context.Context, obj
runtime.Object) (admission.Warnings, error) {
+ route, ok := obj.(*gatewayv1alpha2.TCPRoute)
+ if !ok {
+ return nil, fmt.Errorf("expected a TCPRoute object but got %T",
obj)
+ }
+ tcpRouteLog.Info("Validation for TCPRoute upon creation", "name",
route.GetName(), "namespace", route.GetNamespace())
+ managed, err := isTCPRouteManaged(ctx, v.Client, route)
+ if err != nil {
+ tcpRouteLog.Error(err, "failed to decide controller ownership",
"name", route.GetName(), "namespace", route.GetNamespace())
+ return nil, nil
+ }
+ if !managed {
+ return nil, nil
+ }
+
+ return v.collectWarnings(ctx, route), nil
+}
+
+func (v *TCPRouteCustomValidator) ValidateUpdate(ctx context.Context, oldObj,
newObj runtime.Object) (admission.Warnings, error) {
+ route, ok := newObj.(*gatewayv1alpha2.TCPRoute)
+ if !ok {
+ return nil, fmt.Errorf("expected a TCPRoute object for the
newObj but got %T", newObj)
+ }
+ tcpRouteLog.Info("Validation for TCPRoute upon update", "name",
route.GetName(), "namespace", route.GetNamespace())
+ managed, err := isTCPRouteManaged(ctx, v.Client, route)
+ if err != nil {
+ tcpRouteLog.Error(err, "failed to decide controller ownership",
"name", route.GetName(), "namespace", route.GetNamespace())
+ return nil, nil
+ }
+ if !managed {
+ return nil, nil
+ }
+
+ return v.collectWarnings(ctx, route), nil
+}
+
+func (*TCPRouteCustomValidator) ValidateDelete(context.Context,
runtime.Object) (admission.Warnings, error) {
+ return nil, nil
+}
+
+func (v *TCPRouteCustomValidator) collectWarnings(ctx context.Context, route
*gatewayv1alpha2.TCPRoute) admission.Warnings {
+ serviceVisited := make(map[types.NamespacedName]struct{})
+ namespace := route.GetNamespace()
+
+ var warnings admission.Warnings
+
+ addServiceWarning := func(nn types.NamespacedName) {
+ if nn.Name == "" || nn.Namespace == "" {
+ return
+ }
+ if _, seen := serviceVisited[nn]; seen {
+ return
+ }
+ serviceVisited[nn] = struct{}{}
+ warnings = append(warnings, v.checker.Service(ctx,
reference.ServiceRef{
+ Object: route,
+ NamespacedName: nn,
+ })...)
+ }
+
+ addBackendRef := func(ns, name string, group *gatewayv1alpha2.Group,
kind *gatewayv1alpha2.Kind) {
+ if name == "" {
+ return
+ }
+ if group != nil && string(*group) != corev1.GroupName {
+ return
+ }
+ if kind != nil && *kind != internaltypes.KindService {
+ return
+ }
+ nn := types.NamespacedName{Namespace: ns, Name: name}
+ addServiceWarning(nn)
+ }
+
+ for _, rule := range route.Spec.Rules {
+ for _, backend := range rule.BackendRefs {
+ targetNamespace := namespace
+ if backend.Namespace != nil && *backend.Namespace != ""
{
+ targetNamespace = string(*backend.Namespace)
+ }
+ addBackendRef(targetNamespace, string(backend.Name),
backend.Group, backend.Kind)
+ }
+ }
+
+ return warnings
+}
diff --git a/internal/webhook/v1/tcproute_webhook_test.go
b/internal/webhook/v1/tcproute_webhook_test.go
new file mode 100644
index 00000000..6381ea0a
--- /dev/null
+++ b/internal/webhook/v1/tcproute_webhook_test.go
@@ -0,0 +1,122 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func buildTCPRouteValidator(t *testing.T, objects ...runtime.Object)
*TCPRouteCustomValidator {
+ t.Helper()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, clientgoscheme.AddToScheme(scheme))
+ require.NoError(t, gatewayv1.Install(scheme))
+ require.NoError(t, gatewayv1alpha2.Install(scheme))
+
+ managed := []runtime.Object{
+ &gatewayv1.GatewayClass{
+ ObjectMeta: metav1.ObjectMeta{Name:
"apisix-gateway-class"},
+ Spec: gatewayv1.GatewayClassSpec{
+ ControllerName:
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+ },
+ },
+ &gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-gateway",
Namespace: "default"},
+ Spec: gatewayv1.GatewaySpec{
+ GatewayClassName:
gatewayv1.ObjectName("apisix-gateway-class"),
+ },
+ },
+ }
+ allObjects := append(managed, objects...)
+ builder :=
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
+
+ return NewTCPRouteCustomValidator(builder.Build())
+}
+
+func TestTCPRouteCustomValidator_WarnsForMissingReferences(t *testing.T) {
+ route := &gatewayv1alpha2.TCPRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace:
"default"},
+ Spec: gatewayv1alpha2.TCPRouteSpec{
+ CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+ ParentRefs: []gatewayv1alpha2.ParentReference{{
+ Name:
gatewayv1alpha2.ObjectName("test-gateway"),
+ }},
+ },
+ Rules: []gatewayv1alpha2.TCPRouteRule{{
+ BackendRefs: []gatewayv1alpha2.BackendRef{
+ {
+ BackendObjectReference:
gatewayv1alpha2.BackendObjectReference{
+ Name:
gatewayv1alpha2.ObjectName("missing-svc"),
+ },
+ },
+ },
+ }},
+ },
+ }
+
+ validator := buildTCPRouteValidator(t)
+ warnings, err := validator.ValidateCreate(context.Background(), route)
+ require.NoError(t, err)
+ assert.ElementsMatch(t, []string{
+ "Referenced Service 'default/missing-svc' not found",
+ }, warnings)
+}
+
+func TestTCPRouteCustomValidator_NoWarningsWhenResourcesExist(t *testing.T) {
+ objs := []runtime.Object{
+ &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "backend",
Namespace: "default"}},
+ }
+
+ validator := buildTCPRouteValidator(t, objs...)
+
+ route := &gatewayv1alpha2.TCPRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace:
"default"},
+ Spec: gatewayv1alpha2.TCPRouteSpec{
+ CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+ ParentRefs: []gatewayv1alpha2.ParentReference{{
+ Name:
gatewayv1alpha2.ObjectName("test-gateway"),
+ }},
+ },
+ Rules: []gatewayv1alpha2.TCPRouteRule{{
+ BackendRefs: []gatewayv1alpha2.BackendRef{
+ {
+ BackendObjectReference:
gatewayv1alpha2.BackendObjectReference{
+ Name:
gatewayv1alpha2.ObjectName("backend"),
+ },
+ },
+ },
+ }},
+ },
+ }
+
+ warnings, err := validator.ValidateCreate(context.Background(), route)
+ require.NoError(t, err)
+ assert.Empty(t, warnings)
+}