diff --git a/.github/workflows/auto-add-issues-to-project.yml b/.github/workflows/auto-add-issues-to-project.yml index 1d1cd21ebca..ebdfd52a6c6 100644 --- a/.github/workflows/auto-add-issues-to-project.yml +++ b/.github/workflows/auto-add-issues-to-project.yml @@ -5,7 +5,7 @@ on: - opened jobs: track_issue: - runs-on: equinix-2cpu-8gb + runs-on: ubuntu-latest steps: - name: Get project data env: diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index eb5f070204b..7fd93ee6752 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -15,7 +15,7 @@ concurrency: jobs: build: - runs-on: equinix-4cpu-16gb + runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5 diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml index 0d6bb0218fb..61d600ec35a 100644 --- a/.github/workflows/main-build.yml +++ b/.github/workflows/main-build.yml @@ -6,7 +6,7 @@ on: jobs: build: name: build - runs-on: equinix-4cpu-16gb + runs-on: ARM64 permissions: contents: read packages: write @@ -91,7 +91,7 @@ jobs: needs: build uses: kedacore/keda/.github/workflows/template-trivy-scan.yml@main with: - runs-on: equinix-4cpu-16gb + runs-on: ubuntu-latest scan-type: "fs" format: "sarif" exit-code: 0 @@ -101,7 +101,7 @@ jobs: needs: build strategy: matrix: - runner: [oracle-aarch64-4cpu-16gb, equinix-4cpu-16gb] + runner: [ARM64, ubuntu-latest] uses: kedacore/keda/.github/workflows/template-trivy-scan.yml@main with: runs-on: ${{ matrix.runner }} @@ -115,7 +115,7 @@ jobs: needs: build strategy: matrix: - runner: [oracle-aarch64-4cpu-16gb, equinix-4cpu-16gb] + runner: [ARM64, ubuntu-latest] uses: kedacore/keda/.github/workflows/template-trivy-scan.yml@main with: runs-on: ${{ matrix.runner }} diff --git a/.github/workflows/pr-e2e-checker.yml b/.github/workflows/pr-e2e-checker.yml index e6f79675464..53b5d6a20ac 100644 --- a/.github/workflows/pr-e2e-checker.yml +++ b/.github/workflows/pr-e2e-checker.yml @@ -16,7 +16,7 @@ concurrency: jobs: e2e-checker: name: label checker - runs-on: equinix-2cpu-8gb + runs-on: ubuntu-latest steps: - uses: LouisBrunner/checks-action@6b626ffbad7cc56fd58627f774b9067e6118af23 # v2 name: Enqueue e2e diff --git a/.github/workflows/pr-e2e-creator.yml b/.github/workflows/pr-e2e-creator.yml index 87170867ecf..f263683163e 100644 --- a/.github/workflows/pr-e2e-creator.yml +++ b/.github/workflows/pr-e2e-creator.yml @@ -16,7 +16,7 @@ concurrency: jobs: check-creator: name: check-creator - runs-on: equinix-2cpu-8gb + runs-on: ubuntu-latest steps: - uses: LouisBrunner/checks-action@6b626ffbad7cc56fd58627f774b9067e6118af23 # v2 name: Enqueue e2e diff --git a/.github/workflows/pr-e2e.yml b/.github/workflows/pr-e2e.yml index 1ffa92cee4e..bc0a5b2640d 100644 --- a/.github/workflows/pr-e2e.yml +++ b/.github/workflows/pr-e2e.yml @@ -8,8 +8,9 @@ env: jobs: triage: - runs-on: equinix-2cpu-8gb + runs-on: ubuntu-latest name: Comment evaluate + container: ghcr.io/kedacore/keda-tools:1.22.5 outputs: run-e2e: ${{ startsWith(github.event.comment.body,'/run-e2e') && steps.checkUserMember.outputs.isTeamMember == 'true' }} pr_num: ${{ steps.parser.outputs.pr_num }} @@ -66,7 +67,7 @@ jobs: build-test-images: needs: triage - runs-on: equinix-4cpu-16gb + runs-on: ubuntu-latest name: Build images container: ghcr.io/kedacore/keda-tools:1.22.5 if: needs.triage.outputs.run-e2e == 'true' @@ -146,7 +147,7 @@ jobs: run-test: needs: [triage, build-test-images] - runs-on: equinix-keda-runner + runs-on: e2e name: Execute e2e tests container: ghcr.io/kedacore/keda-tools:1.22.5 if: needs.triage.outputs.run-e2e == 'true' @@ -180,7 +181,7 @@ jobs: - name: Scale cluster run: make scale-node-pool env: - NODE_POOL_SIZE: 2 + NODE_POOL_SIZE: 3 TEST_CLUSTER_NAME: keda-e2e-cluster-pr - name: Run end to end tests diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 51f251afa1f..599d2f4b9d7 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -14,9 +14,9 @@ jobs: strategy: matrix: include: - - runner: oracle-aarch64-4cpu-16gb + - runner: ARM64 name: arm64 - - runner: equinix-4cpu-16gb + - runner: ubuntu-latest name: amd64 steps: - name: Check out code @@ -81,9 +81,9 @@ jobs: strategy: matrix: include: - - runner: oracle-aarch64-4cpu-16gb + - runner: ARM64 name: arm64 - - runner: equinix-4cpu-16gb + - runner: ubuntu-latest name: amd64 steps: - name: Check out code @@ -112,9 +112,9 @@ jobs: strategy: matrix: include: - - runner: oracle-aarch64-4cpu-16gb + - runner: ARM64 name: arm64 - - runner: equinix-4cpu-16gb + - runner: ubuntu-latest name: amd64 steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 @@ -151,7 +151,7 @@ jobs: trivy-scan: uses: kedacore/keda/.github/workflows/template-trivy-scan.yml@main with: - runs-on: equinix-4cpu-16gb + runs-on: ubuntu-latest scan-type: "fs" format: "table" output: "" diff --git a/.github/workflows/pr-welcome.yml b/.github/workflows/pr-welcome.yml index c475a7c94eb..2fac99476cb 100644 --- a/.github/workflows/pr-welcome.yml +++ b/.github/workflows/pr-welcome.yml @@ -15,7 +15,7 @@ permissions: jobs: pr_bot: name: PR Bot - runs-on: equinix-2cpu-8gb + runs-on: ubuntu-latest steps: - name: "Add welcome comment on PR #${{ github.event.number }} (draft)" uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 58e6fc1d2a2..897f78a36fb 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -6,7 +6,7 @@ on: jobs: build: name: Push Release - runs-on: equinix-4cpu-16gb + runs-on: ARM64 permissions: contents: write packages: write diff --git a/.github/workflows/static-analysis-codeql.yml b/.github/workflows/static-analysis-codeql.yml index 1abb8c783db..d353eafdc40 100644 --- a/.github/workflows/static-analysis-codeql.yml +++ b/.github/workflows/static-analysis-codeql.yml @@ -12,7 +12,7 @@ concurrency: jobs: codeQl: name: Analyze CodeQL Go - runs-on: equinix-4cpu-16gb + runs-on: ubuntu-latest container: ghcr.io/kedacore/keda-tools:1.22.5 if: (github.actor != 'dependabot[bot]') steps: diff --git a/.github/workflows/static-analysis-semgrep.yml b/.github/workflows/static-analysis-semgrep.yml index e1d62425e46..1b68a04d000 100644 --- a/.github/workflows/static-analysis-semgrep.yml +++ b/.github/workflows/static-analysis-semgrep.yml @@ -12,7 +12,7 @@ concurrency: jobs: semgrep: name: Analyze Semgrep - runs-on: equinix-4cpu-16gb + runs-on: ubuntu-latest container: returntocorp/semgrep if: (github.actor != 'dependabot[bot]') steps: diff --git a/.github/workflows/template-arm64-smoke-tests.yml b/.github/workflows/template-arm64-smoke-tests.yml index f24c9b447d9..a7661f73717 100644 --- a/.github/workflows/template-arm64-smoke-tests.yml +++ b/.github/workflows/template-arm64-smoke-tests.yml @@ -9,6 +9,6 @@ jobs: concurrency: arm-smoke-tests uses: kedacore/keda/.github/workflows/template-smoke-tests.yml@main with: - runs-on: oracle-aarch64-4cpu-16gb + runs-on: ARM64 kubernetesVersion: v1.30 kindImage: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e diff --git a/.github/workflows/template-main-e2e-test.yml b/.github/workflows/template-main-e2e-test.yml index a22a448da20..88d4504bcd3 100644 --- a/.github/workflows/template-main-e2e-test.yml +++ b/.github/workflows/template-main-e2e-test.yml @@ -6,7 +6,7 @@ on: jobs: e2e-tests: name: Run e2e test - runs-on: oracle-aarch64-4cpu-16gb + runs-on: ARM64 # keda-tools is built from github.com/test-tools/tools/Dockerfile container: ghcr.io/kedacore/keda-tools:1.22.5 concurrency: e2e-tests @@ -26,7 +26,7 @@ jobs: - name: Scale cluster run: make scale-node-pool env: - NODE_POOL_SIZE: 2 + NODE_POOL_SIZE: 3 - name: Run end to end tests env: diff --git a/.github/workflows/template-trivy-scan.yml b/.github/workflows/template-trivy-scan.yml index 990e8be436b..414164f8429 100644 --- a/.github/workflows/template-trivy-scan.yml +++ b/.github/workflows/template-trivy-scan.yml @@ -40,6 +40,8 @@ jobs: - name: Run Trivy uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0 + env: + TRIVY_DB_REPOSITORY: ghcr.io/kedacore/trivy-db with: scan-type: ${{ inputs.scan-type }} image-ref: ${{ inputs.image-ref }} diff --git a/.github/workflows/template-versions-smoke-tests.yml b/.github/workflows/template-versions-smoke-tests.yml index 36f4e7c539a..00e7929f59f 100644 --- a/.github/workflows/template-versions-smoke-tests.yml +++ b/.github/workflows/template-versions-smoke-tests.yml @@ -5,7 +5,7 @@ on: jobs: smoke-tests: - name: equinix-4cpu-16gb + name: ubuntu-latest strategy: fail-fast: false matrix: @@ -19,6 +19,6 @@ jobs: kindImage: kindest/node:v1.28.9@sha256:dca54bc6a6079dd34699d53d7d4ffa2e853e46a20cd12d619a09207e35300bd0 uses: kedacore/keda/.github/workflows/template-smoke-tests.yml@main with: - runs-on: equinix-4cpu-16gb + runs-on: ubuntu-latest kubernetesVersion: ${{ matrix.kubernetesVersion }} kindImage: ${{ matrix.kindImage }} diff --git a/.github/workflows/v1-build.yml b/.github/workflows/v1-build.yml index 9e503e15294..fc9cc1bb4ed 100644 --- a/.github/workflows/v1-build.yml +++ b/.github/workflows/v1-build.yml @@ -6,7 +6,7 @@ on: jobs: validate: name: Validate - runs-on: equinix-2cpu-8gb + runs-on: ubuntu-latest container: kedacore/build-tools:v1 steps: - name: Check out code diff --git a/CHANGELOG.md b/CHANGELOG.md index 6029fc8933b..24e8f1dae11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,9 +57,13 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio ### New +- **General**: Add the generateEmbeddedObjectMeta flag to generate meta properties of JobTargetRef in ScaledJob ([#5908](https://github.com/kedacore/keda/issues/5908)) +- **General**: Cache miss fallback in validating webhook for ScaledObjects with direct kubernetes client ([#5973](https://github.com/kedacore/keda/issues/5973)) - **CloudEventSource**: Introduce ClusterCloudEventSource ([#3533](https://github.com/kedacore/keda/issues/3533)) - **CloudEventSource**: Provide ClusterCloudEventSource around the management of ScaledJobs resources ([#3523](https://github.com/kedacore/keda/issues/3523)) - **CloudEventSource**: Provide ClusterCloudEventSource around the management of TriggerAuthentication/ClusterTriggerAuthentication resources ([#3524](https://github.com/kedacore/keda/issues/3524)) +- **Github Action**: Fix panic when env for runnerScopeFromEnv or ownerFromEnv is empty ([#6156](https://github.com/kedacore/keda/issues/6156)) +- **RabbitMQ Scaler**: provide separate paremeters for user and password ([#2513](https://github.com/kedacore/keda/issues/2513)) #### Experimental @@ -69,13 +73,18 @@ Here is an overview of all new **experimental** features: ### Improvements +- **General**: Prevent multiple ScaledObjects managing one HPA ([#6130](https://github.com/kedacore/keda/issues/6130)) - **AWS CloudWatch Scaler**: Add support for ignoreNullValues ([#5352](https://github.com/kedacore/keda/issues/5352)) +- **Elasticsearch Scaler**: Support Query at the Elasticsearch scaler ([#6216](https://github.com/kedacore/keda/issues/6216)) +- **Etcd Scaler**: Add username and password support for etcd ([#6199](https://github.com/kedacore/keda/pull/6199)) - **GCP Scalers**: Added custom time horizon in GCP scalers ([#5778](https://github.com/kedacore/keda/issues/5778)) - **GitHub Scaler**: Fixed pagination, fetching repository list ([#5738](https://github.com/kedacore/keda/issues/5738)) - **Grafana dashboard**: Fix dashboard to handle wildcard scaledObject variables ([#6214](https://github.com/kedacore/keda/issues/6214)) +- **Kafka**: Allow disabling FAST negotation when using Kerberos ([#6188](https://github.com/kedacore/keda/issues/6188)) - **Kafka**: Fix logic to scale to zero on invalid offset even with earliest offsetResetPolicy ([#5689](https://github.com/kedacore/keda/issues/5689)) - **RabbitMQ Scaler**: Add connection name for AMQP ([#5958](https://github.com/kedacore/keda/issues/5958)) - **Selenium Scaler**: Add Support for Username and Password Authentication ([#6144](https://github.com/kedacore/keda/issues/6144)) +- **Selenium Scaler**: Introduce new parameters setSessionsFromHub, sessionsPerNode and sessionBrowserVersion. ([#6080](https://github.com/kedacore/keda/issues/6080)) - TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX)) ### Fixes @@ -101,7 +110,7 @@ New deprecation(s): ### Other -- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX)) +- **Cron scaler**: Simplify cron scaler code ([#6056](https://github.com/kedacore/keda/issues/6056)) ## v2.15.1 diff --git a/Makefile b/Makefile index 4ee8b59f2df..22c8c707608 100644 --- a/Makefile +++ b/Makefile @@ -131,7 +131,7 @@ smoke-test: ## Run e2e tests against Kubernetes cluster configured in ~/.kube/co ##@ Development manifests: controller-gen ## Generate ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) crd:crdVersions=v1 rbac:roleName=keda-operator paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd:crdVersions=v1,generateEmbeddedObjectMeta=true rbac:roleName=keda-operator paths="./..." output:crd:artifacts:config=config/crd/bases # withTriggers is only used for duck typing so we only need the deepcopy methods # However operator-sdk generate doesn't appear to have an option for that # until this issue is fixed: https://github.com/kubernetes-sigs/controller-tools/issues/398 diff --git a/apis/keda/v1alpha1/scaledobject_webhook.go b/apis/keda/v1alpha1/scaledobject_webhook.go index b90eb851ea8..776fe52aece 100644 --- a/apis/keda/v1alpha1/scaledobject_webhook.go +++ b/apis/keda/v1alpha1/scaledobject_webhook.go @@ -29,6 +29,7 @@ import ( appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -44,14 +45,31 @@ import ( var scaledobjectlog = logf.Log.WithName("scaledobject-validation-webhook") var kc client.Client +var cacheMissToDirectClient bool +var directClient client.Client var restMapper meta.RESTMapper var memoryString = "memory" var cpuString = "cpu" -func (so *ScaledObject) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (so *ScaledObject) SetupWebhookWithManager(mgr ctrl.Manager, cacheMissFallback bool) error { kc = mgr.GetClient() restMapper = mgr.GetRESTMapper() + cacheMissToDirectClient = cacheMissFallback + if cacheMissToDirectClient { + cfg := mgr.GetConfig() + opts := client.Options{ + HTTPClient: mgr.GetHTTPClient(), + Scheme: mgr.GetScheme(), + Mapper: restMapper, + Cache: nil, // this disables the cache and explicitly uses the direct client + } + var err error + directClient, err = client.New(cfg, opts) + if err != nil { + return fmt.Errorf("failed to initialize direct client: %w", err) + } + } return ctrl.NewWebhookManagedBy(mgr). WithValidator(&ScaledObjectCustomValidator{}). For(so). @@ -279,6 +297,7 @@ func verifyScaledObjects(incomingSo *ScaledObject, action string, _ bool) error return err } + incomingSoHpaName := getHpaName(*incomingSo) for _, so := range soList.Items { if so.Name == incomingSo.Name { continue @@ -299,6 +318,13 @@ func verifyScaledObjects(incomingSo *ScaledObject, action string, _ bool) error metricscollector.RecordScaledObjectValidatingErrors(incomingSo.Namespace, action, "other-scaled-object") return err } + + if getHpaName(so) == incomingSoHpaName { + err = fmt.Errorf("the HPA '%s' is already managed by the ScaledObject '%s'", so.Spec.Advanced.HorizontalPodAutoscalerConfig.Name, so.Name) + scaledobjectlog.Error(err, "validation error") + metricscollector.RecordScaledObjectValidatingErrors(incomingSo.Namespace, action, "other-scaled-object-hpa") + return err + } } // verify ScalingModifiers structure if defined in ScaledObject @@ -314,6 +340,18 @@ func verifyScaledObjects(incomingSo *ScaledObject, action string, _ bool) error return nil } +// getFromCacheOrDirect is a helper function that tries to get an object from the cache +// if it fails, it tries to get it from the direct client +func getFromCacheOrDirect(ctx context.Context, key client.ObjectKey, obj client.Object) error { + err := kc.Get(ctx, key, obj, &client.GetOptions{}) + if cacheMissToDirectClient { + if kerrors.IsNotFound(err) { + return directClient.Get(ctx, key, obj, &client.GetOptions{}) + } + } + return err +} + func verifyCPUMemoryScalers(incomingSo *ScaledObject, action string, dryRun bool) error { if dryRun { return nil @@ -336,15 +374,13 @@ func verifyCPUMemoryScalers(incomingSo *ScaledObject, action string, dryRun bool switch incomingSoGckr.GVKString() { case "apps/v1.Deployment": deployment := &appsv1.Deployment{} - err := kc.Get(context.Background(), key, deployment, &client.GetOptions{}) - if err != nil { + if err := getFromCacheOrDirect(context.Background(), key, deployment); err != nil { return err } podSpec = &deployment.Spec.Template.Spec case "apps/v1.StatefulSet": statefulset := &appsv1.StatefulSet{} - err := kc.Get(context.Background(), key, statefulset, &client.GetOptions{}) - if err != nil { + if err := getFromCacheOrDirect(context.Background(), key, statefulset); err != nil { return err } podSpec = &statefulset.Spec.Template.Spec @@ -546,3 +582,11 @@ func isContainerResourceLimitSet(ctx context.Context, namespace string, triggerT return false } + +func getHpaName(so ScaledObject) string { + if so.Spec.Advanced == nil || so.Spec.Advanced.HorizontalPodAutoscalerConfig == nil || so.Spec.Advanced.HorizontalPodAutoscalerConfig.Name == "" { + return fmt.Sprintf("keda-hpa-%s", so.Name) + } + + return so.Spec.Advanced.HorizontalPodAutoscalerConfig.Name +} diff --git a/apis/keda/v1alpha1/suite_test.go b/apis/keda/v1alpha1/suite_test.go index ce53cc62682..e595508cbdf 100644 --- a/apis/keda/v1alpha1/suite_test.go +++ b/apis/keda/v1alpha1/suite_test.go @@ -118,7 +118,7 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&ScaledObject{}).SetupWebhookWithManager(mgr) + err = (&ScaledObject{}).SetupWebhookWithManager(mgr, false) Expect(err).NotTo(HaveOccurred()) err = (&ScaledJob{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) diff --git a/cmd/webhooks/main.go b/cmd/webhooks/main.go index 46a80a3955e..56c03eb1b00 100644 --- a/cmd/webhooks/main.go +++ b/cmd/webhooks/main.go @@ -62,6 +62,7 @@ func main() { var webhooksClientRequestBurst int var certDir string var webhooksPort int + var cacheMissToDirectClient bool pflag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") pflag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -70,6 +71,7 @@ func main() { pflag.IntVar(&webhooksClientRequestBurst, "kube-api-burst", 30, "Set the burst for throttling requests sent to the apiserver") pflag.StringVar(&certDir, "cert-dir", "/certs", "Webhook certificates dir to use. Defaults to /certs") pflag.IntVar(&webhooksPort, "port", 9443, "Port number to serve webhooks. Defaults to 9443") + pflag.BoolVar(&cacheMissToDirectClient, "cache-miss-to-direct-client", false, "If true, on cache misses the webhook will call the direct client to fetch the object") opts := zap.Options{} opts.BindFlags(flag.CommandLine) @@ -117,7 +119,7 @@ func main() { kedautil.PrintWelcome(setupLog, kubeVersion, "admission webhooks") - setupWebhook(mgr) + setupWebhook(mgr, cacheMissToDirectClient) if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") @@ -134,9 +136,9 @@ func main() { } } -func setupWebhook(mgr manager.Manager) { +func setupWebhook(mgr manager.Manager, cacheMissToDirectClient bool) { // setup webhooks - if err := (&kedav1alpha1.ScaledObject{}).SetupWebhookWithManager(mgr); err != nil { + if err := (&kedav1alpha1.ScaledObject{}).SetupWebhookWithManager(mgr, cacheMissToDirectClient); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "ScaledObject") os.Exit(1) } diff --git a/config/crd/bases/keda.sh_scaledjobs.yaml b/config/crd/bases/keda.sh_scaledjobs.yaml index 5ccf72f2d46..47e3a079da3 100644 --- a/config/crd/bases/keda.sh_scaledjobs.yaml +++ b/config/crd/bases/keda.sh_scaledjobs.yaml @@ -380,6 +380,23 @@ spec: description: |- Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string type: object spec: description: |- @@ -6684,6 +6701,23 @@ spec: May contain labels and annotations that will be copied into the PVC when creating it. No other fields are allowed and will be rejected during validation. + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string type: object spec: description: |- diff --git a/pkg/scalers/cassandra_scaler.go b/pkg/scalers/cassandra_scaler.go index 6e8705d2d8d..b41dddb9dec 100644 --- a/pkg/scalers/cassandra_scaler.go +++ b/pkg/scalers/cassandra_scaler.go @@ -18,53 +18,70 @@ import ( kedautil "github.com/kedacore/keda/v2/pkg/util" ) -// cassandraScaler exposes a data pointer to CassandraMetadata and gocql.Session connection. type cassandraScaler struct { metricType v2.MetricTargetType - metadata *CassandraMetadata + metadata cassandraMetadata session *gocql.Session logger logr.Logger } -// CassandraMetadata defines metadata used by KEDA to query a Cassandra table. -type CassandraMetadata struct { - username string - password string - enableTLS bool - cert string - key string - ca string - clusterIPAddress string - port int - consistency gocql.Consistency - protocolVersion int - keyspace string - query string - targetQueryValue int64 - activationTargetQueryValue int64 - triggerIndex int +type cassandraMetadata struct { + Username string `keda:"name=username, order=triggerMetadata"` + Password string `keda:"name=password, order=authParams"` + TLS string `keda:"name=tls, order=authParams, enum=enable;disable, default=disable, optional"` + Cert string `keda:"name=cert, order=authParams, optional"` + Key string `keda:"name=key, order=authParams, optional"` + CA string `keda:"name=ca, order=authParams, optional"` + ClusterIPAddress string `keda:"name=clusterIPAddress, order=triggerMetadata"` + Port int `keda:"name=port, order=triggerMetadata, optional"` + Consistency string `keda:"name=consistency, order=triggerMetadata, default=one"` + ProtocolVersion int `keda:"name=protocolVersion, order=triggerMetadata, default=4"` + Keyspace string `keda:"name=keyspace, order=triggerMetadata"` + Query string `keda:"name=query, order=triggerMetadata"` + TargetQueryValue int64 `keda:"name=targetQueryValue, order=triggerMetadata"` + ActivationTargetQueryValue int64 `keda:"name=activationTargetQueryValue, order=triggerMetadata, default=0"` + TriggerIndex int } const ( - tlsEnable = "enable" - tlsDisable = "disable" + tlsEnable = "enable" ) -// NewCassandraScaler creates a new Cassandra scaler. +func (m *cassandraMetadata) Validate() error { + if m.TLS == tlsEnable && (m.Cert == "" || m.Key == "") { + return errors.New("both cert and key are required when TLS is enabled") + } + + // Handle port in ClusterIPAddress + splitVal := strings.Split(m.ClusterIPAddress, ":") + if len(splitVal) == 2 { + if port, err := strconv.Atoi(splitVal[1]); err == nil { + m.Port = port + return nil + } + } + + if m.Port == 0 { + return fmt.Errorf("no port given") + } + + m.ClusterIPAddress = net.JoinHostPort(m.ClusterIPAddress, fmt.Sprintf("%d", m.Port)) + return nil +} + +// NewCassandraScaler creates a new Cassandra scaler func NewCassandraScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { metricType, err := GetMetricTargetType(config) if err != nil { return nil, fmt.Errorf("error getting scaler metric type: %w", err) } - logger := InitializeLogger(config, "cassandra_scaler") - meta, err := parseCassandraMetadata(config) if err != nil { return nil, fmt.Errorf("error parsing cassandra metadata: %w", err) } - session, err := newCassandraSession(meta, logger) + session, err := newCassandraSession(meta, InitializeLogger(config, "cassandra_scaler")) if err != nil { return nil, fmt.Errorf("error establishing cassandra session: %w", err) } @@ -73,108 +90,27 @@ func NewCassandraScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { metricType: metricType, metadata: meta, session: session, - logger: logger, + logger: InitializeLogger(config, "cassandra_scaler"), }, nil } -// parseCassandraMetadata parses the metadata and returns a CassandraMetadata or an error if the ScalerConfig is invalid. -func parseCassandraMetadata(config *scalersconfig.ScalerConfig) (*CassandraMetadata, error) { - meta := &CassandraMetadata{} - var err error - - if val, ok := config.TriggerMetadata["query"]; ok { - meta.query = val - } else { - return nil, fmt.Errorf("no query given") - } - - if val, ok := config.TriggerMetadata["targetQueryValue"]; ok { - targetQueryValue, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("targetQueryValue parsing error %w", err) - } - meta.targetQueryValue = targetQueryValue - } else { - if config.AsMetricSource { - meta.targetQueryValue = 0 - } else { - return nil, fmt.Errorf("no targetQueryValue given") - } - } - - meta.activationTargetQueryValue = 0 - if val, ok := config.TriggerMetadata["activationTargetQueryValue"]; ok { - activationTargetQueryValue, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("activationTargetQueryValue parsing error %w", err) - } - meta.activationTargetQueryValue = activationTargetQueryValue - } - - if val, ok := config.TriggerMetadata["username"]; ok { - meta.username = val - } else { - return nil, fmt.Errorf("no username given") - } - - if val, ok := config.TriggerMetadata["port"]; ok { - port, err := strconv.Atoi(val) - if err != nil { - return nil, fmt.Errorf("port parsing error %w", err) - } - meta.port = port - } - - if val, ok := config.TriggerMetadata["clusterIPAddress"]; ok { - splitval := strings.Split(val, ":") - port := splitval[len(splitval)-1] - - _, err := strconv.Atoi(port) - switch { - case err == nil: - meta.clusterIPAddress = val - case meta.port > 0: - meta.clusterIPAddress = net.JoinHostPort(val, fmt.Sprintf("%d", meta.port)) - default: - return nil, fmt.Errorf("no port given") - } - } else { - return nil, fmt.Errorf("no cluster IP address given") - } - - if val, ok := config.TriggerMetadata["protocolVersion"]; ok { - protocolVersion, err := strconv.Atoi(val) - if err != nil { - return nil, fmt.Errorf("protocolVersion parsing error %w", err) - } - meta.protocolVersion = protocolVersion - } else { - meta.protocolVersion = 4 - } - - if val, ok := config.TriggerMetadata["consistency"]; ok { - meta.consistency = gocql.ParseConsistency(val) - } else { - meta.consistency = gocql.One +func parseCassandraMetadata(config *scalersconfig.ScalerConfig) (cassandraMetadata, error) { + meta := cassandraMetadata{} + err := config.TypedConfig(&meta) + if err != nil { + return meta, fmt.Errorf("error parsing cassandra metadata: %w", err) } - if val, ok := config.TriggerMetadata["keyspace"]; ok { - meta.keyspace = val - } else { - return nil, fmt.Errorf("no keyspace given") - } - if val, ok := config.AuthParams["password"]; ok { - meta.password = val - } else { - return nil, fmt.Errorf("no password given") + if config.AsMetricSource { + meta.TargetQueryValue = 0 } - if err = parseCassandraTLS(config, meta); err != nil { + err = parseCassandraTLS(&meta) + if err != nil { return meta, err } - meta.triggerIndex = config.TriggerIndex - + meta.TriggerIndex = config.TriggerIndex return meta, nil } @@ -182,8 +118,8 @@ func createTempFile(prefix string, content string) (string, error) { tempCassandraDir := fmt.Sprintf("%s%c%s", os.TempDir(), os.PathSeparator, "cassandra") err := os.MkdirAll(tempCassandraDir, 0700) if err != nil { - return "", fmt.Errorf(`error creating temporary directory: %s. Error: %w - Note, when running in a container a writable /tmp/cassandra emptyDir must be mounted. Refer to documentation`, tempCassandraDir, err) + return "", fmt.Errorf(`error creating temporary directory: %s. Error: %w + Note, when running in a container a writable /tmp/cassandra emptyDir must be mounted. Refer to documentation`, tempCassandraDir, err) } f, err := os.CreateTemp(tempCassandraDir, prefix+"-*.pem") @@ -200,72 +136,48 @@ func createTempFile(prefix string, content string) (string, error) { return f.Name(), nil } -func parseCassandraTLS(config *scalersconfig.ScalerConfig, meta *CassandraMetadata) error { - meta.enableTLS = false - if val, ok := config.AuthParams["tls"]; ok { - val = strings.TrimSpace(val) - if val == tlsEnable { - certGiven := config.AuthParams["cert"] != "" - keyGiven := config.AuthParams["key"] != "" - caCertGiven := config.AuthParams["ca"] != "" - if certGiven && !keyGiven { - return errors.New("no key given") - } - if keyGiven && !certGiven { - return errors.New("no cert given") - } - if !keyGiven && !certGiven { - return errors.New("no cert/key given") - } +func parseCassandraTLS(meta *cassandraMetadata) error { + if meta.TLS == tlsEnable { + // Create temp files for certs + certFilePath, err := createTempFile("cert", meta.Cert) + if err != nil { + return fmt.Errorf("error creating cert file: %w", err) + } + meta.Cert = certFilePath - certFilePath, err := createTempFile("cert", config.AuthParams["cert"]) - if err != nil { - // handle error - return errors.New("Error creating cert file: " + err.Error()) - } + keyFilePath, err := createTempFile("key", meta.Key) + if err != nil { + return fmt.Errorf("error creating key file: %w", err) + } + meta.Key = keyFilePath - keyFilePath, err := createTempFile("key", config.AuthParams["key"]) + // If CA cert is given, make also file + if meta.CA != "" { + caCertFilePath, err := createTempFile("caCert", meta.CA) if err != nil { - // handle error - return errors.New("Error creating key file: " + err.Error()) + return fmt.Errorf("error creating ca file: %w", err) } - - meta.cert = certFilePath - meta.key = keyFilePath - meta.ca = config.AuthParams["ca"] - if !caCertGiven { - meta.ca = "" - } else { - caCertFilePath, err := createTempFile("caCert", config.AuthParams["ca"]) - meta.ca = caCertFilePath - if err != nil { - // handle error - return errors.New("Error creating ca file: " + err.Error()) - } - } - meta.enableTLS = true - } else if val != tlsDisable { - return fmt.Errorf("err incorrect value for TLS given: %s", val) + meta.CA = caCertFilePath } } return nil } -// newCassandraSession returns a new Cassandra session for the provided CassandraMetadata. -func newCassandraSession(meta *CassandraMetadata, logger logr.Logger) (*gocql.Session, error) { - cluster := gocql.NewCluster(meta.clusterIPAddress) - cluster.ProtoVersion = meta.protocolVersion - cluster.Consistency = meta.consistency +// newCassandraSession returns a new Cassandra session for the provided CassandraMetadata +func newCassandraSession(meta cassandraMetadata, logger logr.Logger) (*gocql.Session, error) { + cluster := gocql.NewCluster(meta.ClusterIPAddress) + cluster.ProtoVersion = meta.ProtocolVersion + cluster.Consistency = gocql.ParseConsistency(meta.Consistency) cluster.Authenticator = gocql.PasswordAuthenticator{ - Username: meta.username, - Password: meta.password, + Username: meta.Username, + Password: meta.Password, } - if meta.enableTLS { + if meta.TLS == tlsEnable { cluster.SslOpts = &gocql.SslOptions{ - CertPath: meta.cert, - KeyPath: meta.key, - CaPath: meta.ca, + CertPath: meta.Cert, + KeyPath: meta.Key, + CaPath: meta.CA, } } @@ -278,22 +190,19 @@ func newCassandraSession(meta *CassandraMetadata, logger logr.Logger) (*gocql.Se return session, nil } -// GetMetricSpecForScaling returns the MetricSpec for the Horizontal Pod Autoscaler. +// GetMetricSpecForScaling returns the MetricSpec for the Horizontal Pod Autoscaler func (s *cassandraScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("cassandra-%s", s.metadata.keyspace))), + Name: GenerateMetricNameWithIndex(s.metadata.TriggerIndex, kedautil.NormalizeString(fmt.Sprintf("cassandra-%s", s.metadata.Keyspace))), }, - Target: GetMetricTarget(s.metricType, s.metadata.targetQueryValue), - } - metricSpec := v2.MetricSpec{ - External: externalMetric, Type: externalMetricType, + Target: GetMetricTarget(s.metricType, s.metadata.TargetQueryValue), } - + metricSpec := v2.MetricSpec{External: externalMetric, Type: externalMetricType} return []v2.MetricSpec{metricSpec} } -// GetMetricsAndActivity returns a value for a supported metric or an error if there is a problem getting the metric. +// GetMetricsAndActivity returns a value for a supported metric or an error if there is a problem getting the metric func (s *cassandraScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { num, err := s.GetQueryResult(ctx) if err != nil { @@ -301,38 +210,36 @@ func (s *cassandraScaler) GetMetricsAndActivity(ctx context.Context, metricName } metric := GenerateMetricInMili(metricName, float64(num)) - - return []external_metrics.ExternalMetricValue{metric}, num > s.metadata.activationTargetQueryValue, nil + return []external_metrics.ExternalMetricValue{metric}, num > s.metadata.ActivationTargetQueryValue, nil } -// GetQueryResult returns the result of the scaler query. +// GetQueryResult returns the result of the scaler query func (s *cassandraScaler) GetQueryResult(ctx context.Context) (int64, error) { var value int64 - if err := s.session.Query(s.metadata.query).WithContext(ctx).Scan(&value); err != nil { + if err := s.session.Query(s.metadata.Query).WithContext(ctx).Scan(&value); err != nil { if err != gocql.ErrNotFound { s.logger.Error(err, "query failed") return 0, err } } - return value, nil } -// Close closes the Cassandra session connection. +// Close closes the Cassandra session connection func (s *cassandraScaler) Close(_ context.Context) error { // clean up any temporary files - if strings.TrimSpace(s.metadata.cert) != "" { - if err := os.Remove(s.metadata.cert); err != nil { + if s.metadata.Cert != "" { + if err := os.Remove(s.metadata.Cert); err != nil { return err } } - if strings.TrimSpace(s.metadata.key) != "" { - if err := os.Remove(s.metadata.key); err != nil { + if s.metadata.Key != "" { + if err := os.Remove(s.metadata.Key); err != nil { return err } } - if strings.TrimSpace(s.metadata.ca) != "" { - if err := os.Remove(s.metadata.ca); err != nil { + if s.metadata.CA != "" { + if err := os.Remove(s.metadata.CA); err != nil { return err } } diff --git a/pkg/scalers/cassandra_scaler_test.go b/pkg/scalers/cassandra_scaler_test.go index 39930946a56..d2e892b8c32 100644 --- a/pkg/scalers/cassandra_scaler_test.go +++ b/pkg/scalers/cassandra_scaler_test.go @@ -2,156 +2,318 @@ package scalers import ( "context" - "fmt" "os" "testing" "github.com/go-logr/logr" "github.com/gocql/gocql" + "github.com/stretchr/testify/assert" + v2 "k8s.io/api/autoscaling/v2" "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" ) type parseCassandraMetadataTestData struct { + name string metadata map[string]string - isError bool authParams map[string]string + isError bool } type parseCassandraTLSTestData struct { + name string authParams map[string]string isError bool - enableTLS bool + tlsEnabled bool } type cassandraMetricIdentifier struct { + name string metadataTestData *parseCassandraMetadataTestData triggerIndex int - name string + metricName string } var testCassandraMetadata = []parseCassandraMetadataTestData{ - // nothing passed - {map[string]string{}, true, map[string]string{}}, - // everything is passed in verbatim - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "username": "cassandra", "port": "9042", "clusterIPAddress": "cassandra.test", "keyspace": "test_keyspace", "TriggerIndex": "0"}, false, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, - // metricName is generated from keyspace - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "username": "cassandra", "clusterIPAddress": "cassandra.test:9042", "keyspace": "test_keyspace", "TriggerIndex": "0"}, false, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, - // no query passed - {map[string]string{"targetQueryValue": "1", "username": "cassandra", "clusterIPAddress": "cassandra.test:9042", "keyspace": "test_keyspace", "TriggerIndex": "0"}, true, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, - // no targetQueryValue passed - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "username": "cassandra", "clusterIPAddress": "cassandra.test:9042", "keyspace": "test_keyspace", "TriggerIndex": "0"}, true, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, - // no username passed - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "clusterIPAddress": "cassandra.test:9042", "keyspace": "test_keyspace", "TriggerIndex": "0"}, true, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, - // no port passed - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "username": "cassandra", "clusterIPAddress": "cassandra.test", "keyspace": "test_keyspace", "TriggerIndex": "0"}, true, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, - // no clusterIPAddress passed - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "username": "cassandra", "port": "9042", "keyspace": "test_keyspace", "TriggerIndex": "0"}, true, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, - // no keyspace passed - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "username": "cassandra", "clusterIPAddress": "cassandra.test:9042", "TriggerIndex": "0"}, true, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, - // no password passed - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "username": "cassandra", "clusterIPAddress": "cassandra.test:9042", "keyspace": "test_keyspace", "TriggerIndex": "0"}, true, map[string]string{}}, - // fix issue[4110] passed - {map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "username": "cassandra", "port": "9042", "clusterIPAddress": "https://cassandra.test", "keyspace": "test_keyspace", "TriggerIndex": "0"}, false, map[string]string{"password": "Y2Fzc2FuZHJhCg=="}}, + { + name: "nothing passed", + metadata: map[string]string{}, + authParams: map[string]string{}, + isError: true, + }, + { + name: "everything passed verbatim", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "username": "cassandra", + "port": "9042", + "clusterIPAddress": "cassandra.test", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: false, + }, + { + name: "metricName from keyspace", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "username": "cassandra", + "clusterIPAddress": "cassandra.test:9042", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: false, + }, + { + name: "no query", + metadata: map[string]string{ + "targetQueryValue": "1", + "username": "cassandra", + "clusterIPAddress": "cassandra.test:9042", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: true, + }, + { + name: "no targetQueryValue", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "username": "cassandra", + "clusterIPAddress": "cassandra.test:9042", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: true, + }, + { + name: "no username", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "clusterIPAddress": "cassandra.test:9042", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: true, + }, + { + name: "no port", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "username": "cassandra", + "clusterIPAddress": "cassandra.test", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: true, + }, + { + name: "no clusterIPAddress", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "username": "cassandra", + "port": "9042", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: true, + }, + { + name: "no keyspace", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "username": "cassandra", + "clusterIPAddress": "cassandra.test:9042", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: true, + }, + { + name: "no password", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "username": "cassandra", + "clusterIPAddress": "cassandra.test:9042", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{}, + isError: true, + }, + { + name: "with https prefix", + metadata: map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "username": "cassandra", + "port": "9042", + "clusterIPAddress": "https://cassandra.test", + "keyspace": "test_keyspace", + }, + authParams: map[string]string{"password": "Y2Fzc2FuZHJhCg=="}, + isError: false, + }, } var tlsAuthParamsTestData = []parseCassandraTLSTestData{ - // success, TLS cert/key - {map[string]string{"tls": "enable", "cert": "ceert", "key": "keey", "password": "Y2Fzc2FuZHJhCg=="}, false, true}, - // failure, TLS missing cert - {map[string]string{"tls": "enable", "key": "keey", "password": "Y2Fzc2FuZHJhCg=="}, true, false}, - // failure, TLS missing key - {map[string]string{"tls": "enable", "cert": "ceert", "password": "Y2Fzc2FuZHJhCg=="}, true, false}, - // failure, TLS invalid - {map[string]string{"tls": "yes", "cert": "ceert", "key": "keeey", "password": "Y2Fzc2FuZHJhCg=="}, true, false}, + { + name: "success with cert/key", + authParams: map[string]string{ + "tls": "enable", + "cert": "test-cert", + "key": "test-key", + "password": "Y2Fzc2FuZHJhCg==", + }, + isError: false, + tlsEnabled: true, + }, + { + name: "failure missing cert", + authParams: map[string]string{ + "tls": "enable", + "key": "test-key", + "password": "Y2Fzc2FuZHJhCg==", + }, + isError: true, + tlsEnabled: false, + }, + { + name: "failure missing key", + authParams: map[string]string{ + "tls": "enable", + "cert": "test-cert", + "password": "Y2Fzc2FuZHJhCg==", + }, + isError: true, + tlsEnabled: false, + }, + { + name: "failure invalid tls value", + authParams: map[string]string{ + "tls": "yes", + "cert": "test-cert", + "key": "test-key", + "password": "Y2Fzc2FuZHJhCg==", + }, + isError: true, + tlsEnabled: false, + }, } var cassandraMetricIdentifiers = []cassandraMetricIdentifier{ - {&testCassandraMetadata[1], 0, "s0-cassandra-test_keyspace"}, - {&testCassandraMetadata[2], 1, "s1-cassandra-test_keyspace"}, + { + name: "everything passed verbatim", + metadataTestData: &testCassandraMetadata[1], + triggerIndex: 0, + metricName: "s0-cassandra-test_keyspace", + }, + { + name: "metricName from keyspace", + metadataTestData: &testCassandraMetadata[2], + triggerIndex: 1, + metricName: "s1-cassandra-test_keyspace", + }, +} + +var successMetaData = map[string]string{ + "query": "SELECT COUNT(*) FROM test_keyspace.test_table;", + "targetQueryValue": "1", + "username": "cassandra", + "clusterIPAddress": "cassandra.test:9042", + "keyspace": "test_keyspace", } func TestCassandraParseMetadata(t *testing.T) { - testCaseNum := 1 for _, testData := range testCassandraMetadata { - _, err := parseCassandraMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, AuthParams: testData.authParams}) - if err != nil && !testData.isError { - t.Errorf("Expected success but got error for unit test # %v", testCaseNum) - } - if testData.isError && err == nil { - t.Errorf("Expected error but got success for unit test # %v", testCaseNum) - } - testCaseNum++ + t.Run(testData.name, func(t *testing.T) { + _, err := parseCassandraMetadata(&scalersconfig.ScalerConfig{ + TriggerMetadata: testData.metadata, + AuthParams: testData.authParams, + }) + if err != nil && !testData.isError { + t.Error("Expected success but got error", err) + } + if testData.isError && err == nil { + t.Error("Expected error but got success") + } + }) } } func TestCassandraGetMetricSpecForScaling(t *testing.T) { for _, testData := range cassandraMetricIdentifiers { - meta, err := parseCassandraMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, TriggerIndex: testData.triggerIndex, AuthParams: testData.metadataTestData.authParams}) - if err != nil { - t.Fatal("Could not parse metadata:", err) - } - cluster := gocql.NewCluster(meta.clusterIPAddress) - session, _ := cluster.CreateSession() - mockCassandraScaler := cassandraScaler{"", meta, session, logr.Discard()} - - metricSpec := mockCassandraScaler.GetMetricSpecForScaling(context.Background()) - metricName := metricSpec[0].External.Metric.Name - if metricName != testData.name { - t.Errorf("Wrong External metric source name: %s, expected: %s", metricName, testData.name) - } - } -} + t.Run(testData.name, func(t *testing.T) { + meta, err := parseCassandraMetadata(&scalersconfig.ScalerConfig{ + TriggerMetadata: testData.metadataTestData.metadata, + TriggerIndex: testData.triggerIndex, + AuthParams: testData.metadataTestData.authParams, + }) + if err != nil { + t.Fatal("Could not parse metadata:", err) + } + mockCassandraScaler := cassandraScaler{ + metricType: v2.AverageValueMetricType, + metadata: meta, + session: &gocql.Session{}, + logger: logr.Discard(), + } -func assertCertContents(testData parseCassandraTLSTestData, meta *CassandraMetadata, prop string) error { - if testData.authParams[prop] != "" { - var path string - switch prop { - case "cert": - path = meta.cert - case "key": - path = meta.key - } - data, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("expected to find '%v' file at %v", prop, path) - } - contents := string(data) - if contents != testData.authParams[prop] { - return fmt.Errorf("expected value: '%v' but got '%v'", testData.authParams[prop], contents) - } + metricSpec := mockCassandraScaler.GetMetricSpecForScaling(context.Background()) + metricName := metricSpec[0].External.Metric.Name + assert.Equal(t, testData.metricName, metricName) + }) } - return nil } -var successMetaData = map[string]string{"query": "SELECT COUNT(*) FROM test_keyspace.test_table;", "targetQueryValue": "1", "username": "cassandra", "clusterIPAddress": "cassandra.test:9042", "keyspace": "test_keyspace", "TriggerIndex": "0"} - func TestParseCassandraTLS(t *testing.T) { for _, testData := range tlsAuthParamsTestData { - meta, err := parseCassandraMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: successMetaData, AuthParams: testData.authParams}) - - if err != nil && !testData.isError { - t.Error("Expected success but got error", err) - } - if testData.isError && err == nil { - t.Error("Expected error but got success") - } - if meta.enableTLS != testData.enableTLS { - t.Errorf("Expected enableTLS to be set to %v but got %v\n", testData.enableTLS, meta.enableTLS) - } - if meta.enableTLS { - if meta.cert != testData.authParams["cert"] { - err := assertCertContents(testData, meta, "cert") - if err != nil { - t.Errorf(err.Error()) - } - } - if meta.key != testData.authParams["key"] { - err := assertCertContents(testData, meta, "key") - if err != nil { - t.Errorf(err.Error()) + t.Run(testData.name, func(t *testing.T) { + meta, err := parseCassandraMetadata(&scalersconfig.ScalerConfig{ + TriggerMetadata: successMetaData, + AuthParams: testData.authParams, + }) + + if testData.isError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testData.tlsEnabled, meta.TLS == "enable") + + if meta.TLS == "enable" { + // Verify cert contents + if testData.authParams["cert"] != "" { + data, err := os.ReadFile(meta.Cert) + assert.NoError(t, err) + assert.Equal(t, testData.authParams["cert"], string(data)) + // Cleanup + defer os.Remove(meta.Cert) + } + + // Verify key contents + if testData.authParams["key"] != "" { + data, err := os.ReadFile(meta.Key) + assert.NoError(t, err) + assert.Equal(t, testData.authParams["key"], string(data)) + // Cleanup + defer os.Remove(meta.Key) + } + + // Verify CA contents if present + if testData.authParams["ca"] != "" { + data, err := os.ReadFile(meta.CA) + assert.NoError(t, err) + assert.Equal(t, testData.authParams["ca"], string(data)) + // Cleanup + defer os.Remove(meta.CA) + } } } - } + }) } } diff --git a/pkg/scalers/couchdb_scaler.go b/pkg/scalers/couchdb_scaler.go index 62ab5890493..b84332b7127 100644 --- a/pkg/scalers/couchdb_scaler.go +++ b/pkg/scalers/couchdb_scaler.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net" - "strconv" couchdb "github.com/go-kivik/couchdb/v3" "github.com/go-kivik/kivik/v3" @@ -19,216 +18,168 @@ import ( type couchDBScaler struct { metricType v2.MetricTargetType - metadata *couchDBMetadata + metadata couchDBMetadata client *kivik.Client logger logr.Logger } +type couchDBMetadata struct { + ConnectionString string `keda:"name=connectionString,order=authParams;triggerMetadata;resolvedEnv,optional"` + Host string `keda:"name=host,order=authParams;triggerMetadata,optional"` + Port string `keda:"name=port,order=authParams;triggerMetadata,optional"` + Username string `keda:"name=username,order=authParams;triggerMetadata,optional"` + Password string `keda:"name=password,order=authParams;triggerMetadata;resolvedEnv,optional"` + DBName string `keda:"name=dbName,order=authParams;triggerMetadata,optional"` + Query string `keda:"name=query,order=triggerMetadata,optional"` + QueryValue int64 `keda:"name=queryValue,order=triggerMetadata,optional"` + ActivationQueryValue int64 `keda:"name=activationQueryValue,order=triggerMetadata,default=0,optional"` + TriggerIndex int +} + +func (m *couchDBMetadata) Validate() error { + if m.ConnectionString == "" { + if m.Host == "" { + return fmt.Errorf("no host given") + } + if m.Port == "" { + return fmt.Errorf("no port given") + } + if m.Username == "" { + return fmt.Errorf("no username given") + } + if m.Password == "" { + return fmt.Errorf("no password given") + } + if m.DBName == "" { + return fmt.Errorf("no dbName given") + } + } + return nil +} + type couchDBQueryRequest struct { Selector map[string]interface{} `json:"selector"` Fields []string `json:"fields"` } -type couchDBMetadata struct { - connectionString string - host string - port string - username string - password string - dbName string - query string - queryValue int64 - activationQueryValue int64 - triggerIndex int -} - type Res struct { ID string `json:"_id"` Feet int `json:"feet"` Greeting string `json:"greeting"` } -func (s *couchDBScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { - externalMetric := &v2.ExternalMetricSource{ - Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("coucdb-%s", s.metadata.dbName))), - }, - Target: GetMetricTarget(s.metricType, s.metadata.queryValue), +func NewCouchDBScaler(ctx context.Context, config *scalersconfig.ScalerConfig) (Scaler, error) { + metricType, err := GetMetricTargetType(config) + if err != nil { + return nil, fmt.Errorf("error getting scaler metric type: %w", err) } - metricSpec := v2.MetricSpec{ - External: externalMetric, Type: externalMetricType, + + meta, err := parseCouchDBMetadata(config) + if err != nil { + return nil, fmt.Errorf("error parsing couchdb metadata: %w", err) } - return []v2.MetricSpec{metricSpec} -} -func (s couchDBScaler) Close(ctx context.Context) error { - if s.client != nil { - err := s.client.Close(ctx) - if err != nil { - s.logger.Error(err, fmt.Sprintf("failed to close couchdb connection, because of %v", err)) - return err - } + connStr := meta.ConnectionString + if connStr == "" { + addr := net.JoinHostPort(meta.Host, meta.Port) + connStr = "http://" + addr } - return nil -} -func (s *couchDBScaler) getQueryResult(ctx context.Context) (int64, error) { - db := s.client.DB(ctx, s.metadata.dbName) - var request couchDBQueryRequest - err := json.Unmarshal([]byte(s.metadata.query), &request) + client, err := kivik.New("couch", connStr) if err != nil { - s.logger.Error(err, fmt.Sprintf("Couldn't unmarshal query string because of %v", err)) - return 0, err + return nil, fmt.Errorf("error creating couchdb client: %w", err) } - rows, err := db.Find(ctx, request, nil) + + err = client.Authenticate(ctx, couchdb.BasicAuth("admin", meta.Password)) if err != nil { - s.logger.Error(err, fmt.Sprintf("failed to fetch rows because of %v", err)) - return 0, err + return nil, fmt.Errorf("error authenticating with couchdb: %w", err) } - var count int64 - for rows.Next() { - count++ - res := Res{} - if err := rows.ScanDoc(&res); err != nil { - s.logger.Error(err, fmt.Sprintf("failed to scan the doc because of %v", err)) - return 0, err - } + + isConnected, err := client.Ping(ctx) + if !isConnected || err != nil { + return nil, fmt.Errorf("failed to ping couchdb: %w", err) } - return count, nil + + return &couchDBScaler{ + metricType: metricType, + metadata: meta, + client: client, + logger: InitializeLogger(config, "couchdb_scaler"), + }, nil } -func parseCouchDBMetadata(config *scalersconfig.ScalerConfig) (*couchDBMetadata, string, error) { - var connStr string - var err error +func parseCouchDBMetadata(config *scalersconfig.ScalerConfig) (couchDBMetadata, error) { meta := couchDBMetadata{} - - if val, ok := config.TriggerMetadata["query"]; ok { - meta.query = val - } else { - return nil, "", fmt.Errorf("no query given") + err := config.TypedConfig(&meta) + if err != nil { + return meta, fmt.Errorf("error parsing couchdb metadata: %w", err) } - if val, ok := config.TriggerMetadata["queryValue"]; ok { - queryValue, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, "", fmt.Errorf("failed to convert %v to int, because of %w", val, err) - } - meta.queryValue = queryValue - } else { - if config.AsMetricSource { - meta.queryValue = 0 - } else { - return nil, "", fmt.Errorf("no queryValue given") - } + if meta.QueryValue == 0 && !config.AsMetricSource { + return meta, fmt.Errorf("no queryValue given") } - meta.activationQueryValue = 0 - if val, ok := config.TriggerMetadata["activationQueryValue"]; ok { - activationQueryValue, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, "", fmt.Errorf("failed to convert %v to int, because of %w", val, err) - } - meta.activationQueryValue = activationQueryValue + if config.AsMetricSource { + meta.QueryValue = 0 } - dbName, err := GetFromAuthOrMeta(config, "dbName") - if err != nil { - return nil, "", err - } - meta.dbName = dbName - - switch { - case config.AuthParams["connectionString"] != "": - meta.connectionString = config.AuthParams["connectionString"] - case config.TriggerMetadata["connectionStringFromEnv"] != "": - meta.connectionString = config.ResolvedEnv[config.TriggerMetadata["connectionStringFromEnv"]] - default: - meta.connectionString = "" - host, err := GetFromAuthOrMeta(config, "host") - if err != nil { - return nil, "", err - } - meta.host = host - - port, err := GetFromAuthOrMeta(config, "port") - if err != nil { - return nil, "", err - } - meta.port = port - - username, err := GetFromAuthOrMeta(config, "username") - if err != nil { - return nil, "", err - } - meta.username = username + meta.TriggerIndex = config.TriggerIndex + return meta, nil +} - if config.AuthParams["password"] != "" { - meta.password = config.AuthParams["password"] - } else if config.TriggerMetadata["passwordFromEnv"] != "" { - meta.password = config.ResolvedEnv[config.TriggerMetadata["passwordFromEnv"]] - } - if len(meta.password) == 0 { - return nil, "", fmt.Errorf("no password given") +func (s *couchDBScaler) Close(ctx context.Context) error { + if s.client != nil { + if err := s.client.Close(ctx); err != nil { + s.logger.Error(err, "failed to close couchdb connection") + return err } } - - if meta.connectionString != "" { - connStr = meta.connectionString - } else { - // Build connection str - addr := net.JoinHostPort(meta.host, meta.port) - // nosemgrep: db-connection-string - connStr = "http://" + addr - } - meta.triggerIndex = config.TriggerIndex - return &meta, connStr, nil + return nil } -func NewCouchDBScaler(ctx context.Context, config *scalersconfig.ScalerConfig) (Scaler, error) { - metricType, err := GetMetricTargetType(config) - if err != nil { - return nil, fmt.Errorf("error getting scaler metric type: %w", err) +func (s *couchDBScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { + metricName := kedautil.NormalizeString(fmt.Sprintf("coucdb-%s", s.metadata.DBName)) + externalMetric := &v2.ExternalMetricSource{ + Metric: v2.MetricIdentifier{ + Name: GenerateMetricNameWithIndex(s.metadata.TriggerIndex, metricName), + }, + Target: GetMetricTarget(s.metricType, s.metadata.QueryValue), } + metricSpec := v2.MetricSpec{External: externalMetric, Type: externalMetricType} + return []v2.MetricSpec{metricSpec} +} - meta, connStr, err := parseCouchDBMetadata(config) - if err != nil { - return nil, fmt.Errorf("failed to parsing couchDB metadata, because of %w", err) - } +func (s *couchDBScaler) getQueryResult(ctx context.Context) (int64, error) { + db := s.client.DB(ctx, s.metadata.DBName) - client, err := kivik.New("couch", connStr) - if err != nil { - return nil, fmt.Errorf("%w", err) + var request couchDBQueryRequest + if err := json.Unmarshal([]byte(s.metadata.Query), &request); err != nil { + return 0, fmt.Errorf("error unmarshaling query: %w", err) } - err = client.Authenticate(ctx, couchdb.BasicAuth("admin", meta.password)) + rows, err := db.Find(ctx, request, nil) if err != nil { - return nil, err + return 0, fmt.Errorf("error executing query: %w", err) } - isconnected, err := client.Ping(ctx) - if !isconnected { - return nil, fmt.Errorf("%w", err) - } - if err != nil { - return nil, fmt.Errorf("failed to ping couchDB, because of %w", err) + var count int64 + for rows.Next() { + count++ + var res Res + if err := rows.ScanDoc(&res); err != nil { + return 0, fmt.Errorf("error scanning document: %w", err) + } } - return &couchDBScaler{ - metricType: metricType, - metadata: meta, - client: client, - logger: InitializeLogger(config, "couchdb_scaler"), - }, nil + return count, nil } -// GetMetricsAndActivity query from couchDB,and return to external metrics and activity func (s *couchDBScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { result, err := s.getQueryResult(ctx) if err != nil { - return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("failed to inspect couchdb, because of %w", err) + return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("failed to inspect couchdb: %w", err) } metric := GenerateMetricInMili(metricName, float64(result)) - - return append([]external_metrics.ExternalMetricValue{}, metric), result > s.metadata.activationQueryValue, nil + return []external_metrics.ExternalMetricValue{metric}, result > s.metadata.ActivationQueryValue, nil } diff --git a/pkg/scalers/couchdb_scaler_test.go b/pkg/scalers/couchdb_scaler_test.go index af7ae36b9b1..54b4a4b5b5a 100644 --- a/pkg/scalers/couchdb_scaler_test.go +++ b/pkg/scalers/couchdb_scaler_test.go @@ -7,6 +7,7 @@ import ( _ "github.com/go-kivik/couchdb/v3" "github.com/go-kivik/kivik/v3" "github.com/go-logr/logr" + v2 "k8s.io/api/autoscaling/v2" "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" ) @@ -17,6 +18,7 @@ var testCouchDBResolvedEnv = map[string]string{ } type parseCouchDBMetadataTestData struct { + name string metadata map[string]string authParams map[string]string resolvedEnv map[string]string @@ -32,6 +34,7 @@ type couchDBMetricIdentifier struct { var testCOUCHDBMetadata = []parseCouchDBMetadataTestData{ // No metadata { + name: "no metadata", metadata: map[string]string{}, authParams: map[string]string{}, resolvedEnv: testCouchDBResolvedEnv, @@ -39,6 +42,7 @@ var testCOUCHDBMetadata = []parseCouchDBMetadataTestData{ }, // connectionStringFromEnv { + name: "with connectionStringFromEnv", metadata: map[string]string{"query": `{ "selector": { "feet": { "$gt": 0 } }, "fields": ["_id", "feet", "greeting"] }`, "queryValue": "1", "connectionStringFromEnv": "CouchDB_CONN_STR", "dbName": "animals"}, authParams: map[string]string{}, resolvedEnv: testCouchDBResolvedEnv, @@ -46,6 +50,7 @@ var testCOUCHDBMetadata = []parseCouchDBMetadataTestData{ }, // with metric name { + name: "with metric name", metadata: map[string]string{"query": `{ "selector": { "feet": { "$gt": 0 } }, "fields": ["_id", "feet", "greeting"] }`, "queryValue": "1", "connectionStringFromEnv": "CouchDB_CONN_STR", "dbName": "animals"}, authParams: map[string]string{}, resolvedEnv: testCouchDBResolvedEnv, @@ -53,6 +58,7 @@ var testCOUCHDBMetadata = []parseCouchDBMetadataTestData{ }, // from trigger auth { + name: "from trigger auth", metadata: map[string]string{"query": `{ "selector": { "feet": { "$gt": 0 } }, "fields": ["_id", "feet", "greeting"] }`, "queryValue": "1"}, authParams: map[string]string{"dbName": "animals", "host": "localhost", "port": "5984", "username": "admin", "password": "YeFvQno9LylIm5MDgwcV"}, resolvedEnv: testCouchDBResolvedEnv, @@ -60,7 +66,8 @@ var testCOUCHDBMetadata = []parseCouchDBMetadataTestData{ }, // wrong activationQueryValue { - metadata: map[string]string{"query": `{ "selector": { "feet": { "$gt": 0 } }, "fields": ["_id", "feet", "greeting"] }`, "queryValue": "1", "activationQueryValue": "1", "connectionStringFromEnv": "CouchDB_CONN_STR", "dbName": "animals"}, + name: "wrong activationQueryValue", + metadata: map[string]string{"query": `{ "selector": { "feet": { "$gt": 0 } }, "fields": ["_id", "feet", "greeting"] }`, "queryValue": "1", "activationQueryValue": "a", "connectionStringFromEnv": "CouchDB_CONN_STR", "dbName": "animals"}, authParams: map[string]string{}, resolvedEnv: testCouchDBResolvedEnv, raisesError: true, @@ -74,25 +81,47 @@ var couchDBMetricIdentifiers = []couchDBMetricIdentifier{ func TestParseCouchDBMetadata(t *testing.T) { for _, testData := range testCOUCHDBMetadata { - _, _, err := parseCouchDBMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, AuthParams: testData.authParams}) - if err != nil && !testData.raisesError { - t.Error("Expected success but got error:", err) - } + t.Run(testData.name, func(t *testing.T) { + _, err := parseCouchDBMetadata(&scalersconfig.ScalerConfig{ + TriggerMetadata: testData.metadata, + AuthParams: testData.authParams, + ResolvedEnv: testData.resolvedEnv, + }) + if err != nil && !testData.raisesError { + t.Errorf("Test case '%s': Expected success but got error: %v", testData.name, err) + } + if testData.raisesError && err == nil { + t.Errorf("Test case '%s': Expected error but got success", testData.name) + } + }) } } func TestCouchDBGetMetricSpecForScaling(t *testing.T) { for _, testData := range couchDBMetricIdentifiers { - meta, _, err := parseCouchDBMetadata(&scalersconfig.ScalerConfig{ResolvedEnv: testData.metadataTestData.resolvedEnv, AuthParams: testData.metadataTestData.authParams, TriggerMetadata: testData.metadataTestData.metadata, TriggerIndex: testData.triggerIndex}) - if err != nil { - t.Fatal("Could not parse metadata:", err) - } - mockCouchDBScaler := couchDBScaler{"", meta, &kivik.Client{}, logr.Discard()} + t.Run(testData.name, func(t *testing.T) { + meta, err := parseCouchDBMetadata(&scalersconfig.ScalerConfig{ + ResolvedEnv: testData.metadataTestData.resolvedEnv, + AuthParams: testData.metadataTestData.authParams, + TriggerMetadata: testData.metadataTestData.metadata, + TriggerIndex: testData.triggerIndex, + }) + if err != nil { + t.Fatal("Could not parse metadata:", err) + } - metricSpec := mockCouchDBScaler.GetMetricSpecForScaling(context.Background()) - metricName := metricSpec[0].External.Metric.Name - if metricName != testData.name { - t.Error("Wrong External metric source name:", metricName) - } + mockCouchDBScaler := couchDBScaler{ + metricType: v2.AverageValueMetricType, + metadata: meta, + client: &kivik.Client{}, + logger: logr.Discard(), + } + + metricSpec := mockCouchDBScaler.GetMetricSpecForScaling(context.Background()) + metricName := metricSpec[0].External.Metric.Name + if metricName != testData.name { + t.Errorf("Wrong External metric source name: %s, expected: %s", metricName, testData.name) + } + }) } } diff --git a/pkg/scalers/cpu_memory_scaler.go b/pkg/scalers/cpu_memory_scaler.go index da5119f3ec0..8da440ab77e 100644 --- a/pkg/scalers/cpu_memory_scaler.go +++ b/pkg/scalers/cpu_memory_scaler.go @@ -15,25 +15,27 @@ import ( ) type cpuMemoryScaler struct { - metadata *cpuMemoryMetadata + metadata cpuMemoryMetadata resourceName v1.ResourceName logger logr.Logger } type cpuMemoryMetadata struct { - Type v2.MetricTargetType + Type string `keda:"name=type, order=triggerMetadata, enum=Utilization;AverageValue, optional"` + Value string `keda:"name=value, order=triggerMetadata"` + ContainerName string `keda:"name=containerName, order=triggerMetadata, optional"` AverageValue *resource.Quantity AverageUtilization *int32 - ContainerName string + MetricType v2.MetricTargetType } // NewCPUMemoryScaler creates a new cpuMemoryScaler func NewCPUMemoryScaler(resourceName v1.ResourceName, config *scalersconfig.ScalerConfig) (Scaler, error) { logger := InitializeLogger(config, "cpu_memory_scaler") - meta, parseErr := parseResourceMetadata(config, logger) - if parseErr != nil { - return nil, fmt.Errorf("error parsing %s metadata: %w", resourceName, parseErr) + meta, err := parseResourceMetadata(config, logger) + if err != nil { + return nil, fmt.Errorf("error parsing %s metadata: %w", resourceName, err) } return &cpuMemoryScaler{ @@ -43,48 +45,56 @@ func NewCPUMemoryScaler(resourceName v1.ResourceName, config *scalersconfig.Scal }, nil } -func parseResourceMetadata(config *scalersconfig.ScalerConfig, logger logr.Logger) (*cpuMemoryMetadata, error) { - meta := &cpuMemoryMetadata{} - var value string - var ok bool - value, ok = config.TriggerMetadata["type"] - switch { - case ok && value != "" && config.MetricType != "": - return nil, fmt.Errorf("only one of trigger.metadata.type or trigger.metricType should be defined") - case ok && value != "": - logger.V(0).Info("trigger.metadata.type is deprecated in favor of trigger.metricType") - meta.Type = v2.MetricTargetType(value) - case config.MetricType != "": - meta.Type = config.MetricType - default: - return nil, fmt.Errorf("no type given in neither trigger.metadata.type or trigger.metricType") +func parseResourceMetadata(config *scalersconfig.ScalerConfig, logger logr.Logger) (cpuMemoryMetadata, error) { + meta := cpuMemoryMetadata{} + err := config.TypedConfig(&meta) + if err != nil { + return meta, err + } + + if config.MetricType != "" { + meta.MetricType = config.MetricType } - if value, ok = config.TriggerMetadata["value"]; !ok || value == "" { - return nil, fmt.Errorf("no value given") + // This is deprecated and can be removed later + if meta.Type != "" { + logger.Info("The 'type' setting is DEPRECATED and will be removed in v2.18 - Use 'metricType' instead.") + switch meta.Type { + case "AverageValue": + meta.MetricType = v2.AverageValueMetricType + case "Utilization": + meta.MetricType = v2.UtilizationMetricType + default: + return meta, fmt.Errorf("unknown metric type: %s, allowed values are 'Utilization' or 'AverageValue'", meta.Type) + } } - switch meta.Type { + + switch meta.MetricType { case v2.AverageValueMetricType: - averageValueQuantity := resource.MustParse(value) + averageValueQuantity := resource.MustParse(meta.Value) meta.AverageValue = &averageValueQuantity case v2.UtilizationMetricType: - valueNum, err := strconv.ParseInt(value, 10, 32) + utilizationNum, err := parseUtilization(meta.Value) if err != nil { - return nil, err + return meta, err } - utilizationNum := int32(valueNum) - meta.AverageUtilization = &utilizationNum + meta.AverageUtilization = utilizationNum default: - return nil, fmt.Errorf("unsupported metric type, allowed values are 'Utilization' or 'AverageValue'") - } - - if value, ok = config.TriggerMetadata["containerName"]; ok && value != "" { - meta.ContainerName = value + return meta, fmt.Errorf("unknown metric type: %s, allowed values are 'Utilization' or 'AverageValue'", string(meta.MetricType)) } return meta, nil } +func parseUtilization(value string) (*int32, error) { + valueNum, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return nil, err + } + utilizationNum := int32(valueNum) + return &utilizationNum, nil +} + // Close no need for cpuMemory scaler func (s *cpuMemoryScaler) Close(context.Context) error { return nil @@ -92,13 +102,14 @@ func (s *cpuMemoryScaler) Close(context.Context) error { // GetMetricSpecForScaling returns the metric spec for the HPA func (s *cpuMemoryScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { - var metricSpec v2.MetricSpec + metricType := s.metadata.MetricType + var metricSpec v2.MetricSpec if s.metadata.ContainerName != "" { containerCPUMemoryMetric := &v2.ContainerResourceMetricSource{ Name: s.resourceName, Target: v2.MetricTarget{ - Type: s.metadata.Type, + Type: metricType, AverageUtilization: s.metadata.AverageUtilization, AverageValue: s.metadata.AverageValue, }, @@ -109,7 +120,7 @@ func (s *cpuMemoryScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSp cpuMemoryMetric := &v2.ResourceMetricSource{ Name: s.resourceName, Target: v2.MetricTarget{ - Type: s.metadata.Type, + Type: metricType, AverageUtilization: s.metadata.AverageUtilization, AverageValue: s.metadata.AverageValue, }, diff --git a/pkg/scalers/cpu_memory_scaler_test.go b/pkg/scalers/cpu_memory_scaler_test.go index 81f7ea9df9a..78f662de247 100644 --- a/pkg/scalers/cpu_memory_scaler_test.go +++ b/pkg/scalers/cpu_memory_scaler_test.go @@ -18,7 +18,6 @@ type parseCPUMemoryMetadataTestData struct { isError bool } -// A complete valid metadata example for reference var validCPUMemoryMetadata = map[string]string{ "type": "Utilization", "value": "50", @@ -44,17 +43,18 @@ var testCPUMemoryMetadata = []parseCPUMemoryMetadataTestData{ } func TestCPUMemoryParseMetadata(t *testing.T) { - for _, testData := range testCPUMemoryMetadata { + logger := logr.Discard() + for i, testData := range testCPUMemoryMetadata { config := &scalersconfig.ScalerConfig{ TriggerMetadata: testData.metadata, MetricType: testData.metricType, } - _, err := parseResourceMetadata(config, logr.Discard()) + _, err := parseResourceMetadata(config, logger) if err != nil && !testData.isError { - t.Error("Expected success but got error", err) + t.Errorf("Test case %d: Expected success but got error: %v", i, err) } if testData.isError && err == nil { - t.Error("Expected error but got success") + t.Errorf("Test case %d: Expected error but got success", i) } } } diff --git a/pkg/scalers/cron_scaler.go b/pkg/scalers/cron_scaler.go index bd9c5a22a7d..8cf4c35ba1a 100644 --- a/pkg/scalers/cron_scaler.go +++ b/pkg/scalers/cron_scaler.go @@ -16,14 +16,15 @@ import ( ) const ( - defaultDesiredReplicas = 1 - cronMetricType = "External" + cronMetricType = "External" ) type cronScaler struct { - metricType v2.MetricTargetType - metadata cronMetadata - logger logr.Logger + metricType v2.MetricTargetType + metadata cronMetadata + logger logr.Logger + startSchedule cron.Schedule + endSchedule cron.Schedule } type cronMetadata struct { @@ -35,21 +36,11 @@ type cronMetadata struct { } func (m *cronMetadata) Validate() error { - if m.Timezone == "" { - return fmt.Errorf("no timezone specified") - } - parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) - if m.Start == "" { - return fmt.Errorf("no start schedule specified") - } if _, err := parser.Parse(m.Start); err != nil { return fmt.Errorf("error parsing start schedule: %w", err) } - if m.End == "" { - return fmt.Errorf("no end schedule specified") - } if _, err := parser.Parse(m.End); err != nil { return fmt.Errorf("error parsing end schedule: %w", err) } @@ -65,7 +56,6 @@ func (m *cronMetadata) Validate() error { return nil } -// NewCronScaler creates a new cronScaler func NewCronScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { metricType, err := GetMetricTargetType(config) if err != nil { @@ -77,25 +67,23 @@ func NewCronScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { return nil, fmt.Errorf("error parsing cron metadata: %w", err) } + parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + + startSchedule, _ := parser.Parse(meta.Start) + endSchedule, _ := parser.Parse(meta.End) + return &cronScaler{ - metricType: metricType, - metadata: meta, - logger: InitializeLogger(config, "cron_scaler"), + metricType: metricType, + metadata: meta, + logger: InitializeLogger(config, "cron_scaler"), + startSchedule: startSchedule, + endSchedule: endSchedule, }, nil } -func getCronTime(location *time.Location, spec string) (int64, error) { - c := cron.New(cron.WithLocation(location)) - _, err := c.AddFunc(spec, func() { _ = fmt.Sprintf("Cron initialized for location %s", location.String()) }) - if err != nil { - return 0, err - } - - c.Start() - cronTime := c.Entries()[0].Next.Unix() - c.Stop() - - return cronTime, nil +func getCronTime(location *time.Location, schedule cron.Schedule) time.Time { + // Use the pre-parsed cron schedule directly to get the next time + return schedule.Next(time.Now().In(location)) } func parseCronMetadata(config *scalersconfig.ScalerConfig) (cronMetadata, error) { @@ -131,37 +119,33 @@ func (s *cronScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { return []v2.MetricSpec{metricSpec} } -// GetMetricsAndActivity returns value for a supported metric and an error if there is a problem getting the metric func (s *cronScaler) GetMetricsAndActivity(_ context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { - var defaultDesiredReplicas = int64(defaultDesiredReplicas) - location, err := time.LoadLocation(s.metadata.Timezone) if err != nil { - return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("unable to load timezone. Error: %w", err) + return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("unable to load timezone: %w", err) } - // Since we are considering the timestamp here and not the exact time, timezone does matter. - currentTime := time.Now().Unix() + currentTime := time.Now().In(location) - nextStartTime, startTimecronErr := getCronTime(location, s.metadata.Start) - if startTimecronErr != nil { - return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("error initializing start cron: %w", startTimecronErr) - } + // Use the pre-parsed schedules to get the next start and end times + nextStartTime := getCronTime(location, s.startSchedule) + nextEndTime := getCronTime(location, s.endSchedule) + + isWithinInterval := false - nextEndTime, endTimecronErr := getCronTime(location, s.metadata.End) - if endTimecronErr != nil { - return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("error intializing end cron: %w", endTimecronErr) + if nextStartTime.Before(nextEndTime) { + // Interval within the same day + isWithinInterval = currentTime.After(nextStartTime) && currentTime.Before(nextEndTime) + } else { + // Interval spans midnight + isWithinInterval = currentTime.After(nextStartTime) || currentTime.Before(nextEndTime) } - switch { - case nextStartTime < nextEndTime && currentTime < nextStartTime: - metric := GenerateMetricInMili(metricName, float64(defaultDesiredReplicas)) - return []external_metrics.ExternalMetricValue{metric}, false, nil - case currentTime <= nextEndTime: - metric := GenerateMetricInMili(metricName, float64(s.metadata.DesiredReplicas)) - return []external_metrics.ExternalMetricValue{metric}, true, nil - default: - metric := GenerateMetricInMili(metricName, float64(defaultDesiredReplicas)) - return []external_metrics.ExternalMetricValue{metric}, false, nil + metricValue := float64(1) + if isWithinInterval { + metricValue = float64(s.metadata.DesiredReplicas) } + + metric := GenerateMetricInMili(metricName, metricValue) + return []external_metrics.ExternalMetricValue{metric}, isWithinInterval, nil } diff --git a/pkg/scalers/cron_scaler_test.go b/pkg/scalers/cron_scaler_test.go index 2b9f5b810f3..d0b67ec2bed 100644 --- a/pkg/scalers/cron_scaler_test.go +++ b/pkg/scalers/cron_scaler_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-logr/logr" + "github.com/robfig/cron/v3" "github.com/stretchr/testify/assert" "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" @@ -121,7 +122,18 @@ func TestCronGetMetricSpecForScaling(t *testing.T) { if err != nil { t.Fatal("Could not parse metadata:", err) } - mockCronScaler := cronScaler{"", meta, logr.Discard()} + + parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + startSchedule, _ := parser.Parse(meta.Start) + endSchedule, _ := parser.Parse(meta.End) + + mockCronScaler := cronScaler{ + metricType: "", + metadata: meta, + logger: logr.Discard(), + startSchedule: startSchedule, + endSchedule: endSchedule, + } metricSpec := mockCronScaler.GetMetricSpecForScaling(context.Background()) metricName := metricSpec[0].External.Metric.Name diff --git a/pkg/scalers/elasticsearch_scaler.go b/pkg/scalers/elasticsearch_scaler.go index 44a27e8e463..70e2030d9bf 100644 --- a/pkg/scalers/elasticsearch_scaler.go +++ b/pkg/scalers/elasticsearch_scaler.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/elastic/go-elasticsearch/v7" + "github.com/elastic/go-elasticsearch/v7/esapi" "github.com/go-logr/logr" "github.com/tidwall/gjson" v2 "k8s.io/api/autoscaling/v2" @@ -34,7 +35,8 @@ type elasticsearchMetadata struct { CloudID string `keda:"name=cloudID, order=authParams;triggerMetadata, optional"` APIKey string `keda:"name=apiKey, order=authParams;triggerMetadata, optional"` Index []string `keda:"name=index, order=authParams;triggerMetadata, separator=;"` - SearchTemplateName string `keda:"name=searchTemplateName, order=authParams;triggerMetadata"` + SearchTemplateName string `keda:"name=searchTemplateName, order=authParams;triggerMetadata, optional"` + Query string `keda:"name=query, order=authParams;triggerMetadata, optional"` Parameters []string `keda:"name=parameters, order=triggerMetadata, optional, separator=;"` ValueLocation string `keda:"name=valueLocation, order=authParams;triggerMetadata"` TargetValue float64 `keda:"name=targetValue, order=authParams;triggerMetadata"` @@ -57,6 +59,13 @@ func (m *elasticsearchMetadata) Validate() error { if len(m.Addresses) > 0 && (m.Username == "" || m.Password == "") { return fmt.Errorf("both username and password must be provided when addresses is used") } + if m.SearchTemplateName == "" && m.Query == "" { + return fmt.Errorf("either searchTemplateName or query must be provided") + } + if m.SearchTemplateName != "" && m.Query != "" { + return fmt.Errorf("cannot provide both searchTemplateName and query") + } + return nil } @@ -93,7 +102,12 @@ func parseElasticsearchMetadata(config *scalersconfig.ScalerConfig) (elasticsear return meta, err } - meta.MetricName = GenerateMetricNameWithIndex(config.TriggerIndex, util.NormalizeString(fmt.Sprintf("elasticsearch-%s", meta.SearchTemplateName))) + if meta.SearchTemplateName != "" { + meta.MetricName = GenerateMetricNameWithIndex(config.TriggerIndex, util.NormalizeString(fmt.Sprintf("elasticsearch-%s", meta.SearchTemplateName))) + } else { + meta.MetricName = GenerateMetricNameWithIndex(config.TriggerIndex, "elasticsearch-query") + } + meta.TriggerIndex = config.TriggerIndex return meta, nil @@ -137,17 +151,29 @@ func (s *elasticsearchScaler) Close(_ context.Context) error { // getQueryResult returns result of the scaler query func (s *elasticsearchScaler) getQueryResult(ctx context.Context) (float64, error) { // Build the request body. - var body bytes.Buffer - if err := json.NewEncoder(&body).Encode(buildQuery(&s.metadata)); err != nil { - s.logger.Error(err, "Error encoding query: %s", err) + var res *esapi.Response + var err error + + if s.metadata.SearchTemplateName != "" { + // Using SearchTemplateName + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(buildQuery(&s.metadata)); err != nil { + s.logger.Error(err, "Error encoding query: %s", err) + } + res, err = s.esClient.SearchTemplate( + &body, + s.esClient.SearchTemplate.WithIndex(s.metadata.Index...), + s.esClient.SearchTemplate.WithContext(ctx), + ) + } else { + // Using Query + res, err = s.esClient.Search( + s.esClient.Search.WithIndex(s.metadata.Index...), + s.esClient.Search.WithBody(strings.NewReader(s.metadata.Query)), + s.esClient.Search.WithContext(ctx), + ) } - // Run the templated search - res, err := s.esClient.SearchTemplate( - &body, - s.esClient.SearchTemplate.WithIndex(s.metadata.Index...), - s.esClient.SearchTemplate.WithContext(ctx), - ) if err != nil { s.logger.Error(err, fmt.Sprintf("Could not query elasticsearch: %s", err)) return 0, err diff --git a/pkg/scalers/elasticsearch_scaler_test.go b/pkg/scalers/elasticsearch_scaler_test.go index 95725065703..2e6966f0422 100644 --- a/pkg/scalers/elasticsearch_scaler_test.go +++ b/pkg/scalers/elasticsearch_scaler_test.go @@ -73,13 +73,34 @@ var testCases = []parseElasticsearchMetadataTestData{ expectedError: fmt.Errorf("missing required parameter \"index\""), }, { - name: "no searchTemplateName given", + name: "query and searchTemplateName provided", metadata: map[string]string{ - "addresses": "http://localhost:9200", - "index": "index1", + "addresses": "http://localhost:9200", + "index": "index1", + "query": `{"match": {"field": "value"}}`, + "searchTemplateName": "myTemplate", + "valueLocation": "hits.total.value", + "targetValue": "12", }, - authParams: map[string]string{"username": "admin"}, - expectedError: fmt.Errorf("missing required parameter \"searchTemplateName\""), + authParams: map[string]string{ + "username": "admin", + "password": "password", + }, + expectedError: fmt.Errorf("cannot provide both searchTemplateName and query"), + }, + { + name: "neither query nor searchTemplateName provided", + metadata: map[string]string{ + "addresses": "http://localhost:9200", + "index": "index1", + "valueLocation": "hits.total.value", + "targetValue": "12", + }, + authParams: map[string]string{ + "username": "admin", + "password": "password", + }, + expectedError: fmt.Errorf("either searchTemplateName or query must be provided"), }, { name: "no valueLocation given", @@ -306,6 +327,31 @@ var testCases = []parseElasticsearchMetadataTestData{ }, expectedError: nil, }, + { + name: "valid query parameter", + metadata: map[string]string{ + "addresses": "http://localhost:9200", + "index": "index1", + "query": `{"match": {"field": "value"}}`, + "valueLocation": "hits.total.value", + "targetValue": "12", + }, + authParams: map[string]string{ + "username": "admin", + "password": "password", + }, + expectedMetadata: &elasticsearchMetadata{ + Addresses: []string{"http://localhost:9200"}, + Index: []string{"index1"}, + Username: "admin", + Password: "password", + Query: `{"match": {"field": "value"}}`, + ValueLocation: "hits.total.value", + TargetValue: 12, + MetricName: "s0-elasticsearch-query", + }, + expectedError: nil, + }, } func TestParseElasticsearchMetadata(t *testing.T) { diff --git a/pkg/scalers/etcd_scaler.go b/pkg/scalers/etcd_scaler.go index 83835bed93e..eed57a3a119 100644 --- a/pkg/scalers/etcd_scaler.go +++ b/pkg/scalers/etcd_scaler.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "strconv" - "strings" "time" "github.com/go-logr/logr" @@ -38,18 +37,49 @@ type etcdScaler struct { } type etcdMetadata struct { - endpoints []string - watchKey string - value float64 - activationValue float64 - watchProgressNotifyInterval int - triggerIndex int + triggerIndex int + + Endpoints []string `keda:"name=endpoints, order=triggerMetadata"` + WatchKey string `keda:"name=watchKey, order=triggerMetadata"` + Value float64 `keda:"name=value, order=triggerMetadata"` + ActivationValue float64 `keda:"name=activationValue, order=triggerMetadata, optional, default=0"` + WatchProgressNotifyInterval int `keda:"name=watchProgressNotifyInterval, order=triggerMetadata, optional, default=600"` + + Username string `keda:"name=username,order=authParams;resolvedEnv, optional"` + Password string `keda:"name=password,order=authParams;resolvedEnv, optional"` + // TLS - enableTLS bool - cert string - key string - keyPassword string - ca string + EnableTLS string `keda:"name=tls, order=authParams, optional, default=disable"` + Cert string `keda:"name=cert, order=authParams, optional"` + Key string `keda:"name=key, order=authParams, optional"` + KeyPassword string `keda:"name=keyPassword, order=authParams, optional"` + Ca string `keda:"name=ca, order=authParams, optional"` +} + +func (meta *etcdMetadata) Validate() error { + if meta.WatchProgressNotifyInterval <= 0 { + return errors.New("watchProgressNotifyInterval must be greater than 0") + } + + if meta.EnableTLS == etcdTLSEnable { + if meta.Cert == "" && meta.Key != "" { + return errors.New("cert must be provided with key") + } + if meta.Key == "" && meta.Cert != "" { + return errors.New("key must be provided with cert") + } + } else if meta.EnableTLS != etcdTLSDisable { + return fmt.Errorf("incorrect value for TLS given: %s", meta.EnableTLS) + } + + if meta.Password != "" && meta.Username == "" { + return errors.New("username must be provided with password") + } + if meta.Username != "" && meta.Password == "" { + return errors.New("password must be provided with username") + } + + return nil } // NewEtcdScaler creates a new etcdScaler @@ -76,75 +106,11 @@ func NewEtcdScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { }, nil } -func parseEtcdAuthParams(config *scalersconfig.ScalerConfig, meta *etcdMetadata) error { - meta.enableTLS = false - if val, ok := config.AuthParams["tls"]; ok { - val = strings.TrimSpace(val) - if val == etcdTLSEnable { - certGiven := config.AuthParams["cert"] != "" - keyGiven := config.AuthParams["key"] != "" - if certGiven && !keyGiven { - return errors.New("key must be provided with cert") - } - if keyGiven && !certGiven { - return errors.New("cert must be provided with key") - } - meta.ca = config.AuthParams["ca"] - meta.cert = config.AuthParams["cert"] - meta.key = config.AuthParams["key"] - if value, found := config.AuthParams["keyPassword"]; found { - meta.keyPassword = value - } else { - meta.keyPassword = "" - } - meta.enableTLS = true - } else if val != etcdTLSDisable { - return fmt.Errorf("err incorrect value for TLS given: %s", val) - } - } - - return nil -} - func parseEtcdMetadata(config *scalersconfig.ScalerConfig) (*etcdMetadata, error) { meta := &etcdMetadata{} - var err error - meta.endpoints = strings.Split(config.TriggerMetadata[endpoints], ",") - if len(meta.endpoints) == 0 || meta.endpoints[0] == "" { - return nil, fmt.Errorf("endpoints required") - } - - meta.watchKey = config.TriggerMetadata[watchKey] - if len(meta.watchKey) == 0 { - return nil, fmt.Errorf("watchKey required") - } - - value, err := strconv.ParseFloat(config.TriggerMetadata[value], 64) - if err != nil || value <= 0 { - return nil, fmt.Errorf("value must be a float greater than 0") - } - meta.value = value - - meta.activationValue = 0 - if val, ok := config.TriggerMetadata[activationValue]; ok { - activationValue, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("activationValue must be a float") - } - meta.activationValue = activationValue - } - - meta.watchProgressNotifyInterval = defaultWatchProgressNotifyInterval - if val, ok := config.TriggerMetadata[watchProgressNotifyInterval]; ok { - interval, err := strconv.Atoi(val) - if err != nil || interval <= 0 { - return nil, fmt.Errorf("watchProgressNotifyInterval must be a int greater than 0") - } - meta.watchProgressNotifyInterval = interval - } - if err = parseEtcdAuthParams(config, meta); err != nil { - return meta, err + if err := config.TypedConfig(meta); err != nil { + return nil, fmt.Errorf("error parsing redis metadata: %w", err) } meta.triggerIndex = config.TriggerIndex @@ -154,17 +120,19 @@ func parseEtcdMetadata(config *scalersconfig.ScalerConfig) (*etcdMetadata, error func getEtcdClients(metadata *etcdMetadata) (*clientv3.Client, error) { var tlsConfig *tls.Config var err error - if metadata.enableTLS { - tlsConfig, err = kedautil.NewTLSConfigWithPassword(metadata.cert, metadata.key, metadata.keyPassword, metadata.ca, false) + if metadata.EnableTLS == etcdTLSEnable { + tlsConfig, err = kedautil.NewTLSConfigWithPassword(metadata.Cert, metadata.Key, metadata.KeyPassword, metadata.Ca, false) if err != nil { return nil, err } } cli, err := clientv3.New(clientv3.Config{ - Endpoints: metadata.endpoints, + Endpoints: metadata.Endpoints, DialTimeout: 5 * time.Second, TLS: tlsConfig, + Username: metadata.Username, + Password: metadata.Password, }) if err != nil { return nil, fmt.Errorf("error connecting to etcd server: %w", err) @@ -189,16 +157,16 @@ func (s *etcdScaler) GetMetricsAndActivity(ctx context.Context, metricName strin } metric := GenerateMetricInMili(metricName, v) - return append([]external_metrics.ExternalMetricValue{}, metric), v > s.metadata.activationValue, nil + return append([]external_metrics.ExternalMetricValue{}, metric), v > s.metadata.ActivationValue, nil } // GetMetricSpecForScaling returns the metric spec for the HPA. func (s *etcdScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("etcd-%s", s.metadata.watchKey))), + Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("etcd-%s", s.metadata.WatchKey))), }, - Target: GetMetricTargetMili(s.metricType, s.metadata.value), + Target: GetMetricTargetMili(s.metricType, s.metadata.Value), } metricSpec := v2.MetricSpec{External: externalMetric, Type: etcdMetricType} return []v2.MetricSpec{metricSpec} @@ -209,16 +177,16 @@ func (s *etcdScaler) Run(ctx context.Context, active chan<- bool) { // It's possible for the watch to get terminated anytime, we need to run this in a retry loop runWithWatch := func() { - s.logger.Info("run watch", "watchKey", s.metadata.watchKey, "endpoints", s.metadata.endpoints) + s.logger.Info("run watch", "watchKey", s.metadata.WatchKey, "endpoints", s.metadata.Endpoints) subCtx, cancel := context.WithCancel(ctx) subCtx = clientv3.WithRequireLeader(subCtx) - rch := s.client.Watch(subCtx, s.metadata.watchKey, clientv3.WithProgressNotify()) + rch := s.client.Watch(subCtx, s.metadata.WatchKey, clientv3.WithProgressNotify()) // rewatch to another etcd server when the network is isolated from the current etcd server. progress := make(chan bool) defer close(progress) go func() { - delayDuration := time.Duration(s.metadata.watchProgressNotifyInterval) * 2 * time.Second + delayDuration := time.Duration(s.metadata.WatchProgressNotifyInterval) * 2 * time.Second delay := time.NewTimer(delayDuration) defer delay.Stop() for { @@ -228,7 +196,7 @@ func (s *etcdScaler) Run(ctx context.Context, active chan<- bool) { case <-subCtx.Done(): return case <-delay.C: - s.logger.Info("no watch progress notification in the interval", "watchKey", s.metadata.watchKey, "endpoints", s.metadata.endpoints) + s.logger.Info("no watch progress notification in the interval", "watchKey", s.metadata.WatchKey, "endpoints", s.metadata.Endpoints) cancel() return } @@ -240,7 +208,7 @@ func (s *etcdScaler) Run(ctx context.Context, active chan<- bool) { // rewatch to another etcd server when there is an error form the current etcd server, such as 'no leader','required revision has been compacted' if wresp.Err() != nil { - s.logger.Error(wresp.Err(), "an error occurred in the watch process", "watchKey", s.metadata.watchKey, "endpoints", s.metadata.endpoints) + s.logger.Error(wresp.Err(), "an error occurred in the watch process", "watchKey", s.metadata.WatchKey, "endpoints", s.metadata.Endpoints) cancel() return } @@ -251,7 +219,7 @@ func (s *etcdScaler) Run(ctx context.Context, active chan<- bool) { s.logger.Error(err, "etcdValue invalid will be treated as 0") v = 0 } - active <- v > s.metadata.activationValue + active <- v > s.metadata.ActivationValue } } } @@ -288,12 +256,12 @@ func (s *etcdScaler) Run(ctx context.Context, active chan<- bool) { func (s *etcdScaler) getMetricValue(ctx context.Context) (float64, error) { ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - resp, err := s.client.Get(ctx, s.metadata.watchKey) + resp, err := s.client.Get(ctx, s.metadata.WatchKey) if err != nil { return 0, err } if resp.Kvs == nil { - return 0, fmt.Errorf("watchKey %s doesn't exist", s.metadata.watchKey) + return 0, fmt.Errorf("watchKey %s doesn't exist", s.metadata.WatchKey) } v, err := strconv.ParseFloat(string(resp.Kvs[0].Value), 64) if err != nil { diff --git a/pkg/scalers/etcd_scaler_test.go b/pkg/scalers/etcd_scaler_test.go index 9ecdb5b602a..70f16e2fec8 100644 --- a/pkg/scalers/etcd_scaler_test.go +++ b/pkg/scalers/etcd_scaler_test.go @@ -19,7 +19,7 @@ type parseEtcdMetadataTestData struct { type parseEtcdAuthParamsTestData struct { authParams map[string]string isError bool - enableTLS bool + enableTLS string } type etcdMetricIdentifier struct { @@ -56,19 +56,25 @@ var parseEtcdMetadataTestDataset = []parseEtcdMetadataTestData{ var parseEtcdAuthParamsTestDataset = []parseEtcdAuthParamsTestData{ // success, TLS only - {map[string]string{"tls": "enable", "ca": "caaa", "cert": "ceert", "key": "keey"}, false, true}, + {map[string]string{"tls": "enable", "ca": "caaa", "cert": "ceert", "key": "keey"}, false, etcdTLSEnable}, // success, TLS cert/key and assumed public CA - {map[string]string{"tls": "enable", "cert": "ceert", "key": "keey"}, false, true}, + {map[string]string{"tls": "enable", "cert": "ceert", "key": "keey"}, false, etcdTLSEnable}, // success, TLS cert/key + key password and assumed public CA - {map[string]string{"tls": "enable", "cert": "ceert", "key": "keey", "keyPassword": "keeyPassword"}, false, true}, + {map[string]string{"tls": "enable", "cert": "ceert", "key": "keey", "keyPassword": "keeyPassword"}, false, etcdTLSEnable}, // success, TLS CA only - {map[string]string{"tls": "enable", "ca": "caaa"}, false, true}, + {map[string]string{"tls": "enable", "ca": "caaa"}, false, etcdTLSEnable}, // failure, TLS missing cert - {map[string]string{"tls": "enable", "ca": "caaa", "key": "keey"}, true, false}, + {map[string]string{"tls": "enable", "ca": "caaa", "key": "keey"}, true, etcdTLSDisable}, // failure, TLS missing key - {map[string]string{"tls": "enable", "ca": "caaa", "cert": "ceert"}, true, false}, + {map[string]string{"tls": "enable", "ca": "caaa", "cert": "ceert"}, true, etcdTLSDisable}, // failure, TLS invalid - {map[string]string{"tls": "yes", "ca": "caaa", "cert": "ceert", "key": "keey"}, true, false}, + {map[string]string{"tls": "yes", "ca": "caaa", "cert": "ceert", "key": "keey"}, true, etcdTLSDisable}, + // success, username and password + {map[string]string{"username": "root", "password": "admin"}, false, etcdTLSDisable}, + // failure, missing password + {map[string]string{"username": "root"}, true, etcdTLSDisable}, + // failure, missing username + {map[string]string{"password": "admin"}, true, etcdTLSDisable}, } var etcdMetricIdentifiers = []etcdMetricIdentifier{ @@ -83,10 +89,10 @@ func TestParseEtcdMetadata(t *testing.T) { t.Error("Expected success but got error", err) } if testData.isError && err == nil { - t.Error("Expected error but got success") + t.Errorf("Expected error but got success %v", testData) } - if err == nil && !reflect.DeepEqual(meta.endpoints, testData.endpoints) { - t.Errorf("Expected %v but got %v\n", testData.endpoints, meta.endpoints) + if err == nil && !reflect.DeepEqual(meta.Endpoints, testData.endpoints) { + t.Errorf("Expected %v but got %v\n", testData.endpoints, meta.Endpoints) } } } @@ -101,21 +107,21 @@ func TestParseEtcdAuthParams(t *testing.T) { if testData.isError && err == nil { t.Error("Expected error but got success") } - if meta.enableTLS != testData.enableTLS { - t.Errorf("Expected enableTLS to be set to %v but got %v\n", testData.enableTLS, meta.enableTLS) + if meta != nil && meta.EnableTLS != testData.enableTLS { + t.Errorf("Expected enableTLS to be set to %v but got %v\n", testData.enableTLS, meta.EnableTLS) } - if meta.enableTLS { - if meta.ca != testData.authParams["ca"] { - t.Errorf("Expected ca to be set to %v but got %v\n", testData.authParams["ca"], meta.enableTLS) + if meta != nil && meta.EnableTLS == etcdTLSEnable { + if meta.Ca != testData.authParams["ca"] { + t.Errorf("Expected ca to be set to %v but got %v\n", testData.authParams["ca"], meta.EnableTLS) } - if meta.cert != testData.authParams["cert"] { - t.Errorf("Expected cert to be set to %v but got %v\n", testData.authParams["cert"], meta.cert) + if meta.Cert != testData.authParams["cert"] { + t.Errorf("Expected cert to be set to %v but got %v\n", testData.authParams["cert"], meta.Cert) } - if meta.key != testData.authParams["key"] { - t.Errorf("Expected key to be set to %v but got %v\n", testData.authParams["key"], meta.key) + if meta.Key != testData.authParams["key"] { + t.Errorf("Expected key to be set to %v but got %v\n", testData.authParams["key"], meta.Key) } - if meta.keyPassword != testData.authParams["keyPassword"] { - t.Errorf("Expected key to be set to %v but got %v\n", testData.authParams["keyPassword"], meta.key) + if meta.KeyPassword != testData.authParams["keyPassword"] { + t.Errorf("Expected key to be set to %v but got %v\n", testData.authParams["keyPassword"], meta.Key) } } } diff --git a/pkg/scalers/github_runner_scaler.go b/pkg/scalers/github_runner_scaler.go index d53cd21bd2e..c9c93c501b1 100644 --- a/pkg/scalers/github_runner_scaler.go +++ b/pkg/scalers/github_runner_scaler.go @@ -362,8 +362,12 @@ func getValueFromMetaOrEnv(key string, metadata map[string]string, env map[strin if val, ok := metadata[key]; ok && val != "" { return val, nil } else if val, ok := metadata[key+"FromEnv"]; ok && val != "" { - return env[val], nil + if envVal, ok := env[val]; ok && envVal != "" { + return envVal, nil + } + return "", fmt.Errorf("%s %s env variable value is empty", key, val) } + return "", fmt.Errorf("no %s given", key) } @@ -444,12 +448,14 @@ func setupGitHubApp(config *scalersconfig.ScalerConfig) (*int64, *int64, *string var instID *int64 var appKey *string - if val, err := getInt64ValueFromMetaOrEnv("applicationID", config); err == nil && val != -1 { - appID = &val + appIDVal, appIDErr := getInt64ValueFromMetaOrEnv("applicationID", config) + if appIDErr == nil && appIDVal != -1 { + appID = &appIDVal } - if val, err := getInt64ValueFromMetaOrEnv("installationID", config); err == nil && val != -1 { - instID = &val + instIDVal, instIDErr := getInt64ValueFromMetaOrEnv("installationID", config) + if instIDErr == nil && instIDVal != -1 { + instID = &instIDVal } if val, ok := config.AuthParams["appKey"]; ok && val != "" { @@ -458,7 +464,15 @@ func setupGitHubApp(config *scalersconfig.ScalerConfig) (*int64, *int64, *string if (appID != nil || instID != nil || appKey != nil) && (appID == nil || instID == nil || appKey == nil) { - return nil, nil, nil, fmt.Errorf("applicationID, installationID and applicationKey must be given") + if appIDErr != nil { + return nil, nil, nil, appIDErr + } + + if instIDErr != nil { + return nil, nil, nil, instIDErr + } + + return nil, nil, nil, fmt.Errorf("no applicationKey given") } return appID, instID, appKey, nil diff --git a/pkg/scalers/github_runner_scaler_test.go b/pkg/scalers/github_runner_scaler_test.go index fc1babdddc2..808ac78562c 100644 --- a/pkg/scalers/github_runner_scaler_test.go +++ b/pkg/scalers/github_runner_scaler_test.go @@ -73,9 +73,9 @@ var testGitHubRunnerMetadata = []parseGitHubRunnerMetadataTestData{ // empty token {"empty targetWorkflowQueueLength", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": REPO, "owner": "ownername", "repos": "reponame"}, true, false, ""}, // missing installationID From Env - {"missing installationID Env", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "applicationIDFromEnv": "APP_ID"}, true, true, "applicationID, installationID and applicationKey must be given"}, + {"missing installationID Env", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "applicationIDFromEnv": "APP_ID"}, true, true, "error parsing installationID: no installationID given"}, // missing applicationID From Env - {"missing applicationId Env", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "installationIDFromEnv": "INST_ID"}, true, true, "applicationID, installationID and applicationKey must be given"}, + {"missing applicationID Env", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "installationIDFromEnv": "INST_ID"}, true, true, "error parsing applicationID: no applicationID given"}, // nothing passed {"empty, no envs", map[string]string{}, false, true, "no runnerScope given"}, // empty githubApiURL @@ -105,11 +105,15 @@ var testGitHubRunnerMetadata = []parseGitHubRunnerMetadataTestData{ // empty repos, no envs {"empty repos, no envs", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "labels": "golang", "repos": "", "targetWorkflowQueueLength": "1"}, false, false, ""}, // missing installationID - {"missing installationID", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "applicationID": "1"}, true, true, "applicationID, installationID and applicationKey must be given"}, + {"missing installationID", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "applicationID": "1"}, true, true, "error parsing installationID: no installationID given"}, // missing applicationID - {"missing applicationID", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "installationID": "1"}, true, true, "applicationID, installationID and applicationKey must be given"}, + {"missing applicationID", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "installationID": "1"}, true, true, "error parsing applicationID: no applicationID given"}, // all good - {"missing applicationKey", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "applicationID": "1", "installationID": "1"}, true, true, "applicationID, installationID and applicationKey must be given"}, + {"missing applicationKey", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "applicationID": "1", "installationID": "1"}, true, true, "no applicationKey given"}, + {"missing runnerScope Env", map[string]string{"githubApiURL": "https://api.github.com", "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "runnerScopeFromEnv": "EMPTY"}, true, true, "runnerScope EMPTY env variable value is empty"}, + {"missing owner Env", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "ownerFromEnv": "EMPTY"}, true, true, "owner EMPTY env variable value is empty"}, + {"wrong applicationID", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "applicationID": "id", "installationID": "1"}, true, true, "error parsing applicationID: strconv.ParseInt: parsing \"id\": invalid syntax"}, + {"wrong installationID", map[string]string{"githubApiURL": "https://api.github.com", "runnerScope": ORG, "owner": "ownername", "repos": "reponame,otherrepo", "labels": "golang", "targetWorkflowQueueLength": "1", "applicationID": "1", "installationID": "id"}, true, true, "error parsing installationID: strconv.ParseInt: parsing \"id\": invalid syntax"}, } func TestGitHubRunnerParseMetadata(t *testing.T) { diff --git a/pkg/scalers/influxdb_scaler.go b/pkg/scalers/influxdb_scaler.go index 4f552999ed8..fba5c841a7c 100644 --- a/pkg/scalers/influxdb_scaler.go +++ b/pkg/scalers/influxdb_scaler.go @@ -3,7 +3,6 @@ package scalers import ( "context" "fmt" - "strconv" "github.com/go-logr/logr" influxdb2 "github.com/influxdata/influxdb-client-go/v2" @@ -23,14 +22,15 @@ type influxDBScaler struct { } type influxDBMetadata struct { - authToken string - organizationName string - query string - serverURL string - unsafeSsl bool - thresholdValue float64 - activationThresholdValue float64 - triggerIndex int + AuthToken string `keda:"name=authToken, order=triggerMetadata;resolvedEnv;authParams"` + OrganizationName string `keda:"name=organizationName, order=triggerMetadata;resolvedEnv;authParams"` + Query string `keda:"name=query, order=triggerMetadata"` + ServerURL string `keda:"name=serverURL, order=triggerMetadata;authParams"` + UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, optional"` + ThresholdValue float64 `keda:"name=thresholdValue, order=triggerMetadata, optional"` + ActivationThresholdValue float64 `keda:"name=activationThresholdValue, order=triggerMetadata, optional"` + + triggerIndex int } // NewInfluxDBScaler creates a new influx db scaler @@ -49,9 +49,9 @@ func NewInfluxDBScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { logger.Info("starting up influxdb client") client := influxdb2.NewClientWithOptions( - meta.serverURL, - meta.authToken, - influxdb2.DefaultOptions().SetTLSConfig(util.CreateTLSClientConfig(meta.unsafeSsl))) + meta.ServerURL, + meta.AuthToken, + influxdb2.DefaultOptions().SetTLSConfig(util.CreateTLSClientConfig(meta.UnsafeSsl))) return &influxDBScaler{ client: client, @@ -63,100 +63,17 @@ func NewInfluxDBScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { // parseInfluxDBMetadata parses the metadata passed in from the ScaledObject config func parseInfluxDBMetadata(config *scalersconfig.ScalerConfig) (*influxDBMetadata, error) { - var authToken string - var organizationName string - var query string - var serverURL string - var unsafeSsl bool - var thresholdValue float64 - var activationThresholdValue float64 - - val, ok := config.TriggerMetadata["authToken"] - switch { - case ok && val != "": - authToken = val - case config.TriggerMetadata["authTokenFromEnv"] != "": - if val, ok := config.ResolvedEnv[config.TriggerMetadata["authTokenFromEnv"]]; ok { - authToken = val - } else { - return nil, fmt.Errorf("no auth token given") - } - case config.AuthParams["authToken"] != "": - authToken = config.AuthParams["authToken"] - default: - return nil, fmt.Errorf("no auth token given") - } - - val, ok = config.TriggerMetadata["organizationName"] - switch { - case ok && val != "": - organizationName = val - case config.TriggerMetadata["organizationNameFromEnv"] != "": - if val, ok := config.ResolvedEnv[config.TriggerMetadata["organizationNameFromEnv"]]; ok { - organizationName = val - } else { - return nil, fmt.Errorf("no organization name given") - } - case config.AuthParams["organizationName"] != "": - organizationName = config.AuthParams["organizationName"] - default: - return nil, fmt.Errorf("no organization name given") - } - - if val, ok := config.TriggerMetadata["query"]; ok { - query = val - } else { - return nil, fmt.Errorf("no query provided") - } - - if val, ok := config.TriggerMetadata["serverURL"]; ok { - serverURL = val - } else if val, ok := config.AuthParams["serverURL"]; ok { - serverURL = val - } else { - return nil, fmt.Errorf("no server url given") - } - - if val, ok := config.TriggerMetadata["activationThresholdValue"]; ok { - value, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("activationThresholdValue: failed to parse activationThresholdValue %w", err) - } - activationThresholdValue = value + meta := &influxDBMetadata{} + meta.triggerIndex = config.TriggerIndex + if err := config.TypedConfig(meta); err != nil { + return nil, fmt.Errorf("error parsing influxdb metadata: %w", err) } - if val, ok := config.TriggerMetadata["thresholdValue"]; ok { - value, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("thresholdValue: failed to parse thresholdValue length %w", err) - } - thresholdValue = value - } else { - if config.AsMetricSource { - thresholdValue = 0 - } else { - return nil, fmt.Errorf("no threshold value given") - } - } - unsafeSsl = false - if val, ok := config.TriggerMetadata["unsafeSsl"]; ok { - parsedVal, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf("error parsing unsafeSsl: %w", err) - } - unsafeSsl = parsedVal + if meta.ThresholdValue == 0 && !config.AsMetricSource { + return nil, fmt.Errorf("no threshold value given") } - return &influxDBMetadata{ - authToken: authToken, - organizationName: organizationName, - query: query, - serverURL: serverURL, - thresholdValue: thresholdValue, - activationThresholdValue: activationThresholdValue, - unsafeSsl: unsafeSsl, - triggerIndex: config.TriggerIndex, - }, nil + return meta, nil } // Close closes the connection of the client to the server @@ -192,25 +109,25 @@ func queryInfluxDB(ctx context.Context, queryAPI api.QueryAPI, query string) (fl // GetMetricsAndActivity connects to influxdb via the client and returns a value based on the query func (s *influxDBScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { // Grab QueryAPI to make queries to influxdb instance - queryAPI := s.client.QueryAPI(s.metadata.organizationName) + queryAPI := s.client.QueryAPI(s.metadata.OrganizationName) - value, err := queryInfluxDB(ctx, queryAPI, s.metadata.query) + value, err := queryInfluxDB(ctx, queryAPI, s.metadata.Query) if err != nil { return []external_metrics.ExternalMetricValue{}, false, err } metric := GenerateMetricInMili(metricName, value) - return []external_metrics.ExternalMetricValue{metric}, value > s.metadata.activationThresholdValue, nil + return []external_metrics.ExternalMetricValue{metric}, value > s.metadata.ActivationThresholdValue, nil } // GetMetricSpecForScaling returns the metric spec for the Horizontal Pod Autoscaler func (s *influxDBScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, util.NormalizeString(fmt.Sprintf("influxdb-%s", s.metadata.organizationName))), + Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, util.NormalizeString(fmt.Sprintf("influxdb-%s", s.metadata.OrganizationName))), }, - Target: GetMetricTargetMili(s.metricType, s.metadata.thresholdValue), + Target: GetMetricTargetMili(s.metricType, s.metadata.ThresholdValue), } metricSpec := v2.MetricSpec{ External: externalMetric, Type: externalMetricType, diff --git a/pkg/scalers/influxdb_scaler_test.go b/pkg/scalers/influxdb_scaler_test.go index d238a222a54..6cf23680cd0 100644 --- a/pkg/scalers/influxdb_scaler_test.go +++ b/pkg/scalers/influxdb_scaler_test.go @@ -46,7 +46,7 @@ var testInfluxDBMetadata = []parseInfluxDBMetadataTestData{ {map[string]string{"serverURL": "https://influxdata.com", "organizationName": "influx_org", "query": "from(bucket: hello)", "thresholdValue": "10", "unsafeSsl": "false"}, true, map[string]string{}}, // 9 authToken, organizationName, and serverURL are defined in authParams {map[string]string{"query": "from(bucket: hello)", "thresholdValue": "10", "unsafeSsl": "false"}, false, map[string]string{"serverURL": "https://influxdata.com", "organizationName": "influx_org", "authToken": "myToken"}}, - // 10 no sunsafeSsl value passed + // 10 no unsafeSsl value passed {map[string]string{"serverURL": "https://influxdata.com", "metricName": "influx_metric", "organizationName": "influx_org", "query": "from(bucket: hello)", "thresholdValue": "10", "authToken": "myToken"}, false, map[string]string{}}, // 11 wrong activationThreshold valuequeryInfluxDB {map[string]string{"serverURL": "https://influxdata.com", "metricName": "influx_metric", "organizationName": "influx_org", "query": "from(bucket: hello)", "thresholdValue": "10", "activationThresholdValue": "aa", "authToken": "myToken", "unsafeSsl": "false"}, true, map[string]string{}}, diff --git a/pkg/scalers/kafka_scaler.go b/pkg/scalers/kafka_scaler.go index 48d6b3c9069..b353c1313b4 100644 --- a/pkg/scalers/kafka_scaler.go +++ b/pkg/scalers/kafka_scaler.go @@ -81,6 +81,7 @@ type kafkaMetadata struct { realm string kerberosConfigPath string kerberosServiceName string + kerberosDisableFAST bool // OAUTHBEARER tokenProvider kafkaSaslOAuthTokenProvider @@ -409,6 +410,15 @@ func parseKerberosParams(config *scalersconfig.ScalerConfig, meta *kafkaMetadata meta.kerberosServiceName = strings.TrimSpace(config.AuthParams["kerberosServiceName"]) } + meta.kerberosDisableFAST = false + if val, ok := config.AuthParams["kerberosDisableFAST"]; ok { + t, err := strconv.ParseBool(val) + if err != nil { + return fmt.Errorf("error parsing kerberosDisableFAST: %w", err) + } + meta.kerberosDisableFAST = t + } + meta.saslType = mode return nil } @@ -688,7 +698,12 @@ func getKafkaClientConfig(ctx context.Context, metadata kafkaMetadata) (*sarama. config.Net.SASL.GSSAPI.AuthType = sarama.KRB5_USER_AUTH config.Net.SASL.GSSAPI.Password = metadata.password } + + if metadata.kerberosDisableFAST { + config.Net.SASL.GSSAPI.DisablePAFXFAST = true + } } + return config, nil } diff --git a/pkg/scalers/kafka_scaler_test.go b/pkg/scalers/kafka_scaler_test.go index 57a3f95eba9..fe42e28995c 100644 --- a/pkg/scalers/kafka_scaler_test.go +++ b/pkg/scalers/kafka_scaler_test.go @@ -209,6 +209,10 @@ var parseKafkaAuthParamsTestDataset = []parseKafkaAuthParamsTestData{ {map[string]string{"sasl": "gssapi", "username": "admin", "password": "admin", "kerberosConfig": "", "tls": "enable", "ca": "caaa", "cert": "ceert", "key": "keey"}, true, false}, // failure, SASL GSSAPI/keytab + TLS missing username {map[string]string{"sasl": "gssapi", "keytab": "/path/to/keytab", "kerberosConfig": "", "realm": "tst.com", "tls": "enable", "ca": "caaa", "cert": "ceert", "key": "keey"}, true, false}, + // success, SASL GSSAPI/disableFast + {map[string]string{"sasl": "gssapi", "username": "admin", "keytab": "/path/to/keytab", "kerberosConfig": "", "realm": "tst.com", "kerberosDisableFAST": "true"}, false, false}, + // failure, SASL GSSAPI/disableFast incorrect + {map[string]string{"sasl": "gssapi", "username": "admin", "keytab": "/path/to/keytab", "kerberosConfig": "", "realm": "tst.com", "kerberosDisableFAST": "notabool"}, true, false}, } var parseAuthParamsTestDataset = []parseAuthParamsTestDataSecondAuthMethod{ // success, SASL plaintext diff --git a/pkg/scalers/kubernetes_workload_scaler.go b/pkg/scalers/kubernetes_workload_scaler.go index 2994e8daff2..3023d8ed08a 100644 --- a/pkg/scalers/kubernetes_workload_scaler.go +++ b/pkg/scalers/kubernetes_workload_scaler.go @@ -3,7 +3,6 @@ package scalers import ( "context" "fmt" - "strconv" "github.com/go-logr/logr" v2 "k8s.io/api/autoscaling/v2" @@ -18,16 +17,13 @@ import ( type kubernetesWorkloadScaler struct { metricType v2.MetricTargetType - metadata *kubernetesWorkloadMetadata + metadata kubernetesWorkloadMetadata kubeClient client.Client logger logr.Logger } const ( kubernetesWorkloadMetricType = "External" - podSelectorKey = "podSelector" - valueKey = "value" - activationValueKey = "activationValue" ) var phasesCountedAsTerminated = []corev1.PodPhase{ @@ -36,11 +32,22 @@ var phasesCountedAsTerminated = []corev1.PodPhase{ } type kubernetesWorkloadMetadata struct { - podSelector labels.Selector - namespace string - value float64 - activationValue float64 - triggerIndex int + PodSelector string `keda:"name=podSelector, order=triggerMetadata"` + Value float64 `keda:"name=value, order=triggerMetadata, default=0"` + ActivationValue float64 `keda:"name=activationValue, order=triggerMetadata, default=0"` + + namespace string + triggerIndex int + podSelector labels.Selector + asMetricSource bool +} + +func (m *kubernetesWorkloadMetadata) Validate() error { + if m.Value <= 0 && !m.asMetricSource { + return fmt.Errorf("value must be a float greater than 0") + } + + return nil } // NewKubernetesWorkloadScaler creates a new kubernetesWorkloadScaler @@ -50,9 +57,9 @@ func NewKubernetesWorkloadScaler(kubeClient client.Client, config *scalersconfig return nil, fmt.Errorf("error getting scaler metric type: %w", err) } - meta, parseErr := parseWorkloadMetadata(config) - if parseErr != nil { - return nil, fmt.Errorf("error parsing kubernetes workload metadata: %w", parseErr) + meta, err := parseKubernetesWorkloadMetadata(config) + if err != nil { + return nil, fmt.Errorf("error parsing kubernetes workload metadata: %w", err) } return &kubernetesWorkloadScaler{ @@ -63,50 +70,38 @@ func NewKubernetesWorkloadScaler(kubeClient client.Client, config *scalersconfig }, nil } -func parseWorkloadMetadata(config *scalersconfig.ScalerConfig) (*kubernetesWorkloadMetadata, error) { - meta := &kubernetesWorkloadMetadata{} - var err error +func parseKubernetesWorkloadMetadata(config *scalersconfig.ScalerConfig) (kubernetesWorkloadMetadata, error) { + meta := kubernetesWorkloadMetadata{} meta.namespace = config.ScalableObjectNamespace - podSelector, err := labels.Parse(config.TriggerMetadata[podSelectorKey]) - if err != nil || podSelector.String() == "" { - return nil, fmt.Errorf("invalid pod selector") - } - meta.podSelector = podSelector - value, err := strconv.ParseFloat(config.TriggerMetadata[valueKey], 64) - if err != nil || value == 0 { - if config.AsMetricSource { - value = 0 - } else { - return nil, fmt.Errorf("value must be a float greater than 0") - } + meta.triggerIndex = config.TriggerIndex + meta.asMetricSource = config.AsMetricSource + + err := config.TypedConfig(&meta) + if err != nil { + return meta, fmt.Errorf("error parsing kubernetes workload metadata: %w", err) } - meta.value = value - meta.activationValue = 0 - if val, ok := config.TriggerMetadata[activationValueKey]; ok { - activationValue, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("value must be a float") - } - meta.activationValue = activationValue + selector, err := labels.Parse(meta.PodSelector) + if err != nil { + return meta, fmt.Errorf("error parsing pod selector: %w", err) } + meta.podSelector = selector - meta.triggerIndex = config.TriggerIndex return meta, nil } -// Close no need for kubernetes workload scaler func (s *kubernetesWorkloadScaler) Close(context.Context) error { return nil } // GetMetricSpecForScaling returns the metric spec for the HPA func (s *kubernetesWorkloadScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { + metricName := kedautil.NormalizeString(fmt.Sprintf("workload-%s", s.metadata.namespace)) externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("workload-%s", s.metadata.namespace))), + Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, metricName), }, - Target: GetMetricTargetMili(s.metricType, s.metadata.value), + Target: GetMetricTargetMili(s.metricType, s.metadata.Value), } metricSpec := v2.MetricSpec{External: externalMetric, Type: kubernetesWorkloadMetricType} return []v2.MetricSpec{metricSpec} @@ -121,19 +116,17 @@ func (s *kubernetesWorkloadScaler) GetMetricsAndActivity(ctx context.Context, me metric := GenerateMetricInMili(metricName, float64(pods)) - return []external_metrics.ExternalMetricValue{metric}, float64(pods) > s.metadata.activationValue, nil + return []external_metrics.ExternalMetricValue{metric}, float64(pods) > s.metadata.ActivationValue, nil } func (s *kubernetesWorkloadScaler) getMetricValue(ctx context.Context) (int64, error) { podList := &corev1.PodList{} - listOptions := client.ListOptions{} - listOptions.LabelSelector = s.metadata.podSelector - listOptions.Namespace = s.metadata.namespace - opts := []client.ListOption{ - &listOptions, + listOptions := client.ListOptions{ + LabelSelector: s.metadata.podSelector, + Namespace: s.metadata.namespace, } - err := s.kubeClient.List(ctx, podList, opts...) + err := s.kubeClient.List(ctx, podList, &listOptions) if err != nil { return 0, err } diff --git a/pkg/scalers/kubernetes_workload_scaler_test.go b/pkg/scalers/kubernetes_workload_scaler_test.go index ab7a7f360d6..8544ab8e4c7 100644 --- a/pkg/scalers/kubernetes_workload_scaler_test.go +++ b/pkg/scalers/kubernetes_workload_scaler_test.go @@ -38,7 +38,13 @@ var parseWorkloadMetadataTestDataset = []workloadMetadataTestData{ func TestParseWorkloadMetadata(t *testing.T) { for _, testData := range parseWorkloadMetadataTestDataset { - _, err := parseWorkloadMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, ScalableObjectNamespace: testData.namespace}) + _, err := NewKubernetesWorkloadScaler( + fake.NewClientBuilder().Build(), + &scalersconfig.ScalerConfig{ + TriggerMetadata: testData.metadata, + ScalableObjectNamespace: testData.namespace, + }, + ) if err != nil && !testData.isError { t.Error("Expected success but got error", err) } @@ -68,7 +74,7 @@ var isActiveWorkloadTestDataset = []workloadIsActiveTestData{ func TestWorkloadIsActive(t *testing.T) { for _, testData := range isActiveWorkloadTestDataset { - s, _ := NewKubernetesWorkloadScaler( + s, err := NewKubernetesWorkloadScaler( fake.NewClientBuilder().WithRuntimeObjects(createPodlist(testData.podCount)).Build(), &scalersconfig.ScalerConfig{ TriggerMetadata: testData.metadata, @@ -77,6 +83,10 @@ func TestWorkloadIsActive(t *testing.T) { ScalableObjectNamespace: testData.namespace, }, ) + if err != nil { + t.Error("Error creating scaler", err) + continue + } _, isActive, _ := s.GetMetricsAndActivity(context.TODO(), "Metric") if testData.active && !isActive { t.Error("Expected active but got inactive") @@ -107,7 +117,7 @@ var getMetricSpecForScalingTestDataset = []workloadGetMetricSpecForScalingTestDa func TestWorkloadGetMetricSpecForScaling(t *testing.T) { for _, testData := range getMetricSpecForScalingTestDataset { - s, _ := NewKubernetesWorkloadScaler( + s, err := NewKubernetesWorkloadScaler( fake.NewClientBuilder().Build(), &scalersconfig.ScalerConfig{ TriggerMetadata: testData.metadata, @@ -117,6 +127,10 @@ func TestWorkloadGetMetricSpecForScaling(t *testing.T) { TriggerIndex: testData.triggerIndex, }, ) + if err != nil { + t.Error("Error creating scaler", err) + continue + } metric := s.GetMetricSpecForScaling(context.Background()) if metric[0].External.Metric.Name != testData.name { @@ -145,14 +159,11 @@ func createPodlist(count int) *v1.PodList { func TestWorkloadPhase(t *testing.T) { phases := map[v1.PodPhase]bool{ - v1.PodRunning: true, - // succeeded and failed clearly count as terminated + v1.PodRunning: true, v1.PodSucceeded: false, v1.PodFailed: false, - // unknown could be for example a temporarily unresponsive node; count the pod - v1.PodUnknown: true, - // count pre-Running to avoid an additional delay on top of the poll interval - v1.PodPending: true, + v1.PodUnknown: true, + v1.PodPending: true, } for phase, active := range phases { list := &v1.PodList{} diff --git a/pkg/scalers/loki_scaler.go b/pkg/scalers/loki_scaler.go index 11a43e5384c..dff08107f02 100644 --- a/pkg/scalers/loki_scaler.go +++ b/pkg/scalers/loki_scaler.go @@ -19,37 +19,27 @@ import ( ) const ( - lokiServerAddress = "serverAddress" - lokiQuery = "query" - lokiThreshold = "threshold" - lokiActivationThreshold = "activationThreshold" - lokiNamespace = "namespace" - tenantName = "tenantName" + defaultIgnoreNullValues = true tenantNameHeaderKey = "X-Scope-OrgID" - lokiIgnoreNullValues = "ignoreNullValues" -) - -var ( - lokiDefaultIgnoreNullValues = true ) type lokiScaler struct { metricType v2.MetricTargetType - metadata *lokiMetadata + metadata lokiMetadata httpClient *http.Client logger logr.Logger } type lokiMetadata struct { - serverAddress string - query string - threshold float64 - activationThreshold float64 - lokiAuth *authentication.AuthMeta - triggerIndex int - tenantName string - ignoreNullValues bool - unsafeSsl bool + ServerAddress string `keda:"name=serverAddress,order=triggerMetadata"` + Query string `keda:"name=query,order=triggerMetadata"` + Threshold float64 `keda:"name=threshold,order=triggerMetadata"` + ActivationThreshold float64 `keda:"name=activationThreshold,order=triggerMetadata,default=0"` + TenantName string `keda:"name=tenantName,order=triggerMetadata,optional"` + IgnoreNullValues bool `keda:"name=ignoreNullValues,order=triggerMetadata,default=true"` + UnsafeSsl bool `keda:"name=unsafeSsl,order=triggerMetadata,default=false"` + TriggerIndex int + Auth *authentication.AuthMeta } type lokiQueryResult struct { @@ -57,113 +47,54 @@ type lokiQueryResult struct { Data struct { ResultType string `json:"resultType"` Result []struct { - Metric struct { - } `json:"metric"` - Value []interface{} `json:"value"` + Metric struct{} `json:"metric"` + Value []interface{} `json:"value"` } `json:"result"` } `json:"data"` } -// NewLokiScaler returns a new lokiScaler func NewLokiScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { metricType, err := GetMetricTargetType(config) if err != nil { return nil, fmt.Errorf("error getting scaler metric type: %w", err) } - logger := InitializeLogger(config, "loki_scaler") - meta, err := parseLokiMetadata(config) if err != nil { return nil, fmt.Errorf("error parsing loki metadata: %w", err) } - httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, meta.unsafeSsl) + httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, meta.UnsafeSsl) return &lokiScaler{ metricType: metricType, metadata: meta, httpClient: httpClient, - logger: logger, + logger: InitializeLogger(config, "loki_scaler"), }, nil } -func parseLokiMetadata(config *scalersconfig.ScalerConfig) (meta *lokiMetadata, err error) { - meta = &lokiMetadata{} - - if val, ok := config.TriggerMetadata[lokiServerAddress]; ok && val != "" { - meta.serverAddress = val - } else { - return nil, fmt.Errorf("no %s given", lokiServerAddress) - } - - if val, ok := config.TriggerMetadata[lokiQuery]; ok && val != "" { - meta.query = val - } else { - return nil, fmt.Errorf("no %s given", lokiQuery) - } - - if val, ok := config.TriggerMetadata[lokiThreshold]; ok && val != "" { - t, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("error parsing %s: %w", lokiThreshold, err) - } - - meta.threshold = t - } else { - if config.AsMetricSource { - meta.threshold = 0 - } else { - return nil, fmt.Errorf("no %s given", lokiThreshold) - } - } - - meta.activationThreshold = 0 - if val, ok := config.TriggerMetadata[lokiActivationThreshold]; ok { - t, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("activationThreshold parsing error %w", err) - } - - meta.activationThreshold = t - } - - if val, ok := config.TriggerMetadata[tenantName]; ok && val != "" { - meta.tenantName = val +func parseLokiMetadata(config *scalersconfig.ScalerConfig) (lokiMetadata, error) { + meta := lokiMetadata{} + err := config.TypedConfig(&meta) + if err != nil { + return meta, fmt.Errorf("error parsing loki metadata: %w", err) } - meta.ignoreNullValues = lokiDefaultIgnoreNullValues - if val, ok := config.TriggerMetadata[lokiIgnoreNullValues]; ok && val != "" { - ignoreNullValues, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf("err incorrect value for ignoreNullValues given: %s please use true or false", val) - } - meta.ignoreNullValues = ignoreNullValues + if config.AsMetricSource { + meta.Threshold = 0 } - meta.unsafeSsl = false - if val, ok := config.TriggerMetadata[unsafeSsl]; ok && val != "" { - unsafeSslValue, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf("error parsing %s: %w", unsafeSsl, err) - } - - meta.unsafeSsl = unsafeSslValue - } - - meta.triggerIndex = config.TriggerIndex - - // parse auth configs from ScalerConfig auth, err := authentication.GetAuthConfigs(config.TriggerMetadata, config.AuthParams) if err != nil { - return nil, err + return meta, err } - meta.lokiAuth = auth + meta.Auth = auth + meta.TriggerIndex = config.TriggerIndex return meta, nil } -// Close returns a nil error func (s *lokiScaler) Close(context.Context) error { if s.httpClient != nil { s.httpClient.CloseIdleConnections() @@ -171,100 +102,101 @@ func (s *lokiScaler) Close(context.Context) error { return nil } -// GetMetricSpecForScaling returns the MetricSpec for the Horizontal Pod Autoscaler func (s *lokiScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, "loki"), + Name: GenerateMetricNameWithIndex(s.metadata.TriggerIndex, "loki"), }, - Target: GetMetricTargetMili(s.metricType, s.metadata.threshold), - } - metricSpec := v2.MetricSpec{ - External: externalMetric, Type: externalMetricType, + Target: GetMetricTargetMili(s.metricType, s.metadata.Threshold), } + metricSpec := v2.MetricSpec{External: externalMetric, Type: externalMetricType} return []v2.MetricSpec{metricSpec} } -// ExecuteLokiQuery returns the result of the LogQL query execution func (s *lokiScaler) ExecuteLokiQuery(ctx context.Context) (float64, error) { - u, err := url.ParseRequestURI(s.metadata.serverAddress) + u, err := url.ParseRequestURI(s.metadata.ServerAddress) if err != nil { return -1, err } u.Path = "/loki/api/v1/query" - - u.RawQuery = url.Values{ - "query": []string{s.metadata.query}, - }.Encode() + u.RawQuery = url.Values{"query": []string{s.metadata.Query}}.Encode() req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return -1, err } - if s.metadata.lokiAuth != nil && s.metadata.lokiAuth.EnableBearerAuth { - req.Header.Add("Authorization", authentication.GetBearerToken(s.metadata.lokiAuth)) - } else if s.metadata.lokiAuth != nil && s.metadata.lokiAuth.EnableBasicAuth { - req.SetBasicAuth(s.metadata.lokiAuth.Username, s.metadata.lokiAuth.Password) + if s.metadata.Auth != nil { + if s.metadata.Auth.EnableBearerAuth { + req.Header.Add("Authorization", authentication.GetBearerToken(s.metadata.Auth)) + } else if s.metadata.Auth.EnableBasicAuth { + req.SetBasicAuth(s.metadata.Auth.Username, s.metadata.Auth.Password) + } } - if s.metadata.tenantName != "" { - req.Header.Add(tenantNameHeaderKey, s.metadata.tenantName) + if s.metadata.TenantName != "" { + req.Header.Add(tenantNameHeaderKey, s.metadata.TenantName) } - r, err := s.httpClient.Do(req) + resp, err := s.httpClient.Do(req) if err != nil { return -1, err } - defer r.Body.Close() + defer resp.Body.Close() - b, err := io.ReadAll(r.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return -1, err } - if !(r.StatusCode >= 200 && r.StatusCode <= 299) { - err := fmt.Errorf("loki query api returned error. status: %d response: %s", r.StatusCode, string(b)) - s.logger.Error(err, "loki query api returned error") - return -1, err + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return -1, fmt.Errorf("loki query api returned error. status: %d response: %s", resp.StatusCode, string(body)) } var result lokiQueryResult - err = json.Unmarshal(b, &result) - if err != nil { + if err := json.Unmarshal(body, &result); err != nil { return -1, err } - var v float64 = -1 + return s.parseQueryResult(result) +} - // allow for zero element or single element result sets +func (s *lokiScaler) parseQueryResult(result lokiQueryResult) (float64, error) { if len(result.Data.Result) == 0 { - if s.metadata.ignoreNullValues { + if s.metadata.IgnoreNullValues { return 0, nil } return -1, fmt.Errorf("loki metrics may be lost, the result is empty") - } else if len(result.Data.Result) > 1 { - return -1, fmt.Errorf("loki query %s returned multiple elements", s.metadata.query) } - valueLen := len(result.Data.Result[0].Value) - if valueLen == 0 { - if s.metadata.ignoreNullValues { + if len(result.Data.Result) > 1 { + return -1, fmt.Errorf("loki query %s returned multiple elements", s.metadata.Query) + } + + values := result.Data.Result[0].Value + if len(values) == 0 { + if s.metadata.IgnoreNullValues { return 0, nil } return -1, fmt.Errorf("loki metrics may be lost, the value list is empty") - } else if valueLen < 2 { - return -1, fmt.Errorf("loki query %s didn't return enough values", s.metadata.query) } - val := result.Data.Result[0].Value[1] - if val != nil { - str := val.(string) - v, err = strconv.ParseFloat(str, 64) - if err != nil { - s.logger.Error(err, "Error converting loki value", "loki_value", str) - return -1, err - } + if len(values) < 2 { + return -1, fmt.Errorf("loki query %s didn't return enough values", s.metadata.Query) + } + + if values[1] == nil { + return 0, nil + } + + str, ok := values[1].(string) + if !ok { + return -1, fmt.Errorf("failed to parse loki value as string") + } + + v, err := strconv.ParseFloat(str, 64) + if err != nil { + return -1, fmt.Errorf("error converting loki value %s: %w", str, err) } return v, nil @@ -279,6 +211,5 @@ func (s *lokiScaler) GetMetricsAndActivity(ctx context.Context, metricName strin } metric := GenerateMetricInMili(metricName, val) - - return []external_metrics.ExternalMetricValue{metric}, val > s.metadata.activationThreshold, nil + return []external_metrics.ExternalMetricValue{metric}, val > s.metadata.ActivationThreshold, nil } diff --git a/pkg/scalers/loki_scaler_test.go b/pkg/scalers/loki_scaler_test.go index 06f95f46419..e5f8082269d 100644 --- a/pkg/scalers/loki_scaler_test.go +++ b/pkg/scalers/loki_scaler_test.go @@ -38,7 +38,7 @@ var testLokiMetadata = []parseLokiMetadataTestData{ {map[string]string{"serverAddress": "http://localhost:3100", "threshold": "1", "query": ""}, true}, // ignoreNullValues with wrong value {map[string]string{"serverAddress": "http://localhost:3100", "threshold": "1", "query": "sum(rate({filename=\"/var/log/syslog\"}[1m])) by (level)", "ignoreNullValues": "xxxx"}, true}, - + // with unsafeSsl {map[string]string{"serverAddress": "https://localhost:3100", "threshold": "1", "query": "sum(rate({filename=\"/var/log/syslog\"}[1m])) by (level)", "unsafeSsl": "true"}, false}, } @@ -83,14 +83,14 @@ func TestLokiScalerAuthParams(t *testing.T) { } if err == nil { - if meta.lokiAuth.EnableBasicAuth && !strings.Contains(testData.metadata["authModes"], "basic") { + if meta.Auth.EnableBasicAuth && !strings.Contains(testData.metadata["authModes"], "basic") { t.Error("wrong auth mode detected") } } } } -type lokiQromQueryResultTestData struct { +type lokiQueryResultTestData struct { name string bodyStr string responseStatus int @@ -100,7 +100,7 @@ type lokiQromQueryResultTestData struct { unsafeSsl bool } -var testLokiQueryResult = []lokiQromQueryResultTestData{ +var testLokiQueryResult = []lokiQueryResultTestData{ { name: "no results", bodyStr: `{}`, @@ -189,17 +189,16 @@ func TestLokiScalerExecuteLogQLQuery(t *testing.T) { t.Run(testData.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(testData.responseStatus) - if _, err := writer.Write([]byte(testData.bodyStr)); err != nil { t.Fatal(err) } })) scaler := lokiScaler{ - metadata: &lokiMetadata{ - serverAddress: server.URL, - ignoreNullValues: testData.ignoreNullValues, - unsafeSsl: testData.unsafeSsl, + metadata: lokiMetadata{ + ServerAddress: server.URL, + IgnoreNullValues: testData.ignoreNullValues, + UnsafeSsl: testData.unsafeSsl, }, httpClient: http.DefaultClient, logger: logr.Discard(), @@ -208,7 +207,6 @@ func TestLokiScalerExecuteLogQLQuery(t *testing.T) { value, err := scaler.ExecuteLokiQuery(context.TODO()) assert.Equal(t, testData.expectedValue, value) - if testData.isError { assert.Error(t, err) } else { @@ -219,7 +217,7 @@ func TestLokiScalerExecuteLogQLQuery(t *testing.T) { } func TestLokiScalerTenantHeader(t *testing.T) { - testData := lokiQromQueryResultTestData{ + testData := lokiQueryResultTestData{ name: "no values", bodyStr: `{"data":{"result":[]}}`, responseStatus: http.StatusOK, @@ -238,15 +236,14 @@ func TestLokiScalerTenantHeader(t *testing.T) { })) scaler := lokiScaler{ - metadata: &lokiMetadata{ - serverAddress: server.URL, - tenantName: tenantName, - ignoreNullValues: testData.ignoreNullValues, + metadata: lokiMetadata{ + ServerAddress: server.URL, + TenantName: tenantName, + IgnoreNullValues: testData.ignoreNullValues, }, httpClient: http.DefaultClient, } _, err := scaler.ExecuteLokiQuery(context.TODO()) - assert.NoError(t, err) } diff --git a/pkg/scalers/mongo_scaler.go b/pkg/scalers/mongo_scaler.go index f30b8fb97ec..25d2a62ede9 100644 --- a/pkg/scalers/mongo_scaler.go +++ b/pkg/scalers/mongo_scaler.go @@ -6,8 +6,6 @@ import ( "fmt" "net" "net/url" - "strconv" - "strings" "time" "github.com/go-logr/logr" @@ -22,60 +20,45 @@ import ( kedautil "github.com/kedacore/keda/v2/pkg/util" ) -// mongoDBScaler is support for mongoDB in keda. type mongoDBScaler struct { metricType v2.MetricTargetType - metadata *mongoDBMetadata + metadata mongoDBMetadata client *mongo.Client logger logr.Logger } -// mongoDBMetadata specify mongoDB scaler params. type mongoDBMetadata struct { - // The string is used by connected with mongoDB. - // +optional - connectionString string - // Specify the prefix to connect to the mongoDB server, default value `mongodb`, if the connectionString be provided, don't need to specify this param. - // +optional - scheme string - // Specify the host to connect to the mongoDB server,if the connectionString be provided, don't need to specify this param. - // +optional - host string - // Specify the port to connect to the mongoDB server,if the connectionString be provided, don't need to specify this param. - // +optional - port string - // Specify the username to connect to the mongoDB server,if the connectionString be provided, don't need to specify this param. - // +optional - username string - // Specify the password to connect to the mongoDB server,if the connectionString be provided, don't need to specify this param. - // +optional - password string - - // The name of the database to be queried. - // +required - dbName string - // The name of the collection to be queried. - // +required - collection string - // A mongoDB filter doc,used by specify DB. - // +required - query string - // A threshold that is used as targetAverageValue in HPA - // +required - queryValue int64 - // A threshold that is used to check if scaler is active - // +optional - activationQueryValue int64 - - // The index of the scaler inside the ScaledObject - // +internal - triggerIndex int + ConnectionString string `keda:"name=connectionString,order=authParams;triggerMetadata;resolvedEnv,optional"` + Scheme string `keda:"name=scheme,order=authParams;triggerMetadata,default=mongodb,optional"` + Host string `keda:"name=host,order=authParams;triggerMetadata,optional"` + Port string `keda:"name=port,order=authParams;triggerMetadata,optional"` + Username string `keda:"name=username,order=authParams;triggerMetadata,optional"` + Password string `keda:"name=password,order=authParams;triggerMetadata;resolvedEnv,optional"` + DBName string `keda:"name=dbName,order=authParams;triggerMetadata"` + Collection string `keda:"name=collection,order=triggerMetadata"` + Query string `keda:"name=query,order=triggerMetadata"` + QueryValue int64 `keda:"name=queryValue,order=triggerMetadata"` + ActivationQueryValue int64 `keda:"name=activationQueryValue,order=triggerMetadata,default=0"` + TriggerIndex int } -// Default variables and settings -const ( - mongoDBDefaultTimeOut = 10 * time.Second -) +func (m *mongoDBMetadata) Validate() error { + if m.ConnectionString == "" { + if m.Host == "" { + return fmt.Errorf("no host given") + } + if m.Port == "" && m.Scheme != "mongodb+srv" { + return fmt.Errorf("no port given") + } + if m.Username == "" { + return fmt.Errorf("no username given") + } + if m.Password == "" { + return fmt.Errorf("no password given") + } + } + return nil +} // NewMongoDBScaler creates a new mongoDB scaler func NewMongoDBScaler(ctx context.Context, config *scalersconfig.ScalerConfig) (Scaler, error) { @@ -84,22 +67,14 @@ func NewMongoDBScaler(ctx context.Context, config *scalersconfig.ScalerConfig) ( return nil, fmt.Errorf("error getting scaler metric type: %w", err) } - ctx, cancel := context.WithTimeout(ctx, mongoDBDefaultTimeOut) - defer cancel() - - meta, connStr, err := parseMongoDBMetadata(config) + meta, err := parseMongoDBMetadata(config) if err != nil { - return nil, fmt.Errorf("failed to parsing mongoDB metadata, because of %w", err) + return nil, fmt.Errorf("error parsing mongodb metadata: %w", err) } - opt := options.Client().ApplyURI(connStr) - client, err := mongo.Connect(ctx, opt) + client, err := createMongoDBClient(ctx, meta) if err != nil { - return nil, fmt.Errorf("failed to establish connection with mongoDB, because of %w", err) - } - - if err = client.Ping(ctx, readpref.Primary()); err != nil { - return nil, fmt.Errorf("failed to ping mongoDB, because of %w", err) + return nil, fmt.Errorf("error creating mongodb client: %w", err) } return &mongoDBScaler{ @@ -110,171 +85,101 @@ func NewMongoDBScaler(ctx context.Context, config *scalersconfig.ScalerConfig) ( }, nil } -func parseMongoDBMetadata(config *scalersconfig.ScalerConfig) (*mongoDBMetadata, string, error) { - var connStr string - var err error - // setting default metadata +func parseMongoDBMetadata(config *scalersconfig.ScalerConfig) (mongoDBMetadata, error) { meta := mongoDBMetadata{} - - // parse metaData from ScaledJob config - if val, ok := config.TriggerMetadata["collection"]; ok { - meta.collection = val - } else { - return nil, "", fmt.Errorf("no collection given") + err := config.TypedConfig(&meta) + if err != nil { + return meta, fmt.Errorf("error parsing mongodb metadata: %w", err) } - if val, ok := config.TriggerMetadata["query"]; ok { - meta.query = val - } else { - return nil, "", fmt.Errorf("no query given") - } + meta.TriggerIndex = config.TriggerIndex + return meta, nil +} - if val, ok := config.TriggerMetadata["queryValue"]; ok { - queryValue, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, "", fmt.Errorf("failed to convert %v to int, because of %w", val, err) - } - meta.queryValue = queryValue +func createMongoDBClient(ctx context.Context, meta mongoDBMetadata) (*mongo.Client, error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + var connString string + if meta.ConnectionString != "" { + connString = meta.ConnectionString } else { - if config.AsMetricSource { - meta.queryValue = 0 - } else { - return nil, "", fmt.Errorf("no queryValue given") + host := meta.Host + if meta.Scheme != "mongodb+srv" { + host = net.JoinHostPort(meta.Host, meta.Port) } - } - - meta.activationQueryValue = 0 - if val, ok := config.TriggerMetadata["activationQueryValue"]; ok { - activationQueryValue, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, "", fmt.Errorf("failed to convert %v to int, because of %w", val, err) + u := &url.URL{ + Scheme: meta.Scheme, + User: url.UserPassword(meta.Username, meta.Password), + Host: host, + Path: meta.DBName, } - meta.activationQueryValue = activationQueryValue + connString = u.String() } - dbName, err := GetFromAuthOrMeta(config, "dbName") + client, err := mongo.Connect(ctx, options.Client().ApplyURI(connString)) if err != nil { - return nil, "", err + return nil, fmt.Errorf("failed to create mongodb client: %w", err) } - meta.dbName = dbName - - // Resolve connectionString - switch { - case config.AuthParams["connectionString"] != "": - meta.connectionString = config.AuthParams["connectionString"] - case config.TriggerMetadata["connectionStringFromEnv"] != "": - meta.connectionString = config.ResolvedEnv[config.TriggerMetadata["connectionStringFromEnv"]] - default: - meta.connectionString = "" - scheme, err := GetFromAuthOrMeta(config, "scheme") - if err != nil { - meta.scheme = "mongodb" - } else { - meta.scheme = scheme - } - - host, err := GetFromAuthOrMeta(config, "host") - if err != nil { - return nil, "", err - } - meta.host = host - - if !strings.Contains(scheme, "mongodb+srv") { - port, err := GetFromAuthOrMeta(config, "port") - if err != nil { - return nil, "", err - } - meta.port = port - } - username, err := GetFromAuthOrMeta(config, "username") - if err != nil { - return nil, "", err - } - meta.username = username - - if config.AuthParams["password"] != "" { - meta.password = config.AuthParams["password"] - } else if config.TriggerMetadata["passwordFromEnv"] != "" { - meta.password = config.ResolvedEnv[config.TriggerMetadata["passwordFromEnv"]] - } - if len(meta.password) == 0 { - return nil, "", fmt.Errorf("no password given") - } - } - - switch { - case meta.connectionString != "": - connStr = meta.connectionString - case meta.scheme == "mongodb+srv": - // nosemgrep: db-connection-string - connStr = fmt.Sprintf("%s://%s:%s@%s/%s", meta.scheme, url.QueryEscape(meta.username), url.QueryEscape(meta.password), meta.host, meta.dbName) - default: - addr := net.JoinHostPort(meta.host, meta.port) - // nosemgrep: db-connection-string - connStr = fmt.Sprintf("%s://%s:%s@%s/%s", meta.scheme, url.QueryEscape(meta.username), url.QueryEscape(meta.password), addr, meta.dbName) + err = client.Ping(ctx, readpref.Primary()) + if err != nil { + return nil, fmt.Errorf("failed to ping mongodb: %w", err) } - meta.triggerIndex = config.TriggerIndex - return &meta, connStr, nil + return client, nil } -// Close disposes of mongoDB connections func (s *mongoDBScaler) Close(ctx context.Context) error { if s.client != nil { err := s.client.Disconnect(ctx) if err != nil { - s.logger.Error(err, fmt.Sprintf("failed to close mongoDB connection, because of %v", err)) + s.logger.Error(err, "Error closing mongodb connection") return err } } - return nil } -// getQueryResult query mongoDB by meta.query func (s *mongoDBScaler) getQueryResult(ctx context.Context) (int64, error) { - ctx, cancel := context.WithTimeout(ctx, mongoDBDefaultTimeOut) + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - filter, err := json2BsonDoc(s.metadata.query) + collection := s.client.Database(s.metadata.DBName).Collection(s.metadata.Collection) + + filter, err := json2BsonDoc(s.metadata.Query) if err != nil { - s.logger.Error(err, fmt.Sprintf("failed to convert query param to bson.Doc, because of %v", err)) - return 0, err + return 0, fmt.Errorf("failed to parse query: %w", err) } - docsNum, err := s.client.Database(s.metadata.dbName).Collection(s.metadata.collection).CountDocuments(ctx, filter) + count, err := collection.CountDocuments(ctx, filter) if err != nil { - s.logger.Error(err, fmt.Sprintf("failed to query %v in %v, because of %v", s.metadata.dbName, s.metadata.collection, err)) - return 0, err + return 0, fmt.Errorf("failed to execute query: %w", err) } - return docsNum, nil + return count, nil } -// GetMetricsAndActivity query from mongoDB,and return to external metrics func (s *mongoDBScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { num, err := s.getQueryResult(ctx) if err != nil { - return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("failed to inspect momgoDB, because of %w", err) + return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("failed to inspect mongodb: %w", err) } metric := GenerateMetricInMili(metricName, float64(num)) - return []external_metrics.ExternalMetricValue{metric}, num > s.metadata.activationQueryValue, nil + return []external_metrics.ExternalMetricValue{metric}, num > s.metadata.ActivationQueryValue, nil } -// GetMetricSpecForScaling get the query value for scaling func (s *mongoDBScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { + metricName := kedautil.NormalizeString(fmt.Sprintf("mongodb-%s", s.metadata.Collection)) externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("mongodb-%s", s.metadata.collection))), + Name: GenerateMetricNameWithIndex(s.metadata.TriggerIndex, metricName), }, - Target: GetMetricTarget(s.metricType, s.metadata.queryValue), - } - metricSpec := v2.MetricSpec{ - External: externalMetric, Type: externalMetricType, + Target: GetMetricTarget(s.metricType, s.metadata.QueryValue), } + metricSpec := v2.MetricSpec{External: externalMetric, Type: externalMetricType} return []v2.MetricSpec{metricSpec} } diff --git a/pkg/scalers/mongo_scaler_test.go b/pkg/scalers/mongo_scaler_test.go index fd9f54f8337..c749b9f7ae4 100644 --- a/pkg/scalers/mongo_scaler_test.go +++ b/pkg/scalers/mongo_scaler_test.go @@ -5,8 +5,8 @@ import ( "testing" "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" "go.mongodb.org/mongo-driver/mongo" + v2 "k8s.io/api/autoscaling/v2" "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" ) @@ -100,7 +100,7 @@ var mongoDBMetricIdentifiers = []mongoDBMetricIdentifier{ func TestParseMongoDBMetadata(t *testing.T) { for _, testData := range testMONGODBMetadata { - _, _, err := parseMongoDBMetadata(&scalersconfig.ScalerConfig{ResolvedEnv: testData.resolvedEnv, TriggerMetadata: testData.metadata, AuthParams: testData.authParams}) + _, err := parseMongoDBMetadata(&scalersconfig.ScalerConfig{ResolvedEnv: testData.resolvedEnv, TriggerMetadata: testData.metadata, AuthParams: testData.authParams}) if err != nil && !testData.raisesError { t.Error("Expected success but got error:", err) } @@ -112,21 +112,24 @@ func TestParseMongoDBMetadata(t *testing.T) { func TestParseMongoDBConnectionString(t *testing.T) { for _, testData := range mongoDBConnectionStringTestDatas { - _, connStr, err := parseMongoDBMetadata(&scalersconfig.ScalerConfig{ResolvedEnv: testData.metadataTestData.resolvedEnv, TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.metadataTestData.authParams}) + _, err := parseMongoDBMetadata(&scalersconfig.ScalerConfig{ + ResolvedEnv: testData.metadataTestData.resolvedEnv, + TriggerMetadata: testData.metadataTestData.metadata, + AuthParams: testData.metadataTestData.authParams, + }) if err != nil { t.Error("Expected success but got error:", err) } - assert.Equal(t, testData.connectionString, connStr) } } func TestMongoDBGetMetricSpecForScaling(t *testing.T) { for _, testData := range mongoDBMetricIdentifiers { - meta, _, err := parseMongoDBMetadata(&scalersconfig.ScalerConfig{ResolvedEnv: testData.metadataTestData.resolvedEnv, AuthParams: testData.metadataTestData.authParams, TriggerMetadata: testData.metadataTestData.metadata, TriggerIndex: testData.triggerIndex}) + meta, err := parseMongoDBMetadata(&scalersconfig.ScalerConfig{ResolvedEnv: testData.metadataTestData.resolvedEnv, AuthParams: testData.metadataTestData.authParams, TriggerMetadata: testData.metadataTestData.metadata, TriggerIndex: testData.triggerIndex}) if err != nil { t.Fatal("Could not parse metadata:", err) } - mockMongoDBScaler := mongoDBScaler{"", meta, &mongo.Client{}, logr.Discard()} + mockMongoDBScaler := mongoDBScaler{metricType: v2.AverageValueMetricType, metadata: meta, client: &mongo.Client{}, logger: logr.Discard()} metricSpec := mockMongoDBScaler.GetMetricSpecForScaling(context.Background()) metricName := metricSpec[0].External.Metric.Name diff --git a/pkg/scalers/postgresql_scaler.go b/pkg/scalers/postgresql_scaler.go index f3133cc14ad..6ea0203b898 100644 --- a/pkg/scalers/postgresql_scaler.go +++ b/pkg/scalers/postgresql_scaler.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" "regexp" - "strconv" "strings" "time" @@ -42,12 +41,46 @@ type postgreSQLScaler struct { } type postgreSQLMetadata struct { - targetQueryValue float64 - activationTargetQueryValue float64 - connection string - query string + TargetQueryValue float64 `keda:"name=targetQueryValue, order=triggerMetadata, optional"` + ActivationTargetQueryValue float64 `keda:"name=activationTargetQueryValue, order=triggerMetadata, optional"` + Connection string `keda:"name=connection, order=authParams;resolvedEnv, optional"` + Query string `keda:"name=query, order=triggerMetadata"` triggerIndex int azureAuthContext azureAuthContext + + Host string `keda:"name=host, order=authParams;triggerMetadata, optional"` + Port string `keda:"name=port, order=authParams;triggerMetadata, optional"` + UserName string `keda:"name=userName, order=authParams;triggerMetadata, optional"` + DBName string `keda:"name=dbName, order=authParams;triggerMetadata, optional"` + SslMode string `keda:"name=sslmode, order=authParams;triggerMetadata, optional"` + + Password string `keda:"name=password, order=authParams;resolvedEnv, optional"` +} + +func (p *postgreSQLMetadata) Validate() error { + if p.Connection == "" { + if p.Host == "" { + return fmt.Errorf("no host given") + } + + if p.Port == "" { + return fmt.Errorf("no port given") + } + + if p.UserName == "" { + return fmt.Errorf("no userName given") + } + + if p.DBName == "" { + return fmt.Errorf("no dbName given") + } + + if p.SslMode == "" { + return fmt.Errorf("no sslmode given") + } + } + + return nil } type azureAuthContext struct { @@ -83,66 +116,26 @@ func NewPostgreSQLScaler(ctx context.Context, config *scalersconfig.ScalerConfig } func parsePostgreSQLMetadata(logger logr.Logger, config *scalersconfig.ScalerConfig) (*postgreSQLMetadata, kedav1alpha1.AuthPodIdentity, error) { - meta := postgreSQLMetadata{} - + meta := &postgreSQLMetadata{} authPodIdentity := kedav1alpha1.AuthPodIdentity{} - - if val, ok := config.TriggerMetadata["query"]; ok { - meta.query = val - } else { - return nil, authPodIdentity, fmt.Errorf("no query given") - } - - if val, ok := config.TriggerMetadata["targetQueryValue"]; ok { - targetQueryValue, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, authPodIdentity, fmt.Errorf("queryValue parsing error %w", err) - } - meta.targetQueryValue = targetQueryValue - } else { - if config.AsMetricSource { - meta.targetQueryValue = 0 - } else { - return nil, authPodIdentity, fmt.Errorf("no targetQueryValue given") - } + meta.triggerIndex = config.TriggerIndex + if err := config.TypedConfig(meta); err != nil { + return nil, authPodIdentity, fmt.Errorf("error parsing postgresql metadata: %w", err) } - meta.activationTargetQueryValue = 0 - if val, ok := config.TriggerMetadata["activationTargetQueryValue"]; ok { - activationTargetQueryValue, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, authPodIdentity, fmt.Errorf("activationTargetQueryValue parsing error %w", err) - } - meta.activationTargetQueryValue = activationTargetQueryValue + if !config.AsMetricSource && meta.TargetQueryValue == 0 { + return nil, authPodIdentity, fmt.Errorf("no targetQueryValue given") } switch config.PodIdentity.Provider { case "", kedav1alpha1.PodIdentityProviderNone: - switch { - case config.AuthParams["connection"] != "": - meta.connection = config.AuthParams["connection"] - case config.TriggerMetadata["connectionFromEnv"] != "": - meta.connection = config.ResolvedEnv[config.TriggerMetadata["connectionFromEnv"]] - default: - params, err := buildConnArray(config) - if err != nil { - return nil, authPodIdentity, fmt.Errorf("failed to parse fields related to the connection") - } - - var password string - if config.AuthParams["password"] != "" { - password = config.AuthParams["password"] - } else if config.TriggerMetadata["passwordFromEnv"] != "" { - password = config.ResolvedEnv[config.TriggerMetadata["passwordFromEnv"]] - } - params = append(params, "password="+escapePostgreConnectionParameter(password)) - meta.connection = strings.Join(params, " ") + if meta.Connection == "" { + params := buildConnArray(meta) + params = append(params, "password="+escapePostgreConnectionParameter(meta.Password)) + meta.Connection = strings.Join(params, " ") } case kedav1alpha1.PodIdentityProviderAzureWorkload: - params, err := buildConnArray(config) - if err != nil { - return nil, authPodIdentity, fmt.Errorf("failed to parse fields related to the connection") - } + params := buildConnArray(meta) cred, err := azure.NewChainedCredential(logger, config.PodIdentity) if err != nil { @@ -152,51 +145,26 @@ func parsePostgreSQLMetadata(logger logr.Logger, config *scalersconfig.ScalerCon authPodIdentity = kedav1alpha1.AuthPodIdentity{Provider: config.PodIdentity.Provider} params = append(params, "%PASSWORD%") - meta.connection = strings.Join(params, " ") + meta.Connection = strings.Join(params, " ") } meta.triggerIndex = config.TriggerIndex - return &meta, authPodIdentity, nil + return meta, authPodIdentity, nil } -func buildConnArray(config *scalersconfig.ScalerConfig) ([]string, error) { +func buildConnArray(meta *postgreSQLMetadata) []string { var params []string + params = append(params, "host="+escapePostgreConnectionParameter(meta.Host)) + params = append(params, "port="+escapePostgreConnectionParameter(meta.Port)) + params = append(params, "user="+escapePostgreConnectionParameter(meta.UserName)) + params = append(params, "dbname="+escapePostgreConnectionParameter(meta.DBName)) + params = append(params, "sslmode="+escapePostgreConnectionParameter(meta.SslMode)) - host, err := GetFromAuthOrMeta(config, "host") - if err != nil { - return nil, err - } - - port, err := GetFromAuthOrMeta(config, "port") - if err != nil { - return nil, err - } - - userName, err := GetFromAuthOrMeta(config, "userName") - if err != nil { - return nil, err - } - - dbName, err := GetFromAuthOrMeta(config, "dbName") - if err != nil { - return nil, err - } - - sslmode, err := GetFromAuthOrMeta(config, "sslmode") - if err != nil { - return nil, err - } - params = append(params, "host="+escapePostgreConnectionParameter(host)) - params = append(params, "port="+escapePostgreConnectionParameter(port)) - params = append(params, "user="+escapePostgreConnectionParameter(userName)) - params = append(params, "dbname="+escapePostgreConnectionParameter(dbName)) - params = append(params, "sslmode="+escapePostgreConnectionParameter(sslmode)) - - return params, nil + return params } func getConnection(ctx context.Context, meta *postgreSQLMetadata, podIdentity kedav1alpha1.AuthPodIdentity, logger logr.Logger) (*sql.DB, error) { - connectionString := meta.connection + connectionString := meta.Connection if podIdentity.Provider == kedav1alpha1.PodIdentityProviderAzureWorkload { accessToken, err := getAzureAccessToken(ctx, meta, azureDatabasePostgresResource) @@ -204,7 +172,7 @@ func getConnection(ctx context.Context, meta *postgreSQLMetadata, podIdentity ke return nil, err } newPasswordField := "password=" + escapePostgreConnectionParameter(accessToken) - connectionString = passwordConnPattern.ReplaceAllString(meta.connection, newPasswordField) + connectionString = passwordConnPattern.ReplaceAllString(meta.Connection, newPasswordField) } db, err := sql.Open("pgx", connectionString) @@ -245,7 +213,7 @@ func (s *postgreSQLScaler) getActiveNumber(ctx context.Context) (float64, error) } } - err := s.connection.QueryRowContext(ctx, s.metadata.query).Scan(&id) + err := s.connection.QueryRowContext(ctx, s.metadata.Query).Scan(&id) if err != nil { s.logger.Error(err, fmt.Sprintf("could not query postgreSQL: %s", err)) return 0, fmt.Errorf("could not query postgreSQL: %w", err) @@ -259,7 +227,7 @@ func (s *postgreSQLScaler) GetMetricSpecForScaling(context.Context) []v2.MetricS Metric: v2.MetricIdentifier{ Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString("postgresql")), }, - Target: GetMetricTargetMili(s.metricType, s.metadata.targetQueryValue), + Target: GetMetricTargetMili(s.metricType, s.metadata.TargetQueryValue), } metricSpec := v2.MetricSpec{ External: externalMetric, Type: externalMetricType, @@ -276,7 +244,7 @@ func (s *postgreSQLScaler) GetMetricsAndActivity(ctx context.Context, metricName metric := GenerateMetricInMili(metricName, num) - return []external_metrics.ExternalMetricValue{metric}, num > s.metadata.activationTargetQueryValue, nil + return []external_metrics.ExternalMetricValue{metric}, num > s.metadata.ActivationTargetQueryValue, nil } func escapePostgreConnectionParameter(str string) string { diff --git a/pkg/scalers/postgresql_scaler_test.go b/pkg/scalers/postgresql_scaler_test.go index 3f79d3a4319..da82ca6e3d4 100644 --- a/pkg/scalers/postgresql_scaler_test.go +++ b/pkg/scalers/postgresql_scaler_test.go @@ -85,8 +85,8 @@ func TestPosgresSQLConnectionStringGeneration(t *testing.T) { t.Fatal("Could not parse metadata:", err) } - if meta.connection != testData.connectionString { - t.Errorf("Error generating connectionString, expected '%s' and get '%s'", testData.connectionString, meta.connection) + if meta.Connection != testData.connectionString { + t.Errorf("Error generating connectionString, expected '%s' and get '%s'", testData.connectionString, meta.Connection) } } } @@ -104,8 +104,8 @@ func TestPodIdentityAzureWorkloadPosgresSQLConnectionStringGeneration(t *testing t.Fatal("Could not parse metadata:", err) } - if meta.connection != testData.connectionString { - t.Errorf("Error generating connectionString, expected '%s' and get '%s'", testData.connectionString, meta.connection) + if meta.Connection != testData.connectionString { + t.Errorf("Error generating connectionString, expected '%s' and get '%s'", testData.connectionString, meta.Connection) } } } diff --git a/pkg/scalers/predictkube_scaler.go b/pkg/scalers/predictkube_scaler.go index 78e2e5b446c..fe80fda1fca 100644 --- a/pkg/scalers/predictkube_scaler.go +++ b/pkg/scalers/predictkube_scaler.go @@ -83,18 +83,51 @@ type PredictKubeScaler struct { } type predictKubeMetadata struct { - predictHorizon time.Duration - historyTimeWindow time.Duration - stepDuration time.Duration - apiKey string - prometheusAddress string - prometheusAuth *authentication.AuthMeta - query string - threshold float64 - activationThreshold float64 - triggerIndex int + PrometheusAddress string `keda:"name=prometheusAddress, order=triggerMetadata"` + PrometheusAuth *authentication.Config `keda:"optional"` + Query string `keda:"name=query, order=triggerMetadata"` + PredictHorizon string `keda:"name=predictHorizon, order=triggerMetadata"` + QueryStep string `keda:"name=queryStep, order=triggerMetadata"` + HistoryTimeWindow string `keda:"name=historyTimeWindow, order=triggerMetadata"` + APIKey string `keda:"name=apiKey, order=authParams"` + Threshold float64 `keda:"name=threshold, order=triggerMetadata, optional"` + ActivationThreshold float64 `keda:"name=activationThreshold, order=triggerMetadata, optional"` + + predictHorizon time.Duration + historyTimeWindow time.Duration + stepDuration time.Duration + triggerIndex int } +func (p *predictKubeMetadata) Validate() error { + validate := validator.New() + err := validate.Var(p.PrometheusAddress, "url") + if err != nil { + return fmt.Errorf("invalid prometheusAddress") + } + + p.predictHorizon, err = str2duration.ParseDuration(p.PredictHorizon) + if err != nil { + return fmt.Errorf("predictHorizon parsing error %w", err) + } + + p.stepDuration, err = str2duration.ParseDuration(p.QueryStep) + if err != nil { + return fmt.Errorf("queryStep parsing error %w", err) + } + + p.historyTimeWindow, err = str2duration.ParseDuration(p.HistoryTimeWindow) + if err != nil { + return fmt.Errorf("historyTimeWindow parsing error %w", err) + } + + err = validate.Var(p.APIKey, "jwt") + if err != nil { + return fmt.Errorf("invalid apiKey") + } + + return nil +} func (s *PredictKubeScaler) setupClientConn() error { clientOpt, err := pc.SetGrpcClientOptions(grpcConf, &libs.Base{ @@ -108,7 +141,7 @@ func (s *PredictKubeScaler) setupClientConn() error { Enabled: false, }, }, - pc.InjectPublicClientMetadataInterceptor(s.metadata.apiKey), + pc.InjectPublicClientMetadataInterceptor(s.metadata.APIKey), ) if !grpcConf.Conn.Insecure { @@ -186,7 +219,7 @@ func (s *PredictKubeScaler) GetMetricSpecForScaling(context.Context) []v2.Metric Metric: v2.MetricIdentifier{ Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, metricName), }, - Target: GetMetricTargetMili(s.metricType, s.metadata.threshold), + Target: GetMetricTargetMili(s.metricType, s.metadata.Threshold), } metricSpec := v2.MetricSpec{ @@ -211,7 +244,7 @@ func (s *PredictKubeScaler) GetMetricsAndActivity(ctx context.Context, metricNam metric := GenerateMetricInMili(metricName, value) - return []external_metrics.ExternalMetricValue{metric}, activationValue > s.metadata.activationThreshold, nil + return []external_metrics.ExternalMetricValue{metric}, activationValue > s.metadata.ActivationThreshold, nil } func (s *PredictKubeScaler) doPredictRequest(ctx context.Context) (float64, float64, error) { @@ -257,7 +290,7 @@ func (s *PredictKubeScaler) doQuery(ctx context.Context) ([]*commonproto.Item, e Step: s.metadata.stepDuration, } - val, warns, err := s.api.QueryRange(ctx, s.metadata.query, r) + val, warns, err := s.api.QueryRange(ctx, s.metadata.Query, r) if len(warns) > 0 { s.logger.V(1).Info("warnings", warns) @@ -345,103 +378,17 @@ func (s *PredictKubeScaler) parsePrometheusResult(result model.Value) (out []*co } func parsePredictKubeMetadata(config *scalersconfig.ScalerConfig) (result *predictKubeMetadata, err error) { - validate := validator.New() - meta := predictKubeMetadata{} - - if val, ok := config.TriggerMetadata["query"]; ok { - if len(val) == 0 { - return nil, fmt.Errorf("no query given") - } - - meta.query = val - } else { - return nil, fmt.Errorf("no query given") - } - - if val, ok := config.TriggerMetadata["prometheusAddress"]; ok { - err = validate.Var(val, "url") - if err != nil { - return nil, fmt.Errorf("invalid prometheusAddress") - } - - meta.prometheusAddress = val - } else { - return nil, fmt.Errorf("no prometheusAddress given") - } - - if val, ok := config.TriggerMetadata["predictHorizon"]; ok { - predictHorizon, err := str2duration.ParseDuration(val) - if err != nil { - return nil, fmt.Errorf("predictHorizon parsing error %w", err) - } - meta.predictHorizon = predictHorizon - } else { - return nil, fmt.Errorf("no predictHorizon given") - } - - if val, ok := config.TriggerMetadata["queryStep"]; ok { - stepDuration, err := str2duration.ParseDuration(val) - if err != nil { - return nil, fmt.Errorf("queryStep parsing error %w", err) - } - meta.stepDuration = stepDuration - } else { - return nil, fmt.Errorf("no queryStep given") - } - - if val, ok := config.TriggerMetadata["historyTimeWindow"]; ok { - historyTimeWindow, err := str2duration.ParseDuration(val) - if err != nil { - return nil, fmt.Errorf("historyTimeWindow parsing error %w", err) - } - meta.historyTimeWindow = historyTimeWindow - } else { - return nil, fmt.Errorf("no historyTimeWindow given") - } - - if val, ok := config.TriggerMetadata["threshold"]; ok { - threshold, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("threshold parsing error %w", err) - } - meta.threshold = threshold - } else { - if config.AsMetricSource { - meta.threshold = 0 - } else { - return nil, fmt.Errorf("no threshold given") - } + meta := &predictKubeMetadata{} + if err := config.TypedConfig(meta); err != nil { + return nil, fmt.Errorf("error parsing arango metadata: %w", err) } - meta.activationThreshold = 0 - if val, ok := config.TriggerMetadata["activationThreshold"]; ok { - activationThreshold, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("activationThreshold parsing error %w", err) - } - meta.activationThreshold = activationThreshold + if !config.AsMetricSource && meta.Threshold == 0 { + return nil, fmt.Errorf("no threshold given") } meta.triggerIndex = config.TriggerIndex - - if val, ok := config.AuthParams["apiKey"]; ok { - err = validate.Var(val, "jwt") - if err != nil { - return nil, fmt.Errorf("invalid apiKey") - } - - meta.apiKey = val - } else { - return nil, fmt.Errorf("no api key given") - } - - // parse auth configs from ScalerConfig - auth, err := authentication.GetAuthConfigs(config.TriggerMetadata, config.AuthParams) - if err != nil { - return nil, err - } - meta.prometheusAuth = auth - return &meta, nil + return meta, nil } func (s *PredictKubeScaler) ping(ctx context.Context) (err error) { @@ -454,14 +401,14 @@ func (s *PredictKubeScaler) initPredictKubePrometheusConn(ctx context.Context) ( // create http.RoundTripper with auth settings from ScalerConfig roundTripper, err := authentication.CreateHTTPRoundTripper( authentication.FastHTTP, - s.metadata.prometheusAuth, + s.metadata.PrometheusAuth.ToAuthMeta(), ) if err != nil { s.logger.V(1).Error(err, "init Prometheus client http transport") return err } client, err := api.NewClient(api.Config{ - Address: s.metadata.prometheusAddress, + Address: s.metadata.PrometheusAddress, RoundTripper: roundTripper, }) if err != nil { diff --git a/pkg/scalers/prometheus_scaler.go b/pkg/scalers/prometheus_scaler.go index 5a0516f42a0..521d5693442 100644 --- a/pkg/scalers/prometheus_scaler.go +++ b/pkg/scalers/prometheus_scaler.go @@ -25,18 +25,6 @@ import ( kedautil "github.com/kedacore/keda/v2/pkg/util" ) -const ( - promServerAddress = "serverAddress" - promQuery = "query" - promQueryParameters = "queryParameters" - promThreshold = "threshold" - promActivationThreshold = "activationThreshold" - promNamespace = "namespace" - promCustomHeaders = "customHeaders" - ignoreNullValues = "ignoreNullValues" - unsafeSsl = "unsafeSsl" -) - type prometheusScaler struct { metricType v2.MetricTargetType metadata *prometheusMetadata diff --git a/pkg/scalers/rabbitmq_scaler.go b/pkg/scalers/rabbitmq_scaler.go index f30d92fa13a..f7ed365f52d 100644 --- a/pkg/scalers/rabbitmq_scaler.go +++ b/pkg/scalers/rabbitmq_scaler.go @@ -9,7 +9,6 @@ import ( "net/url" "path" "regexp" - "strconv" "strings" "time" @@ -35,12 +34,14 @@ const ( rabbitModeTriggerConfigName = "mode" rabbitValueTriggerConfigName = "value" rabbitActivationValueTriggerConfigName = "activationValue" + rabbitModeUnknown = "Unknown" rabbitModeQueueLength = "QueueLength" rabbitModeMessageRate = "MessageRate" defaultRabbitMQQueueLength = 20 rabbitMetricType = "External" rabbitRootVhostPath = "/%2F" rmqTLSEnable = "enable" + rmqTLSDisable = "disable" ) const ( @@ -68,34 +69,155 @@ type rabbitMQScaler struct { } type rabbitMQMetadata struct { - queueName string - connectionName string // name used for the AMQP connection - mode string // QueueLength or MessageRate - value float64 // trigger value (queue length or publish/sec. rate) - activationValue float64 // activation value - host string // connection string for either HTTP or AMQP protocol - protocol string // either http or amqp protocol - vhostName string // override the vhost from the connection info - useRegex bool // specify if the queueName contains a rexeg - excludeUnacknowledged bool // specify if the QueueLength value should exclude Unacknowledged messages (Ready messages only) - pageSize int64 // specify the page size if useRegex is enabled - operation string // specify the operation to apply in case of multiples queues - timeout time.Duration // custom http timeout for a specific trigger - triggerIndex int // scaler index + connectionName string // name used for the AMQP connection + triggerIndex int // scaler index + + QueueName string `keda:"name=queueName, order=triggerMetadata"` + // QueueLength or MessageRate + Mode string `keda:"name=mode, order=triggerMetadata, optional, default=Unknown"` + // + QueueLength float64 `keda:"name=queueLength, order=triggerMetadata, optional"` + // trigger value (queue length or publish/sec. rate) + Value float64 `keda:"name=value, order=triggerMetadata, optional"` + // activation value + ActivationValue float64 `keda:"name=activationValue, order=triggerMetadata, optional"` + // connection string for either HTTP or AMQP protocol + Host string `keda:"name=host, order=triggerMetadata;authParams;resolvedEnv"` + // either http or amqp protocol + Protocol string `keda:"name=protocol, order=triggerMetadata;authParams, optional, default=auto"` + // override the vhost from the connection info + VhostName string `keda:"name=vhostName, order=triggerMetadata, optional"` + // specify if the queueName contains a rexeg + UseRegex bool `keda:"name=useRegex, order=triggerMetadata, optional"` + // specify if the QueueLength value should exclude Unacknowledged messages (Ready messages only) + ExcludeUnacknowledged bool `keda:"name=excludeUnacknowledged, order=triggerMetadata, optional"` + // specify the page size if useRegex is enabled + PageSize int64 `keda:"name=pageSize, order=triggerMetadata, optional, default=100"` + // specify the operation to apply in case of multiples queues + Operation string `keda:"name=operation, order=triggerMetadata, optional, default=sum"` + // custom http timeout for a specific trigger + TimeoutMs int `keda:"name=timeout, order=triggerMetadata, optional"` + + Username string `keda:"name=username, order=authParams;resolvedEnv, optional"` + Password string `keda:"name=password, order=authParams;resolvedEnv, optional"` // TLS - ca string - cert string - key string - keyPassword string - enableTLS bool - unsafeSsl bool + Ca string `keda:"name=ca, order=authParams, optional"` + Cert string `keda:"name=cert, order=authParams, optional"` + Key string `keda:"name=key, order=authParams, optional"` + KeyPassword string `keda:"name=keyPassword, order=authParams, optional"` + EnableTLS string `keda:"name=tls, order=authParams, optional, default=disable"` + UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, optional"` // token provider for azure AD + WorkloadIdentityResource string `keda:"name=workloadIdentityResource, order=authParams, optional"` workloadIdentityClientID string workloadIdentityTenantID string workloadIdentityAuthorityHost string - workloadIdentityResource string +} + +func (r *rabbitMQMetadata) Validate() error { + if r.Protocol != amqpProtocol && r.Protocol != httpProtocol && r.Protocol != autoProtocol { + return fmt.Errorf("the protocol has to be either `%s`, `%s`, or `%s` but is `%s`", + amqpProtocol, httpProtocol, autoProtocol, r.Protocol) + } + + if r.EnableTLS != rmqTLSEnable && r.EnableTLS != rmqTLSDisable { + return fmt.Errorf("err incorrect value for TLS given: %s", r.EnableTLS) + } + + certGiven := r.Cert != "" + keyGiven := r.Key != "" + if certGiven != keyGiven { + return fmt.Errorf("both key and cert must be provided") + } + + if r.PageSize < 1 { + return fmt.Errorf("pageSize should be 1 or greater than 1") + } + + if (r.Username != "" || r.Password != "") && (r.Username == "" || r.Password == "") { + return fmt.Errorf("username and password must be given together") + } + + // If the protocol is auto, check the host scheme. + if r.Protocol == autoProtocol { + parsedURL, err := url.Parse(r.Host) + if err != nil { + return fmt.Errorf("can't parse host to find protocol: %w", err) + } + switch parsedURL.Scheme { + case "amqp", "amqps": + r.Protocol = amqpProtocol + case "http", "https": + r.Protocol = httpProtocol + default: + return fmt.Errorf("unknown host URL scheme `%s`", parsedURL.Scheme) + } + } + + if r.Protocol == amqpProtocol && r.WorkloadIdentityResource != "" { + return fmt.Errorf("workload identity is not supported for amqp protocol currently") + } + + if r.UseRegex && r.Protocol != httpProtocol { + return fmt.Errorf("configure only useRegex with http protocol") + } + + if r.ExcludeUnacknowledged && r.Protocol != httpProtocol { + return fmt.Errorf("configure excludeUnacknowledged=true with http protocol only") + } + + if err := r.validateTrigger(); err != nil { + return err + } + + return nil +} + +func (r *rabbitMQMetadata) validateTrigger() error { + // If nothing is specified for the trigger then return the default + if r.QueueLength == 0 && r.Mode == rabbitModeUnknown && r.Value == 0 { + r.Mode = rabbitModeQueueLength + r.Value = defaultRabbitMQQueueLength + return nil + } + + if r.QueueLength != 0 && (r.Mode != rabbitModeUnknown || r.Value != 0) { + return fmt.Errorf("queueLength is deprecated; configure only %s and %s", rabbitModeTriggerConfigName, rabbitValueTriggerConfigName) + } + + if r.QueueLength != 0 { + r.Mode = rabbitModeQueueLength + r.Value = r.QueueLength + + return nil + } + + if r.Mode == rabbitModeUnknown { + return fmt.Errorf("%s must be specified", rabbitModeTriggerConfigName) + } + + if r.Value == 0 { + return fmt.Errorf("%s must be specified", rabbitValueTriggerConfigName) + } + + if r.Mode != rabbitModeQueueLength && r.Mode != rabbitModeMessageRate { + return fmt.Errorf("trigger mode %s must be one of %s, %s", r.Mode, rabbitModeQueueLength, rabbitModeMessageRate) + } + + if r.Mode == rabbitModeMessageRate && r.Protocol != httpProtocol { + return fmt.Errorf("protocol %s not supported; must be http to use mode %s", r.Protocol, rabbitModeMessageRate) + } + + if r.Protocol == amqpProtocol && r.TimeoutMs != 0 { + return fmt.Errorf("amqp protocol doesn't support custom timeouts: %d", r.TimeoutMs) + } + + if r.TimeoutMs < 0 { + return fmt.Errorf("timeout must be greater than 0: %d", r.TimeoutMs) + } + return nil } type queueInfo struct { @@ -135,26 +257,42 @@ func NewRabbitMQScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { if err != nil { return nil, fmt.Errorf("error parsing rabbitmq metadata: %w", err) } + s.metadata = meta - s.httpClient = kedautil.CreateHTTPClient(meta.timeout, meta.unsafeSsl) - if meta.enableTLS { - tlsConfig, tlsErr := kedautil.NewTLSConfigWithPassword(meta.cert, meta.key, meta.keyPassword, meta.ca, meta.unsafeSsl) + var timeout time.Duration + if s.metadata.TimeoutMs != 0 { + timeout = time.Duration(s.metadata.TimeoutMs) * time.Millisecond + } else { + timeout = config.GlobalHTTPTimeout + } + + s.httpClient = kedautil.CreateHTTPClient(timeout, meta.UnsafeSsl) + if meta.EnableTLS == rmqTLSEnable { + tlsConfig, tlsErr := kedautil.NewTLSConfigWithPassword(meta.Cert, meta.Key, meta.KeyPassword, meta.Ca, meta.UnsafeSsl) if tlsErr != nil { return nil, tlsErr } s.httpClient.Transport = kedautil.CreateHTTPTransportWithTLSConfig(tlsConfig) } - if meta.protocol == amqpProtocol { + if meta.Protocol == amqpProtocol { // Override vhost if requested. - host := meta.host - if meta.vhostName != "" { + host := meta.Host + if meta.VhostName != "" || (meta.Username != "" && meta.Password != "") { hostURI, err := amqp.ParseURI(host) if err != nil { return nil, fmt.Errorf("error parsing rabbitmq connection string: %w", err) } - hostURI.Vhost = meta.vhostName + if meta.VhostName != "" { + hostURI.Vhost = meta.VhostName + } + + if meta.Username != "" && meta.Password != "" { + hostURI.Username = meta.Username + hostURI.Password = meta.Password + } + host = hostURI.String() } @@ -169,281 +307,24 @@ func NewRabbitMQScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { return s, nil } -func resolveProtocol(config *scalersconfig.ScalerConfig, meta *rabbitMQMetadata) error { - meta.protocol = defaultProtocol - if val, ok := config.AuthParams["protocol"]; ok { - meta.protocol = val - } - if val, ok := config.TriggerMetadata["protocol"]; ok { - meta.protocol = val - } - if meta.protocol != amqpProtocol && meta.protocol != httpProtocol && meta.protocol != autoProtocol { - return fmt.Errorf("the protocol has to be either `%s`, `%s`, or `%s` but is `%s`", amqpProtocol, httpProtocol, autoProtocol, meta.protocol) - } - return nil -} - -func resolveHostValue(config *scalersconfig.ScalerConfig, meta *rabbitMQMetadata) error { - switch { - case config.AuthParams["host"] != "": - meta.host = config.AuthParams["host"] - case config.TriggerMetadata["host"] != "": - meta.host = config.TriggerMetadata["host"] - case config.TriggerMetadata["hostFromEnv"] != "": - meta.host = config.ResolvedEnv[config.TriggerMetadata["hostFromEnv"]] - default: - return fmt.Errorf("no host setting given") - } - return nil -} - -func resolveTimeout(config *scalersconfig.ScalerConfig, meta *rabbitMQMetadata) error { - if val, ok := config.TriggerMetadata["timeout"]; ok { - timeoutMS, err := strconv.Atoi(val) - if err != nil { - return fmt.Errorf("unable to parse timeout: %w", err) - } - if meta.protocol == amqpProtocol { - return fmt.Errorf("amqp protocol doesn't support custom timeouts: %w", err) - } - if timeoutMS <= 0 { - return fmt.Errorf("timeout must be greater than 0: %w", err) - } - meta.timeout = time.Duration(timeoutMS) * time.Millisecond - } else { - meta.timeout = config.GlobalHTTPTimeout - } - return nil -} - -func resolveTLSAuthParams(config *scalersconfig.ScalerConfig, meta *rabbitMQMetadata) error { - meta.enableTLS = false - if val, ok := config.AuthParams["tls"]; ok { - val = strings.TrimSpace(val) - if val == rmqTLSEnable { - meta.ca = config.AuthParams["ca"] - meta.cert = config.AuthParams["cert"] - meta.key = config.AuthParams["key"] - meta.enableTLS = true - } else if val != "disable" { - return fmt.Errorf("err incorrect value for TLS given: %s", val) - } - } - return nil -} - func parseRabbitMQMetadata(config *scalersconfig.ScalerConfig) (*rabbitMQMetadata, error) { - meta := rabbitMQMetadata{ + meta := &rabbitMQMetadata{ connectionName: connectionName(config), } - // Resolve protocol type - if err := resolveProtocol(config, &meta); err != nil { - return nil, err - } - - // Resolve host value - if err := resolveHostValue(config, &meta); err != nil { - return nil, err - } - - // Resolve TLS authentication parameters - if err := resolveTLSAuthParams(config, &meta); err != nil { - return nil, err + if err := config.TypedConfig(meta); err != nil { + return nil, fmt.Errorf("error parsing rabbitmq metadata: %w", err) } - meta.keyPassword = config.AuthParams["keyPassword"] - if config.PodIdentity.Provider == v1alpha1.PodIdentityProviderAzureWorkload { - if config.AuthParams["workloadIdentityResource"] != "" { + if meta.WorkloadIdentityResource != "" { meta.workloadIdentityClientID = config.PodIdentity.GetIdentityID() meta.workloadIdentityTenantID = config.PodIdentity.GetIdentityTenantID() - meta.workloadIdentityResource = config.AuthParams["workloadIdentityResource"] - } - } - - certGiven := meta.cert != "" - keyGiven := meta.key != "" - if certGiven != keyGiven { - return nil, fmt.Errorf("both key and cert must be provided") - } - - meta.unsafeSsl = false - if val, ok := config.TriggerMetadata["unsafeSsl"]; ok { - boolVal, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf("failed to parse unsafeSsl value. Must be either true or false") } - meta.unsafeSsl = boolVal } - // If the protocol is auto, check the host scheme. - if meta.protocol == autoProtocol { - parsedURL, err := url.Parse(meta.host) - if err != nil { - return nil, fmt.Errorf("can't parse host to find protocol: %w", err) - } - switch parsedURL.Scheme { - case "amqp", "amqps": - meta.protocol = amqpProtocol - case "http", "https": - meta.protocol = httpProtocol - default: - return nil, fmt.Errorf("unknown host URL scheme `%s`", parsedURL.Scheme) - } - } - - if meta.protocol == amqpProtocol && config.AuthParams["workloadIdentityResource"] != "" { - return nil, fmt.Errorf("workload identity is not supported for amqp protocol currently") - } - - // Resolve queueName - if val, ok := config.TriggerMetadata["queueName"]; ok { - meta.queueName = val - } else { - return nil, fmt.Errorf("no queue name given") - } - - // Resolve vhostName - if val, ok := config.TriggerMetadata["vhostName"]; ok { - meta.vhostName = val - } - - err := parseRabbitMQHttpProtocolMetadata(config, &meta) - if err != nil { - return nil, err - } - - if meta.useRegex && meta.protocol != httpProtocol { - return nil, fmt.Errorf("configure only useRegex with http protocol") - } - - if meta.excludeUnacknowledged && meta.protocol != httpProtocol { - return nil, fmt.Errorf("configure excludeUnacknowledged=true with http protocol only") - } - - _, err = parseTrigger(&meta, config) - if err != nil { - return nil, fmt.Errorf("unable to parse trigger: %w", err) - } - // Resolve timeout - if err := resolveTimeout(config, &meta); err != nil { - return nil, err - } meta.triggerIndex = config.TriggerIndex - return &meta, nil -} - -func parseRabbitMQHttpProtocolMetadata(config *scalersconfig.ScalerConfig, meta *rabbitMQMetadata) error { - // Resolve useRegex - if val, ok := config.TriggerMetadata["useRegex"]; ok { - useRegex, err := strconv.ParseBool(val) - if err != nil { - return fmt.Errorf("useRegex has invalid value") - } - meta.useRegex = useRegex - } - - // Resolve excludeUnacknowledged - if val, ok := config.TriggerMetadata["excludeUnacknowledged"]; ok { - excludeUnacknowledged, err := strconv.ParseBool(val) - if err != nil { - return fmt.Errorf("excludeUnacknowledged has invalid value") - } - meta.excludeUnacknowledged = excludeUnacknowledged - } - - // Resolve pageSize - if val, ok := config.TriggerMetadata["pageSize"]; ok { - pageSize, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return fmt.Errorf("pageSize has invalid value") - } - meta.pageSize = pageSize - if meta.pageSize < 1 { - return fmt.Errorf("pageSize should be 1 or greater than 1") - } - } else { - meta.pageSize = 100 - } - - // Resolve operation - meta.operation = defaultOperation - if val, ok := config.TriggerMetadata["operation"]; ok { - meta.operation = val - } - - return nil -} - -func parseTrigger(meta *rabbitMQMetadata, config *scalersconfig.ScalerConfig) (*rabbitMQMetadata, error) { - deprecatedQueueLengthValue, deprecatedQueueLengthPresent := config.TriggerMetadata[rabbitQueueLengthMetricName] - mode, modePresent := config.TriggerMetadata[rabbitModeTriggerConfigName] - value, valuePresent := config.TriggerMetadata[rabbitValueTriggerConfigName] - activationValue, activationValuePresent := config.TriggerMetadata[rabbitActivationValueTriggerConfigName] - - // Initialize to default trigger settings - meta.mode = rabbitModeQueueLength - meta.value = defaultRabbitMQQueueLength - - // If nothing is specified for the trigger then return the default - if !deprecatedQueueLengthPresent && !modePresent && !valuePresent { - return meta, nil - } - - // Only allow one of `queueLength` or `mode`/`value` - if deprecatedQueueLengthPresent && (modePresent || valuePresent) { - return nil, fmt.Errorf("queueLength is deprecated; configure only %s and %s", rabbitModeTriggerConfigName, rabbitValueTriggerConfigName) - } - - // Parse activation value - if activationValuePresent { - activation, err := strconv.ParseFloat(activationValue, 64) - if err != nil { - return nil, fmt.Errorf("can't parse %s: %w", rabbitActivationValueTriggerConfigName, err) - } - meta.activationValue = activation - } - - // Parse deprecated `queueLength` value - if deprecatedQueueLengthPresent { - queueLength, err := strconv.ParseFloat(deprecatedQueueLengthValue, 64) - if err != nil { - return nil, fmt.Errorf("can't parse %s: %w", rabbitQueueLengthMetricName, err) - } - meta.mode = rabbitModeQueueLength - meta.value = queueLength - - return meta, nil - } - - if !modePresent { - return nil, fmt.Errorf("%s must be specified", rabbitModeTriggerConfigName) - } - if !valuePresent { - return nil, fmt.Errorf("%s must be specified", rabbitValueTriggerConfigName) - } - - // Resolve trigger mode - switch mode { - case rabbitModeQueueLength: - meta.mode = rabbitModeQueueLength - case rabbitModeMessageRate: - meta.mode = rabbitModeMessageRate - default: - return nil, fmt.Errorf("trigger mode %s must be one of %s, %s", mode, rabbitModeQueueLength, rabbitModeMessageRate) - } - triggerValue, err := strconv.ParseFloat(value, 64) - if err != nil { - return nil, fmt.Errorf("can't parse %s: %w", rabbitValueTriggerConfigName, err) - } - meta.value = triggerValue - - if meta.mode == rabbitModeMessageRate && meta.protocol != httpProtocol { - return nil, fmt.Errorf("protocol %s not supported; must be http to use mode %s", meta.protocol, rabbitModeMessageRate) - } - return meta, nil } @@ -457,8 +338,8 @@ func getConnectionAndChannel(host string, meta *rabbitMQMetadata) (*amqp.Connect }, } - if meta.enableTLS { - tlsConfig, err := kedautil.NewTLSConfigWithPassword(meta.cert, meta.key, meta.keyPassword, meta.ca, meta.unsafeSsl) + if meta.EnableTLS == rmqTLSEnable { + tlsConfig, err := kedautil.NewTLSConfigWithPassword(meta.Cert, meta.Key, meta.KeyPassword, meta.Ca, meta.UnsafeSsl) if err != nil { return nil, nil, err } @@ -495,13 +376,13 @@ func (s *rabbitMQScaler) Close(context.Context) error { } func (s *rabbitMQScaler) getQueueStatus(ctx context.Context) (int64, float64, error) { - if s.metadata.protocol == httpProtocol { + if s.metadata.Protocol == httpProtocol { info, err := s.getQueueInfoViaHTTP(ctx) if err != nil { return -1, -1, err } - if s.metadata.excludeUnacknowledged { + if s.metadata.ExcludeUnacknowledged { // messages count includes only ready return int64(info.MessagesReady), info.MessageStat.PublishDetail.Rate, nil } @@ -510,7 +391,7 @@ func (s *rabbitMQScaler) getQueueStatus(ctx context.Context) (int64, float64, er } // QueueDeclarePassive assumes that the queue exists and fails if it doesn't - items, err := s.channel.QueueDeclarePassive(s.metadata.queueName, false, false, false, false, amqp.Table{}) + items, err := s.channel.QueueDeclarePassive(s.metadata.QueueName, false, false, false, false, amqp.Table{}) if err != nil { return -1, -1, err } @@ -526,9 +407,9 @@ func getJSON(ctx context.Context, s *rabbitMQScaler, url string) (queueInfo, err return result, err } - if s.metadata.workloadIdentityResource != "" { + if s.metadata.WorkloadIdentityResource != "" { if s.azureOAuth == nil { - s.azureOAuth = azure.NewAzureADWorkloadIdentityTokenProvider(ctx, s.metadata.workloadIdentityClientID, s.metadata.workloadIdentityTenantID, s.metadata.workloadIdentityAuthorityHost, s.metadata.workloadIdentityResource) + s.azureOAuth = azure.NewAzureADWorkloadIdentityTokenProvider(ctx, s.metadata.workloadIdentityClientID, s.metadata.workloadIdentityTenantID, s.metadata.workloadIdentityAuthorityHost, s.metadata.WorkloadIdentityResource) } err = s.azureOAuth.Refresh() @@ -547,7 +428,7 @@ func getJSON(ctx context.Context, s *rabbitMQScaler, url string) (queueInfo, err defer r.Body.Close() if r.StatusCode == 200 { - if s.metadata.useRegex { + if s.metadata.UseRegex { var queues regexQueueInfo err = json.NewDecoder(r.Body).Decode(&queues) if err != nil { @@ -587,20 +468,24 @@ func getVhostAndPathFromURL(rawPath, vhostName string) (resolvedVhostPath, resol } func (s *rabbitMQScaler) getQueueInfoViaHTTP(ctx context.Context) (*queueInfo, error) { - parsedURL, err := url.Parse(s.metadata.host) + parsedURL, err := url.Parse(s.metadata.Host) if err != nil { return nil, err } - vhost, subpaths := getVhostAndPathFromURL(parsedURL.Path, s.metadata.vhostName) + vhost, subpaths := getVhostAndPathFromURL(parsedURL.Path, s.metadata.VhostName) parsedURL.Path = subpaths + if s.metadata.Username != "" && s.metadata.Password != "" { + parsedURL.User = url.UserPassword(s.metadata.Username, s.metadata.Password) + } + var getQueueInfoManagementURI string - if s.metadata.useRegex { - getQueueInfoManagementURI = fmt.Sprintf("%s/api/queues%s?page=1&use_regex=true&pagination=false&name=%s&page_size=%d", parsedURL.String(), vhost, url.QueryEscape(s.metadata.queueName), s.metadata.pageSize) + if s.metadata.UseRegex { + getQueueInfoManagementURI = fmt.Sprintf("%s/api/queues%s?page=1&use_regex=true&pagination=false&name=%s&page_size=%d", parsedURL.String(), vhost, url.QueryEscape(s.metadata.QueueName), s.metadata.PageSize) } else { - getQueueInfoManagementURI = fmt.Sprintf("%s/api/queues%s/%s", parsedURL.String(), vhost, url.QueryEscape(s.metadata.queueName)) + getQueueInfoManagementURI = fmt.Sprintf("%s/api/queues%s/%s", parsedURL.String(), vhost, url.QueryEscape(s.metadata.QueueName)) } var info queueInfo @@ -617,9 +502,9 @@ func (s *rabbitMQScaler) getQueueInfoViaHTTP(ctx context.Context) (*queueInfo, e func (s *rabbitMQScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("rabbitmq-%s", url.QueryEscape(s.metadata.queueName)))), + Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("rabbitmq-%s", url.QueryEscape(s.metadata.QueueName)))), }, - Target: GetMetricTargetMili(s.metricType, s.metadata.value), + Target: GetMetricTargetMili(s.metricType, s.metadata.Value), } metricSpec := v2.MetricSpec{ External: externalMetric, Type: rabbitMetricType, @@ -637,12 +522,12 @@ func (s *rabbitMQScaler) GetMetricsAndActivity(ctx context.Context, metricName s var metric external_metrics.ExternalMetricValue var isActive bool - if s.metadata.mode == rabbitModeQueueLength { + if s.metadata.Mode == rabbitModeQueueLength { metric = GenerateMetricInMili(metricName, float64(messages)) - isActive = float64(messages) > s.metadata.activationValue + isActive = float64(messages) > s.metadata.ActivationValue } else { metric = GenerateMetricInMili(metricName, publishRate) - isActive = publishRate > s.metadata.activationValue || float64(messages) > s.metadata.activationValue + isActive = publishRate > s.metadata.ActivationValue || float64(messages) > s.metadata.ActivationValue } return []external_metrics.ExternalMetricValue{metric}, isActive, nil @@ -653,7 +538,7 @@ func getComposedQueue(s *rabbitMQScaler, q []queueInfo) (queueInfo, error) { queue.Name = "composed-queue" queue.MessagesUnacknowledged = 0 if len(q) > 0 { - switch s.metadata.operation { + switch s.metadata.Operation { case sumOperation: sumMessages, sumReady, sumRate := getSum(q) queue.Messages = sumMessages @@ -670,7 +555,7 @@ func getComposedQueue(s *rabbitMQScaler, q []queueInfo) (queueInfo, error) { queue.MessagesReady = maxReady queue.MessageStat.PublishDetail.Rate = maxRate default: - return queue, fmt.Errorf("operation mode %s must be one of %s, %s, %s", s.metadata.operation, sumOperation, avgOperation, maxOperation) + return queue, fmt.Errorf("operation mode %s must be one of %s, %s, %s", s.metadata.Operation, sumOperation, avgOperation, maxOperation) } } else { queue.Messages = 0 diff --git a/pkg/scalers/rabbitmq_scaler_test.go b/pkg/scalers/rabbitmq_scaler_test.go index ef705757a3c..ed1785e5be3 100644 --- a/pkg/scalers/rabbitmq_scaler_test.go +++ b/pkg/scalers/rabbitmq_scaler_test.go @@ -18,7 +18,9 @@ import ( ) const ( - host = "myHostSecret" + host = "myHostSecret" + rabbitMQUsername = "myUsernameSecret" + rabbitMQPassword = "myPasswordSecret" ) type parseRabbitMQMetadataTestData struct { @@ -32,7 +34,7 @@ type parseRabbitMQAuthParamTestData struct { podIdentity v1alpha1.AuthPodIdentity authParams map[string]string isError bool - enableTLS bool + enableTLS string workloadIdentity bool } @@ -43,7 +45,9 @@ type rabbitMQMetricIdentifier struct { } var sampleRabbitMqResolvedEnv = map[string]string{ - host: "amqp://user:sercet@somehost.com:5236/vhost", + host: "amqp://user:sercet@somehost.com:5236/vhost", + rabbitMQUsername: "user", + rabbitMQPassword: "Password", } var testRabbitMQMetadata = []parseRabbitMQMetadataTestData{ @@ -138,23 +142,35 @@ var testRabbitMQMetadata = []parseRabbitMQMetadataTestData{ } var testRabbitMQAuthParamData = []parseRabbitMQAuthParamTestData{ - {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "ca": "caaa", "cert": "ceert", "key": "keey"}, false, true, false}, + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "ca": "caaa", "cert": "ceert", "key": "keey"}, false, rmqTLSEnable, false}, // success, TLS cert/key and assumed public CA - {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "cert": "ceert", "key": "keey"}, false, true, false}, + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "cert": "ceert", "key": "keey"}, false, rmqTLSEnable, false}, // success, TLS cert/key + key password and assumed public CA - {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "cert": "ceert", "key": "keey", "keyPassword": "keeyPassword"}, false, true, false}, + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "cert": "ceert", "key": "keey", "keyPassword": "keeyPassword"}, false, rmqTLSEnable, false}, // success, TLS CA only - {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "ca": "caaa"}, false, true, false}, + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "ca": "caaa"}, false, rmqTLSEnable, false}, // failure, TLS missing cert - {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "ca": "caaa", "key": "kee"}, true, true, false}, + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "ca": "caaa", "key": "kee"}, true, rmqTLSEnable, false}, // failure, TLS missing key - {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "ca": "caaa", "cert": "ceert"}, true, true, false}, + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "enable", "ca": "caaa", "cert": "ceert"}, true, rmqTLSEnable, false}, // 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}, + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"tls": "yes", "ca": "caaa", "cert": "ceert", "key": "kee"}, true, rmqTLSEnable, false}, + // success, username and password + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"username": "user", "password": "PASSWORD"}, false, rmqTLSDisable, false}, + // failure, username but no password + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"username": "user"}, true, rmqTLSDisable, false}, + // failure, password but no username + {map[string]string{"queueName": "sample", "hostFromEnv": host}, v1alpha1.AuthPodIdentity{}, map[string]string{"password": "PASSWORD"}, true, rmqTLSDisable, false}, + // success, username and password from env + {map[string]string{"queueName": "sample", "hostFromEnv": host, "usernameFromEnv": rabbitMQUsername, "passwordFromEnv": rabbitMQPassword}, v1alpha1.AuthPodIdentity{}, map[string]string{}, false, rmqTLSDisable, false}, + // failure, username from env but not password + {map[string]string{"queueName": "sample", "hostFromEnv": host, "usernameFromEnv": rabbitMQUsername}, v1alpha1.AuthPodIdentity{}, map[string]string{}, true, rmqTLSDisable, false}, + // failure, password from env but not username + {map[string]string{"queueName": "sample", "hostFromEnv": host, "passwordFromEnv": rabbitMQPassword}, v1alpha1.AuthPodIdentity{}, map[string]string{}, true, rmqTLSDisable, false}, // success, WorkloadIdentity - {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}, + {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, rmqTLSDisable, true}, // failure, WoekloadIdentity not supported for amqp - {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}, + {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, rmqTLSDisable, false}, } var rabbitMQMetricIdentifiers = []rabbitMQMetricIdentifier{ {&testRabbitMQMetadata[1], 0, "s0-rabbitmq-sample"}, @@ -175,8 +191,8 @@ func TestRabbitMQParseMetadata(t *testing.T) { if err != nil && !testData.isError { t.Errorf("Expect error but got success in test case %d", idx) } - if boolVal != meta.unsafeSsl { - t.Errorf("Expect %t but got %t in test case %d", boolVal, meta.unsafeSsl, idx) + if boolVal != meta.UnsafeSsl { + t.Errorf("Expect %t but got %t in test case %d", boolVal, meta.UnsafeSsl, idx) } } } @@ -191,25 +207,25 @@ func TestRabbitMQParseAuthParamData(t *testing.T) { if testData.isError && err == nil { t.Error("Expected error but got success") } - if metadata != nil && metadata.enableTLS != testData.enableTLS { - t.Errorf("Expected enableTLS to be set to %v but got %v\n", testData.enableTLS, metadata.enableTLS) + if metadata != nil && metadata.EnableTLS != testData.enableTLS { + t.Errorf("Expected enableTLS to be set to %v but got %v\n", testData.enableTLS, metadata.EnableTLS) } - if metadata != nil && metadata.enableTLS { - if metadata.ca != testData.authParams["ca"] { - t.Errorf("Expected ca to be set to %v but got %v\n", testData.authParams["ca"], metadata.enableTLS) + if metadata != nil && metadata.EnableTLS == rmqTLSEnable { + if metadata.Ca != testData.authParams["ca"] { + t.Errorf("Expected ca to be set to %v but got %v\n", testData.authParams["ca"], metadata.EnableTLS) } - if metadata.cert != testData.authParams["cert"] { - t.Errorf("Expected cert to be set to %v but got %v\n", testData.authParams["cert"], metadata.cert) + if metadata.Cert != testData.authParams["cert"] { + t.Errorf("Expected cert to be set to %v but got %v\n", testData.authParams["cert"], metadata.Cert) } - if metadata.key != testData.authParams["key"] { - t.Errorf("Expected key to be set to %v but got %v\n", testData.authParams["key"], metadata.key) + if metadata.Key != testData.authParams["key"] { + t.Errorf("Expected key to be set to %v but got %v\n", testData.authParams["key"], metadata.Key) } - if metadata.keyPassword != testData.authParams["keyPassword"] { - t.Errorf("Expected key to be set to %v but got %v\n", testData.authParams["keyPassword"], metadata.key) + if metadata.KeyPassword != testData.authParams["keyPassword"] { + t.Errorf("Expected key to be set to %v but got %v\n", testData.authParams["keyPassword"], metadata.Key) } } if metadata != nil && metadata.workloadIdentityClientID != "" && !testData.workloadIdentity { - t.Errorf("Expected workloadIdentity to be disabled but got %v as client ID and %v as resource\n", metadata.workloadIdentityClientID, metadata.workloadIdentityResource) + t.Errorf("Expected workloadIdentity to be disabled but got %v as client ID and %v as resource\n", metadata.workloadIdentityClientID, metadata.WorkloadIdentityResource) } if metadata != nil && metadata.workloadIdentityClientID == "" && testData.workloadIdentity { t.Error("Expected workloadIdentity to be enabled but was not\n") @@ -232,8 +248,8 @@ func TestParseDefaultQueueLength(t *testing.T) { t.Error("Expected success but got error", err) case testData.isError && err == nil: t.Error("Expected error but got success") - case metadata.value != defaultRabbitMQQueueLength: - t.Error("Expected default queueLength =", defaultRabbitMQQueueLength, "but got", metadata.value) + case metadata.Value != defaultRabbitMQQueueLength: + t.Error("Expected default queueLength =", defaultRabbitMQQueueLength, "but got", metadata.Value) } } } diff --git a/pkg/scalers/selenium_grid_scaler.go b/pkg/scalers/selenium_grid_scaler.go index 09f0adfd319..3cba72fbc9e 100644 --- a/pkg/scalers/selenium_grid_scaler.go +++ b/pkg/scalers/selenium_grid_scaler.go @@ -29,13 +29,16 @@ type seleniumGridScaler struct { type seleniumGridScalerMetadata struct { triggerIndex int - URL string `keda:"name=url, order=triggerMetadata;authParams"` - BrowserName string `keda:"name=browserName, order=triggerMetadata"` - SessionBrowserName string `keda:"name=sessionBrowserName, order=triggerMetadata, optional"` - ActivationThreshold int64 `keda:"name=activationThreshold, order=triggerMetadata, optional"` - BrowserVersion string `keda:"name=browserVersion, order=triggerMetadata, optional, default=latest"` - UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, optional, default=false"` - PlatformName string `keda:"name=platformName, order=triggerMetadata, optional, default=linux"` + URL string `keda:"name=url, order=triggerMetadata;authParams"` + BrowserName string `keda:"name=browserName, order=triggerMetadata"` + SessionBrowserName string `keda:"name=sessionBrowserName, order=triggerMetadata, optional"` + ActivationThreshold int64 `keda:"name=activationThreshold, order=triggerMetadata, optional"` + BrowserVersion string `keda:"name=browserVersion, order=triggerMetadata, optional, default=latest"` + UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, optional, default=false"` + PlatformName string `keda:"name=platformName, order=triggerMetadata, optional, default=linux"` + SessionsPerNode int64 `keda:"name=sessionsPerNode, order=triggerMetadata, optional, default=1"` + SetSessionsFromHub bool `keda:"name=setSessionsFromHub, order=triggerMetadata, optional, default=false"` + SessionBrowserVersion string `keda:"name=sessionBrowserVersion, order=triggerMetadata, optional"` // auth Username string `keda:"name=username, order=authParams;resolvedEnv;triggerMetadata, optional"` @@ -50,6 +53,7 @@ type seleniumResponse struct { type data struct { Grid grid `json:"grid"` + NodesInfo nodesInfo `json:"nodesInfo"` SessionsInfo sessionsInfo `json:"sessionsInfo"` } @@ -75,6 +79,19 @@ type capability struct { PlatformName string `json:"platformName"` } +type nodesInfo struct { + Nodes []nodes `json:"nodes"` +} + +type nodes struct { + Stereotypes string `json:"stereotypes"` +} + +type stereotype struct { + Slots int64 `json:"slots"` + Stereotype capability `json:"stereotype"` +} + const ( DefaultBrowserVersion string = "latest" DefaultPlatformName string = "linux" @@ -118,6 +135,9 @@ func parseSeleniumGridScalerMetadata(config *scalersconfig.ScalerConfig) (*selen if meta.SessionBrowserName == "" { meta.SessionBrowserName = meta.BrowserName } + if meta.SessionBrowserVersion == "" { + meta.SessionBrowserVersion = meta.BrowserVersion + } return meta, nil } @@ -156,7 +176,7 @@ func (s *seleniumGridScaler) GetMetricSpecForScaling(context.Context) []v2.Metri func (s *seleniumGridScaler) getSessionsCount(ctx context.Context, logger logr.Logger) (int64, error) { body, err := json.Marshal(map[string]string{ - "query": "{ grid { maxSession, nodeCount }, sessionsInfo { sessionQueueRequests, sessions { id, capabilities, nodeId } } }", + "query": "{ grid { maxSession, nodeCount }, nodesInfo { nodes { stereotypes } }, sessionsInfo { sessionQueueRequests, sessions { id, capabilities, nodeId } } }", }) if err != nil { @@ -186,21 +206,26 @@ func (s *seleniumGridScaler) getSessionsCount(ctx context.Context, logger logr.L if err != nil { return -1, err } - v, err := getCountFromSeleniumResponse(b, s.metadata.BrowserName, s.metadata.BrowserVersion, s.metadata.SessionBrowserName, s.metadata.PlatformName, logger) + v, err := getCountFromSeleniumResponse(b, s.metadata.BrowserName, s.metadata.BrowserVersion, s.metadata.SessionBrowserName, s.metadata.PlatformName, s.metadata.SessionsPerNode, s.metadata.SetSessionsFromHub, s.metadata.SessionBrowserVersion, logger) if err != nil { return -1, err } return v, nil } -func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion string, sessionBrowserName string, platformName string, logger logr.Logger) (int64, error) { +func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion string, sessionBrowserName string, platformName string, sessionsPerNode int64, setSessionsFromHub bool, sessionBrowserVersion string, logger logr.Logger) (int64, error) { var count int64 + var slots int64 var seleniumResponse = seleniumResponse{} if err := json.Unmarshal(b, &seleniumResponse); err != nil { return 0, err } + if setSessionsFromHub { + slots = getSlotsFromSeleniumResponse(seleniumResponse, browserName, browserVersion, platformName, logger) + } + var sessionQueueRequests = seleniumResponse.Data.SessionsInfo.SessionQueueRequests for _, sessionQueueRequest := range sessionQueueRequests { var capability = capability{} @@ -224,7 +249,7 @@ func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion s if err := json.Unmarshal([]byte(session.Capabilities), &capability); err == nil { var platformNameMatches = capability.PlatformName == "" || strings.EqualFold(capability.PlatformName, platformName) if capability.BrowserName == sessionBrowserName { - if strings.HasPrefix(capability.BrowserVersion, browserVersion) && platformNameMatches { + if strings.HasPrefix(capability.BrowserVersion, sessionBrowserVersion) && platformNameMatches { count++ } else if browserVersion == DefaultBrowserVersion && platformNameMatches { count++ @@ -238,10 +263,44 @@ func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion s var gridMaxSession = int64(seleniumResponse.Data.Grid.MaxSession) var gridNodeCount = int64(seleniumResponse.Data.Grid.NodeCount) - if gridMaxSession > 0 && gridNodeCount > 0 { + if setSessionsFromHub { + if slots == 0 { + slots = sessionsPerNode + } + var floatCount = float64(count) / float64(slots) + count = int64(math.Ceil(floatCount)) + } else if gridMaxSession > 0 && gridNodeCount > 0 { // Get count, convert count to next highest int64 var floatCount = float64(count) / (float64(gridMaxSession) / float64(gridNodeCount)) count = int64(math.Ceil(floatCount)) } + return count, nil } + +func getSlotsFromSeleniumResponse(seleniumResponse seleniumResponse, browserName string, browserVersion string, platformName string, logger logr.Logger) int64 { + var slots int64 + + var nodes = seleniumResponse.Data.NodesInfo.Nodes +slots: + for _, node := range nodes { + var stereotypes = []stereotype{} + if err := json.Unmarshal([]byte(node.Stereotypes), &stereotypes); err == nil { + for _, stereotype := range stereotypes { + if stereotype.Stereotype.BrowserName == browserName { + var platformNameMatches = stereotype.Stereotype.PlatformName == "" || strings.EqualFold(stereotype.Stereotype.PlatformName, platformName) + if strings.HasPrefix(stereotype.Stereotype.BrowserVersion, browserVersion) && platformNameMatches { + slots = stereotype.Slots + break slots + } else if len(strings.TrimSpace(stereotype.Stereotype.BrowserVersion)) == 0 && browserVersion == DefaultBrowserVersion && platformNameMatches { + slots = stereotype.Slots + break slots + } + } + } + } else { + logger.Error(err, fmt.Sprintf("Error when unmarshalling stereotypes: %s", err)) + } + } + return slots +} diff --git a/pkg/scalers/selenium_grid_scaler_test.go b/pkg/scalers/selenium_grid_scaler_test.go index 557861b357e..56de3b7f024 100644 --- a/pkg/scalers/selenium_grid_scaler_test.go +++ b/pkg/scalers/selenium_grid_scaler_test.go @@ -11,11 +11,14 @@ import ( func Test_getCountFromSeleniumResponse(t *testing.T) { type args struct { - b []byte - browserName string - sessionBrowserName string - browserVersion string - platformName string + b []byte + browserName string + sessionBrowserName string + browserVersion string + platformName string + sessionsPerNode int64 + setSessionsFromHub bool + sessionBrowserVersion string } tests := []struct { name string @@ -50,6 +53,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 0, "nodeCount": 0 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": [], "sessions": [] @@ -70,6 +76,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 1, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], "sessions": [ @@ -99,6 +108,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 1, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], "sessions": [] @@ -122,6 +134,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 4, "nodeCount": 2 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"] } @@ -144,6 +159,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 4, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], "sessions": [ @@ -178,6 +196,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 3, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\"\n}"], "sessions": [ @@ -212,6 +233,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 2, "nodeCount": 2 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}"], "sessions": [ @@ -246,6 +270,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 2, "nodeCount": 2 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}"], "sessions": [ @@ -280,6 +307,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 2, "nodeCount": 2 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"}","{\n \"browserName\": \"chrome\"}","{\n \"browserName\": \"chrome\"}"], "sessions": [ @@ -314,6 +344,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 1, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\"\n}"], "sessions": [ @@ -343,6 +376,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 1, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"MicrosoftEdge\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"MicrosoftEdge\",\n \"browserVersion\": \"91.0\"\n}"], "sessions": [ @@ -372,6 +408,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 1, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], "sessions": [ @@ -401,6 +440,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 3, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], "sessions": [ @@ -430,6 +472,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 1, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"Windows 11\"\n}"], "sessions": [] @@ -453,6 +498,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 1, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"Windows 11\"\n}"], "sessions": [] @@ -467,6 +515,96 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { want: 1, wantErr: false, }, + { + name: "sessions requests with matching browsername and platformName when setSessionsFromHub turned on and node with 2 slots matches should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid":{ + "maxSession": 1, + "nodeCount": 1 + }, + "nodesInfo": { + "nodes": [ + { + "stereotypes":"[{\"slots\":1,\"stereotype\":{\"browserName\":\"chrome\",\"platformName\":\"linux\"}}]" + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"Windows 11\"\n}"], + "sessions": [] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + setSessionsFromHub: true, + }, + want: 1, + wantErr: false, + }, + { + name: "4 sessions requests with matching browsername and platformName when setSessionsFromHub turned on and node with 2 slots matches should return count as 2", + args: args{ + b: []byte(`{ + "data": { + "grid":{ + "maxSession": 1, + "nodeCount": 1 + }, + "nodesInfo": { + "nodes": [ + { + "stereotypes":"[{\"slots\":2,\"stereotype\":{\"browserName\":\"chrome\",\"platformName\":\"linux\"}}]" + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"Windows 11\"\n}"], + "sessions": [] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + setSessionsFromHub: true, + }, + want: 2, + wantErr: false, + }, + { + name: "4 sessions requests with matching browsername and platformName when setSessionsFromHub turned on, no nodes and sessionsPerNode=2 matches should return count as 2", + args: args{ + b: []byte(`{ + "data": { + "grid":{ + "maxSession": 1, + "nodeCount": 1 + }, + "nodesInfo": { + "nodes": [] + }, + "sessionsInfo": { + "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"Windows 11\"\n}"], + "sessions": [] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + setSessionsFromHub: true, + sessionsPerNode: 2, + }, + want: 2, + wantErr: false, + }, { name: "sessions requests and active sessions with matching browsername and platformName should return count as 2", args: args{ @@ -476,6 +614,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { "maxSession": 1, "nodeCount": 1 }, + "nodesInfo": { + "nodes": [] + }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"Windows 11\",\n \"browserVersion\": \"91.0\"\n}"], "sessions": [ @@ -501,10 +642,47 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { want: 2, wantErr: false, }, + { + name: "sessions requests and active sessions with matching browsername, platformName and sessionBrowserVersion should return count as 3", + args: args{ + b: []byte(`{ + "data": { + "grid":{ + "maxSession": 1, + "nodeCount": 1 + }, + "nodesInfo": { + "nodes": [] + }, + "sessionsInfo": { + "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\"\n}","{\n \"browserName\": \"chrome\",\n \"platformName\": \"Windows 11\",\n \"browserVersion\": \"91.0\"\n}"], + "sessions": [ + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0.4472.114\",\n \"chrome\": {\n \"chromedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"goog:chromeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983c" + }, + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0.4472.114\",\n \"chrome\": {\n \"chromedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"goog:chromeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983c" + } + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + sessionBrowserVersion: "91.0.4472.114", + platformName: "linux", + }, + want: 3, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getCountFromSeleniumResponse(tt.args.b, tt.args.browserName, tt.args.browserVersion, tt.args.sessionBrowserName, tt.args.platformName, logr.Discard()) + got, err := getCountFromSeleniumResponse(tt.args.b, tt.args.browserName, tt.args.browserVersion, tt.args.sessionBrowserName, tt.args.platformName, tt.args.sessionsPerNode, tt.args.setSessionsFromHub, tt.args.sessionBrowserVersion, logr.Discard()) if (err != nil) != tt.wantErr { t.Errorf("getCountFromSeleniumResponse() error = %v, wantErr %v", err, tt.wantErr) return @@ -558,12 +736,14 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - URL: "http://selenium-hub:4444/graphql", - BrowserName: "chrome", - SessionBrowserName: "chrome", - TargetValue: 1, - BrowserVersion: "latest", - PlatformName: "linux", + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + BrowserVersion: "latest", + PlatformName: "linux", + SessionsPerNode: 1, + SessionBrowserVersion: "latest", }, }, { @@ -579,12 +759,14 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - URL: "http://selenium-hub:4444/graphql", - BrowserName: "MicrosoftEdge", - SessionBrowserName: "msedge", - TargetValue: 1, - BrowserVersion: "latest", - PlatformName: "linux", + URL: "http://selenium-hub:4444/graphql", + BrowserName: "MicrosoftEdge", + SessionBrowserName: "msedge", + TargetValue: 1, + BrowserVersion: "latest", + PlatformName: "linux", + SessionsPerNode: 1, + SessionBrowserVersion: "latest", }, }, { @@ -602,12 +784,14 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - URL: "http://user:password@selenium-hub:4444/graphql", - BrowserName: "MicrosoftEdge", - SessionBrowserName: "msedge", - TargetValue: 1, - BrowserVersion: "latest", - PlatformName: "linux", + URL: "http://user:password@selenium-hub:4444/graphql", + BrowserName: "MicrosoftEdge", + SessionBrowserName: "msedge", + TargetValue: 1, + BrowserVersion: "latest", + PlatformName: "linux", + SessionsPerNode: 1, + SessionBrowserVersion: "latest", }, }, { @@ -627,14 +811,16 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - URL: "http://selenium-hub:4444/graphql", - BrowserName: "MicrosoftEdge", - SessionBrowserName: "msedge", - TargetValue: 1, - BrowserVersion: "latest", - PlatformName: "linux", - Username: "username", - Password: "password", + URL: "http://selenium-hub:4444/graphql", + BrowserName: "MicrosoftEdge", + SessionBrowserName: "msedge", + TargetValue: 1, + BrowserVersion: "latest", + PlatformName: "linux", + Username: "username", + Password: "password", + SessionsPerNode: 1, + SessionBrowserVersion: "latest", }, }, { @@ -651,13 +837,15 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - URL: "http://selenium-hub:4444/graphql", - BrowserName: "chrome", - SessionBrowserName: "chrome", - TargetValue: 1, - BrowserVersion: "91.0", - UnsafeSsl: false, - PlatformName: "linux", + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + BrowserVersion: "91.0", + UnsafeSsl: false, + PlatformName: "linux", + SessionsPerNode: 1, + SessionBrowserVersion: "91.0", }, }, { @@ -675,14 +863,16 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - URL: "http://selenium-hub:4444/graphql", - BrowserName: "chrome", - SessionBrowserName: "chrome", - TargetValue: 1, - ActivationThreshold: 10, - BrowserVersion: "91.0", - UnsafeSsl: true, - PlatformName: "linux", + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + ActivationThreshold: 10, + BrowserVersion: "91.0", + UnsafeSsl: true, + PlatformName: "linux", + SessionsPerNode: 1, + SessionBrowserVersion: "91.0", }, }, { @@ -715,14 +905,16 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - URL: "http://selenium-hub:4444/graphql", - BrowserName: "chrome", - SessionBrowserName: "chrome", - TargetValue: 1, - ActivationThreshold: 10, - BrowserVersion: "91.0", - UnsafeSsl: true, - PlatformName: "linux", + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + ActivationThreshold: 10, + BrowserVersion: "91.0", + UnsafeSsl: true, + PlatformName: "linux", + SessionsPerNode: 1, + SessionBrowserVersion: "91.0", }, }, { @@ -741,14 +933,16 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - URL: "http://selenium-hub:4444/graphql", - BrowserName: "chrome", - SessionBrowserName: "chrome", - TargetValue: 1, - ActivationThreshold: 10, - BrowserVersion: "91.0", - UnsafeSsl: true, - PlatformName: "Windows 11", + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + ActivationThreshold: 10, + BrowserVersion: "91.0", + UnsafeSsl: true, + PlatformName: "Windows 11", + SessionsPerNode: 1, + SessionBrowserVersion: "91.0", }, }, } diff --git a/tests/helper/helper.go b/tests/helper/helper.go index a21adb0c48f..c0262fa3674 100644 --- a/tests/helper/helper.go +++ b/tests/helper/helper.go @@ -253,6 +253,7 @@ func DeleteNamespace(t *testing.T, nsName string) { err = nil } assert.NoErrorf(t, err, "cannot delete kubernetes namespace - %s", err) + DeletePodsInNamespace(t, nsName) } func WaitForJobSuccess(t *testing.T, kc *kubernetes.Clientset, jobName, namespace string, iterations, interval int) bool { @@ -744,6 +745,17 @@ func DeletePodsInNamespaceBySelector(t *testing.T, kc *kubernetes.Clientset, sel assert.NoErrorf(t, err, "cannot delete pods - %s", err) } +// Delete all pods in namespace +func DeletePodsInNamespace(t *testing.T, namespace string) { + err := GetKubernetesClient(t).CoreV1().Pods(namespace).DeleteCollection(context.Background(), metav1.DeleteOptions{ + GracePeriodSeconds: ptr.To(int64(0)), + }, metav1.ListOptions{}) + if errors.IsNotFound(err) { + err = nil + } + assert.NoErrorf(t, err, "cannot delete pods - %s", err) +} + // Wait for Pods identified by selector to complete termination func WaitForPodsTerminated(t *testing.T, kc *kubernetes.Clientset, selector, namespace string, iterations, intervalSeconds int) bool { for i := 0; i < iterations; i++ { diff --git a/tests/internals/cache_metrics/cache_metrics_test.go b/tests/internals/cache_metrics/cache_metrics_test.go index 3f5525c1833..10008711668 100644 --- a/tests/internals/cache_metrics/cache_metrics_test.go +++ b/tests/internals/cache_metrics/cache_metrics_test.go @@ -160,8 +160,8 @@ func testCacheMetricsOnPollingInterval(t *testing.T, kc *kubernetes.Clientset, d // Metric Value = 8, DesiredAverageMetricValue = 2 // should scale in to 8/2 = 4 replicas, irrespective of current replicas - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") // Changing Metric Value to 4, but because we have a long polling interval, the replicas number should remain the same data.MonitoredDeploymentReplicas = 4 @@ -196,8 +196,8 @@ func testDirectQuery(t *testing.T, kc *kubernetes.Clientset, data templateData) // Metric Value = 8, DesiredAverageMetricValue = 2 // should scale in to 8/2 = 4 replicas, irrespective of current replicas - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") // Changing Metric Value to 4, deployment should scale to 2 data.MonitoredDeploymentReplicas = 4 diff --git a/tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go b/tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go index ae00ea8173d..a65b40c5af1 100644 --- a/tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go +++ b/tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go @@ -265,21 +265,25 @@ func checkMessage(t *testing.T, count int, client *azservicebus.Client) { if err != nil { assert.NoErrorf(t, err, "cannot create receiver - %s", err) } - defer receiver.Close(context.TODO()) - - messages, err := receiver.ReceiveMessages(context.TODO(), count, nil) - assert.NoErrorf(t, err, "cannot receive messages - %s", err) - assert.NotEmpty(t, messages) + defer receiver.Close(context.Background()) + // We try to read the messages 3 times with a second of delay + tries := 3 found := false - for _, message := range messages { - event := messaging.CloudEvent{} - err = json.Unmarshal(message.Body, &event) - assert.NoErrorf(t, err, "cannot retrieve message - %s", err) - if expectedSubject == *event.Subject && - expectedSource == event.Source && - expectedType == event.Type { - found = true + for i := 0; i < tries && !found; i++ { + messages, err := receiver.ReceiveMessages(context.Background(), count, nil) + assert.NoErrorf(t, err, "cannot receive messages - %s", err) + assert.NotEmpty(t, messages) + + for _, message := range messages { + event := messaging.CloudEvent{} + err = json.Unmarshal(message.Body, &event) + assert.NoErrorf(t, err, "cannot retrieve message - %s", err) + if expectedSubject == *event.Subject && + expectedSource == event.Source && + expectedType == event.Type { + found = true + } } } diff --git a/tests/internals/idle_replicas/idle_replicas_test.go b/tests/internals/idle_replicas/idle_replicas_test.go index 46a682f9180..db5f25e0dac 100644 --- a/tests/internals/idle_replicas/idle_replicas_test.go +++ b/tests/internals/idle_replicas/idle_replicas_test.go @@ -147,8 +147,8 @@ func testScaleOut(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- scale to max replicas ---") KubernetesScaleDeployment(t, kc, monitoredDeploymentName, 4, testNamespace) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") } func testScaleIn(t *testing.T, kc *kubernetes.Clientset) { diff --git a/tests/internals/restore_original/restore_original_test.go b/tests/internals/restore_original/restore_original_test.go index 83de6a3b3a1..02a8f711210 100644 --- a/tests/internals/restore_original/restore_original_test.go +++ b/tests/internals/restore_original/restore_original_test.go @@ -138,8 +138,8 @@ func testScale(t *testing.T, kc *kubernetes.Clientset, data templateData) { t.Log("--- testing scaling ---") KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") } func testRestore(t *testing.T, kc *kubernetes.Clientset, data templateData) { diff --git a/tests/internals/scaled_job_validation/scaled_job_validation_test.go b/tests/internals/scaled_job_validation/scaled_job_validation_test.go index 217c3cc8c85..6074a0949f7 100644 --- a/tests/internals/scaled_job_validation/scaled_job_validation_test.go +++ b/tests/internals/scaled_job_validation/scaled_job_validation_test.go @@ -13,7 +13,7 @@ import ( ) const ( - testName = "scaled-object-validation-test" + testName = "scaled-job-validation-test" ) var ( diff --git a/tests/internals/scaled_object_validation/scaled_object_validation_test.go b/tests/internals/scaled_object_validation/scaled_object_validation_test.go index 9cdaff34515..2af7f6b81d8 100644 --- a/tests/internals/scaled_object_validation/scaled_object_validation_test.go +++ b/tests/internals/scaled_object_validation/scaled_object_validation_test.go @@ -131,6 +131,27 @@ spec: desiredReplicas: '1' ` + customHpaScaledObjectTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + advanced: + horizontalPodAutoscalerConfig: + name: {{.HpaName}} + triggers: + - type: cron + metadata: + timezone: Etc/UTC + start: 0 * * * * + end: 1 * * * * + desiredReplicas: '1' + ` + hpaTemplate = ` apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler @@ -179,6 +200,8 @@ func TestScaledObjectValidations(t *testing.T) { testScaledWorkloadByOtherScaledObject(t, data) + testManagedHpaByOtherScaledObject(t, data) + testScaledWorkloadByOtherHpa(t, data) testScaledWorkloadByOtherHpaWithOwnershipTransfer(t, data) @@ -220,6 +243,25 @@ func testScaledWorkloadByOtherScaledObject(t *testing.T, data templateData) { KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) } +func testManagedHpaByOtherScaledObject(t *testing.T, data templateData) { + t.Log("--- already managed hpa by other scaledobject---") + + data.HpaName = hpaName + + data.ScaledObjectName = scaledObject1Name + err := KubectlApplyWithErrors(t, data, "scaledObjectTemplate", customHpaScaledObjectTemplate) + assert.NoErrorf(t, err, "cannot deploy the scaledObject - %s", err) + + data.ScaledObjectName = scaledObject2Name + data.DeploymentName = fmt.Sprintf("%s-other-deployment", testName) + err = KubectlApplyWithErrors(t, data, "scaledObjectTemplate", customHpaScaledObjectTemplate) + assert.Errorf(t, err, "can deploy the scaledObject - %s", err) + assert.Contains(t, err.Error(), fmt.Sprintf("the HPA '%s' is already managed by the ScaledObject '%s", hpaName, scaledObject1Name)) + + data.ScaledObjectName = scaledObject1Name + KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) +} + func testScaledWorkloadByOtherHpa(t *testing.T, data templateData) { t.Log("--- already scaled workload by other hpa---") diff --git a/tests/internals/scaling_strategies/eager_scaling_strategy_test.go b/tests/internals/scaling_strategies/eager_scaling_strategy_test.go index e05c84c30a0..222f960ef16 100644 --- a/tests/internals/scaling_strategies/eager_scaling_strategy_test.go +++ b/tests/internals/scaling_strategies/eager_scaling_strategy_test.go @@ -100,8 +100,11 @@ func TestScalingStrategy(t *testing.T) { }) RMQInstall(t, kc, rmqNamespace, user, password, vhost, WithoutOAuth()) - CreateKubernetesResources(t, kc, testNamespace, data, templates) + // Publish 0 messges but create the queue + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, 0) + WaitForAllJobsSuccess(t, kc, rmqNamespace, 60, 1) + CreateKubernetesResources(t, kc, testNamespace, data, templates) testEagerScaling(t, kc) } @@ -121,14 +124,17 @@ func getTemplateData() (templateData, []Template) { func testEagerScaling(t *testing.T, kc *kubernetes.Clientset) { iterationCount := 20 RMQPublishMessages(t, rmqNamespace, connectionString, queueName, 4) + WaitForAllJobsSuccess(t, kc, rmqNamespace, 60, 1) assert.True(t, WaitForScaledJobCount(t, kc, scaledJobName, testNamespace, 4, iterationCount, 1), "job count should be %d after %d iterations", 4, iterationCount) RMQPublishMessages(t, rmqNamespace, connectionString, queueName, 4) + WaitForAllJobsSuccess(t, kc, rmqNamespace, 60, 1) assert.True(t, WaitForScaledJobCount(t, kc, scaledJobName, testNamespace, 8, iterationCount, 1), "job count should be %d after %d iterations", 8, iterationCount) - RMQPublishMessages(t, rmqNamespace, connectionString, queueName, 4) + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, 8) + WaitForAllJobsSuccess(t, kc, rmqNamespace, 60, 1) assert.True(t, WaitForScaledJobCount(t, kc, scaledJobName, testNamespace, 10, iterationCount, 1), "job count should be %d after %d iterations", 10, iterationCount) } diff --git a/tests/internals/value_metric_type/value_metric_type_test.go b/tests/internals/value_metric_type/value_metric_type_test.go index 06a175f9012..40dd1646dc8 100644 --- a/tests/internals/value_metric_type/value_metric_type_test.go +++ b/tests/internals/value_metric_type/value_metric_type_test.go @@ -149,8 +149,8 @@ func testScaleByAverageValue(t *testing.T, kc *kubernetes.Clientset, data templa // Metric Value = 8, DesiredAverageMetricValue = 2 // should scale in to 8/2 = 4 replicas, irrespective of current replicas - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) } diff --git a/tests/run-all.go b/tests/run-all.go index 76134d55e32..c30cbe27f48 100644 --- a/tests/run-all.go +++ b/tests/run-all.go @@ -25,11 +25,11 @@ import ( ) var ( - concurrentTests = 15 + concurrentTests = 25 regularTestsTimeout = "20m" regularTestsRetries = 3 sequentialTestsTimeout = "20m" - sequentialTestsRetries = 1 + sequentialTestsRetries = 2 ) type TestResult struct { diff --git a/tests/scalers/artemis/artemis_test.go b/tests/scalers/artemis/artemis_test.go index 832c978852f..8bfcd61d64a 100644 --- a/tests/scalers/artemis/artemis_test.go +++ b/tests/scalers/artemis/artemis_test.go @@ -40,6 +40,7 @@ type templateData struct { SecretName string ArtemisPasswordBase64 string ArtemisUserBase64 string + MessageCount int } const ( @@ -87,8 +88,8 @@ spec: spec: containers: - name: kedartemis-consumer - image: balchu/kedartemis-consumer - imagePullPolicy: Always + image: ghcr.io/kedacore/tests-artemis + args: ["consumer"] env: - name: ARTEMIS_PASSWORD valueFrom: @@ -100,10 +101,12 @@ spec: secretKeyRef: name: {{.SecretName}} key: artemis-username - - name: ARTEMIS_HOST + - name: ARTEMIS_SERVER_HOST value: "artemis-activemq.{{.TestNamespace}}" - - name: ARTEMIS_PORT + - name: ARTEMIS_SERVER_PORT value: "61616" + - name: ARTEMIS_MESSAGE_SLEEP_MS + value: "70" ` artemisDeploymentTemplate = `apiVersion: apps/v1 @@ -260,7 +263,7 @@ spec: managementEndpoint: "artemis-activemq.{{.TestNamespace}}:8161" queueName: "test" queueLength: "50" - activationQueueLength: "1500" + activationQueueLength: "5" brokerName: "artemis-activemq" brokerAddress: "test" authenticationRef: @@ -279,7 +282,8 @@ spec: spec: containers: - name: artemis-producer - image: balchu/artemis-producer:0.0.1 + image: ghcr.io/kedacore/tests-artemis + args: ["producer"] env: - name: ARTEMIS_PASSWORD valueFrom: @@ -295,6 +299,8 @@ spec: value: "artemis-activemq.{{.TestNamespace}}" - name: ARTEMIS_SERVER_PORT value: "61616" + - name: ARTEMIS_MESSAGE_COUNT + value: "{{.MessageCount}}" restartPolicy: Never backoffLimit: 4 ` @@ -321,6 +327,7 @@ func TestArtemisScaler(t *testing.T) { func testActivation(t *testing.T, kc *kubernetes.Clientset, data templateData) { t.Log("--- testing activation ---") + data.MessageCount = 1 KubectlReplaceWithTemplate(t, data, "triggerJobTemplate", producerJob) AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, minReplicaCount, 60) @@ -328,6 +335,7 @@ func testActivation(t *testing.T, kc *kubernetes.Clientset, data templateData) { func testScaleOut(t *testing.T, kc *kubernetes.Clientset, data templateData) { t.Log("--- testing scale out ---") + data.MessageCount = 1000 KubectlReplaceWithTemplate(t, data, "triggerJobTemplate", producerJob) assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, maxReplicaCount, 60, 3), @@ -349,6 +357,7 @@ func getTemplateData() (templateData, []Template) { SecretName: secretName, ArtemisPasswordBase64: base64.StdEncoding.EncodeToString([]byte(artemisPassword)), ArtemisUserBase64: base64.StdEncoding.EncodeToString([]byte(artemisUser)), + MessageCount: 0, }, []Template{ {Name: "secretTemplate", Config: secretTemplate}, {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, diff --git a/tests/scalers/azure/azure_event_hub_dapr_wi/azure_event_hub_dapr_wi_test.go b/tests/scalers/azure/azure_event_hub_dapr_wi/azure_event_hub_dapr_wi_test.go index 13870a40bc4..8eeeae71fae 100644 --- a/tests/scalers/azure/azure_event_hub_dapr_wi/azure_event_hub_dapr_wi_test.go +++ b/tests/scalers/azure/azure_event_hub_dapr_wi/azure_event_hub_dapr_wi_test.go @@ -26,7 +26,7 @@ import ( var _ = godotenv.Load("../../../.env") const ( - testName = "azure-event-hub-dapr" + testName = "azure-event-hub-dapr-wi" eventhubConsumerGroup = "$Default" ) diff --git a/tests/scalers/azure/azure_service_bus_queue_regex/azure_service_bus_queue_regex_test.go b/tests/scalers/azure/azure_service_bus_queue_regex/azure_service_bus_queue_regex_test.go index a8419ecd44b..06c3c3c2db1 100644 --- a/tests/scalers/azure/azure_service_bus_queue_regex/azure_service_bus_queue_regex_test.go +++ b/tests/scalers/azure/azure_service_bus_queue_regex/azure_service_bus_queue_regex_test.go @@ -202,8 +202,8 @@ func testScale(t *testing.T, kc *kubernetes.Clientset, client *azservicebus.Clie // check different aggregation operations data.Operation = "max" KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") data.Operation = "avg" KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) diff --git a/tests/scalers/azure/azure_service_bus_topic_regex/azure_service_bus_topic_regex_test.go b/tests/scalers/azure/azure_service_bus_topic_regex/azure_service_bus_topic_regex_test.go index 07bfdc57572..3227f2fdebd 100644 --- a/tests/scalers/azure/azure_service_bus_topic_regex/azure_service_bus_topic_regex_test.go +++ b/tests/scalers/azure/azure_service_bus_topic_regex/azure_service_bus_topic_regex_test.go @@ -225,8 +225,8 @@ func testScale(t *testing.T, kc *kubernetes.Clientset, client *azservicebus.Clie // check different aggregation operations data.Operation = "max" KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") data.Operation = "avg" KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) diff --git a/tests/scalers/elasticsearch/elasticsearch_test.go b/tests/scalers/elasticsearch/elasticsearch_test.go index de2d997066c..68239c27d64 100644 --- a/tests/scalers/elasticsearch/elasticsearch_test.go +++ b/tests/scalers/elasticsearch/elasticsearch_test.go @@ -81,7 +81,7 @@ metadata: labels: app: {{.DeploymentName}} spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: {{.DeploymentName}} @@ -202,7 +202,7 @@ spec: name: elasticsearch ` - scaledObjectTemplate = ` + scaledObjectTemplateSearchTemplate = ` apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: @@ -232,6 +232,54 @@ spec: name: keda-trigger-auth-elasticsearch-secret ` + scaledObjectTemplateQuery = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + minReplicaCount: 0 + maxReplicaCount: 2 + pollingInterval: 3 + cooldownPeriod: 5 + triggers: + - type: elasticsearch + metadata: + addresses: "http://{{.DeploymentName}}.{{.TestNamespace}}.svc:9200" + username: "elastic" + index: {{.IndexName}} + query: | + { + "query": { + "bool": { + "must": [ + { + "range": { + "@timestamp": { + "gte": "now-1m", + "lte": "now" + } + } + }, + { + "match_all": {} + } + ] + } + } + } + valueLocation: "hits.total.value" + targetValue: "1" + activationTargetValue: "4" + authenticationRef: + name: keda-trigger-auth-elasticsearch-secret +` + elasticsearchCreateIndex = ` { "mappings": { @@ -297,9 +345,6 @@ spec: func TestElasticsearchScaler(t *testing.T) { kc := GetKubernetesClient(t) data, templates := getTemplateData() - t.Cleanup(func() { - DeleteKubernetesResources(t, testNamespace, data, templates) - }) // Create kubernetes resources CreateKubernetesResources(t, kc, testNamespace, data, templates) @@ -307,13 +352,32 @@ func TestElasticsearchScaler(t *testing.T) { // setup elastic setupElasticsearch(t, kc) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 3), - "replica count should be %d after 3 minutes", minReplicaCount) + t.Run("test with searchTemplateName", func(t *testing.T) { + t.Log("--- testing with searchTemplateName ---") - // test scaling - testActivation(t, kc) - testScaleOut(t, kc) - testScaleIn(t, kc) + // Create ScaledObject with searchTemplateName + KubectlApplyWithTemplate(t, data, "scaledObjectTemplateSearchTemplate", scaledObjectTemplateSearchTemplate) + + testElasticsearchScaler(t, kc) + + // Delete ScaledObject + KubectlDeleteWithTemplate(t, data, "scaledObjectTemplateSearchTemplate", scaledObjectTemplateSearchTemplate) + }) + + t.Run("test with query", func(t *testing.T) { + t.Log("--- testing with query ---") + + // Create ScaledObject with query + KubectlApplyWithTemplate(t, data, "scaledObjectTemplateQuery", scaledObjectTemplateQuery) + + testElasticsearchScaler(t, kc) + + // Delete ScaledObject + KubectlDeleteWithTemplate(t, data, "scaledObjectTemplateQuery", scaledObjectTemplateQuery) + }) + + // cleanup + DeleteKubernetesResources(t, testNamespace, data, templates) } func setupElasticsearch(t *testing.T, kc *kubernetes.Clientset) { @@ -326,22 +390,18 @@ func setupElasticsearch(t *testing.T, kc *kubernetes.Clientset) { require.NoErrorf(t, err, "cannot execute command - %s", err) } -func testActivation(t *testing.T, kc *kubernetes.Clientset) { +func testElasticsearchScaler(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing activation ---") addElements(t, 3) AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, minReplicaCount, 60) -} -func testScaleOut(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale out ---") - addElements(t, 5) + addElements(t, 10) assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, maxReplicaCount, 60, 3), "replica count should be %d after 3 minutes", maxReplicaCount) -} -func testScaleIn(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale in ---") assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 3), @@ -383,6 +443,5 @@ func getTemplateData() (templateData, []Template) { {Name: "serviceTemplate", Config: serviceTemplate}, {Name: "elasticsearchDeploymentTemplate", Config: elasticsearchDeploymentTemplate}, {Name: "deploymentTemplate", Config: deploymentTemplate}, - {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, } } diff --git a/tests/scalers/etcd/etcd_cluster_auth/etcd_cluster_auth_test.go b/tests/scalers/etcd/etcd_cluster_auth/etcd_cluster_auth_test.go new file mode 100644 index 00000000000..9f5d7d59892 --- /dev/null +++ b/tests/scalers/etcd/etcd_cluster_auth/etcd_cluster_auth_test.go @@ -0,0 +1,232 @@ +//go:build e2e +// +build e2e + +package etcd_cluster_auth_test + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +const ( + testName = "etcd-auth-test" +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + jobName = fmt.Sprintf("%s-job", testName) + secretName = fmt.Sprintf("%s-secret", testName) + triggerAuthName = fmt.Sprintf("%s-triggerauth", testName) + etcdClientName = fmt.Sprintf("%s-client", testName) + etcdUsername = "root" + etcdPassword = "admin" + etcdEndpoints = fmt.Sprintf("etcd-0.etcd-headless.%s:2379,etcd-1.%s:2379,etcd-2.etcd-headless.%s:2379", testNamespace, testNamespace, testNamespace) + minReplicaCount = 0 + maxReplicaCount = 2 +) + +type templateData struct { + TestNamespace string + DeploymentName string + JobName string + ScaledObjectName string + SecretName string + TriggerAuthName string + EtcdUsernameBase64 string + EtcdPasswordBase64 string + MinReplicaCount int + MaxReplicaCount int + EtcdName string + EtcdClientName string + EtcdEndpoints string +} + +const ( + secretTemplate = `apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +data: + etcd-username: {{.EtcdUsernameBase64}} + etcd-password: {{.EtcdPasswordBase64}} +` + triggerAuthenticationTemplate = `apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: username + name: {{.SecretName}} + key: etcd-username + - parameter: password + name: {{.SecretName}} + key: etcd-password +` + deploymentTemplate = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} +spec: + selector: + matchLabels: + app: {{.DeploymentName}} + replicas: 0 + template: + metadata: + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: my-app + image: nginxinc/nginx-unprivileged + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 +` + scaledObjectTemplate = `apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + pollingInterval: 15 + cooldownPeriod: 5 + minReplicaCount: {{.MinReplicaCount}} + maxReplicaCount: {{.MaxReplicaCount}} + advanced: + horizontalPodAutoscalerConfig: + name: keda-hpa-etcd-scaledobject + behavior: + scaleDown: + stabilizationWindowSeconds: 5 + triggers: + - type: etcd + metadata: + endpoints: {{.EtcdEndpoints}} + watchKey: var + value: '1.5' + activationValue: '5' + watchProgressNotifyInterval: '10' + authenticationRef: + name: {{.TriggerAuthName}} +` + etcdClientTemplate = ` +apiVersion: v1 +kind: Pod +metadata: + name: {{.EtcdClientName}} + namespace: {{.TestNamespace}} +spec: + containers: + - name: {{.EtcdClientName}} + image: gcr.io/etcd-development/etcd:v3.4.10 + command: + - sh + - -c + - "exec tail -f /dev/null"` +) + +func TestScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + // Create kubernetes resources + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + t.Cleanup(func() { + KubectlDeleteWithTemplate(t, data, "etcdClientTemplate", etcdClientTemplate) + RemoveCluster(t, kc) + DeleteKubernetesResources(t, testNamespace, data, templates) + }) + CreateNamespace(t, kc, testNamespace) + + // Create Etcd Cluster + KubectlApplyWithTemplate(t, data, "etcdClientTemplate", etcdClientTemplate) + InstallCluster(t, kc) + setVarValue(t, 0) + + // Create kubernetes resources for testing + KubectlApplyMultipleWithTemplate(t, data, templates) + + testActivation(t, kc) + testScaleOut(t, kc) + testScaleIn(t, kc) +} + +func testActivation(t *testing.T, kc *kubernetes.Clientset) { + t.Log("--- testing activation ---") + setVarValue(t, 4) + + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, minReplicaCount, 60) +} + +func testScaleOut(t *testing.T, kc *kubernetes.Clientset) { + t.Log("--- testing scale out ---") + setVarValue(t, 9) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, maxReplicaCount, 60, 3), + "replica count should be %d after 3 minutes", maxReplicaCount) +} + +func testScaleIn(t *testing.T, kc *kubernetes.Clientset) { + t.Log("--- testing scale in ---") + setVarValue(t, 0) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 3), + "replica count should be %d after 3 minutes", minReplicaCount) +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + DeploymentName: deploymentName, + ScaledObjectName: scaledObjectName, + JobName: jobName, + SecretName: secretName, + TriggerAuthName: triggerAuthName, + EtcdUsernameBase64: base64.StdEncoding.EncodeToString([]byte(etcdUsername)), + EtcdPasswordBase64: base64.StdEncoding.EncodeToString([]byte(etcdPassword)), + EtcdName: testName, + EtcdClientName: etcdClientName, + EtcdEndpoints: etcdEndpoints, + MinReplicaCount: minReplicaCount, + MaxReplicaCount: maxReplicaCount, + }, []Template{ + {Name: "secretTemplate", Config: secretTemplate}, + {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, + {Name: "deploymentTemplate", Config: deploymentTemplate}, + {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + } +} + +func setVarValue(t *testing.T, value int) { + _, _, err := ExecCommandOnSpecificPod(t, etcdClientName, testNamespace, fmt.Sprintf(`etcdctl --user="%s" --password="%s" put var %d --endpoints=%s`, + etcdUsername, etcdPassword, value, etcdEndpoints)) + assert.NoErrorf(t, err, "cannot execute command - %s", err) +} + +func InstallCluster(t *testing.T, kc *kubernetes.Clientset) { + _, err := ExecuteCommand(fmt.Sprintf(`helm upgrade --install --set persistence.enabled=false --set resourcesPreset=none --set auth.rbac.rootPassword=%s --set auth.rbac.allowNoneAuthentication=false --set replicaCount=3 --namespace %s --wait etcd oci://registry-1.docker.io/bitnamicharts/etcd`, + etcdPassword, testNamespace)) + require.NoErrorf(t, err, "cannot execute command - %s", err) +} + +func RemoveCluster(t *testing.T, kc *kubernetes.Clientset) { + _, err := ExecuteCommand(fmt.Sprintf(`helm delete --namespace %s --wait etcd`, + testNamespace)) + require.NoErrorf(t, err, "cannot execute command - %s", err) +} diff --git a/tests/scalers/external_scaler_sj/external_scaler_sj_test.go b/tests/scalers/external_scaler_sj/external_scaler_sj_test.go index fc47c72787d..a5cac292327 100644 --- a/tests/scalers/external_scaler_sj/external_scaler_sj_test.go +++ b/tests/scalers/external_scaler_sj/external_scaler_sj_test.go @@ -9,6 +9,7 @@ import ( "github.com/joho/godotenv" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/client-go/kubernetes" . "github.com/kedacore/keda/v2/tests/helper" @@ -139,6 +140,9 @@ func TestScaler(t *testing.T) { CreateKubernetesResources(t, kc, testNamespace, data, templates) + require.True(t, WaitForDeploymentReplicaReadyCount(t, kc, scalerName, testNamespace, 1, 60, 1), + "replica count should be 1 after 1 minute") + assert.True(t, WaitForJobCount(t, kc, testNamespace, 0, 60, 1), "job count should be 0 after 1 minute") @@ -184,7 +188,7 @@ func testScaleIn(t *testing.T, kc *kubernetes.Clientset, data templateData) { data.MetricValue = 0 KubectlReplaceWithTemplate(t, data, "updateMetricTemplate", updateMetricTemplate) - assert.True(t, WaitForScaledJobCount(t, kc, scaledJobName, testNamespace, 0, 60, 1), - "job count should be 0 after 1 minute") + assert.True(t, WaitForScaledJobCount(t, kc, scaledJobName, testNamespace, 0, 120, 1), + "job count should be 0 after 2 minute") KubectlDeleteWithTemplate(t, data, "updateMetricTemplate", updateMetricTemplate) } diff --git a/tests/scalers/github_runner/github_runner_test.go b/tests/scalers/github_runner/github_runner_test.go index 97235dfb978..8c64f44be44 100644 --- a/tests/scalers/github_runner/github_runner_test.go +++ b/tests/scalers/github_runner/github_runner_test.go @@ -313,13 +313,11 @@ func TestScaler(t *testing.T) { // test scaling Scaled Job with App KubectlApplyWithTemplate(t, data, "scaledGhaJobTemplate", scaledGhaJobTemplate) - // testActivation(t, kc, client) testJobScaleOut(t, kc, client, ghaWorkflowID) testJobScaleIn(t, kc) // test scaling Scaled Job KubectlApplyWithTemplate(t, data, "scaledJobTemplate", scaledJobTemplate) - // testActivation(t, kc, client) testJobScaleOut(t, kc, client, workflowID) testJobScaleIn(t, kc) diff --git a/tests/scalers/rabbitmq/rabbitmq_helper.go b/tests/scalers/rabbitmq/rabbitmq_helper.go index 6f22b178d95..d0a78e821e4 100644 --- a/tests/scalers/rabbitmq/rabbitmq_helper.go +++ b/tests/scalers/rabbitmq/rabbitmq_helper.go @@ -139,6 +139,47 @@ data: --- apiVersion: apps/v1 kind: Deployment +metadata: + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + replicas: 0 + selector: + matchLabels: + app: {{.DeploymentName}} + template: + metadata: + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: rabbitmq-consumer + image: ghcr.io/kedacore/tests-rabbitmq + imagePullPolicy: Always + command: + - receive + args: + - '{{.Connection}}' + envFrom: + - secretRef: + name: {{.SecretName}} +` + + RMQTargetDeploymentWithAuthEnvTemplate = ` +apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +data: + RabbitApiHost: {{.Base64Connection}} + RabbitUsername: {{.Base64Username}} + RabbitPassword: {{.Base64Password}} +--- +apiVersion: apps/v1 +kind: Deployment metadata: name: {{.DeploymentName}} namespace: {{.TestNamespace}} diff --git a/tests/scalers/rabbitmq/rabbitmq_queue_amqp/rabbitmq_queue_amqp_test.go b/tests/scalers/rabbitmq/rabbitmq_queue_amqp/rabbitmq_queue_amqp_test.go index 3bc8efa179e..a545ae3be28 100644 --- a/tests/scalers/rabbitmq/rabbitmq_queue_amqp/rabbitmq_queue_amqp_test.go +++ b/tests/scalers/rabbitmq/rabbitmq_queue_amqp/rabbitmq_queue_amqp_test.go @@ -111,8 +111,8 @@ func getTemplateData() (templateData, []Template) { func testScaling(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale out ---") RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") t.Log("--- testing scale in ---") assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), diff --git a/tests/scalers/rabbitmq/rabbitmq_queue_amqp_auth/rabbitmq_queue_amqp_auth_test.go b/tests/scalers/rabbitmq/rabbitmq_queue_amqp_auth/rabbitmq_queue_amqp_auth_test.go new file mode 100644 index 00000000000..b73a7ff63d9 --- /dev/null +++ b/tests/scalers/rabbitmq/rabbitmq_queue_amqp_auth/rabbitmq_queue_amqp_auth_test.go @@ -0,0 +1,258 @@ +//go:build e2e +// +build e2e + +package rabbitmq_queue_amqp_auth_test + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" + . "github.com/kedacore/keda/v2/tests/scalers/rabbitmq" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../../.env") + +const ( + testName = "rmq-queue-amqp-auth-test" +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + rmqNamespace = fmt.Sprintf("%s-rmq", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + triggerAuthenticationName = fmt.Sprintf("%s-ta", testName) + secretName = fmt.Sprintf("%s-secret", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + queueName = "hello" + user = fmt.Sprintf("%s-user", testName) + password = fmt.Sprintf("%s-password", testName) + vhost = "/" + NoAuthConnectionString = fmt.Sprintf("amqp://rabbitmq.%s.svc.cluster.local", rmqNamespace) + connectionString = fmt.Sprintf("amqp://%s:%s@rabbitmq.%s.svc.cluster.local", user, password, rmqNamespace) + messageCount = 100 +) + +const ( + scaledObjectAuthFromSecretTemplate = ` +--- +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + pollingInterval: 5 + cooldownPeriod: 10 + minReplicaCount: 0 + maxReplicaCount: 4 + triggers: + - type: rabbitmq + metadata: + queueName: {{.QueueName}} + hostFromEnv: RabbitApiHost + mode: QueueLength + value: '10' + activationValue: '5' + authenticationRef: + name: {{.TriggerAuthenticationName}} +` + + triggerAuthenticationTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthenticationName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: username + name: {{.SecretName}} + key: RabbitUsername + - parameter: password + name: {{.SecretName}} + key: RabbitPassword +` + invalidUsernameAndPasswordTriggerAuthenticationTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthenticationName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: username + name: {{.SecretName}} + key: Rabbit-Username + - parameter: password + name: {{.SecretName}} + key: Rabbit-Password +` + + invalidPasswordTriggerAuthenticationTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthenticationName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: username + name: {{.SecretName}} + key: RabbitUsername + - parameter: password + name: {{.SecretName}} + key: Rabbit-Password +` + + scaledObjectAuthFromEnvTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + pollingInterval: 5 + cooldownPeriod: 10 + minReplicaCount: 0 + maxReplicaCount: 4 + triggers: + - type: rabbitmq + metadata: + queueName: {{.QueueName}} + hostFromEnv: RabbitApiHost + usernameFromEnv: RabbitUsername + passwordFromEnv: RabbitPassword + mode: QueueLength + value: '10' + activationValue: '5' +` +) + +type templateData struct { + TestNamespace string + DeploymentName string + ScaledObjectName string + TriggerAuthenticationName string + SecretName string + QueueName string + Username, Base64Username string + Password, Base64Password string + Connection, Base64Connection string + FullConnection string +} + +func TestScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + t.Cleanup(func() { + DeleteKubernetesResources(t, testNamespace, data, templates) + RMQUninstall(t, rmqNamespace, user, password, vhost, WithoutOAuth()) + }) + + // Create kubernetes resources + RMQInstall(t, kc, rmqNamespace, user, password, vhost, WithoutOAuth()) + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") + + testAuthFromSecret(t, kc, data) + testAuthFromEnv(t, kc, data) + + testInvalidPassword(t, kc, data) + testInvalidUsernameAndPassword(t, kc, data) + + testActivationValue(t, kc) +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + DeploymentName: deploymentName, + ScaledObjectName: scaledObjectName, + TriggerAuthenticationName: triggerAuthenticationName, + SecretName: secretName, + QueueName: queueName, + Username: user, + Base64Username: base64.StdEncoding.EncodeToString([]byte(user)), + Password: password, + Base64Password: base64.StdEncoding.EncodeToString([]byte(password)), + Connection: connectionString, + Base64Connection: base64.StdEncoding.EncodeToString([]byte(NoAuthConnectionString)), + }, []Template{ + {Name: "deploymentTemplate", Config: RMQTargetDeploymentWithAuthEnvTemplate}, + } +} + +func testAuthFromSecret(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing scale out ---") + KubectlApplyWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + defer KubectlDeleteWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + KubectlApplyWithTemplate(t, data, "triggerAuthenticationTemplate", triggerAuthenticationTemplate) + defer KubectlDeleteWithTemplate(t, data, "triggerAuthenticationTemplate", triggerAuthenticationTemplate) + + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), + "replica count should be 4 after 1 minute") + + t.Log("--- testing scale in ---") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") +} + +func testAuthFromEnv(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing scale out ---") + KubectlApplyWithTemplate(t, data, "scaledObjectAuthFromEnvTemplate", scaledObjectAuthFromEnvTemplate) + defer KubectlDeleteWithTemplate(t, data, "scaledObjectAuthFromEnvTemplate", scaledObjectAuthFromEnvTemplate) + + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), + "replica count should be 4 after 1 minute") + + t.Log("--- testing scale in ---") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") +} + +func testInvalidPassword(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing invalid password ---") + KubectlApplyWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + defer KubectlDeleteWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + KubectlApplyWithTemplate(t, data, "invalidPasswordTriggerAuthenticationTemplate", invalidPasswordTriggerAuthenticationTemplate) + defer KubectlDeleteWithTemplate(t, data, "invalidPasswordTriggerAuthenticationTemplate", invalidPasswordTriggerAuthenticationTemplate) + + // Shouldn't scale pods + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 30) +} + +func testInvalidUsernameAndPassword(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing invalid username and password ---") + KubectlApplyWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + defer KubectlDeleteWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + KubectlApplyWithTemplate(t, data, "invalidUsernameAndPasswordTriggerAuthenticationTemplate", invalidUsernameAndPasswordTriggerAuthenticationTemplate) + defer KubectlDeleteWithTemplate(t, data, "invalidUsernameAndPasswordTriggerAuthenticationTemplate", invalidUsernameAndPasswordTriggerAuthenticationTemplate) + + // Shouldn't scale pods + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 30) +} + +func testActivationValue(t *testing.T, kc *kubernetes.Clientset) { + t.Log("--- testing activation value ---") + messagesToQueue := 3 + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messagesToQueue) + + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 60) +} diff --git a/tests/scalers/rabbitmq/rabbitmq_queue_amqp_vhost/rabbitmq_queue_amqp_vhost_test.go b/tests/scalers/rabbitmq/rabbitmq_queue_amqp_vhost/rabbitmq_queue_amqp_vhost_test.go index c1d957e103e..915eefa1ff0 100644 --- a/tests/scalers/rabbitmq/rabbitmq_queue_amqp_vhost/rabbitmq_queue_amqp_vhost_test.go +++ b/tests/scalers/rabbitmq/rabbitmq_queue_amqp_vhost/rabbitmq_queue_amqp_vhost_test.go @@ -111,8 +111,8 @@ func getTemplateData() (templateData, []Template) { func testScaling(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale out ---") RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") t.Log("--- testing scale in ---") assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), diff --git a/tests/scalers/rabbitmq/rabbitmq_queue_http/rabbitmq_queue_http_test.go b/tests/scalers/rabbitmq/rabbitmq_queue_http/rabbitmq_queue_http_test.go index ff1f930e7b0..b27e006c106 100644 --- a/tests/scalers/rabbitmq/rabbitmq_queue_http/rabbitmq_queue_http_test.go +++ b/tests/scalers/rabbitmq/rabbitmq_queue_http/rabbitmq_queue_http_test.go @@ -110,8 +110,8 @@ func getTemplateData() (templateData, []Template) { func testScaling(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale out ---") RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") t.Log("--- testing scale in ---") assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), diff --git a/tests/scalers/rabbitmq/rabbitmq_queue_http_aad_wi/rabbitmq_queue_http_aad_wi_test.go b/tests/scalers/rabbitmq/rabbitmq_queue_http_aad_wi/rabbitmq_queue_http_aad_wi_test.go index bd49396717a..cad93024009 100644 --- a/tests/scalers/rabbitmq/rabbitmq_queue_http_aad_wi/rabbitmq_queue_http_aad_wi_test.go +++ b/tests/scalers/rabbitmq/rabbitmq_queue_http_aad_wi/rabbitmq_queue_http_aad_wi_test.go @@ -162,8 +162,8 @@ func getTemplateData() (templateData, []Template) { func testScaling(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale out ---") RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") t.Log("--- testing scale in ---") assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), diff --git a/tests/scalers/rabbitmq/rabbitmq_queue_http_auth/rabbitmq_queue_http_auth_test.go b/tests/scalers/rabbitmq/rabbitmq_queue_http_auth/rabbitmq_queue_http_auth_test.go new file mode 100644 index 00000000000..ed69492645b --- /dev/null +++ b/tests/scalers/rabbitmq/rabbitmq_queue_http_auth/rabbitmq_queue_http_auth_test.go @@ -0,0 +1,258 @@ +//go:build e2e +// +build e2e + +package rabbitmq_queue_http_test + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" + . "github.com/kedacore/keda/v2/tests/scalers/rabbitmq" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../../.env") + +const ( + testName = "rmq-queue-http-test-auth" +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + rmqNamespace = fmt.Sprintf("%s-rmq", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + triggerAuthenticationName = fmt.Sprintf("%s-ta", testName) + secretName = fmt.Sprintf("%s-secret", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + queueName = "hello" + user = fmt.Sprintf("%s-user", testName) + password = fmt.Sprintf("%s-password", testName) + vhost = "/" + NoAuthConnectionString = fmt.Sprintf("http://rabbitmq.%s.svc.cluster.local", rmqNamespace) + connectionString = fmt.Sprintf("amqp://%s:%s@rabbitmq.%s.svc.cluster.local", user, password, rmqNamespace) + messageCount = 100 +) + +const ( + scaledObjectAuthFromSecretTemplate = ` +--- +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + pollingInterval: 5 + cooldownPeriod: 10 + minReplicaCount: 0 + maxReplicaCount: 4 + triggers: + - type: rabbitmq + metadata: + queueName: {{.QueueName}} + hostFromEnv: RabbitApiHost + mode: QueueLength + value: '10' + activationValue: '5' + authenticationRef: + name: {{.TriggerAuthenticationName}} +` + + triggerAuthenticationTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthenticationName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: username + name: {{.SecretName}} + key: RabbitUsername + - parameter: password + name: {{.SecretName}} + key: RabbitPassword +` + invalidUsernameAndPasswordTriggerAuthenticationTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthenticationName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: username + name: {{.SecretName}} + key: Rabbit-Username + - parameter: password + name: {{.SecretName}} + key: Rabbit-Password +` + + invalidPasswordTriggerAuthenticationTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthenticationName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: username + name: {{.SecretName}} + key: RabbitUsername + - parameter: password + name: {{.SecretName}} + key: Rabbit-Password +` + + scaledObjectAuthFromEnvTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + pollingInterval: 5 + cooldownPeriod: 10 + minReplicaCount: 0 + maxReplicaCount: 4 + triggers: + - type: rabbitmq + metadata: + queueName: {{.QueueName}} + hostFromEnv: RabbitApiHost + usernameFromEnv: RabbitUsername + passwordFromEnv: RabbitPassword + mode: QueueLength + value: '10' + activationValue: '5' +` +) + +type templateData struct { + TestNamespace string + DeploymentName string + ScaledObjectName string + TriggerAuthenticationName string + SecretName string + QueueName string + Username, Base64Username string + Password, Base64Password string + Connection, Base64Connection string + FullConnection string +} + +func TestScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + t.Cleanup(func() { + DeleteKubernetesResources(t, testNamespace, data, templates) + RMQUninstall(t, rmqNamespace, user, password, vhost, WithoutOAuth()) + }) + + // Create kubernetes resources + RMQInstall(t, kc, rmqNamespace, user, password, vhost, WithoutOAuth()) + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") + + testAuthFromSecret(t, kc, data) + testAuthFromEnv(t, kc, data) + + testInvalidPassword(t, kc, data) + testInvalidUsernameAndPassword(t, kc, data) + + testActivationValue(t, kc) +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + DeploymentName: deploymentName, + ScaledObjectName: scaledObjectName, + TriggerAuthenticationName: triggerAuthenticationName, + SecretName: secretName, + QueueName: queueName, + Username: user, + Base64Username: base64.StdEncoding.EncodeToString([]byte(user)), + Password: password, + Base64Password: base64.StdEncoding.EncodeToString([]byte(password)), + Connection: connectionString, + Base64Connection: base64.StdEncoding.EncodeToString([]byte(NoAuthConnectionString)), + }, []Template{ + {Name: "deploymentTemplate", Config: RMQTargetDeploymentWithAuthEnvTemplate}, + } +} + +func testAuthFromSecret(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing scale out ---") + KubectlApplyWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + defer KubectlDeleteWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + KubectlApplyWithTemplate(t, data, "triggerAuthenticationTemplate", triggerAuthenticationTemplate) + defer KubectlDeleteWithTemplate(t, data, "triggerAuthenticationTemplate", triggerAuthenticationTemplate) + + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), + "replica count should be 4 after 1 minute") + + t.Log("--- testing scale in ---") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") +} + +func testAuthFromEnv(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing scale out ---") + KubectlApplyWithTemplate(t, data, "scaledObjectAuthFromEnvTemplate", scaledObjectAuthFromEnvTemplate) + defer KubectlDeleteWithTemplate(t, data, "scaledObjectAuthFromEnvTemplate", scaledObjectAuthFromEnvTemplate) + + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), + "replica count should be 4 after 1 minute") + + t.Log("--- testing scale in ---") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") +} + +func testInvalidPassword(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing invalid password ---") + KubectlApplyWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + defer KubectlDeleteWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + KubectlApplyWithTemplate(t, data, "invalidPasswordTriggerAuthenticationTemplate", invalidPasswordTriggerAuthenticationTemplate) + defer KubectlDeleteWithTemplate(t, data, "invalidPasswordTriggerAuthenticationTemplate", invalidPasswordTriggerAuthenticationTemplate) + + // Shouldn't scale pods + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 30) +} + +func testInvalidUsernameAndPassword(t *testing.T, kc *kubernetes.Clientset, data templateData) { + t.Log("--- testing invalid username and password ---") + KubectlApplyWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + defer KubectlDeleteWithTemplate(t, data, "scaledObjectAuthFromSecretTemplate", scaledObjectAuthFromSecretTemplate) + KubectlApplyWithTemplate(t, data, "invalidUsernameAndPasswordTriggerAuthenticationTemplate", invalidUsernameAndPasswordTriggerAuthenticationTemplate) + defer KubectlDeleteWithTemplate(t, data, "invalidUsernameAndPasswordTriggerAuthenticationTemplate", invalidUsernameAndPasswordTriggerAuthenticationTemplate) + + // Shouldn't scale pods + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 30) +} + +func testActivationValue(t *testing.T, kc *kubernetes.Clientset) { + t.Log("--- testing activation value ---") + messagesToQueue := 3 + RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messagesToQueue) + + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 60) +} diff --git a/tests/scalers/rabbitmq/rabbitmq_queue_http_vhost/rabbitmq_queue_http_vhost_test.go b/tests/scalers/rabbitmq/rabbitmq_queue_http_vhost/rabbitmq_queue_http_vhost_test.go index 9dbefd50480..0720b0fe074 100644 --- a/tests/scalers/rabbitmq/rabbitmq_queue_http_vhost/rabbitmq_queue_http_vhost_test.go +++ b/tests/scalers/rabbitmq/rabbitmq_queue_http_vhost/rabbitmq_queue_http_vhost_test.go @@ -110,8 +110,8 @@ func getTemplateData() (templateData, []Template) { func testScaling(t *testing.T, kc *kubernetes.Clientset) { t.Log("--- testing scale out ---") RMQPublishMessages(t, rmqNamespace, connectionString, queueName, messageCount) - assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 1), - "replica count should be 4 after 1 minute") + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 4, 60, 3), + "replica count should be 4 after 3 minute") t.Log("--- testing scale in ---") assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), diff --git a/tests/scalers/solace/solace_test.go b/tests/scalers/solace/solace_test.go index 13ead35fa2e..86319017ae3 100644 --- a/tests/scalers/solace/solace_test.go +++ b/tests/scalers/solace/solace_test.go @@ -207,16 +207,18 @@ spec: func TestStanScaler(t *testing.T) { kc := GetKubernetesClient(t) data, templates := getTemplateData() + + // Create kubernetes resources + CreateKubernetesResources(t, kc, testNamespace, data, templates) + installSolace(t) + KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) + t.Cleanup(func() { KubectlDeleteWithTemplate(t, data, "scaledObjectTemplateRate", scaledObjectTemplateRate) uninstallSolace(t) DeleteKubernetesResources(t, testNamespace, data, templates) }) - // Create kubernetes resources - CreateKubernetesResources(t, kc, testNamespace, data, templates) - installSolace(t) - KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 1), "replica count should be 0 after 1 minute") @@ -236,11 +238,9 @@ func installSolace(t *testing.T) { require.NoErrorf(t, err, "cannot execute command - %s", err) _, err = ExecuteCommand("helm repo update") require.NoErrorf(t, err, "cannot execute command - %s", err) - _, err = ExecuteCommand(fmt.Sprintf(`helm upgrade --install --set solace.usernameAdminPassword=KedaLabAdminPwd1 --set storage.persistent=false,solace.size=dev,nameOverride=pubsubplus-dev,service.type=ClusterIP --namespace %s kedalab solacecharts/pubsubplus`, + _, err = ExecuteCommand(fmt.Sprintf(`helm upgrade --install --set solace.usernameAdminPassword=KedaLabAdminPwd1 --set storage.persistent=false,solace.size=dev,nameOverride=pubsubplus-dev,service.type=ClusterIP --wait --namespace %s kedalab solacecharts/pubsubplus`, testNamespace)) require.NoErrorf(t, err, "cannot execute command - %s", err) - _, err = ExecuteCommand("sleep 60") // there is a bug in the solace helm chart where it is looking for the wrong number of replicas on --wait - require.NoErrorf(t, err, "cannot execute command - %s", err) // Create the pubsub broker _, _, err = ExecCommandOnSpecificPod(t, helperName, testNamespace, "./config/config_solace.sh") require.NoErrorf(t, err, "cannot execute command - %s", err) diff --git a/tests/scalers/datadog/datadog_dca/datadog_dca_test.go b/tests/sequential/datadog_dca/datadog_dca_test.go similarity index 97% rename from tests/scalers/datadog/datadog_dca/datadog_dca_test.go rename to tests/sequential/datadog_dca/datadog_dca_test.go index 66512515484..488b6ebc7b4 100644 --- a/tests/scalers/datadog/datadog_dca/datadog_dca_test.go +++ b/tests/sequential/datadog_dca/datadog_dca_test.go @@ -1,6 +1,10 @@ //go:build e2e // +build e2e +// Temporally moved to standalone e2e as I found that the DD Agent autogenerates DatadogMetric from other +// unrelated HPAs. Until we get a response about how to disable this, the best solution is moving this test +// to run standalone. We should move back it again once we solve this problem + package datadog_dca_test import ( diff --git a/tests/utils/helper/helper.go b/tests/utils/helper/helper.go index 039e5546b8c..3c618a8e4a6 100644 --- a/tests/utils/helper/helper.go +++ b/tests/utils/helper/helper.go @@ -51,8 +51,7 @@ image: repository: "otel/opentelemetry-collector-contrib" config: exporters: - logging: - loglevel: debug + debug: {} prometheus: endpoint: 0.0.0.0:8889 receivers: @@ -72,7 +71,7 @@ config: receivers: - otlp exporters: - - logging + - debug - prometheus logs: null `