diff --git a/CHANGELOG.md b/CHANGELOG.md index f6444770500..5ff6afd7b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,12 +53,12 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio - **Governance**: KEDA transitioned to CNCF Graduated project ([#63](https://github.com/kedacore/governance/issues/63)) ### Improvements - - **General**: Add more events for user checking ([#796](https://github.com/kedacore/keda/issues/3764)) - **General**: Add ScaledObject/ScaledJob names to output of `kubectl get triggerauthentication/clustertriggerauthentication` ([#796](https://github.com/kedacore/keda/issues/796)) - **General**: Add standalone CRD generation to release workflow ([#2726](https://github.com/kedacore/keda/issues/2726)) - **General**: Adding a changelog validating script to check for formatting and order ([#3190](https://github.com/kedacore/keda/issues/3190)) - **General**: Update golangci-lint version documented in CONTRIBUTING.md since old version doesn't support go 1.20 (N/A) +- **Azure Pod Identity**: Introduce validation to prevent usage of empty identity ID for Azure identity providers ([#4528](https://github.com/kedacore/keda/issues/4528)) ### Fixes diff --git a/apis/keda/v1alpha1/scaledobject_webhook_test.go b/apis/keda/v1alpha1/scaledobject_webhook_test.go index fbde5787df1..1325f2d5288 100644 --- a/apis/keda/v1alpha1/scaledobject_webhook_test.go +++ b/apis/keda/v1alpha1/scaledobject_webhook_test.go @@ -18,133 +18,18 @@ package v1alpha1 import ( "context" - "crypto/tls" - "fmt" - "net" - "path/filepath" - "testing" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - admissionv1beta1 "k8s.io/api/admission/v1beta1" appsv1 "k8s.io/api/apps/v1" v2 "k8s.io/api/autoscaling/v2" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" "k8s.io/utils/pointer" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - //+kubebuilder:scaffold:imports ) -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment -var ctx context.Context -var cancel context.CancelFunc - -const ( - workloadName = "deployment-name" - soName = "test-so" -) - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecs(t, "Webhook Suite") -} - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - ctx, cancel = context.WithCancel(context.Background()) - - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: false, - WebhookInstallOptions: envtest.WebhookInstallOptions{ - Paths: []string{filepath.Join("..", "..", "..", "config", "webhooks")}, - }, - } - - var err error - // cfg is defined in this file globally. - done := make(chan interface{}) - go func() { - defer GinkgoRecover() - cfg, err = testEnv.Start() - close(done) - }() - Eventually(done).WithTimeout(time.Minute).Should(BeClosed()) - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - scheme := runtime.NewScheme() - err = AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = clientgoscheme.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = admissionv1beta1.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - //+kubebuilder:scaffold:scheme - - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient).NotTo(BeNil()) - - // start webhook server using Manager - webhookInstallOptions := &testEnv.WebhookInstallOptions - mgr, err := ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme, - Host: webhookInstallOptions.LocalServingHost, - Port: webhookInstallOptions.LocalServingPort, - CertDir: webhookInstallOptions.LocalServingCertDir, - LeaderElection: false, - MetricsBindAddress: "0", - }) - Expect(err).NotTo(HaveOccurred()) - - err = (&ScaledObject{}).SetupWebhookWithManager(mgr) - Expect(err).NotTo(HaveOccurred()) - - //+kubebuilder:scaffold:webhook - - go func() { - defer GinkgoRecover() - err = mgr.Start(ctx) - Expect(err).NotTo(HaveOccurred()) - }() - - // wait for the webhook server to get ready - dialer := &net.Dialer{Timeout: time.Second} - addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) - Eventually(func() error { - conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) - if err != nil { - return err - } - conn.Close() - return nil - }).Should(Succeed()) - -}) - var _ = It("should validate the so creation when there isn't any hpa", func() { namespaceName := "valid" diff --git a/apis/keda/v1alpha1/suite_test.go b/apis/keda/v1alpha1/suite_test.go new file mode 100644 index 00000000000..bb2b8bf54c7 --- /dev/null +++ b/apis/keda/v1alpha1/suite_test.go @@ -0,0 +1,141 @@ +/* +Copyright 2023 The KEDA Authors + +Licensed 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 v1alpha1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +const ( + workloadName = "deployment-name" + soName = "test-so" +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.Background()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "config", "webhooks")}, + }, + } + var err error + // cfg is defined in this file globally. + done := make(chan interface{}) + go func() { + defer GinkgoRecover() + cfg, err = testEnv.Start() + close(done) + }() + Eventually(done).WithTimeout(time.Minute).Should(BeClosed()) + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := runtime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = clientgoscheme.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + LeaderElection: false, + MetricsBindAddress: "0", + }) + Expect(err).NotTo(HaveOccurred()) + + err = (&ScaledObject{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + err = (&TriggerAuthentication{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + err = (&ClusterTriggerAuthentication{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + +}) diff --git a/apis/keda/v1alpha1/triggerauthentication_types.go b/apis/keda/v1alpha1/triggerauthentication_types.go index c2fb5d9ab5f..4603a68eec1 100755 --- a/apis/keda/v1alpha1/triggerauthentication_types.go +++ b/apis/keda/v1alpha1/triggerauthentication_types.go @@ -132,7 +132,14 @@ const ( type AuthPodIdentity struct { Provider PodIdentityProvider `json:"provider"` // +optional - IdentityID string `json:"identityId"` + IdentityID *string `json:"identityId"` +} + +func (a *AuthPodIdentity) GetIdentityID() string { + if a.IdentityID == nil { + return "" + } + return *a.IdentityID } // AuthSecretTargetRef is used to authenticate using a reference to a secret diff --git a/apis/keda/v1alpha1/triggerauthentication_webhook.go b/apis/keda/v1alpha1/triggerauthentication_webhook.go new file mode 100644 index 00000000000..72b14e1b388 --- /dev/null +++ b/apis/keda/v1alpha1/triggerauthentication_webhook.go @@ -0,0 +1,121 @@ +/* +Copyright 2023 The KEDA Authors + +Licensed 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 v1alpha1 + +import ( + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var triggerauthenticationlog = logf.Log.WithName("triggerauthentication-validation-webhook") + +func (ta *TriggerAuthentication) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(ta). + Complete() +} + +func (cta *ClusterTriggerAuthentication) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(cta). + Complete() +} + +// +kubebuilder:webhook:path=/validate-keda-sh-v1alpha1-triggerauthentication,mutating=false,failurePolicy=ignore,sideEffects=None,groups=keda.sh,resources=triggerauthentications,verbs=create;update,versions=v1alpha1,name=vstriggerauthentication.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &TriggerAuthentication{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (ta *TriggerAuthentication) ValidateCreate() (admission.Warnings, error) { + val, _ := json.MarshalIndent(ta, "", " ") + triggerauthenticationlog.Info(fmt.Sprintf("validating triggerauthentication creation for %s", string(val))) + return validateSpec(&ta.Spec) +} + +func (ta *TriggerAuthentication) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + val, _ := json.MarshalIndent(ta, "", " ") + scaledobjectlog.V(1).Info(fmt.Sprintf("validating triggerauthentication update for %s", string(val))) + + oldTa := old.(*TriggerAuthentication) + if isTriggerAuthenticationRemovingFinalizer(ta.ObjectMeta, oldTa.ObjectMeta, ta.Spec, oldTa.Spec) { + triggerauthenticationlog.V(1).Info("finalizer removal, skipping validation") + return nil, nil + } + return validateSpec(&ta.Spec) +} + +func (ta *TriggerAuthentication) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} + +// +kubebuilder:webhook:path=/validate-keda-sh-v1alpha1-clustertriggerauthentication,mutating=false,failurePolicy=ignore,sideEffects=None,groups=keda.sh,resources=clustertriggerauthentications,verbs=create;update,versions=v1alpha1,name=vsclustertriggerauthentication.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &ClusterTriggerAuthentication{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (cta *ClusterTriggerAuthentication) ValidateCreate() (admission.Warnings, error) { + val, _ := json.MarshalIndent(cta, "", " ") + triggerauthenticationlog.Info(fmt.Sprintf("validating clustertriggerauthentication creation for %s", string(val))) + return validateSpec(&cta.Spec) +} + +func (cta *ClusterTriggerAuthentication) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + val, _ := json.MarshalIndent(cta, "", " ") + scaledobjectlog.V(1).Info(fmt.Sprintf("validating clustertriggerauthentication update for %s", string(val))) + + oldCta := old.(*ClusterTriggerAuthentication) + if isTriggerAuthenticationRemovingFinalizer(cta.ObjectMeta, oldCta.ObjectMeta, cta.Spec, oldCta.Spec) { + triggerauthenticationlog.V(1).Info("finalizer removal, skipping validation") + return nil, nil + } + + return validateSpec(&cta.Spec) +} + +func (cta *ClusterTriggerAuthentication) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} + +func isTriggerAuthenticationRemovingFinalizer(om metav1.ObjectMeta, oldOm metav1.ObjectMeta, spec TriggerAuthenticationSpec, oldSpec TriggerAuthenticationSpec) bool { + taSpec, _ := json.MarshalIndent(spec, "", " ") + oldTaSpec, _ := json.MarshalIndent(oldSpec, "", " ") + taSpecString := string(taSpec) + oldTaSpecString := string(oldTaSpec) + + return len(om.Finalizers) == 0 && len(oldOm.Finalizers) == 1 && taSpecString == oldTaSpecString +} + +func validateSpec(spec *TriggerAuthenticationSpec) (admission.Warnings, error) { + if spec.PodIdentity != nil { + switch spec.PodIdentity.Provider { + case PodIdentityProviderAzure, PodIdentityProviderAzureWorkload: + if spec.PodIdentity.IdentityID != nil && *spec.PodIdentity.IdentityID == "" { + return nil, fmt.Errorf("identityid of PodIdentity should not be empty. If it's set, identityId has to be different than \"\"") + } + default: + return nil, nil + } + } + return nil, nil +} diff --git a/apis/keda/v1alpha1/triggerauthentication_webhook_test.go b/apis/keda/v1alpha1/triggerauthentication_webhook_test.go new file mode 100644 index 00000000000..b18585ff97b --- /dev/null +++ b/apis/keda/v1alpha1/triggerauthentication_webhook_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2023 The KEDA Authors + +Licensed 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 v1alpha1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = It("validate triggerauthentication when IdentityID is nil", func() { + namespaceName := "nilidentityid" + namespace := createNamespace(namespaceName) + err := k8sClient.Create(context.Background(), namespace) + Expect(err).ToNot(HaveOccurred()) + + spec := createTriggerAuthenticationSpecWithPodIdentity(nil) + ta := createTriggerAuthentication("nilidentityidta", namespaceName, "TriggerAuthentication", spec) + Eventually(func() error { + return k8sClient.Create(context.Background(), ta) + }).ShouldNot(HaveOccurred()) +}) + +var _ = It("validate triggerauthentication when IdentityID is empty", func() { + namespaceName := "emptyidentityid" + namespace := createNamespace(namespaceName) + err := k8sClient.Create(context.Background(), namespace) + Expect(err).ToNot(HaveOccurred()) + + identityID := "" + spec := createTriggerAuthenticationSpecWithPodIdentity(&identityID) + ta := createTriggerAuthentication("emptyidentityidta", namespaceName, "TriggerAuthentication", spec) + Eventually(func() error { + return k8sClient.Create(context.Background(), ta) + }).Should(HaveOccurred()) +}) + +var _ = It("validate triggerauthentication when IdentityID is not empty", func() { + namespaceName := "identityid" + namespace := createNamespace(namespaceName) + err := k8sClient.Create(context.Background(), namespace) + Expect(err).ToNot(HaveOccurred()) + + identityID := "12345" + spec := createTriggerAuthenticationSpecWithPodIdentity(&identityID) + ta := createTriggerAuthentication("identityidta", namespaceName, "TriggerAuthentication", spec) + Eventually(func() error { + return k8sClient.Create(context.Background(), ta) + }).ShouldNot(HaveOccurred()) +}) + +var _ = It("validate clustertriggerauthentication when IdentityID is nil", func() { + namespaceName := "clusternilidentityid" + namespace := createNamespace(namespaceName) + err := k8sClient.Create(context.Background(), namespace) + Expect(err).ToNot(HaveOccurred()) + + spec := createTriggerAuthenticationSpecWithPodIdentity(nil) + ta := createTriggerAuthentication("clusternilidentityidta", namespaceName, "ClusterTriggerAuthentication", spec) + Eventually(func() error { + return k8sClient.Create(context.Background(), ta) + }).ShouldNot(HaveOccurred()) +}) + +var _ = It("validate clustertriggerauthentication when IdentityID is empty", func() { + namespaceName := "clusteremptyidentityid" + namespace := createNamespace(namespaceName) + err := k8sClient.Create(context.Background(), namespace) + Expect(err).ToNot(HaveOccurred()) + + identityID := "" + spec := createTriggerAuthenticationSpecWithPodIdentity(&identityID) + ta := createTriggerAuthentication("clusteremptyidentityidta", namespaceName, "ClusterTriggerAuthentication", spec) + Eventually(func() error { + return k8sClient.Create(context.Background(), ta) + }).Should(HaveOccurred()) +}) + +var _ = It("validate clustertriggerauthentication when IdentityID is not empty", func() { + namespaceName := "clusteridentityid" + namespace := createNamespace(namespaceName) + err := k8sClient.Create(context.Background(), namespace) + Expect(err).ToNot(HaveOccurred()) + + identityID := "12345" + spec := createTriggerAuthenticationSpecWithPodIdentity(&identityID) + ta := createTriggerAuthentication("clusteridentityidta", namespaceName, "ClusterTriggerAuthentication", spec) + Eventually(func() error { + return k8sClient.Create(context.Background(), ta) + }).ShouldNot(HaveOccurred()) +}) + +func createTriggerAuthenticationSpecWithPodIdentity(identityID *string) TriggerAuthenticationSpec { + return TriggerAuthenticationSpec{ + PodIdentity: &AuthPodIdentity{ + Provider: PodIdentityProviderAzure, + IdentityID: identityID, + }, + } +} + +func createTriggerAuthentication(name, namespace, targetKind string, spec TriggerAuthenticationSpec) *TriggerAuthentication { + return &TriggerAuthentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: targetKind, + APIVersion: "keda.sh", + }, + Spec: spec, + } +} diff --git a/cmd/webhooks/main.go b/cmd/webhooks/main.go index 9a79928a200..ab410cccaec 100644 --- a/cmd/webhooks/main.go +++ b/cmd/webhooks/main.go @@ -131,4 +131,12 @@ func setupWebhook(mgr manager.Manager) { setupLog.Error(err, "unable to create webhook", "webhook", "ScaledObject") os.Exit(1) } + if err := (&kedav1alpha1.TriggerAuthentication{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "TriggerAuthentication") + os.Exit(1) + } + if err := (&kedav1alpha1.ClusterTriggerAuthentication{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "ClusterTriggerAuthentication") + os.Exit(1) + } } diff --git a/config/webhooks/validation_webhooks.yaml b/config/webhooks/validation_webhooks.yaml index 4eb57d09b7c..3561df56e22 100644 --- a/config/webhooks/validation_webhooks.yaml +++ b/config/webhooks/validation_webhooks.yaml @@ -33,3 +33,51 @@ webhooks: - scaledobjects sideEffects: None timeoutSeconds: 10 +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: keda-admission-webhooks + namespace: keda + path: /validate-keda-sh-v1alpha1-triggerauthentication + failurePolicy: Ignore + matchPolicy: Equivalent + name: vstriggerauthentication.kb.io + namespaceSelector: {} + objectSelector: {} + rules: + - apiGroups: + - keda.sh + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - triggerauthentications + sideEffects: None + timeoutSeconds: 10 +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: keda-admission-webhooks + namespace: keda + path: /validate-keda-sh-v1alpha1-clustertriggerauthentication + failurePolicy: Ignore + matchPolicy: Equivalent + name: vsclustertriggerauthentication.kb.io + namespaceSelector: {} + objectSelector: {} + rules: + - apiGroups: + - keda.sh + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - clustertriggerauthentications + sideEffects: None + timeoutSeconds: 10 diff --git a/pkg/scalers/azure/azure_app_insights.go b/pkg/scalers/azure/azure_app_insights.go index c4c64d78c34..461d61a2cf9 100644 --- a/pkg/scalers/azure/azure_app_insights.go +++ b/pkg/scalers/azure/azure_app_insights.go @@ -69,10 +69,10 @@ func getAuthConfig(ctx context.Context, info AppInsightsInfo, podIdentity kedav1 case kedav1alpha1.PodIdentityProviderAzure: config := auth.NewMSIConfig() config.Resource = info.AppInsightsResourceURL - config.ClientID = podIdentity.IdentityID + config.ClientID = podIdentity.GetIdentityID() return config case kedav1alpha1.PodIdentityProviderAzureWorkload: - return NewAzureADWorkloadIdentityConfig(ctx, podIdentity.IdentityID, info.AppInsightsResourceURL) + return NewAzureADWorkloadIdentityConfig(ctx, podIdentity.GetIdentityID(), info.AppInsightsResourceURL) } return nil } diff --git a/pkg/scalers/azure/azure_data_explorer.go b/pkg/scalers/azure/azure_data_explorer.go index 915e5ece3d2..3785ddc644b 100644 --- a/pkg/scalers/azure/azure_data_explorer.go +++ b/pkg/scalers/azure/azure_data_explorer.go @@ -91,7 +91,7 @@ func getDataExplorerAuthConfig(metadata *DataExplorerMetadata) (*kusto.Connectio case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload: azureDataExplorerLogger.V(1).Info(fmt.Sprintf("Creating Azure Data Explorer Client using podIdentity %s", metadata.PodIdentity.Provider)) - creds, chainedErr := NewChainedCredential(metadata.PodIdentity.IdentityID, metadata.PodIdentity.Provider) + creds, chainedErr := NewChainedCredential(metadata.PodIdentity.GetIdentityID(), metadata.PodIdentity.Provider) if chainedErr != nil { return nil, chainedErr } diff --git a/pkg/scalers/azure/azure_eventhub.go b/pkg/scalers/azure/azure_eventhub.go index e7c3b93b2e3..d4ca34b879c 100644 --- a/pkg/scalers/azure/azure_eventhub.go +++ b/pkg/scalers/azure/azure_eventhub.go @@ -53,7 +53,7 @@ func GetEventHubClient(ctx context.Context, info EventHubInfo) (*eventhub.Hub, e envJWTProviderOption := aad.JWTProviderWithAzureEnvironment(&env) resourceURLJWTProviderOption := aad.JWTProviderWithResourceURI(info.EventHubResourceURL) clientIDJWTProviderOption := func(config *aad.TokenProviderConfiguration) error { - config.ClientID = info.PodIdentity.IdentityID + config.ClientID = info.PodIdentity.GetIdentityID() return nil } @@ -68,7 +68,7 @@ func GetEventHubClient(ctx context.Context, info EventHubInfo) (*eventhub.Hub, e // User wants to use AAD Workload Identity env := azure.Environment{ActiveDirectoryEndpoint: info.ActiveDirectoryEndpoint, ServiceBusEndpointSuffix: info.ServiceBusEndpointSuffix} hubEnvOptions := eventhub.HubWithEnvironment(env) - provider := NewAzureADWorkloadIdentityTokenProvider(ctx, info.PodIdentity.IdentityID, info.EventHubResourceURL) + provider := NewAzureADWorkloadIdentityTokenProvider(ctx, info.PodIdentity.GetIdentityID(), info.EventHubResourceURL) return eventhub.NewHub(info.Namespace, info.EventHubName, provider, hubEnvOptions) } diff --git a/pkg/scalers/azure/azure_managed_prometheus_http_round_tripper.go b/pkg/scalers/azure/azure_managed_prometheus_http_round_tripper.go index b3bd400d798..16e1aa1bdfe 100644 --- a/pkg/scalers/azure/azure_managed_prometheus_http_round_tripper.go +++ b/pkg/scalers/azure/azure_managed_prometheus_http_round_tripper.go @@ -36,7 +36,7 @@ func TryAndGetAzureManagedPrometheusHTTPRoundTripper(podIdentity kedav1alpha1.Au return nil, fmt.Errorf("trigger metadata cannot be nil") } - chainedCred, err := NewChainedCredential(podIdentity.IdentityID, podIdentity.Provider) + chainedCred, err := NewChainedCredential(podIdentity.GetIdentityID(), podIdentity.Provider) if err != nil { return nil, err } diff --git a/pkg/scalers/azure/azure_monitor.go b/pkg/scalers/azure/azure_monitor.go index 08ef4fa21a1..0ed25ff561c 100644 --- a/pkg/scalers/azure/azure_monitor.go +++ b/pkg/scalers/azure/azure_monitor.go @@ -89,11 +89,11 @@ func createMetricsClient(ctx context.Context, info MonitorInfo, podIdentity keda case kedav1alpha1.PodIdentityProviderAzure: config := auth.NewMSIConfig() config.Resource = info.AzureResourceManagerEndpoint - config.ClientID = podIdentity.IdentityID + config.ClientID = podIdentity.GetIdentityID() authConfig = config case kedav1alpha1.PodIdentityProviderAzureWorkload: - authConfig = NewAzureADWorkloadIdentityConfig(ctx, podIdentity.IdentityID, info.AzureResourceManagerEndpoint) + authConfig = NewAzureADWorkloadIdentityConfig(ctx, podIdentity.GetIdentityID(), info.AzureResourceManagerEndpoint) } authorizer, _ := authConfig.Authorizer() diff --git a/pkg/scalers/azure/azure_storage.go b/pkg/scalers/azure/azure_storage.go index 759fa62917c..12ac8e9f18d 100644 --- a/pkg/scalers/azure/azure_storage.go +++ b/pkg/scalers/azure/azure_storage.go @@ -111,7 +111,7 @@ func ParseAzureStorageQueueConnection(ctx context.Context, httpClient util.HTTPD return credential, endpoint, nil default: - return nil, nil, fmt.Errorf("azure queues doesn't support %s pod identity type", podIdentity) + return nil, nil, fmt.Errorf("azure queues doesn't support %s pod identity type", podIdentity.Provider) } } @@ -139,7 +139,7 @@ func ParseAzureStorageBlobConnection(ctx context.Context, httpClient util.HTTPDo return credential, endpoint, nil default: - return nil, nil, fmt.Errorf("azure queues doesn't support %s pod identity type", podIdentity) + return nil, nil, fmt.Errorf("azure queues doesn't support %s pod identity type", podIdentity.Provider) } } @@ -207,9 +207,9 @@ func parseAccessTokenAndEndpoint(ctx context.Context, httpClient util.HTTPDoer, switch podIdentity.Provider { case kedav1alpha1.PodIdentityProviderAzure: - token, err = GetAzureADPodIdentityToken(ctx, httpClient, podIdentity.IdentityID, storageResource) + token, err = GetAzureADPodIdentityToken(ctx, httpClient, podIdentity.GetIdentityID(), storageResource) case kedav1alpha1.PodIdentityProviderAzureWorkload: - token, err = GetAzureADWorkloadIdentityToken(ctx, podIdentity.IdentityID, storageResource) + token, err = GetAzureADWorkloadIdentityToken(ctx, podIdentity.GetIdentityID(), storageResource) } if err != nil { diff --git a/pkg/scalers/azure_blob_scaler.go b/pkg/scalers/azure_blob_scaler.go index 3fc0758b81b..8b154364b21 100644 --- a/pkg/scalers/azure_blob_scaler.go +++ b/pkg/scalers/azure_blob_scaler.go @@ -165,7 +165,7 @@ func parseAzureBlobMetadata(config *ScalerConfig, logger logr.Logger) (*azure.Bl return nil, kedav1alpha1.AuthPodIdentity{}, fmt.Errorf("no accountName given") } default: - return nil, kedav1alpha1.AuthPodIdentity{}, fmt.Errorf("pod identity %s not supported for azure storage blobs", config.PodIdentity) + return nil, kedav1alpha1.AuthPodIdentity{}, fmt.Errorf("pod identity %s not supported for azure storage blobs", config.PodIdentity.Provider) } meta.ScalerIndex = config.ScalerIndex diff --git a/pkg/scalers/azure_log_analytics_scaler.go b/pkg/scalers/azure_log_analytics_scaler.go index b57ba616a32..77cb7b74a39 100644 --- a/pkg/scalers/azure_log_analytics_scaler.go +++ b/pkg/scalers/azure_log_analytics_scaler.go @@ -162,7 +162,7 @@ func parseAzureLogAnalyticsMetadata(config *ScalerConfig) (*azureLogAnalyticsMet case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload: meta.podIdentity = config.PodIdentity default: - return nil, fmt.Errorf("error parsing metadata. Details: Log Analytics Scaler doesn't support pod identity %s", config.PodIdentity) + return nil, fmt.Errorf("error parsing metadata. Details: Log Analytics Scaler doesn't support pod identity %s", config.PodIdentity.Provider) } // Getting workspaceId @@ -469,7 +469,7 @@ func (s *azureLogAnalyticsScaler) getAuthorizationToken(ctx context.Context) (to switch s.metadata.podIdentity.Provider { case kedav1alpha1.PodIdentityProviderAzureWorkload: - aadToken, err := azure.GetAzureADWorkloadIdentityToken(ctx, s.metadata.podIdentity.IdentityID, s.metadata.logAnalyticsResourceURL) + aadToken, err := azure.GetAzureADWorkloadIdentityToken(ctx, s.metadata.podIdentity.GetIdentityID(), s.metadata.logAnalyticsResourceURL) if err != nil { return tokenData{}, nil } @@ -554,10 +554,10 @@ func (s *azureLogAnalyticsScaler) executeAADApicall(ctx context.Context) ([]byte func (s *azureLogAnalyticsScaler) executeIMDSApicall(ctx context.Context) ([]byte, int, error) { var urlStr string - if s.metadata.podIdentity.IdentityID == "" { + if s.metadata.podIdentity.GetIdentityID() == "" { urlStr = fmt.Sprintf(azure.MSIURL, s.metadata.logAnalyticsResourceURL) } else { - urlStr = fmt.Sprintf(azure.MSIURLWithClientID, s.metadata.logAnalyticsResourceURL, url.QueryEscape(s.metadata.podIdentity.IdentityID)) + urlStr = fmt.Sprintf(azure.MSIURLWithClientID, s.metadata.logAnalyticsResourceURL, url.QueryEscape(s.metadata.podIdentity.GetIdentityID())) } request, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil) diff --git a/pkg/scalers/azure_monitor_scaler.go b/pkg/scalers/azure_monitor_scaler.go index f4ef32ca953..f6a044814ec 100644 --- a/pkg/scalers/azure_monitor_scaler.go +++ b/pkg/scalers/azure_monitor_scaler.go @@ -207,7 +207,7 @@ func parseAzurePodIdentityParams(config *ScalerConfig) (clientID string, clientP case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload: // no params required to be parsed default: - return "", "", fmt.Errorf("azure Monitor doesn't support pod identity %s", config.PodIdentity) + return "", "", fmt.Errorf("azure Monitor doesn't support pod identity %s", config.PodIdentity.Provider) } return clientID, clientPassword, nil diff --git a/pkg/scalers/azure_queue_scaler.go b/pkg/scalers/azure_queue_scaler.go index 1088bd7e5b9..23fcd15815f 100644 --- a/pkg/scalers/azure_queue_scaler.go +++ b/pkg/scalers/azure_queue_scaler.go @@ -150,7 +150,7 @@ func parseAzureQueueMetadata(config *ScalerConfig, logger logr.Logger) (*azureQu return nil, kedav1alpha1.AuthPodIdentity{}, fmt.Errorf("no accountName given") } default: - return nil, kedav1alpha1.AuthPodIdentity{}, fmt.Errorf("pod identity %s not supported for azure storage queues", config.PodIdentity) + return nil, kedav1alpha1.AuthPodIdentity{}, fmt.Errorf("pod identity %s not supported for azure storage queues", config.PodIdentity.Provider) } meta.scalerIndex = config.ScalerIndex diff --git a/pkg/scalers/azure_servicebus_scaler.go b/pkg/scalers/azure_servicebus_scaler.go index 977d60b4c9b..7ad8ee71acd 100755 --- a/pkg/scalers/azure_servicebus_scaler.go +++ b/pkg/scalers/azure_servicebus_scaler.go @@ -214,7 +214,7 @@ func parseAzureServiceBusMetadata(config *ScalerConfig, logger logr.Logger) (*az } default: - return nil, fmt.Errorf("azure service bus doesn't support pod identity %s", config.PodIdentity) + return nil, fmt.Errorf("azure service bus doesn't support pod identity %s", config.PodIdentity.Provider) } meta.scalerIndex = config.ScalerIndex @@ -297,7 +297,7 @@ func (s *azureServiceBusScaler) getServiceBusAdminClient() (*admin.Client, error case "", kedav1alpha1.PodIdentityProviderNone: client, err = admin.NewClientFromConnectionString(s.metadata.connection, nil) case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload: - creds, chainedErr := azure.NewChainedCredential(s.podIdentity.IdentityID, s.podIdentity.Provider) + creds, chainedErr := azure.NewChainedCredential(s.podIdentity.GetIdentityID(), s.podIdentity.Provider) if chainedErr != nil { return nil, chainedErr } diff --git a/pkg/scalers/rabbitmq_scaler.go b/pkg/scalers/rabbitmq_scaler.go index fda8504c378..64288b1285e 100644 --- a/pkg/scalers/rabbitmq_scaler.go +++ b/pkg/scalers/rabbitmq_scaler.go @@ -241,7 +241,7 @@ func parseRabbitMQMetadata(config *ScalerConfig) (*rabbitMQMetadata, error) { if config.PodIdentity.Provider == v1alpha1.PodIdentityProviderAzureWorkload { if config.AuthParams["workloadIdentityResource"] != "" { - meta.workloadIdentityClientID = config.PodIdentity.IdentityID + meta.workloadIdentityClientID = config.PodIdentity.GetIdentityID() meta.workloadIdentityResource = config.AuthParams["workloadIdentityResource"] } } diff --git a/pkg/scalers/rabbitmq_scaler_test.go b/pkg/scalers/rabbitmq_scaler_test.go index efce946a1fc..303249e2d44 100644 --- a/pkg/scalers/rabbitmq_scaler_test.go +++ b/pkg/scalers/rabbitmq_scaler_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + kedautil "github.com/kedacore/keda/v2/pkg/util" ) const ( @@ -150,9 +151,9 @@ var testRabbitMQAuthParamData = []parseRabbitMQAuthParamTestData{ // failure, TLS invalid {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "yes", "ca": "caaa", "cert": "ceert", "key": "kee"}, true, true, false}, // success, WorkloadIdentity - {map[string]string{"queueName": "sample", "hostFromEnv": host, "protocol": "http"}, v1alpha1.AuthPodIdentity{Provider: v1alpha1.PodIdentityProviderAzureWorkload, IdentityID: "client-id"}, map[string]string{"workloadIdentityResource": "rabbitmq-resource-id"}, false, false, true}, + {map[string]string{"queueName": "sample", "hostFromEnv": host, "protocol": "http"}, v1alpha1.AuthPodIdentity{Provider: v1alpha1.PodIdentityProviderAzureWorkload, IdentityID: kedautil.StringPointer("client-id")}, map[string]string{"workloadIdentityResource": "rabbitmq-resource-id"}, false, false, true}, // failure, WoekloadIdentity not supported for amqp - {map[string]string{"queueName": "sample", "hostFromEnv": host, "protocol": "amqp"}, v1alpha1.AuthPodIdentity{Provider: v1alpha1.PodIdentityProviderAzureWorkload, IdentityID: "client-id"}, map[string]string{"workloadIdentityResource": "rabbitmq-resource-id"}, true, false, false}, + {map[string]string{"queueName": "sample", "hostFromEnv": host, "protocol": "amqp"}, v1alpha1.AuthPodIdentity{Provider: v1alpha1.PodIdentityProviderAzureWorkload, IdentityID: kedautil.StringPointer("client-id")}, map[string]string{"workloadIdentityResource": "rabbitmq-resource-id"}, true, false, false}, } var rabbitMQMetricIdentifiers = []rabbitMQMetricIdentifier{ {&testRabbitMQMetadata[1], 0, "s0-rabbitmq-sample"}, diff --git a/pkg/scaling/resolver/azure_keyvault_handler.go b/pkg/scaling/resolver/azure_keyvault_handler.go index 62263accb53..f7fe36544c2 100644 --- a/pkg/scaling/resolver/azure_keyvault_handler.go +++ b/pkg/scaling/resolver/azure_keyvault_handler.go @@ -133,12 +133,12 @@ func (vh *AzureKeyVaultHandler) getAuthConfig(ctx context.Context, client client case kedav1alpha1.PodIdentityProviderAzure: config := auth.NewMSIConfig() config.Resource = keyVaultResourceURL - config.ClientID = podIdentity.IdentityID + config.ClientID = podIdentity.GetIdentityID() return config, nil case kedav1alpha1.PodIdentityProviderAzureWorkload: - return azure.NewAzureADWorkloadIdentityConfig(ctx, podIdentity.IdentityID, keyVaultResourceURL), nil + return azure.NewAzureADWorkloadIdentityConfig(ctx, podIdentity.GetIdentityID(), keyVaultResourceURL), nil default: - return nil, fmt.Errorf("key vault does not support pod identity provider - %s", podIdentity) + return nil, fmt.Errorf("key vault does not support pod identity provider - %s", podIdentity.Provider) } } diff --git a/pkg/scaling/resolver/scale_resolvers.go b/pkg/scaling/resolver/scale_resolvers.go index 91dc9f63cfd..c2a9c39c9a6 100644 --- a/pkg/scaling/resolver/scale_resolvers.go +++ b/pkg/scaling/resolver/scale_resolvers.go @@ -181,7 +181,8 @@ func ResolveAuthRefAndPodIdentity(ctx context.Context, client client.Client, log if podTemplateSpec != nil { authParams, podIdentity := resolveAuthRef(ctx, client, logger, triggerAuthRef, &podTemplateSpec.Spec, namespace, secretsLister) - if podIdentity.Provider == kedav1alpha1.PodIdentityProviderAwsEKS { + switch podIdentity.Provider { + case kedav1alpha1.PodIdentityProviderAwsEKS: serviceAccountName := defaultServiceAccount if podTemplateSpec.Spec.ServiceAccountName != "" { serviceAccountName = podTemplateSpec.Spec.ServiceAccountName @@ -193,8 +194,13 @@ func ResolveAuthRefAndPodIdentity(ctx context.Context, client client.Client, log fmt.Errorf("error getting service account: '%s', error: %w", serviceAccountName, err) } authParams["awsRoleArn"] = serviceAccount.Annotations[kedav1alpha1.PodIdentityAnnotationEKS] - } else if podIdentity.Provider == kedav1alpha1.PodIdentityProviderAwsKiam { + case kedav1alpha1.PodIdentityProviderAwsKiam: authParams["awsRoleArn"] = podTemplateSpec.ObjectMeta.Annotations[kedav1alpha1.PodIdentityAnnotationKiam] + case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload: + if podIdentity.IdentityID != nil && *podIdentity.IdentityID == "" { + return nil, kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderNone}, fmt.Errorf("IdentityID of PodIdentity should not be empty") + } + default: } return authParams, podIdentity, nil } diff --git a/pkg/scaling/resolver/scale_resolvers_test.go b/pkg/scaling/resolver/scale_resolvers_test.go index 859b5a7c701..7db11297362 100644 --- a/pkg/scaling/resolver/scale_resolvers_test.go +++ b/pkg/scaling/resolver/scale_resolvers_test.go @@ -422,7 +422,7 @@ func TestResolveAuthRef(t *testing.T) { t.Errorf("Returned authParams are different: %s", diff) } if gotPodIdentity != test.expectedPodIdentity { - t.Errorf("Unexpected podidentity, wanted: %q got: %q", test.expectedPodIdentity, gotPodIdentity) + t.Errorf("Unexpected podidentity, wanted: %q got: %q", test.expectedPodIdentity.Provider, gotPodIdentity.Provider) } }) } diff --git a/pkg/util/conver_types.go b/pkg/util/conver_types.go new file mode 100644 index 00000000000..6d7559a8ee9 --- /dev/null +++ b/pkg/util/conver_types.go @@ -0,0 +1,22 @@ +/* +Copyright 2023 The KEDA Authors + +Licensed 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 util + +// String returns a pointer to the string value passed in. +func StringPointer(v string) *string { + return &v +} diff --git a/tests/internals/trigger_authentication_validation/trigger_authentication_validation_test.go b/tests/internals/trigger_authentication_validation/trigger_authentication_validation_test.go new file mode 100644 index 00000000000..f15099cfd7b --- /dev/null +++ b/tests/internals/trigger_authentication_validation/trigger_authentication_validation_test.go @@ -0,0 +1,224 @@ +//go:build e2e +// +build e2e + +package trigger_authentication_validation_test + +import ( + "context" + "fmt" + "testing" + + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../../.env") + +const ( + testName = "azure-aad-pod-identity-test" +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + triggerAuthEmptyIDName = fmt.Sprintf("%s-ta-empty", testName) + triggerAuthNilIDName = fmt.Sprintf("%s-ta-nil", testName) + clusterTriggerAuthEmptyIDName = fmt.Sprintf("%s-cta-empty", testName) + clusterTriggerAuthNilIDName = fmt.Sprintf("%s-cta-nil", testName) + triggerAuthWorkloadEmptyIDName = fmt.Sprintf("%s-ta-workload-empty", testName) + triggerAuthWorkloadNilIDName = fmt.Sprintf("%s-ta-workload-nil", testName) + clusterTriggerAuthWorkloadEmptyIDName = fmt.Sprintf("%s-cta-workload-empty", testName) + clusterTriggerAuthWorkloadNilIDName = fmt.Sprintf("%s-cta-workload-nil", testName) +) + +type templateData struct { + TestNamespace string + TriggerAuthEmptyIDName string + TriggerAuthNilIDName string + ClusterTriggerAuthEmptyIDName string + ClusterTriggerAuthNilIDName string + TriggerAuthWorkloadEmptyIDName string + TriggerAuthWorkloadNilIDName string + ClusterTriggerAuthWorkloadEmptyIDName string + ClusterTriggerAuthWorkloadNilIDName string +} + +const ( + triggerAuthEmptyIDTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthEmptyIDName}} + namespace: {{.TestNamespace}} +spec: + podIdentity: + provider: azure + identityId: "" +` + + triggerAuthNilIDTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthNilIDName}} + namespace: {{.TestNamespace}} +spec: + podIdentity: + provider: azure +` + clusterTriggerAuthEmptyIDTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ClusterTriggerAuthentication +metadata: + name: {{.ClusterTriggerAuthEmptyIDName}} +spec: + podIdentity: + provider: azure + identityId: "" +` + + clusterTriggerAuthNilIDTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ClusterTriggerAuthentication +metadata: + name: {{.ClusterTriggerAuthNilIDName}} +spec: + podIdentity: + provider: azure +` + + triggerAuthWorkloadEmptyIDTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthWorkloadEmptyIDName}} + namespace: {{.TestNamespace}} +spec: + podIdentity: + provider: azure-workload + identityId: "" +` + + triggerAuthWorkloadNilIDTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthWorkloadNilIDName}} + namespace: {{.TestNamespace}} +spec: + podIdentity: + provider: azure-workload +` + clusterTriggerAuthWorkloadEmptyIDTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ClusterTriggerAuthentication +metadata: + name: {{.ClusterTriggerAuthWorkloadEmptyIDName}} +spec: + podIdentity: + provider: azure-workload + identityId: "" +` + + clusterTriggerAuthWorkloadNilIDTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ClusterTriggerAuthentication +metadata: + name: {{.ClusterTriggerAuthWorkloadNilIDName}} +spec: + podIdentity: + provider: azure-workload +` +) + +func TestScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + + // Create kubernetes resources + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + // test auth + testTriggerAuthenticationWithEmptyID(t, kc, data) + testTriggerAuthenticationWithNilID(t, kc, data) + testClusterTriggerAuthenticationWithEmptyID(t, kc, data) + testClusterTriggerAuthenticationWithNilID(t, kc, data) + + // cleanup + DeleteKubernetesResources(t, testNamespace, data, templates) +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + TriggerAuthEmptyIDName: triggerAuthEmptyIDName, + TriggerAuthNilIDName: triggerAuthNilIDName, + ClusterTriggerAuthEmptyIDName: clusterTriggerAuthEmptyIDName, + ClusterTriggerAuthNilIDName: clusterTriggerAuthNilIDName, + TriggerAuthWorkloadEmptyIDName: triggerAuthWorkloadEmptyIDName, + TriggerAuthWorkloadNilIDName: triggerAuthWorkloadNilIDName, + ClusterTriggerAuthWorkloadEmptyIDName: clusterTriggerAuthWorkloadEmptyIDName, + ClusterTriggerAuthWorkloadNilIDName: clusterTriggerAuthWorkloadNilIDName, + }, []Template{} +} + +// expect triggerauthentication should not be created with empty identity id +func testTriggerAuthenticationWithEmptyID(t *testing.T, _ *kubernetes.Clientset, data templateData) { + t.Log("--- create triggerauthentication with empty identity id ---") + + err := KubectlApplyWithErrors(t, data, "triggerAuthEmptyIDTemplate", triggerAuthEmptyIDTemplate) + assert.Errorf(t, err, "can deploy TriggerAuthtication - %s", err) + + err = KubectlApplyWithErrors(t, data, "triggerAuthWorkloadEmptyIDTemplate", triggerAuthWorkloadEmptyIDTemplate) + assert.Errorf(t, err, "can deploy TriggerAuthtication with azureworkload - %s", err) +} + +// expect triggerauthentication can be created without identity id property +func testTriggerAuthenticationWithNilID(t *testing.T, _ *kubernetes.Clientset, data templateData) { + t.Log("--- create triggerauthentication with nil identity id ---") + + kedaKc := GetKedaKubernetesClient(t) + KubectlApplyWithTemplate(t, data, "triggerAuthNilITemplate", triggerAuthNilIDTemplate) + + triggerauthentication, _ := kedaKc.TriggerAuthentications(testNamespace).Get(context.Background(), triggerAuthNilIDName, v1.GetOptions{}) + assert.NotNil(t, triggerauthentication) + + KubectlApplyWithTemplate(t, data, "triggerAuthWorkloadNilITemplate", triggerAuthWorkloadNilIDTemplate) + + triggerauthentication, _ = kedaKc.TriggerAuthentications(testNamespace).Get(context.Background(), triggerAuthWorkloadNilIDName, v1.GetOptions{}) + assert.NotNil(t, triggerauthentication) +} + +// expect clustertriggerauthentication should not be created with empty identity id +func testClusterTriggerAuthenticationWithEmptyID(t *testing.T, _ *kubernetes.Clientset, data templateData) { + t.Log("--- create clustertriggerauthentication with empty identity id ---") + + err := KubectlApplyWithErrors(t, data, "clusterTriggerAuthEmptyIDTemplate", clusterTriggerAuthEmptyIDTemplate) + assert.Errorf(t, err, "can deploy ClusterTriggerAuthtication - %s", err) + + err = KubectlApplyWithErrors(t, data, "clusterTriggerAuthWorkloadEmptyIDTemplate", clusterTriggerAuthWorkloadEmptyIDTemplate) + assert.Errorf(t, err, "can deploy ClusterTriggerAuthtication with azureworkload - %s", err) +} + +// expect clustertriggerauthentication can be created without identity id property +func testClusterTriggerAuthenticationWithNilID(t *testing.T, _ *kubernetes.Clientset, data templateData) { + t.Log("--- create clustertriggerauthentication with nil identity id ---") + + kedaKc := GetKedaKubernetesClient(t) + KubectlApplyWithTemplate(t, data, "clusterTriggerAuthNilIDTemplate", clusterTriggerAuthNilIDTemplate) + + clustertriggerauthentication, _ := kedaKc.ClusterTriggerAuthentications().Get(context.Background(), clusterTriggerAuthNilIDTemplate, v1.GetOptions{}) + assert.NotNil(t, clustertriggerauthentication) + + KubectlApplyWithTemplate(t, data, "clusterTriggerAuthWorkloadNilIDTemplate", clusterTriggerAuthWorkloadNilIDTemplate) + + clustertriggerauthentication, _ = kedaKc.ClusterTriggerAuthentications().Get(context.Background(), clusterTriggerAuthWorkloadNilIDTemplate, v1.GetOptions{}) + assert.NotNil(t, clustertriggerauthentication) +}