This is an automated email from the ASF dual-hosted git repository. ronething pushed a commit to branch feat/add_resource_verify in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
commit ab2624e2592d3d958cd7b7b4a32558a706f24002 Author: Ashing Zheng <[email protected]> AuthorDate: Fri Sep 26 16:01:42 2025 +0800 feat: add apisixtls and apisixconsumer Signed-off-by: Ashing Zheng <[email protected]> --- internal/manager/webhooks.go | 6 + internal/webhook/v1/apisixconsumer_webhook.go | 116 +++++++++++++++++ internal/webhook/v1/apisixconsumer_webhook_test.go | 137 +++++++++++++++++++++ internal/webhook/v1/apisixtls_webhook.go | 98 +++++++++++++++ internal/webhook/v1/apisixtls_webhook_test.go | 116 +++++++++++++++++ 5 files changed, 473 insertions(+) diff --git a/internal/manager/webhooks.go b/internal/manager/webhooks.go index be0bd360..91242972 100644 --- a/internal/manager/webhooks.go +++ b/internal/manager/webhooks.go @@ -38,5 +38,11 @@ func setupWebhooks(_ context.Context, mgr manager.Manager) error { if err := webhookv1.SetupGatewayProxyWebhookWithManager(mgr); err != nil { return err } + if err := webhookv1.SetupApisixConsumerWebhookWithManager(mgr); err != nil { + return err + } + if err := webhookv1.SetupApisixTlsWebhookWithManager(mgr); err != nil { + return err + } return nil } diff --git a/internal/webhook/v1/apisixconsumer_webhook.go b/internal/webhook/v1/apisixconsumer_webhook.go new file mode 100644 index 00000000..ee0ddeb2 --- /dev/null +++ b/internal/webhook/v1/apisixconsumer_webhook.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" + "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" + + apisixv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference" +) + +var apisixConsumerLog = logf.Log.WithName("apisixconsumer-resource") + +func SetupApisixConsumerWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&apisixv2.ApisixConsumer{}). + WithValidator(&ApisixConsumerCustomValidator{Client: mgr.GetClient()}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixconsumer,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=apisixconsumers,verbs=create;update,versions=v2,name=vapisixconsumer-v2.kb.io,admissionReviewVersions=v1 + +type ApisixConsumerCustomValidator struct { + Client client.Client +} + +var _ webhook.CustomValidator = &ApisixConsumerCustomValidator{} + +func (v *ApisixConsumerCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + consumer, ok := obj.(*apisixv2.ApisixConsumer) + if !ok { + return nil, fmt.Errorf("expected an ApisixConsumer object but got %T", obj) + } + apisixConsumerLog.Info("Validation for ApisixConsumer upon creation", "name", consumer.GetName(), "namespace", consumer.GetNamespace()) + + return v.collectWarnings(ctx, consumer), nil +} + +func (v *ApisixConsumerCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + consumer, ok := newObj.(*apisixv2.ApisixConsumer) + if !ok { + 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()) + + return v.collectWarnings(ctx, consumer), nil +} + +func (*ApisixConsumerCustomValidator) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +func (v *ApisixConsumerCustomValidator) collectWarnings(ctx context.Context, consumer *apisixv2.ApisixConsumer) admission.Warnings { + checker := reference.NewChecker(v.Client, apisixConsumerLog) + namespace := consumer.GetNamespace() + var warnings admission.Warnings + + addSecretWarning := func(ref *corev1.LocalObjectReference) { + if ref == nil || ref.Name == "" { + return + } + + warnings = append(warnings, checker.Secret(ctx, reference.SecretRef{ + Object: consumer, + NamespacedName: types.NamespacedName{ + Namespace: namespace, + Name: ref.Name, + }, + })...) + } + + params := consumer.Spec.AuthParameter + if params.BasicAuth != nil { + addSecretWarning(params.BasicAuth.SecretRef) + } + if params.KeyAuth != nil { + addSecretWarning(params.KeyAuth.SecretRef) + } + if params.WolfRBAC != nil { + addSecretWarning(params.WolfRBAC.SecretRef) + } + if params.JwtAuth != nil { + addSecretWarning(params.JwtAuth.SecretRef) + } + if params.HMACAuth != nil { + addSecretWarning(params.HMACAuth.SecretRef) + } + if params.LDAPAuth != nil { + addSecretWarning(params.LDAPAuth.SecretRef) + } + + return warnings +} diff --git a/internal/webhook/v1/apisixconsumer_webhook_test.go b/internal/webhook/v1/apisixconsumer_webhook_test.go new file mode 100644 index 00000000..2e88a6a9 --- /dev/null +++ b/internal/webhook/v1/apisixconsumer_webhook_test.go @@ -0,0 +1,137 @@ +// 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/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" + + apisixv2 "github.com/apache/apisix-ingress-controller/api/v2" +) + +func buildApisixConsumerValidator(t *testing.T, objects ...runtime.Object) *ApisixConsumerCustomValidator { + t.Helper() + + scheme := runtime.NewScheme() + require.NoError(t, clientgoscheme.AddToScheme(scheme)) + require.NoError(t, apisixv2.AddToScheme(scheme)) + + builder := fake.NewClientBuilder().WithScheme(scheme) + if len(objects) > 0 { + builder = builder.WithRuntimeObjects(objects...) + } + + return &ApisixConsumerCustomValidator{Client: builder.Build()} +} + +func TestApisixConsumerValidator_MissingBasicAuthSecret(t *testing.T) { + consumer := &apisixv2.ApisixConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "demo", + Namespace: "default", + }, + Spec: apisixv2.ApisixConsumerSpec{ + AuthParameter: apisixv2.ApisixConsumerAuthParameter{ + BasicAuth: &apisixv2.ApisixConsumerBasicAuth{ + SecretRef: &corev1.LocalObjectReference{Name: "basic-auth"}, + }, + }, + }, + } + + validator := buildApisixConsumerValidator(t) + + warnings, err := validator.ValidateCreate(context.Background(), consumer) + require.NoError(t, err) + require.Equal(t, 1, len(warnings)) + require.Equal(t, "Referenced Secret 'default/basic-auth' not found", warnings[0]) +} + +func TestApisixConsumerValidator_MultipleSecretWarnings(t *testing.T) { + consumer := &apisixv2.ApisixConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "demo", + Namespace: "default", + }, + Spec: apisixv2.ApisixConsumerSpec{ + AuthParameter: apisixv2.ApisixConsumerAuthParameter{ + BasicAuth: &apisixv2.ApisixConsumerBasicAuth{ + SecretRef: &corev1.LocalObjectReference{Name: "basic-auth"}, + }, + JwtAuth: &apisixv2.ApisixConsumerJwtAuth{ + SecretRef: &corev1.LocalObjectReference{Name: "jwt-auth"}, + }, + HMACAuth: &apisixv2.ApisixConsumerHMACAuth{ + SecretRef: &corev1.LocalObjectReference{Name: "hmac-auth"}, + }, + }, + }, + } + + basicAuthSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic-auth", + Namespace: "default", + }, + } + + validator := buildApisixConsumerValidator(t, basicAuthSecret) + + warnings, err := validator.ValidateCreate(context.Background(), consumer) + require.NoError(t, err) + require.Len(t, warnings, 2) + require.ElementsMatch(t, []string{ + "Referenced Secret 'default/jwt-auth' not found", + "Referenced Secret 'default/hmac-auth' not found", + }, warnings) +} + +func TestApisixConsumerValidator_NoWarningsWhenSecretsExist(t *testing.T) { + consumer := &apisixv2.ApisixConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "demo", + Namespace: "default", + }, + Spec: apisixv2.ApisixConsumerSpec{ + AuthParameter: apisixv2.ApisixConsumerAuthParameter{ + KeyAuth: &apisixv2.ApisixConsumerKeyAuth{ + SecretRef: &corev1.LocalObjectReference{Name: "key-auth"}, + }, + WolfRBAC: &apisixv2.ApisixConsumerWolfRBAC{ + SecretRef: &corev1.LocalObjectReference{Name: "wolf-rbac"}, + }, + }, + }, + } + + secrets := []runtime.Object{ + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "key-auth", Namespace: "default"}}, + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wolf-rbac", Namespace: "default"}}, + } + + validator := buildApisixConsumerValidator(t, secrets...) + + warnings, err := validator.ValidateCreate(context.Background(), consumer) + require.NoError(t, err) + require.Empty(t, warnings) +} diff --git a/internal/webhook/v1/apisixtls_webhook.go b/internal/webhook/v1/apisixtls_webhook.go new file mode 100644 index 00000000..19cbb32a --- /dev/null +++ b/internal/webhook/v1/apisixtls_webhook.go @@ -0,0 +1,98 @@ +// 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" + + "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" + + apisixv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference" +) + +var apisixTlsLog = logf.Log.WithName("apisixtls-resource") + +func SetupApisixTlsWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&apisixv2.ApisixTls{}). + WithValidator(&ApisixTlsCustomValidator{Client: mgr.GetClient()}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixtls,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=apisixtlses,verbs=create;update,versions=v2,name=vapisixtls-v2.kb.io,admissionReviewVersions=v1 + +type ApisixTlsCustomValidator struct { + Client client.Client +} + +var _ webhook.CustomValidator = &ApisixTlsCustomValidator{} + +func (v *ApisixTlsCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + tls, ok := obj.(*apisixv2.ApisixTls) + if !ok { + 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()) + + return v.collectWarnings(ctx, tls), nil +} + +func (v *ApisixTlsCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + tls, ok := newObj.(*apisixv2.ApisixTls) + if !ok { + 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()) + + return v.collectWarnings(ctx, tls), nil +} + +func (*ApisixTlsCustomValidator) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +func (v *ApisixTlsCustomValidator) collectWarnings(ctx context.Context, tls *apisixv2.ApisixTls) admission.Warnings { + checker := reference.NewChecker(v.Client, apisixTlsLog) + var warnings admission.Warnings + + warnings = append(warnings, checker.Secret(ctx, reference.SecretRef{ + Object: tls, + NamespacedName: types.NamespacedName{ + Namespace: tls.Spec.Secret.Namespace, + Name: tls.Spec.Secret.Name, + }, + })...) + + if client := tls.Spec.Client; client != nil { + warnings = append(warnings, checker.Secret(ctx, reference.SecretRef{ + Object: tls, + NamespacedName: types.NamespacedName{ + Namespace: client.CASecret.Namespace, + Name: client.CASecret.Name, + }, + })...) + } + + return warnings +} diff --git a/internal/webhook/v1/apisixtls_webhook_test.go b/internal/webhook/v1/apisixtls_webhook_test.go new file mode 100644 index 00000000..9337c46b --- /dev/null +++ b/internal/webhook/v1/apisixtls_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/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" + + apisixv2 "github.com/apache/apisix-ingress-controller/api/v2" +) + +func buildApisixTlsValidator(t *testing.T, objects ...runtime.Object) *ApisixTlsCustomValidator { + t.Helper() + + scheme := runtime.NewScheme() + require.NoError(t, clientgoscheme.AddToScheme(scheme)) + require.NoError(t, apisixv2.AddToScheme(scheme)) + + builder := fake.NewClientBuilder().WithScheme(scheme) + if len(objects) > 0 { + builder = builder.WithRuntimeObjects(objects...) + } + + return &ApisixTlsCustomValidator{Client: builder.Build()} +} + +func newApisixTls() *apisixv2.ApisixTls { + return &apisixv2.ApisixTls{ + ObjectMeta: metav1.ObjectMeta{ + Name: "demo", + Namespace: "default", + }, + Spec: apisixv2.ApisixTlsSpec{ + Hosts: []apisixv2.HostType{"example.com"}, + Secret: apisixv2.ApisixSecret{ + Name: "server-cert", + Namespace: "default", + }, + }, + } +} + +func TestApisixTlsValidator_MissingServerSecret(t *testing.T) { + tls := newApisixTls() + validator := buildApisixTlsValidator(t) + + warnings, err := validator.ValidateCreate(context.Background(), tls) + require.NoError(t, err) + require.Len(t, warnings, 1) + require.Contains(t, warnings[0], "Referenced Secret 'default/server-cert' not found") +} + +func TestApisixTlsValidator_MissingClientSecret(t *testing.T) { + tls := newApisixTls() + tls.Spec.Client = &apisixv2.ApisixMutualTlsClientConfig{ + CASecret: apisixv2.ApisixSecret{ + Name: "mtls-ca", + Namespace: "mtls", + }, + } + + serverSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "server-cert", + Namespace: "default", + }, + } + + validator := buildApisixTlsValidator(t, serverSecret) + + warnings, err := validator.ValidateCreate(context.Background(), tls) + require.NoError(t, err) + require.Len(t, warnings, 1) + require.Contains(t, warnings[0], "Referenced Secret 'mtls/mtls-ca' not found") +} + +func TestApisixTlsValidator_NoWarningsWhenSecretsExist(t *testing.T) { + tls := newApisixTls() + tls.Spec.Client = &apisixv2.ApisixMutualTlsClientConfig{ + CASecret: apisixv2.ApisixSecret{ + Name: "mtls-ca", + Namespace: "mtls", + }, + } + + objects := []runtime.Object{ + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "server-cert", Namespace: "default"}}, + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "mtls-ca", Namespace: "mtls"}}, + } + + validator := buildApisixTlsValidator(t, objects...) + + warnings, err := validator.ValidateCreate(context.Background(), tls) + require.NoError(t, err) + require.Empty(t, warnings) +}
