diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml index 39be7f802da..61d600ec35a 100644 --- a/.github/workflows/main-build.yml +++ b/.github/workflows/main-build.yml @@ -29,13 +29,13 @@ jobs: echo "build_cache=$(go env GOCACHE)" >> $GITHUB_OUTPUT - name: Go modules cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ${{ steps.go-paths.outputs.mod_cache }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Go build cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ${{ steps.go-paths.outputs.build_cache }} key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }} @@ -91,9 +91,9 @@ jobs: needs: build uses: kedacore/keda/.github/workflows/template-trivy-scan.yml@main with: - runs-on: 'ubuntu-latest' - scan-type: 'fs' - format: 'sarif' + runs-on: ubuntu-latest + scan-type: "fs" + format: "sarif" exit-code: 0 publish: true @@ -105,9 +105,9 @@ jobs: uses: kedacore/keda/.github/workflows/template-trivy-scan.yml@main with: runs-on: ${{ matrix.runner }} - scan-type: 'image' + scan-type: "image" image-ref: ghcr.io/kedacore/keda-metrics-apiserver:main - format: 'sarif' + format: "sarif" exit-code: 0 publish: true @@ -119,8 +119,8 @@ jobs: uses: kedacore/keda/.github/workflows/template-trivy-scan.yml@main with: runs-on: ${{ matrix.runner }} - scan-type: 'image' + scan-type: "image" image-ref: ghcr.io/kedacore/keda:main - format: 'sarif' + format: "sarif" exit-code: 0 publish: true diff --git a/.github/workflows/pr-e2e-checker.yml b/.github/workflows/pr-e2e-checker.yml index 48851b07607..53b5d6a20ac 100644 --- a/.github/workflows/pr-e2e-checker.yml +++ b/.github/workflows/pr-e2e-checker.yml @@ -29,7 +29,7 @@ jobs: - uses: LouisBrunner/checks-action@6b626ffbad7cc56fd58627f774b9067e6118af23 # v2 name: Skip e2e - if: ${{ contains(github.event.pull_request.labels.*.name, env.SKIP_E2E_TAG )}} + if: ${{ contains(github.event.pull_request.labels.*.name, env.SKIP_E2E_TAG )}} with: token: ${{ secrets.GITHUB_TOKEN }} sha: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/pr-e2e.yml b/.github/workflows/pr-e2e.yml index 7c0d80b9e51..72d710d4b2d 100644 --- a/.github/workflows/pr-e2e.yml +++ b/.github/workflows/pr-e2e.yml @@ -10,6 +10,7 @@ jobs: triage: 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 }} @@ -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' diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index b1afd55464c..599d2f4b9d7 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -42,13 +42,13 @@ jobs: echo "build_cache=$(go env GOCACHE)" >> $GITHUB_OUTPUT - name: Go modules cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ${{ steps.go-paths.outputs.mod_cache }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Go build cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ${{ steps.go-paths.outputs.build_cache }} key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }} @@ -151,7 +151,7 @@ jobs: trivy-scan: uses: kedacore/keda/.github/workflows/template-trivy-scan.yml@main with: - runs-on: "ubuntu-latest" + 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 fee45ed3cdb..2fac99476cb 100644 --- a/.github/workflows/pr-welcome.yml +++ b/.github/workflows/pr-welcome.yml @@ -4,7 +4,7 @@ on: pull_request_target: types: [opened, ready_for_review] branches: - - 'main' + - "main" pull_request_review: types: [submitted, edited] @@ -17,7 +17,7 @@ jobs: name: PR Bot runs-on: ubuntu-latest steps: - - name: 'Add welcome comment on PR #${{ github.event.number }} (draft)' + - name: "Add welcome comment on PR #${{ github.event.number }} (draft)" uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 if: github.event_name == 'pull_request_target' && github.event.pull_request.action == 'opened' && github.event.pull_request.draft with: @@ -29,7 +29,7 @@ jobs: issue_number: ${{ github.event.number }}, body: 'Thank you for your contribution! 🙏 Let us know when you are ready for a review by publishing the PR.' }); - - name: 'Add welcome comment on PR #${{ github.event.number }}' + - name: "Add welcome comment on PR #${{ github.event.number }}" uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 if: github.event_name == 'pull_request_target' && (github.event.pull_request.action == 'opened' || github.event.pull_request.action == 'ready_for_review') with: @@ -41,7 +41,7 @@ jobs: issue_number: ${{ github.event.number }}, body: 'Thank you for your contribution! 🙏 We will review your PR as soon as possible.\n\n\n While you are waiting, make sure to:\n\n\n- Add an entry in [our changelog](https://github.com/kedacore/keda/blob/main/CHANGELOG.md) in alphabetical order and link related issue\n- Update the [documentation](https://github.com/kedacore/keda-docs), if needed\n- Add unit & [e2e](https://github.com/kedacore/keda/blob/main/tests/README.md) tests for your changes\n- GitHub checks are passing\n- Is the DCO check failing? Here is [how you can fix DCO issues](https://github.com/kedacore/keda/blob/main/CONTRIBUTING.md#i-didnt-sign-my-commit-now-what)\n\n\nLearn more about:\n- Our [contribution guide](https://github.com/kedacore/keda/blob/main/CONTRIBUTING.md)' }); - - name: 'Apply review required label' + - name: "Apply review required label" uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 if: github.event_name == 'pull_request_target' && (github.event.pull_request.action == 'opened'|| github.event.pull_request.action == 'ready_for_review') with: @@ -52,7 +52,7 @@ jobs: repo: context.repo.repo, labels: ["requires-pr-review"] }) - - name: 'Remove review required label' + - name: "Remove review required label" uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 if: github.event_name == 'pull_request_review' && (github.event.review.state == 'submitted' || github.event.review.state == 'edited') with: diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 0e4523c6979..897f78a36fb 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -2,7 +2,7 @@ name: release-build on: push: tags: - - 'v*' + - "v*" jobs: build: name: Push Release @@ -29,13 +29,13 @@ jobs: echo "build_cache=$(go env GOCACHE)" >> $GITHUB_OUTPUT - name: Go modules cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ${{ steps.go-paths.outputs.mod_cache }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Go build cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: ${{ steps.go-paths.outputs.build_cache }} key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }} diff --git a/.github/workflows/static-analysis-codeql.yml b/.github/workflows/static-analysis-codeql.yml index 076a7857ad3..d353eafdc40 100644 --- a/.github/workflows/static-analysis-codeql.yml +++ b/.github/workflows/static-analysis-codeql.yml @@ -2,7 +2,7 @@ name: "CodeQL" on: push: - branches: [ "main" ] + branches: ["main"] pull_request: {} concurrency: @@ -16,22 +16,22 @@ jobs: container: ghcr.io/kedacore/keda-tools:1.22.5 if: (github.actor != 'dependabot[bot]') steps: - - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - - name: Register workspace path - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Checkout repository + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - name: Register workspace path + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: go - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - queries: +security-and-quality + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: go + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + queries: +security-and-quality - - name: Autobuild - uses: github/codeql-action/autobuild@v3 + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:go" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:go" diff --git a/.github/workflows/template-main-e2e-test.yml b/.github/workflows/template-main-e2e-test.yml index b8c993e5854..d6dbb992e5a 100644 --- a/.github/workflows/template-main-e2e-test.yml +++ b/.github/workflows/template-main-e2e-test.yml @@ -33,7 +33,7 @@ jobs: AWS_RUN_IDENTITY_TESTS: true AZURE_RUN_WORKLOAD_IDENTITY_TESTS: true GCP_RUN_IDENTITY_TESTS: true - ENABLE_OPENTELEMETRY : true + ENABLE_OPENTELEMETRY: true run: make e2e-test - name: Delete all e2e related namespaces diff --git a/.github/workflows/template-smoke-tests.yml b/.github/workflows/template-smoke-tests.yml index fc3a39f112b..c69d161b1a2 100644 --- a/.github/workflows/template-smoke-tests.yml +++ b/.github/workflows/template-smoke-tests.yml @@ -25,7 +25,7 @@ jobs: - name: Install prerequisites run: | sudo apt update - sudo apt install curl make ca-certificates gcc libc-dev -y + sudo apt install curl make ca-certificates gcc libc-dev wget -y env: DEBIAN_FRONTEND: noninteractive 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 fd8f1a8eb9b..00e7929f59f 100644 --- a/.github/workflows/template-versions-smoke-tests.yml +++ b/.github/workflows/template-versions-smoke-tests.yml @@ -11,12 +11,12 @@ jobs: matrix: kubernetesVersion: [v1.30, v1.29, v1.28] include: - - kubernetesVersion: v1.30 - kindImage: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e - - kubernetesVersion: v1.29 - kindImage: kindest/node:v1.29.4@sha256:3abb816a5b1061fb15c6e9e60856ec40d56b7b52bcea5f5f1350bc6e2320b6f8 - - kubernetesVersion: v1.28 - kindImage: kindest/node:v1.28.9@sha256:dca54bc6a6079dd34699d53d7d4ffa2e853e46a20cd12d619a09207e35300bd0 + - kubernetesVersion: v1.30 + kindImage: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e + - kubernetesVersion: v1.29 + kindImage: kindest/node:v1.29.4@sha256:3abb816a5b1061fb15c6e9e60856ec40d56b7b52bcea5f5f1350bc6e2320b6f8 + - kubernetesVersion: v1.28 + kindImage: kindest/node:v1.28.9@sha256:dca54bc6a6079dd34699d53d7d4ffa2e853e46a20cd12d619a09207e35300bd0 uses: kedacore/keda/.github/workflows/template-smoke-tests.yml@main with: runs-on: ubuntu-latest diff --git a/.github/workflows/v1-build.yml b/.github/workflows/v1-build.yml index ea70ec4c36d..fc9cc1bb4ed 100644 --- a/.github/workflows/v1-build.yml +++ b/.github/workflows/v1-build.yml @@ -15,7 +15,7 @@ jobs: fetch-depth: 1 - name: Go modules cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: /go/pkg key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} diff --git a/BUILD.md b/BUILD.md index fc80fe51ec1..560083ef37f 100644 --- a/BUILD.md +++ b/BUILD.md @@ -5,7 +5,7 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [Building](#building) - - [Quick start with Visual Studio Code Remote - Containers](#quick-start-with-visual-studio-code-remote---containers) + - [Quick start with Visual Studio Code Dev Containers](#quick-start-with-visual-studio-code-dev-containers) - [Locally directly](#locally-directly) - [Deploying](#deploying) - [Custom KEDA locally outside cluster](#custom-keda-locally-outside-cluster) @@ -25,14 +25,14 @@ ## Building -### Quick start with [Visual Studio Code Remote - Containers](https://code.visualstudio.com/docs/remote/containers) +### Quick start with [Visual Studio Code Dev Containers](https://code.visualstudio.com/docs/remote/containers) This helps you pull and build quickly - dev containers launch the project inside a container with all the tooling required for a consistent and seamless developer experience. This means you don't have to install and configure your dev environment as the container handles this for you. -To get started install [VSCode](https://code.visualstudio.com/) and the [Remote Containers extensions]( +To get started install [VSCode](https://code.visualstudio.com/) and the [Dev Containers extensions]( https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) Clone the repo and launch code: @@ -43,7 +43,7 @@ cd keda code . ``` -Once VSCode launches run `CTRL+SHIFT+P -> Remote-Containers: Reopen in container` and then use the integrated +Once VSCode launches run `CTRL+SHIFT+P -> Dev Containers: Reopen in container` and then use the integrated terminal to run: ```bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 910ea023df0..eca4f75096b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,9 +57,11 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio ### New +- **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)) #### Experimental @@ -72,13 +74,19 @@ Here is an overview of all new **experimental** features: - **AWS CloudWatch Scaler**: Add support for ignoreNullValues ([#5352](https://github.com/kedacore/keda/issues/5352)) - **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 -- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX)) +- **AWS Secret Manager**: Pod identity overrides are honored ([#6195](https://github.com/kedacore/keda/issues/6195)) +- **Azure Event Hub Scaler**: Checkpointer errors are correctly handled ([#6084](https://github.com/kedacore/keda/issues/6084)) +- **Metrics API Scaler**: Prometheus metrics can have multiple labels ([#6077](https://github.com/kedacore/keda/issues/6077)) ### Deprecations diff --git a/apis/keda/v1alpha1/scaledobject_webhook.go b/apis/keda/v1alpha1/scaledobject_webhook.go index b3602d16739..b96c984445b 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). @@ -312,6 +330,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 @@ -334,15 +364,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 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/grafana/keda-dashboard.json b/config/grafana/keda-dashboard.json index afcb3d4f0bb..26c10b6c541 100644 --- a/config/grafana/keda-dashboard.json +++ b/config/grafana/keda-dashboard.json @@ -546,7 +546,7 @@ "uid": "${datasource}" }, "editorMode": "code", - "expr": "sum by(metric) (keda_scaler_metrics_value{exported_namespace=~\"$namespace\", metric=~\"$metric\", scaledObject=\"$scaledObject\"})", + "expr": "sum by(metric) (keda_scaler_metrics_value{exported_namespace=~\"$namespace\", metric=~\"$metric\", scaledObject=~\"$scaledObject\"})", "legendFormat": "{{ metric }}", "range": true, "refId": "A" @@ -640,7 +640,7 @@ }, "editorMode": "code", "exemplar": false, - "expr": "kube_horizontalpodautoscaler_status_current_replicas{namespace=\"$namespace\",horizontalpodautoscaler=\"keda-hpa-$scaledObject\"}", + "expr": "kube_horizontalpodautoscaler_status_current_replicas{namespace=\"$namespace\",horizontalpodautoscaler=~\"keda-hpa-$scaledObject\"}", "format": "time_series", "instant": false, "interval": "", @@ -655,7 +655,7 @@ }, "editorMode": "code", "exemplar": false, - "expr": "kube_horizontalpodautoscaler_spec_max_replicas{namespace=\"$namespace\",horizontalpodautoscaler=\"keda-hpa-$scaledObject\"}", + "expr": "kube_horizontalpodautoscaler_spec_max_replicas{namespace=\"$namespace\",horizontalpodautoscaler=~\"keda-hpa-$scaledObject\"}", "format": "time_series", "hide": false, "instant": false, @@ -763,7 +763,7 @@ }, "editorMode": "code", "exemplar": false, - "expr": "delta(kube_horizontalpodautoscaler_status_current_replicas{namespace=\"$namespace\",horizontalpodautoscaler=\"keda-hpa-$scaledObject\"}[1m])", + "expr": "delta(kube_horizontalpodautoscaler_status_current_replicas{namespace=\"$namespace\",horizontalpodautoscaler=~\"keda-hpa-$scaledObject\"}[1m])", "format": "time_series", "instant": false, "interval": "", @@ -832,7 +832,7 @@ }, "editorMode": "code", "exemplar": false, - "expr": "kube_horizontalpodautoscaler_status_current_replicas{namespace=\"$namespace\",horizontalpodautoscaler=\"keda-hpa-$scaledObject\"}", + "expr": "kube_horizontalpodautoscaler_status_current_replicas{namespace=\"$namespace\",horizontalpodautoscaler=~\"keda-hpa-$scaledObject\"}", "instant": true, "legendFormat": "current_replicas", "range": false, @@ -845,7 +845,7 @@ }, "editorMode": "code", "exemplar": false, - "expr": "kube_horizontalpodautoscaler_spec_max_replicas{namespace=\"$namespace\",horizontalpodautoscaler=\"keda-hpa-$scaledObject\"}", + "expr": "kube_horizontalpodautoscaler_spec_max_replicas{namespace=\"$namespace\",horizontalpodautoscaler=~\"keda-hpa-$scaledObject\"}", "hide": false, "instant": true, "legendFormat": "max_replicas", diff --git a/controllers/keda/scaledjob_controller.go b/controllers/keda/scaledjob_controller.go index bb1193be8b2..4a145c7c024 100755 --- a/controllers/keda/scaledjob_controller.go +++ b/controllers/keda/scaledjob_controller.go @@ -367,7 +367,7 @@ func (r *ScaledJobReconciler) updatePromMetrics(scaledJob *kedav1alpha1.ScaledJo metricscollector.IncrementCRDTotal(metricscollector.ScaledJobResource, scaledJob.Namespace) metricsData.namespace = scaledJob.Namespace - triggerTypes := make([]string, len(scaledJob.Spec.Triggers)) + triggerTypes := make([]string, 0, len(scaledJob.Spec.Triggers)) for _, trigger := range scaledJob.Spec.Triggers { metricscollector.IncrementTriggerTotal(trigger.Type) triggerTypes = append(triggerTypes, trigger.Type) diff --git a/controllers/keda/scaledobject_controller.go b/controllers/keda/scaledobject_controller.go index d6f9e29449a..b18c84ae61d 100755 --- a/controllers/keda/scaledobject_controller.go +++ b/controllers/keda/scaledobject_controller.go @@ -582,7 +582,7 @@ func (r *ScaledObjectReconciler) updatePromMetrics(scaledObject *kedav1alpha1.Sc metricscollector.IncrementCRDTotal(metricscollector.ScaledObjectResource, scaledObject.Namespace) metricsData.namespace = scaledObject.Namespace - triggerTypes := make([]string, len(scaledObject.Spec.Triggers)) + triggerTypes := make([]string, 0, len(scaledObject.Spec.Triggers)) for _, trigger := range scaledObject.Spec.Triggers { metricscollector.IncrementTriggerTotal(trigger.Type) triggerTypes = append(triggerTypes, trigger.Type) diff --git a/go.mod b/go.mod index b6eff2b66cc..4322a6cafee 100644 --- a/go.mod +++ b/go.mod @@ -210,6 +210,7 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dennwc/varint v1.0.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/eapache/go-resiliency v1.6.0 // indirect @@ -224,6 +225,8 @@ require ( github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -254,6 +257,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect + github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect @@ -308,6 +312,7 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/prometheus v0.49.0 github.com/rivo/uniseg v0.4.4 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/samber/lo v1.39.0 // indirect diff --git a/go.sum b/go.sum index 8ecdbc874fd..e7cefad3489 100644 --- a/go.sum +++ b/go.sum @@ -866,6 +866,8 @@ github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= +github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= @@ -891,6 +893,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= +github.com/aws/aws-sdk-go v1.48.14 h1:nVLrp+F84SG+xGiFMfe1TE6ZV6smF+42tuuNgYGV30s= +github.com/aws/aws-sdk-go v1.48.14/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.16.12/go.mod h1:C+Ym0ag2LIghJbXhfXZ0YEEp49rBWowxKzJLUoob0ts= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= @@ -1015,6 +1019,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= @@ -1088,6 +1094,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-kivik/couchdb/v3 v3.4.1 h1:TlGYEFOmG5a0pN6MpDkIDdd+sn75+w5aSDTcEou02kk= github.com/go-kivik/couchdb/v3 v3.4.1/go.mod h1:scodbTTSS6vOAacJXaCx6XZ57qw8YH1JOvhMwvP0vuw= @@ -1099,6 +1106,8 @@ github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2C github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= @@ -1309,6 +1318,8 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww= +github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= @@ -1539,6 +1550,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -1590,10 +1603,14 @@ github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cY github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= +github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/prometheus v0.49.0 h1:i0CEhreJo3ZcZNeK7ulISinCac0MgL0krVOGgNmfFRY= +github.com/prometheus/prometheus v0.49.0/go.mod h1:aDogiyqmv3aBIWDb5z5Sdcxuuf2BOfiJwOIm9JGpMnI= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= diff --git a/pkg/scalers/aws_dynamodb_scaler_test.go b/pkg/scalers/aws_dynamodb_scaler_test.go index f6ec68314e8..72d7664203d 100644 --- a/pkg/scalers/aws_dynamodb_scaler_test.go +++ b/pkg/scalers/aws_dynamodb_scaler_test.go @@ -39,13 +39,13 @@ type parseDynamoDBMetadataTestData struct { var ( // ErrAwsDynamoNoTableName is returned when "tableName" is missing from the config. - ErrAwsDynamoNoTableName = errors.New("missing required parameter \"tableName\"") + ErrAwsDynamoNoTableName = errors.New(`missing required parameter "tableName"`) // ErrAwsDynamoNoAwsRegion is returned when "awsRegion" is missing from the config. - ErrAwsDynamoNoAwsRegion = errors.New("missing required parameter \"awsRegion\"") + ErrAwsDynamoNoAwsRegion = errors.New(`missing required parameter "awsRegion"`) // ErrAwsDynamoNoKeyConditionExpression is returned when "keyConditionExpression" is missing from the config. - ErrAwsDynamoNoKeyConditionExpression = errors.New("missing required parameter \"keyConditionExpression\"") + ErrAwsDynamoNoKeyConditionExpression = errors.New(`missing required parameter "keyConditionExpression"`) ) var dynamoTestCases = []parseDynamoDBMetadataTestData{ @@ -114,7 +114,7 @@ var dynamoTestCases = []parseDynamoDBMetadataTestData{ "targetValue": "no-valid", }, authParams: map[string]string{}, - expectedError: errors.New("error parsing DynamoDb metadata: unable to set param \"targetValue\" value"), + expectedError: errors.New(`error parsing DynamoDb metadata: unable to set param "targetValue" value`), }, { name: "invalid activationTargetValue given", @@ -128,7 +128,7 @@ var dynamoTestCases = []parseDynamoDBMetadataTestData{ "activationTargetValue": "no-valid", }, authParams: map[string]string{}, - expectedError: errors.New("unable to set param \"activationTargetValue\""), + expectedError: errors.New(`unable to set param "activationTargetValue"`), }, { name: "malformed expressionAttributeNames", diff --git a/pkg/scalers/azure_eventhub_scaler.go b/pkg/scalers/azure_eventhub_scaler.go index de262e38b1e..e967cc08454 100644 --- a/pkg/scalers/azure_eventhub_scaler.go +++ b/pkg/scalers/azure_eventhub_scaler.go @@ -18,7 +18,6 @@ limitations under the License. import ( "context" - "errors" "fmt" "math" "strconv" @@ -276,7 +275,6 @@ func (s *azureEventHubScaler) GetUnprocessedEventCountInPartition(ctx context.Co checkpoint, err = azure.GetCheckpointFromBlobStorage(ctx, s.blobStorageClient, s.metadata.eventHubInfo, partitionInfo.PartitionID) if err != nil { // if blob not found return the total partition event count - err = errors.Unwrap(err) if bloberror.HasCode(err, bloberror.BlobNotFound, bloberror.ContainerNotFound) { s.logger.V(1).Error(err, fmt.Sprintf("Blob container : %s not found to use checkpoint strategy, getting unprocessed event count without checkpoint", s.metadata.eventHubInfo.BlobContainer)) return GetUnprocessedEventCountWithoutCheckpoint(partitionInfo), azure.Checkpoint{}, nil diff --git a/pkg/scalers/cpu_memory_scaler.go b/pkg/scalers/cpu_memory_scaler.go index da5119f3ec0..ce845414966 100644 --- a/pkg/scalers/cpu_memory_scaler.go +++ b/pkg/scalers/cpu_memory_scaler.go @@ -15,25 +15,38 @@ 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 +} + +func (m *cpuMemoryMetadata) Validate() error { + return nil } // 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) + } + + if err := meta.Validate(); err != nil { + if meta.MetricType == "" { + return nil, fmt.Errorf("metricType is required") + } + return nil, fmt.Errorf("validation error: %w", err) } return &cpuMemoryScaler{ @@ -43,48 +56,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 +113,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 +131,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/elasticsearch_scaler.go b/pkg/scalers/elasticsearch_scaler.go index da7c1c2c1a8..44a27e8e463 100644 --- a/pkg/scalers/elasticsearch_scaler.go +++ b/pkg/scalers/elasticsearch_scaler.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "io" "strconv" @@ -22,28 +21,45 @@ import ( type elasticsearchScaler struct { metricType v2.MetricTargetType - metadata *elasticsearchMetadata + metadata elasticsearchMetadata esClient *elasticsearch.Client logger logr.Logger } type elasticsearchMetadata struct { - addresses []string - unsafeSsl bool - username string - password string - cloudID string - apiKey string - indexes []string - searchTemplateName string - parameters []string - valueLocation string - targetValue float64 - activationTargetValue float64 - metricName string + Addresses []string `keda:"name=addresses, order=authParams;triggerMetadata, optional"` + UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, default=false"` + Username string `keda:"name=username, order=authParams;triggerMetadata, optional"` + Password string `keda:"name=password, order=authParams;resolvedEnv;triggerMetadata, optional"` + 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"` + 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"` + ActivationTargetValue float64 `keda:"name=activationTargetValue, order=triggerMetadata, default=0"` + MetricName string `keda:"name=metricName, order=triggerMetadata, optional"` + + TriggerIndex int +} + +func (m *elasticsearchMetadata) Validate() error { + if (m.CloudID != "" || m.APIKey != "") && (len(m.Addresses) > 0 || m.Username != "" || m.Password != "") { + return fmt.Errorf("can't provide both cloud config and endpoint addresses") + } + if (m.CloudID == "" && m.APIKey == "") && (len(m.Addresses) == 0 && m.Username == "" && m.Password == "") { + return fmt.Errorf("must provide either cloud config or endpoint addresses") + } + if (m.CloudID != "" && m.APIKey == "") || (m.CloudID == "" && m.APIKey != "") { + return fmt.Errorf("both cloudID and apiKey must be provided when cloudID or apiKey is used") + } + if len(m.Addresses) > 0 && (m.Username == "" || m.Password == "") { + return fmt.Errorf("both username and password must be provided when addresses is used") + } + return nil } -// NewElasticsearchScaler creates a new elasticsearch scaler func NewElasticsearchScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { metricType, err := GetMetricTargetType(config) if err != nil { @@ -69,184 +85,37 @@ func NewElasticsearchScaler(config *scalersconfig.ScalerConfig) (Scaler, error) }, nil } -const defaultUnsafeSsl = false - -func hasCloudConfig(meta *elasticsearchMetadata) bool { - if meta.cloudID != "" { - return true - } - if meta.apiKey != "" { - return true - } - return false -} - -func hasEndpointsConfig(meta *elasticsearchMetadata) bool { - if len(meta.addresses) > 0 { - return true - } - if meta.username != "" { - return true - } - if meta.password != "" { - return true - } - return false -} - -func extractEndpointsConfig(config *scalersconfig.ScalerConfig, meta *elasticsearchMetadata) error { - addresses, err := GetFromAuthOrMeta(config, "addresses") - if err != nil { - return err - } - - meta.addresses = splitAndTrimBySep(addresses, ",") - if val, ok := config.AuthParams["username"]; ok { - meta.username = val - } else if val, ok := config.TriggerMetadata["username"]; ok { - meta.username = val - } - - if config.AuthParams["password"] != "" { - meta.password = config.AuthParams["password"] - } else if config.TriggerMetadata["passwordFromEnv"] != "" { - meta.password = config.ResolvedEnv[config.TriggerMetadata["passwordFromEnv"]] - } - - return nil -} - -func extractCloudConfig(config *scalersconfig.ScalerConfig, meta *elasticsearchMetadata) error { - cloudID, err := GetFromAuthOrMeta(config, "cloudID") - if err != nil { - return err - } - meta.cloudID = cloudID - - apiKey, err := GetFromAuthOrMeta(config, "apiKey") - if err != nil { - return err - } - meta.apiKey = apiKey - return nil -} - -var ( - // ErrElasticsearchMissingAddressesOrCloudConfig is returned when endpoint addresses or cloud config is missing. - ErrElasticsearchMissingAddressesOrCloudConfig = errors.New("must provide either endpoint addresses or cloud config") - - // ErrElasticsearchConfigConflict is returned when both endpoint addresses and cloud config are provided. - ErrElasticsearchConfigConflict = errors.New("can't provide endpoint addresses and cloud config at the same time") -) - -func parseElasticsearchMetadata(config *scalersconfig.ScalerConfig) (*elasticsearchMetadata, error) { +func parseElasticsearchMetadata(config *scalersconfig.ScalerConfig) (elasticsearchMetadata, error) { meta := elasticsearchMetadata{} + err := config.TypedConfig(&meta) - var err error - addresses, err := GetFromAuthOrMeta(config, "addresses") - cloudID, errCloudConfig := GetFromAuthOrMeta(config, "cloudID") - if err != nil && errCloudConfig != nil { - return nil, ErrElasticsearchMissingAddressesOrCloudConfig - } - - if err == nil && addresses != "" { - err = extractEndpointsConfig(config, &meta) - if err != nil { - return nil, err - } - } - if errCloudConfig == nil && cloudID != "" { - err = extractCloudConfig(config, &meta) - if err != nil { - return nil, err - } - } - - if hasEndpointsConfig(&meta) && hasCloudConfig(&meta) { - return nil, ErrElasticsearchConfigConflict - } - - if val, ok := config.TriggerMetadata["unsafeSsl"]; ok { - unsafeSsl, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf("error parsing unsafeSsl: %w", err) - } - meta.unsafeSsl = unsafeSsl - } else { - meta.unsafeSsl = defaultUnsafeSsl - } - - index, err := GetFromAuthOrMeta(config, "index") - if err != nil { - return nil, err - } - meta.indexes = splitAndTrimBySep(index, ";") - - searchTemplateName, err := GetFromAuthOrMeta(config, "searchTemplateName") - if err != nil { - return nil, err - } - meta.searchTemplateName = searchTemplateName - - if val, ok := config.TriggerMetadata["parameters"]; ok { - meta.parameters = splitAndTrimBySep(val, ";") - } - - valueLocation, err := GetFromAuthOrMeta(config, "valueLocation") - if err != nil { - return nil, err - } - meta.valueLocation = valueLocation - - targetValueString, err := GetFromAuthOrMeta(config, "targetValue") - if err != nil { - if config.AsMetricSource { - targetValueString = "0" - } else { - return nil, err - } - } - targetValue, err := strconv.ParseFloat(targetValueString, 64) if err != nil { - return nil, fmt.Errorf("targetValue parsing error: %w", err) + return meta, err } - meta.targetValue = targetValue - meta.activationTargetValue = 0 - if val, ok := config.TriggerMetadata["activationTargetValue"]; ok { - activationTargetValue, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("activationTargetValue parsing error: %w", err) - } - meta.activationTargetValue = activationTargetValue - } + meta.MetricName = GenerateMetricNameWithIndex(config.TriggerIndex, util.NormalizeString(fmt.Sprintf("elasticsearch-%s", meta.SearchTemplateName))) + meta.TriggerIndex = config.TriggerIndex - meta.metricName = GenerateMetricNameWithIndex(config.TriggerIndex, util.NormalizeString(fmt.Sprintf("elasticsearch-%s", meta.searchTemplateName))) - return &meta, nil + return meta, nil } -// newElasticsearchClient creates elasticsearch db connection -func newElasticsearchClient(meta *elasticsearchMetadata, logger logr.Logger) (*elasticsearch.Client, error) { +func newElasticsearchClient(meta elasticsearchMetadata, logger logr.Logger) (*elasticsearch.Client, error) { var config elasticsearch.Config - if hasCloudConfig(meta) { + if meta.CloudID != "" { config = elasticsearch.Config{ - CloudID: meta.cloudID, - APIKey: meta.apiKey, + CloudID: meta.CloudID, + APIKey: meta.APIKey, } } else { config = elasticsearch.Config{ - Addresses: meta.addresses, - } - if meta.username != "" { - config.Username = meta.username - } - if meta.password != "" { - config.Password = meta.password + Addresses: meta.Addresses, + Username: meta.Username, + Password: meta.Password, } } - config.Transport = util.CreateHTTPTransport(meta.unsafeSsl) + config.Transport = util.CreateHTTPTransport(meta.UnsafeSsl) esClient, err := elasticsearch.NewClient(config) if err != nil { logger.Error(err, fmt.Sprintf("Found error when creating client: %s", err)) @@ -269,14 +138,14 @@ func (s *elasticsearchScaler) Close(_ context.Context) error { 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 { + if err := json.NewEncoder(&body).Encode(buildQuery(&s.metadata)); err != nil { s.logger.Error(err, "Error encoding query: %s", err) } // Run the templated search res, err := s.esClient.SearchTemplate( &body, - s.esClient.SearchTemplate.WithIndex(s.metadata.indexes...), + s.esClient.SearchTemplate.WithIndex(s.metadata.Index...), s.esClient.SearchTemplate.WithContext(ctx), ) if err != nil { @@ -289,7 +158,7 @@ func (s *elasticsearchScaler) getQueryResult(ctx context.Context) (float64, erro if err != nil { return 0, err } - v, err := getValueFromSearch(b, s.metadata.valueLocation) + v, err := getValueFromSearch(b, s.metadata.ValueLocation) if err != nil { return 0, err } @@ -298,14 +167,16 @@ func (s *elasticsearchScaler) getQueryResult(ctx context.Context) (float64, erro func buildQuery(metadata *elasticsearchMetadata) map[string]interface{} { parameters := map[string]interface{}{} - for _, p := range metadata.parameters { + for _, p := range metadata.Parameters { if p != "" { - kv := splitAndTrimBySep(p, ":") - parameters[kv[0]] = kv[1] + kv := strings.Split(p, ":") + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + parameters[key] = value } } query := map[string]interface{}{ - "id": metadata.searchTemplateName, + "id": metadata.SearchTemplateName, } if len(parameters) > 0 { query["params"] = parameters @@ -333,9 +204,9 @@ func getValueFromSearch(body []byte, valueLocation string) (float64, error) { func (s *elasticsearchScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: s.metadata.metricName, + Name: s.metadata.MetricName, }, - Target: GetMetricTargetMili(s.metricType, s.metadata.targetValue), + Target: GetMetricTargetMili(s.metricType, s.metadata.TargetValue), } metricSpec := v2.MetricSpec{ External: externalMetric, Type: externalMetricType, @@ -352,14 +223,5 @@ func (s *elasticsearchScaler) GetMetricsAndActivity(ctx context.Context, metricN metric := GenerateMetricInMili(metricName, num) - return []external_metrics.ExternalMetricValue{metric}, num > s.metadata.activationTargetValue, nil -} - -// Splits a string separated by a specified separator and trims space from all the elements. -func splitAndTrimBySep(s string, sep string) []string { - x := strings.Split(s, sep) - for i := range x { - x[i] = strings.Trim(x[i], " ") - } - return x + return []external_metrics.ExternalMetricValue{metric}, num > s.metadata.ActivationTargetValue, nil } diff --git a/pkg/scalers/elasticsearch_scaler_test.go b/pkg/scalers/elasticsearch_scaler_test.go index 43f9deb0f15..95725065703 100644 --- a/pkg/scalers/elasticsearch_scaler_test.go +++ b/pkg/scalers/elasticsearch_scaler_test.go @@ -3,7 +3,6 @@ package scalers import ( "context" "fmt" - "strconv" "testing" "github.com/stretchr/testify/assert" @@ -38,25 +37,40 @@ var testCases = []parseElasticsearchMetadataTestData{ name: "must provide either endpoint addresses or cloud config", metadata: map[string]string{}, authParams: map[string]string{}, - expectedError: ErrElasticsearchMissingAddressesOrCloudConfig, + expectedError: fmt.Errorf("must provide either cloud config or endpoint addresses"), }, { name: "no apiKey given", metadata: map[string]string{"cloudID": "my-cluster:xxxxxxxxxxx"}, authParams: map[string]string{}, - expectedError: ErrScalerConfigMissingField, + expectedError: fmt.Errorf("both cloudID and apiKey must be provided when cloudID or apiKey is used"), }, { name: "can't provide endpoint addresses and cloud config at the same time", metadata: map[string]string{"addresses": "http://localhost:9200", "cloudID": "my-cluster:xxxxxxxxxxx"}, authParams: map[string]string{"username": "admin", "apiKey": "xxxxxxxxx"}, - expectedError: ErrElasticsearchConfigConflict, + expectedError: fmt.Errorf("can't provide both cloud config and endpoint addresses"), + }, + { + name: "both username and password must be provided when addresses is used", + metadata: map[string]string{ + "addresses": "http://localhost:9200", + "unsafeSsl": "true", + "index": "index1", + "searchTemplateName": "myAwesomeSearch", + "parameters": "param1:value1", + "valueLocation": "hits.hits[0]._source.value", + "targetValue": "12.2", + "activationTargetValue": "3.33", + }, + authParams: map[string]string{"username": "admin"}, + expectedError: fmt.Errorf("both username and password must be provided when addresses is used"), }, { name: "no index given", metadata: map[string]string{"addresses": "http://localhost:9200"}, authParams: map[string]string{"username": "admin"}, - expectedError: ErrScalerConfigMissingField, + expectedError: fmt.Errorf("missing required parameter \"index\""), }, { name: "no searchTemplateName given", @@ -65,7 +79,7 @@ var testCases = []parseElasticsearchMetadataTestData{ "index": "index1", }, authParams: map[string]string{"username": "admin"}, - expectedError: ErrScalerConfigMissingField, + expectedError: fmt.Errorf("missing required parameter \"searchTemplateName\""), }, { name: "no valueLocation given", @@ -75,7 +89,7 @@ var testCases = []parseElasticsearchMetadataTestData{ "searchTemplateName": "searchTemplateName", }, authParams: map[string]string{"username": "admin"}, - expectedError: ErrScalerConfigMissingField, + expectedError: fmt.Errorf("missing required parameter \"valueLocation\""), }, { name: "no targetValue given", @@ -86,7 +100,7 @@ var testCases = []parseElasticsearchMetadataTestData{ "valueLocation": "toto", }, authParams: map[string]string{"username": "admin"}, - expectedError: ErrScalerConfigMissingField, + expectedError: fmt.Errorf("missing required parameter \"targetValue\""), }, { name: "invalid targetValue", @@ -98,7 +112,7 @@ var testCases = []parseElasticsearchMetadataTestData{ "targetValue": "AA", }, authParams: map[string]string{"username": "admin"}, - expectedError: strconv.ErrSyntax, + expectedError: fmt.Errorf("unable to set param \"targetValue\""), }, { name: "invalid activationTargetValue", @@ -111,7 +125,7 @@ var testCases = []parseElasticsearchMetadataTestData{ "activationTargetValue": "AA", }, authParams: map[string]string{"username": "admin"}, - expectedError: strconv.ErrSyntax, + expectedError: fmt.Errorf("unable to set param \"activationTargetValue\""), }, { name: "all fields ok", @@ -130,17 +144,17 @@ var testCases = []parseElasticsearchMetadataTestData{ "password": "password", }, expectedMetadata: &elasticsearchMetadata{ - addresses: []string{"http://localhost:9200"}, - unsafeSsl: true, - indexes: []string{"index1"}, - username: "admin", - password: "password", - searchTemplateName: "myAwesomeSearch", - parameters: []string{"param1:value1"}, - valueLocation: "hits.hits[0]._source.value", - targetValue: 12.2, - activationTargetValue: 3.33, - metricName: "s0-elasticsearch-myAwesomeSearch", + Addresses: []string{"http://localhost:9200"}, + UnsafeSsl: true, + Index: []string{"index1"}, + Username: "admin", + Password: "password", + SearchTemplateName: "myAwesomeSearch", + Parameters: []string{"param1:value1"}, + ValueLocation: "hits.hits[0]._source.value", + TargetValue: 12.2, + ActivationTargetValue: 3.33, + MetricName: "s0-elasticsearch-myAwesomeSearch", }, expectedError: nil, }, @@ -160,16 +174,16 @@ var testCases = []parseElasticsearchMetadataTestData{ "password": "password", }, expectedMetadata: &elasticsearchMetadata{ - addresses: []string{"http://localhost:9200"}, - unsafeSsl: false, - indexes: []string{"index1", "index2"}, - username: "admin", - password: "password", - searchTemplateName: "myAwesomeSearch", - parameters: []string{"param1:value1"}, - valueLocation: "hits.hits[0]._source.value", - targetValue: 12, - metricName: "s0-elasticsearch-myAwesomeSearch", + Addresses: []string{"http://localhost:9200"}, + UnsafeSsl: false, + Index: []string{"index1", "index2"}, + Username: "admin", + Password: "password", + SearchTemplateName: "myAwesomeSearch", + Parameters: []string{"param1:value1"}, + ValueLocation: "hits.hits[0]._source.value", + TargetValue: 12, + MetricName: "s0-elasticsearch-myAwesomeSearch", }, expectedError: nil, }, @@ -189,16 +203,16 @@ var testCases = []parseElasticsearchMetadataTestData{ "password": "password", }, expectedMetadata: &elasticsearchMetadata{ - addresses: []string{"http://localhost:9200"}, - unsafeSsl: false, - indexes: []string{"index1", "index2"}, - username: "admin", - password: "password", - searchTemplateName: "myAwesomeSearch", - parameters: []string{"param1:value1"}, - valueLocation: "hits.hits[0]._source.value", - targetValue: 12, - metricName: "s0-elasticsearch-myAwesomeSearch", + Addresses: []string{"http://localhost:9200"}, + UnsafeSsl: false, + Index: []string{"index1", "index2"}, + Username: "admin", + Password: "password", + SearchTemplateName: "myAwesomeSearch", + Parameters: []string{"param1:value1"}, + ValueLocation: "hits.hits[0]._source.value", + TargetValue: 12, + MetricName: "s0-elasticsearch-myAwesomeSearch", }, expectedError: nil, }, @@ -218,16 +232,16 @@ var testCases = []parseElasticsearchMetadataTestData{ "password": "password", }, expectedMetadata: &elasticsearchMetadata{ - addresses: []string{"http://localhost:9200", "http://localhost:9201"}, - unsafeSsl: false, - indexes: []string{"index1"}, - username: "admin", - password: "password", - searchTemplateName: "myAwesomeSearch", - parameters: []string{"param1:value1"}, - valueLocation: "hits.hits[0]._source.value", - targetValue: 12, - metricName: "s0-elasticsearch-myAwesomeSearch", + Addresses: []string{"http://localhost:9200", "http://localhost:9201"}, + UnsafeSsl: false, + Index: []string{"index1"}, + Username: "admin", + Password: "password", + SearchTemplateName: "myAwesomeSearch", + Parameters: []string{"param1:value1"}, + ValueLocation: "hits.hits[0]._source.value", + TargetValue: 12, + MetricName: "s0-elasticsearch-myAwesomeSearch", }, expectedError: nil, }, @@ -247,16 +261,16 @@ var testCases = []parseElasticsearchMetadataTestData{ "password": "password", }, expectedMetadata: &elasticsearchMetadata{ - addresses: []string{"http://localhost:9200", "http://localhost:9201"}, - unsafeSsl: false, - indexes: []string{"index1"}, - username: "admin", - password: "password", - searchTemplateName: "myAwesomeSearch", - parameters: []string{"param1:value1"}, - valueLocation: "hits.hits[0]._source.value", - targetValue: 12, - metricName: "s0-elasticsearch-myAwesomeSearch", + Addresses: []string{"http://localhost:9200", "http://localhost:9201"}, + UnsafeSsl: false, + Index: []string{"index1"}, + Username: "admin", + Password: "password", + SearchTemplateName: "myAwesomeSearch", + Parameters: []string{"param1:value1"}, + ValueLocation: "hits.hits[0]._source.value", + TargetValue: 12, + MetricName: "s0-elasticsearch-myAwesomeSearch", }, expectedError: nil, }, @@ -279,16 +293,16 @@ var testCases = []parseElasticsearchMetadataTestData{ "ELASTICSEARCH_PASSWORD": "password", }, expectedMetadata: &elasticsearchMetadata{ - addresses: []string{"http://localhost:9200", "http://localhost:9201"}, - unsafeSsl: false, - indexes: []string{"index1"}, - username: "admin", - password: "password", - searchTemplateName: "myAwesomeSearch", - parameters: []string{"param1:value1"}, - valueLocation: "hits.hits[0]._source.value", - targetValue: 12, - metricName: "s0-elasticsearch-myAwesomeSearch", + Addresses: []string{"http://localhost:9200", "http://localhost:9201"}, + UnsafeSsl: false, + Index: []string{"index1"}, + Username: "admin", + Password: "password", + SearchTemplateName: "myAwesomeSearch", + Parameters: []string{"param1:value1"}, + ValueLocation: "hits.hits[0]._source.value", + TargetValue: 12, + MetricName: "s0-elasticsearch-myAwesomeSearch", }, expectedError: nil, }, @@ -303,11 +317,12 @@ func TestParseElasticsearchMetadata(t *testing.T) { ResolvedEnv: tc.resolvedEnv, }) if tc.expectedError != nil { - assert.ErrorIs(t, err, tc.expectedError) + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedError.Error()) } else { assert.NoError(t, err) fmt.Println(tc.name) - assert.Equal(t, tc.expectedMetadata, metadata) + assert.Equal(t, tc.expectedMetadata, &metadata) } }) } @@ -329,16 +344,16 @@ func TestUnsafeSslDefaultValue(t *testing.T) { "password": "password", }, expectedMetadata: &elasticsearchMetadata{ - addresses: []string{"http://localhost:9200"}, - unsafeSsl: false, - indexes: []string{"index1"}, - username: "admin", - password: "password", - searchTemplateName: "myAwesomeSearch", - parameters: []string{"param1:value1"}, - valueLocation: "hits.hits[0]._source.value", - targetValue: 12, - metricName: "s0-elasticsearch-myAwesomeSearch", + Addresses: []string{"http://localhost:9200"}, + UnsafeSsl: false, + Index: []string{"index1"}, + Username: "admin", + Password: "password", + SearchTemplateName: "myAwesomeSearch", + Parameters: []string{"param1:value1"}, + ValueLocation: "hits.hits[0]._source.value", + TargetValue: 12, + MetricName: "s0-elasticsearch-myAwesomeSearch", }, expectedError: nil, } @@ -347,7 +362,7 @@ func TestUnsafeSslDefaultValue(t *testing.T) { AuthParams: tc.authParams, }) assert.NoError(t, err) - assert.Equal(t, tc.expectedMetadata, metadata) + assert.Equal(t, tc.expectedMetadata, &metadata) } func TestBuildQuery(t *testing.T) { @@ -443,7 +458,7 @@ func TestBuildQuery(t *testing.T) { AuthParams: tc.authParams, }) assert.NoError(t, err) - assert.Equal(t, tc.expectedQuery, buildQuery(metadata)) + assert.Equal(t, tc.expectedQuery, buildQuery(&metadata)) }) } } @@ -462,7 +477,8 @@ func TestElasticsearchGetMetricSpecForScaling(t *testing.T) { TriggerIndex: testData.triggerIndex, }) if testData.metadataTestData.expectedError != nil { - assert.ErrorIs(t, err, testData.metadataTestData.expectedError) + assert.Error(t, err) + assert.Contains(t, err.Error(), testData.metadataTestData.expectedError.Error()) continue } if err != nil { 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/kafka_scaler.go b/pkg/scalers/kafka_scaler.go index 1d05b4a6527..b353c1313b4 100644 --- a/pkg/scalers/kafka_scaler.go +++ b/pkg/scalers/kafka_scaler.go @@ -49,8 +49,9 @@ type kafkaScaler struct { } const ( - stringEnable = "enable" - stringDisable = "disable" + stringEnable = "enable" + stringDisable = "disable" + defaultUnsafeSsl = false ) type kafkaMetadata struct { @@ -80,6 +81,7 @@ type kafkaMetadata struct { realm string kerberosConfigPath string kerberosServiceName string + kerberosDisableFAST bool // OAUTHBEARER tokenProvider kafkaSaslOAuthTokenProvider @@ -408,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 } @@ -687,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/metrics_api_scaler.go b/pkg/scalers/metrics_api_scaler.go index b3a678b3cc5..77b658217db 100644 --- a/pkg/scalers/metrics_api_scaler.go +++ b/pkg/scalers/metrics_api_scaler.go @@ -1,8 +1,6 @@ package scalers import ( - "bufio" - "bytes" "context" "encoding/xml" "errors" @@ -14,6 +12,8 @@ import ( "strings" "github.com/go-logr/logr" + "github.com/prometheus/common/expfmt" + "github.com/prometheus/prometheus/promql/parser" "github.com/tidwall/gjson" "gopkg.in/yaml.v3" v2 "k8s.io/api/autoscaling/v2" @@ -264,25 +264,67 @@ func GetValueFromResponse(body []byte, valueLocation string, format APIFormat) ( // getValueFromPrometheusResponse uses provided valueLocation to access the numeric value in provided body func getValueFromPrometheusResponse(body []byte, valueLocation string) (float64, error) { - scanner := bufio.NewScanner(bytes.NewReader(body)) - for scanner.Scan() { - line := scanner.Text() - fields := strings.Fields(line) - if len(fields) == 0 || strings.HasPrefix(fields[0], "#") { - continue - } - if len(fields) == 2 && strings.HasPrefix(fields[0], valueLocation) { - value, err := strconv.ParseFloat(fields[1], 64) - if err != nil { - return 0, err - } - return value, nil + matchers, err := parser.ParseMetricSelector(valueLocation) + if err != nil { + return 0, err + } + metricName := "" + for _, v := range matchers { + if v.Name == "__name__" { + metricName = v.Value } } - - if err := scanner.Err(); err != nil { + // Ensure EOL + reader := strings.NewReader(strings.ReplaceAll(string(body), "\r\n", "\n")) + familiesParser := expfmt.TextParser{} + families, err := familiesParser.TextToMetricFamilies(reader) + if err != nil { return 0, err } + family, ok := families[metricName] + if !ok { + return 0, fmt.Errorf("metric '%s' not found", metricName) + } + + metrics := family.GetMetric() + for _, metric := range metrics { + labels := metric.GetLabel() + match := true + for _, matcher := range matchers { + matcherFound := false + if matcher == nil { + continue + } + // The name has been already validated, + // so we can skip it and check the other labels + if matcher.Name == "__name__" { + continue + } + for _, label := range labels { + if *label.Name == matcher.Name && + *label.Value == matcher.Value { + matcherFound = true + } + } + if !matcherFound { + match = false + } + } + if match { + untyped := metric.GetUntyped() + if untyped != nil && untyped.Value != nil { + return *untyped.Value, nil + } + counter := metric.GetCounter() + if counter != nil && counter.Value != nil { + return *counter.Value, nil + } + gauge := metric.GetGauge() + if gauge != nil && gauge.Value != nil { + return *gauge.Value, nil + } + } + } return 0, fmt.Errorf("value %s not found", valueLocation) } diff --git a/pkg/scalers/metrics_api_scaler_test.go b/pkg/scalers/metrics_api_scaler_test.go index cef44a7bfef..a580d8e1af1 100644 --- a/pkg/scalers/metrics_api_scaler_test.go +++ b/pkg/scalers/metrics_api_scaler_test.go @@ -125,6 +125,16 @@ func TestMetricsAPIGetMetricSpecForScaling(t *testing.T) { func TestGetValueFromResponse(t *testing.T) { inputJSON := []byte(`{"components":[{"id": "82328e93e", "tasks": 32, "str": "64", "k":"1k","wrong":"NaN"}],"count":2.43}`) inputYAML := []byte(`{components: [{id: 82328e93e, tasks: 32, str: '64', k: 1k, wrong: NaN}], count: 2.43}`) + inputPrometheus := []byte(`# HELP backend_queue_size Total number of items + # TYPE backend_queue_size counter + backend_queue_size{queueName="zero"} 0 + backend_queue_size{queueName="one"} 1 + backend_queue_size{queueName="two", instance="random"} 2 + backend_queue_size{queueName="two", instance="zero"} 20 + # HELP random_metric Random metric generate to include noise + # TYPE random_metric counter + random_metric 10 + `) testCases := []struct { name string @@ -143,6 +153,12 @@ func TestGetValueFromResponse(t *testing.T) { {name: "string", input: inputYAML, key: "components.0.str", format: YAMLFormat, expectVal: 64}, {name: "{}.[].{}", input: inputYAML, key: "components.0.tasks", format: YAMLFormat, expectVal: 32}, {name: "invalid data", input: inputYAML, key: "components.0.wrong", format: YAMLFormat, expectErr: true}, + + {name: "no labels", input: inputPrometheus, key: "random_metric", format: PrometheusFormat, expectVal: 10}, + {name: "one label", input: inputPrometheus, key: "backend_queue_size{queueName=\"one\"}", format: PrometheusFormat, expectVal: 1}, + {name: "multiple labels not queried", input: inputPrometheus, key: "backend_queue_size{queueName=\"two\"}", format: PrometheusFormat, expectVal: 2}, + {name: "multiple labels queried", input: inputPrometheus, key: "backend_queue_size{queueName=\"two\", instance=\"zero\"}", format: PrometheusFormat, expectVal: 20}, + {name: "invalid data", input: inputPrometheus, key: "backend_queue_size{invalid=test}", format: PrometheusFormat, expectErr: true}, } for _, tc := range testCases { diff --git a/pkg/scalers/redis_scaler.go b/pkg/scalers/redis_scaler.go index 81c318facaa..3746735dce6 100644 --- a/pkg/scalers/redis_scaler.go +++ b/pkg/scalers/redis_scaler.go @@ -6,7 +6,6 @@ import ( "fmt" "net" "strconv" - "strings" "github.com/go-logr/logr" "github.com/redis/go-redis/v9" @@ -33,9 +32,10 @@ var ( // ErrRedisUnequalHostsAndPorts is returned when the number of hosts and ports are unequal. ErrRedisUnequalHostsAndPorts = errors.New("not enough hosts or ports given. number of hosts should be equal to the number of ports") -) -type redisAddressParser func(metadata, resolvedEnv, authParams map[string]string) (redisConnectionInfo, error) + // ErrRedisParse is returned when "listName" is missing from the config. + ErrRedisParse = errors.New("error parsing redis metadata") +) type redisScaler struct { metricType v2.MetricTargetType @@ -46,31 +46,78 @@ type redisScaler struct { } type redisConnectionInfo struct { - addresses []string - username string - password string - sentinelUsername string - sentinelPassword string - sentinelMaster string - hosts []string - ports []string - enableTLS bool - unsafeSsl bool - cert string - key string - keyPassword string - ca string + Addresses []string `keda:"name=address;addresses, order=triggerMetadata;authParams;resolvedEnv"` + Username string `keda:"name=username, order=triggerMetadata;resolvedEnv;authParams"` + Password string `keda:"name=password, order=triggerMetadata;resolvedEnv;authParams"` + SentinelUsername string `keda:"name=sentinelUsername, order=triggerMetadata;authParams;resolvedEnv"` + SentinelPassword string `keda:"name=sentinelPassword, order=triggerMetadata;authParams;resolvedEnv"` + SentinelMaster string `keda:"name=sentinelMaster, order=triggerMetadata;authParams;resolvedEnv"` + Hosts []string `keda:"name=host;hosts, order=triggerMetadata;resolvedEnv;authParams"` + Ports []string `keda:"name=port;ports, order=triggerMetadata;resolvedEnv;authParams"` + EnableTLS bool + UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, optional, default=false"` + Cert string `keda:"name=Cert;cert, order=authParams"` + Key string `keda:"name=key, order=authParams"` + KeyPassword string `keda:"name=keyPassword, order=authParams"` + Ca string `keda:"name=ca, order=authParams"` } type redisMetadata struct { - listLength int64 - activationListLength int64 - listName string - databaseIndex int - connectionInfo redisConnectionInfo + ListLength int64 `keda:"name=listLength, order=triggerMetadata, optional, default=5"` + ActivationListLength int64 `keda:"name=activationListLength, order=triggerMetadata, optional"` + ListName string `keda:"name=listName, order=triggerMetadata"` + DatabaseIndex int `keda:"name=databaseIndex, order=triggerMetadata, optional"` + MetadataEnableTLS string `keda:"name=enableTLS, order=triggerMetadata, optional"` + AuthParamEnableTLS string `keda:"name=tls, order=authParams, optional"` + ConnectionInfo redisConnectionInfo `keda:"optional"` triggerIndex int } +func (rci *redisConnectionInfo) SetEnableTLS(metadataEnableTLS string, authParamEnableTLS string) error { + EnableTLS := defaultEnableTLS + + if metadataEnableTLS != "" && authParamEnableTLS != "" { + return errors.New("unable to set `tls` in both ScaledObject and TriggerAuthentication together") + } + + if metadataEnableTLS != "" { + tls, err := strconv.ParseBool(metadataEnableTLS) + if err != nil { + return fmt.Errorf("EnableTLS parsing error %w", err) + } + EnableTLS = tls + } + + // parse tls config defined in auth params + if authParamEnableTLS != "" { + switch authParamEnableTLS { + case stringEnable: + EnableTLS = true + case stringDisable: + EnableTLS = false + default: + return fmt.Errorf("error incorrect TLS value given, got %s", authParamEnableTLS) + } + } + rci.EnableTLS = EnableTLS + return nil +} + +func (r *redisMetadata) Validate() error { + err := validateRedisAddress(&r.ConnectionInfo) + + if err != nil { + return err + } + + err = r.ConnectionInfo.SetEnableTLS(r.MetadataEnableTLS, r.AuthParamEnableTLS) + if err == nil { + r.MetadataEnableTLS, r.AuthParamEnableTLS = "", "" + } + + return err +} + // NewRedisScaler creates a new redisScaler func NewRedisScaler(ctx context.Context, isClustered, isSentinel bool, config *scalersconfig.ScalerConfig) (Scaler, error) { luaScript := ` @@ -94,30 +141,21 @@ func NewRedisScaler(ctx context.Context, isClustered, isSentinel bool, config *s logger := InitializeLogger(config, "redis_scaler") + meta, err := parseRedisMetadata(config) + if err != nil { + return nil, fmt.Errorf("error parsing redis metadata: %w", err) + } + if isClustered { - meta, err := parseRedisMetadata(config, parseRedisClusterAddress) - if err != nil { - return nil, fmt.Errorf("error parsing redis metadata: %w", err) - } return createClusteredRedisScaler(ctx, meta, luaScript, metricType, logger) } else if isSentinel { - meta, err := parseRedisMetadata(config, parseRedisSentinelAddress) - if err != nil { - return nil, fmt.Errorf("error parsing redis metadata: %w", err) - } return createSentinelRedisScaler(ctx, meta, luaScript, metricType, logger) } - - meta, err := parseRedisMetadata(config, parseRedisAddress) - if err != nil { - return nil, fmt.Errorf("error parsing redis metadata: %w", err) - } - return createRedisScaler(ctx, meta, luaScript, metricType, logger) } func createClusteredRedisScaler(ctx context.Context, meta *redisMetadata, script string, metricType v2.MetricTargetType, logger logr.Logger) (Scaler, error) { - client, err := getRedisClusterClient(ctx, meta.connectionInfo) + client, err := getRedisClusterClient(ctx, meta.ConnectionInfo) if err != nil { return nil, fmt.Errorf("connection to redis cluster failed: %w", err) } @@ -131,7 +169,7 @@ func createClusteredRedisScaler(ctx context.Context, meta *redisMetadata, script } listLengthFn := func(ctx context.Context) (int64, error) { - cmd := client.Eval(ctx, script, []string{meta.listName}) + cmd := client.Eval(ctx, script, []string{meta.ListName}) if cmd.Err() != nil { return -1, cmd.Err() } @@ -149,7 +187,7 @@ func createClusteredRedisScaler(ctx context.Context, meta *redisMetadata, script } func createSentinelRedisScaler(ctx context.Context, meta *redisMetadata, script string, metricType v2.MetricTargetType, logger logr.Logger) (Scaler, error) { - client, err := getRedisSentinelClient(ctx, meta.connectionInfo, meta.databaseIndex) + client, err := getRedisSentinelClient(ctx, meta.ConnectionInfo, meta.DatabaseIndex) if err != nil { return nil, fmt.Errorf("connection to redis sentinel failed: %w", err) } @@ -158,7 +196,7 @@ func createSentinelRedisScaler(ctx context.Context, meta *redisMetadata, script } func createRedisScaler(ctx context.Context, meta *redisMetadata, script string, metricType v2.MetricTargetType, logger logr.Logger) (Scaler, error) { - client, err := getRedisClient(ctx, meta.connectionInfo, meta.databaseIndex) + client, err := getRedisClient(ctx, meta.ConnectionInfo, meta.DatabaseIndex) if err != nil { return nil, fmt.Errorf("connection to redis failed: %w", err) } @@ -176,7 +214,7 @@ func createRedisScalerWithClient(client *redis.Client, meta *redisMetadata, scri } listLengthFn := func(ctx context.Context) (int64, error) { - cmd := client.Eval(ctx, script, []string{meta.listName}) + cmd := client.Eval(ctx, script, []string{meta.ListName}) if cmd.Err() != nil { return -1, cmd.Err() } @@ -193,110 +231,14 @@ func createRedisScalerWithClient(client *redis.Client, meta *redisMetadata, scri } } -func parseTLSConfigIntoConnectionInfo(config *scalersconfig.ScalerConfig, connInfo *redisConnectionInfo) error { - enableTLS := defaultEnableTLS - if val, ok := config.TriggerMetadata["enableTLS"]; ok { - tls, err := strconv.ParseBool(val) - if err != nil { - return fmt.Errorf("enableTLS parsing error %w", err) - } - enableTLS = tls - } - - connInfo.unsafeSsl = false - if val, ok := config.TriggerMetadata["unsafeSsl"]; ok { - parsedVal, err := strconv.ParseBool(val) - if err != nil { - return fmt.Errorf("error parsing unsafeSsl: %w", err) - } - connInfo.unsafeSsl = parsedVal - } - - // parse tls config defined in auth params - if val, ok := config.AuthParams["tls"]; ok { - val = strings.TrimSpace(val) - if enableTLS { - return errors.New("unable to set `tls` in both ScaledObject and TriggerAuthentication together") - } - switch val { - case stringEnable: - enableTLS = true - case stringDisable: - enableTLS = false - default: - return fmt.Errorf("error incorrect TLS value given, got %s", val) - } - } - if enableTLS { - 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") - } - connInfo.ca = config.AuthParams["ca"] - connInfo.cert = config.AuthParams["cert"] - connInfo.key = config.AuthParams["key"] - if value, found := config.AuthParams["keyPassword"]; found { - connInfo.keyPassword = value - } else { - connInfo.keyPassword = "" - } - } - connInfo.enableTLS = enableTLS - return nil -} - -func parseRedisMetadata(config *scalersconfig.ScalerConfig, parserFn redisAddressParser) (*redisMetadata, error) { - connInfo, err := parserFn(config.TriggerMetadata, config.ResolvedEnv, config.AuthParams) - if err != nil { - return nil, err - } - meta := redisMetadata{ - connectionInfo: connInfo, - } - - err = parseTLSConfigIntoConnectionInfo(config, &meta.connectionInfo) - if err != nil { - return nil, err - } - - meta.listLength = defaultListLength - if val, ok := config.TriggerMetadata["listLength"]; ok { - listLength, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("list length parsing error: %w", err) - } - meta.listLength = listLength - } - - meta.activationListLength = defaultActivationListLength - if val, ok := config.TriggerMetadata["activationListLength"]; ok { - activationListLength, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("activationListLength parsing error %w", err) - } - meta.activationListLength = activationListLength - } - - if val, ok := config.TriggerMetadata["listName"]; ok { - meta.listName = val - } else { - return nil, ErrRedisNoListName +func parseRedisMetadata(config *scalersconfig.ScalerConfig) (*redisMetadata, error) { + meta := &redisMetadata{} + if err := config.TypedConfig(meta); err != nil { + return nil, fmt.Errorf("error parsing redis metadata: %w", err) } - meta.databaseIndex = defaultDBIdx - if val, ok := config.TriggerMetadata["databaseIndex"]; ok { - dbIndex, err := strconv.ParseInt(val, 10, 32) - if err != nil { - return nil, fmt.Errorf("databaseIndex: parsing error %w", err) - } - meta.databaseIndex = int(dbIndex) - } meta.triggerIndex = config.TriggerIndex - return &meta, nil + return meta, nil } func (s *redisScaler) Close(context.Context) error { @@ -305,12 +247,12 @@ func (s *redisScaler) Close(context.Context) error { // GetMetricSpecForScaling returns the metric spec for the HPA func (s *redisScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { - metricName := util.NormalizeString(fmt.Sprintf("redis-%s", s.metadata.listName)) + metricName := util.NormalizeString(fmt.Sprintf("redis-%s", s.metadata.ListName)) externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, metricName), }, - Target: GetMetricTarget(s.metricType, s.metadata.listLength), + Target: GetMetricTarget(s.metricType, s.metadata.ListLength), } metricSpec := v2.MetricSpec{ External: externalMetric, Type: externalMetricType, @@ -329,189 +271,34 @@ func (s *redisScaler) GetMetricsAndActivity(ctx context.Context, metricName stri metric := GenerateMetricInMili(metricName, float64(listLen)) - return []external_metrics.ExternalMetricValue{metric}, listLen > s.metadata.activationListLength, nil -} - -func parseRedisAddress(metadata, resolvedEnv, authParams map[string]string) (redisConnectionInfo, error) { - info := redisConnectionInfo{} - switch { - case authParams["address"] != "": - info.addresses = append(info.addresses, authParams["address"]) - case metadata["address"] != "": - info.addresses = append(info.addresses, metadata["address"]) - case metadata["addressFromEnv"] != "": - info.addresses = append(info.addresses, resolvedEnv[metadata["addressFromEnv"]]) - default: - switch { - case authParams["host"] != "": - info.hosts = append(info.hosts, authParams["host"]) - case metadata["host"] != "": - info.hosts = append(info.hosts, metadata["host"]) - case metadata["hostFromEnv"] != "": - info.hosts = append(info.hosts, resolvedEnv[metadata["hostFromEnv"]]) - } - - switch { - case authParams["port"] != "": - info.ports = append(info.ports, authParams["port"]) - case metadata["port"] != "": - info.ports = append(info.ports, metadata["port"]) - case metadata["portFromEnv"] != "": - info.ports = append(info.ports, resolvedEnv[metadata["portFromEnv"]]) - } - - if len(info.hosts) != 0 && len(info.ports) != 0 { - info.addresses = append(info.addresses, net.JoinHostPort(info.hosts[0], info.ports[0])) - } - } - - if len(info.addresses) == 0 || len(info.addresses[0]) == 0 { - return info, fmt.Errorf("no address or host given. address should be in the format of host:port or you should set the host/port values") - } - - switch { - case authParams["username"] != "": - info.username = authParams["username"] - case metadata["username"] != "": - info.username = metadata["username"] - case metadata["usernameFromEnv"] != "": - info.username = resolvedEnv[metadata["usernameFromEnv"]] - } - - if authParams["password"] != "" { - info.password = authParams["password"] - } else if metadata["passwordFromEnv"] != "" { - info.password = resolvedEnv[metadata["passwordFromEnv"]] - } - - return info, nil + return []external_metrics.ExternalMetricValue{metric}, listLen > s.metadata.ActivationListLength, nil } -func parseRedisMultipleAddress(metadata, resolvedEnv, authParams map[string]string) (redisConnectionInfo, error) { - info := redisConnectionInfo{} - switch { - case authParams["addresses"] != "": - info.addresses = splitAndTrim(authParams["addresses"]) - case metadata["addresses"] != "": - info.addresses = splitAndTrim(metadata["addresses"]) - case metadata["addressesFromEnv"] != "": - info.addresses = splitAndTrim(resolvedEnv[metadata["addressesFromEnv"]]) - default: - switch { - case authParams["hosts"] != "": - info.hosts = splitAndTrim(authParams["hosts"]) - case metadata["hosts"] != "": - info.hosts = splitAndTrim(metadata["hosts"]) - case metadata["hostsFromEnv"] != "": - info.hosts = splitAndTrim(resolvedEnv[metadata["hostsFromEnv"]]) +func validateRedisAddress(c *redisConnectionInfo) error { + if len(c.Hosts) != 0 && len(c.Ports) != 0 { + if len(c.Hosts) != len(c.Ports) { + return ErrRedisUnequalHostsAndPorts } - - switch { - case authParams["ports"] != "": - info.ports = splitAndTrim(authParams["ports"]) - case metadata["ports"] != "": - info.ports = splitAndTrim(metadata["ports"]) - case metadata["portsFromEnv"] != "": - info.ports = splitAndTrim(resolvedEnv[metadata["portsFromEnv"]]) - } - - if len(info.hosts) != 0 && len(info.ports) != 0 { - if len(info.hosts) != len(info.ports) { - return info, ErrRedisUnequalHostsAndPorts - } - for i := range info.hosts { - info.addresses = append(info.addresses, net.JoinHostPort(info.hosts[i], info.ports[i])) - } + for i := range c.Hosts { + c.Addresses = append(c.Addresses, net.JoinHostPort(c.Hosts[i], c.Ports[i])) } } + // } - if len(info.addresses) == 0 { - return info, ErrRedisNoAddresses - } - - return info, nil -} - -func parseRedisClusterAddress(metadata, resolvedEnv, authParams map[string]string) (redisConnectionInfo, error) { - info, err := parseRedisMultipleAddress(metadata, resolvedEnv, authParams) - if err != nil { - return redisConnectionInfo{}, err - } - - switch { - case authParams["username"] != "": - info.username = authParams["username"] - case metadata["username"] != "": - info.username = metadata["username"] - case metadata["usernameFromEnv"] != "": - info.username = resolvedEnv[metadata["usernameFromEnv"]] - } - - if authParams["password"] != "" { - info.password = authParams["password"] - } else if metadata["passwordFromEnv"] != "" { - info.password = resolvedEnv[metadata["passwordFromEnv"]] - } - - return info, nil -} - -func parseRedisSentinelAddress(metadata, resolvedEnv, authParams map[string]string) (redisConnectionInfo, error) { - info, err := parseRedisMultipleAddress(metadata, resolvedEnv, authParams) - if err != nil { - return redisConnectionInfo{}, err - } - - switch { - case authParams["username"] != "": - info.username = authParams["username"] - case metadata["username"] != "": - info.username = metadata["username"] - case metadata["usernameFromEnv"] != "": - info.username = resolvedEnv[metadata["usernameFromEnv"]] - } - - if authParams["password"] != "" { - info.password = authParams["password"] - } else if metadata["passwordFromEnv"] != "" { - info.password = resolvedEnv[metadata["passwordFromEnv"]] - } - - switch { - case authParams["sentinelUsername"] != "": - info.sentinelUsername = authParams["sentinelUsername"] - case metadata["sentinelUsername"] != "": - info.sentinelUsername = metadata["sentinelUsername"] - case metadata["sentinelUsernameFromEnv"] != "": - info.sentinelUsername = resolvedEnv[metadata["sentinelUsernameFromEnv"]] - } - - if authParams["sentinelPassword"] != "" { - info.sentinelPassword = authParams["sentinelPassword"] - } else if metadata["sentinelPasswordFromEnv"] != "" { - info.sentinelPassword = resolvedEnv[metadata["sentinelPasswordFromEnv"]] - } - - switch { - case authParams["sentinelMaster"] != "": - info.sentinelMaster = authParams["sentinelMaster"] - case metadata["sentinelMaster"] != "": - info.sentinelMaster = metadata["sentinelMaster"] - case metadata["sentinelMasterFromEnv"] != "": - info.sentinelMaster = resolvedEnv[metadata["sentinelMasterFromEnv"]] + if len(c.Addresses) == 0 || len(c.Addresses[0]) == 0 { + return ErrRedisNoAddresses } - - return info, nil + return nil } func getRedisClusterClient(ctx context.Context, info redisConnectionInfo) (*redis.ClusterClient, error) { options := &redis.ClusterOptions{ - Addrs: info.addresses, - Username: info.username, - Password: info.password, + Addrs: info.Addresses, + Username: info.Username, + Password: info.Password, } - if info.enableTLS { - tlsConfig, err := util.NewTLSConfigWithPassword(info.cert, info.key, info.keyPassword, info.ca, info.unsafeSsl) + if info.EnableTLS { + tlsConfig, err := util.NewTLSConfigWithPassword(info.Cert, info.Key, info.KeyPassword, info.Ca, info.UnsafeSsl) if err != nil { return nil, err } @@ -528,16 +315,16 @@ func getRedisClusterClient(ctx context.Context, info redisConnectionInfo) (*redi func getRedisSentinelClient(ctx context.Context, info redisConnectionInfo, dbIndex int) (*redis.Client, error) { options := &redis.FailoverOptions{ - Username: info.username, - Password: info.password, + Username: info.Username, + Password: info.Password, DB: dbIndex, - SentinelAddrs: info.addresses, - SentinelUsername: info.sentinelUsername, - SentinelPassword: info.sentinelPassword, - MasterName: info.sentinelMaster, + SentinelAddrs: info.Addresses, + SentinelUsername: info.SentinelUsername, + SentinelPassword: info.SentinelPassword, + MasterName: info.SentinelMaster, } - if info.enableTLS { - tlsConfig, err := util.NewTLSConfigWithPassword(info.cert, info.key, info.keyPassword, info.ca, info.unsafeSsl) + if info.EnableTLS { + tlsConfig, err := util.NewTLSConfigWithPassword(info.Cert, info.Key, info.KeyPassword, info.Ca, info.UnsafeSsl) if err != nil { return nil, err } @@ -554,13 +341,13 @@ func getRedisSentinelClient(ctx context.Context, info redisConnectionInfo, dbInd func getRedisClient(ctx context.Context, info redisConnectionInfo, dbIndex int) (*redis.Client, error) { options := &redis.Options{ - Addr: info.addresses[0], - Username: info.username, - Password: info.password, + Addr: info.Addresses[0], + Username: info.Username, + Password: info.Password, DB: dbIndex, } - if info.enableTLS { - tlsConfig, err := util.NewTLSConfigWithPassword(info.cert, info.key, info.keyPassword, info.ca, info.unsafeSsl) + if info.EnableTLS { + tlsConfig, err := util.NewTLSConfigWithPassword(info.Cert, info.Key, info.KeyPassword, info.Ca, info.UnsafeSsl) if err != nil { return nil, err } @@ -575,12 +362,3 @@ func getRedisClient(ctx context.Context, info redisConnectionInfo, dbIndex int) } return c, nil } - -// Splits a string separated by comma and trims space from all the elements. -func splitAndTrim(s string) []string { - x := strings.Split(s, ",") - for i := range x { - x[i] = strings.Trim(x[i], " ") - } - return x -} diff --git a/pkg/scalers/redis_scaler_test.go b/pkg/scalers/redis_scaler_test.go index 9ce7a062e70..734a1c359a0 100644 --- a/pkg/scalers/redis_scaler_test.go +++ b/pkg/scalers/redis_scaler_test.go @@ -2,7 +2,6 @@ package scalers import ( "context" - "strconv" "testing" "github.com/go-logr/logr" @@ -25,7 +24,7 @@ type parseRedisMetadataTestData struct { metadata map[string]string isError bool authParams map[string]string - enableTLS bool + EnableTLS bool } type redisMetricIdentifier struct { @@ -83,7 +82,7 @@ func TestRedisParseMetadata(t *testing.T) { testCaseNum := 0 for _, testData := range testRedisMetadata { testCaseNum++ - meta, err := parseRedisMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, ResolvedEnv: testRedisResolvedEnv, AuthParams: testData.authParams}, parseRedisAddress) + meta, err := parseRedisMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, ResolvedEnv: testRedisResolvedEnv, AuthParams: testData.authParams}) if err != nil && !testData.isError { t.Errorf("Expected success but got error for unit test # %v", testCaseNum) } @@ -93,21 +92,21 @@ func TestRedisParseMetadata(t *testing.T) { if testData.isError { continue } - if meta.connectionInfo.enableTLS != testData.enableTLS { - t.Errorf("Expected enableTLS to be set to %v but got %v for unit test #%v\n", testData.enableTLS, meta.connectionInfo.enableTLS, testCaseNum) + if meta.ConnectionInfo.EnableTLS != testData.EnableTLS { + t.Errorf("Expected EnableTLS to be set to %v but got %v for unit test #%v\n", testData.EnableTLS, meta.ConnectionInfo.EnableTLS, testCaseNum) } - if meta.connectionInfo.enableTLS { - if meta.connectionInfo.ca != testData.authParams["ca"] { - t.Errorf("Expected ca to be set to %v but got %v for unit test #%v\n", testData.authParams["ca"], meta.connectionInfo.enableTLS, testCaseNum) + if meta.ConnectionInfo.EnableTLS { + if meta.ConnectionInfo.Ca != testData.authParams["ca"] { + t.Errorf("Expected ca to be set to %v but got %v for unit test #%v\n", testData.authParams["ca"], meta.ConnectionInfo.EnableTLS, testCaseNum) } - if meta.connectionInfo.cert != testData.authParams["cert"] { - t.Errorf("Expected cert to be set to %v but got %v for unit test #%v\n", testData.authParams["cert"], meta.connectionInfo.cert, testCaseNum) + if meta.ConnectionInfo.Cert != testData.authParams["cert"] { + t.Errorf("Expected Cert to be set to %v but got %v for unit test #%v\n", testData.authParams["cert"], meta.ConnectionInfo.Cert, testCaseNum) } - if meta.connectionInfo.key != testData.authParams["key"] { - t.Errorf("Expected key to be set to %v but got %v for unit test #%v\n", testData.authParams["key"], meta.connectionInfo.key, testCaseNum) + if meta.ConnectionInfo.Key != testData.authParams["key"] { + t.Errorf("Expected key to be set to %v but got %v for unit test #%v\n", testData.authParams["key"], meta.ConnectionInfo.Key, testCaseNum) } - if meta.connectionInfo.keyPassword != testData.authParams["keyPassword"] { - t.Errorf("Expected key to be set to %v but got %v for unit test #%v\n", testData.authParams["keyPassword"], meta.connectionInfo.key, testCaseNum) + if meta.ConnectionInfo.KeyPassword != testData.authParams["keyPassword"] { + t.Errorf("Expected key to be set to %v but got %v for unit test #%v\n", testData.authParams["keyPassword"], meta.ConnectionInfo.Key, testCaseNum) } } } @@ -115,7 +114,7 @@ func TestRedisParseMetadata(t *testing.T) { func TestRedisGetMetricSpecForScaling(t *testing.T) { for _, testData := range redisMetricIdentifiers { - meta, err := parseRedisMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, ResolvedEnv: testRedisResolvedEnv, AuthParams: testData.metadataTestData.authParams, TriggerIndex: testData.triggerIndex}, parseRedisAddress) + meta, err := parseRedisMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, ResolvedEnv: testRedisResolvedEnv, AuthParams: testData.metadataTestData.authParams, TriggerIndex: testData.triggerIndex}) if err != nil { t.Fatal("Could not parse metadata:", err) } @@ -152,10 +151,11 @@ func TestParseRedisClusterMetadata(t *testing.T) { wantErr: ErrRedisNoAddresses, }, { - name: "unequal number of hosts/ports", + name: "unequal number of Hosts/Ports", metadata: map[string]string{ - "hosts": "a, b, c", - "ports": "1, 2", + "listName": "mylist", + "hosts": "a, b, c", + "ports": "1, 2", }, wantMeta: nil, wantErr: ErrRedisUnequalHostsAndPorts, @@ -168,7 +168,7 @@ func TestParseRedisClusterMetadata(t *testing.T) { "listLength": "5", }, wantMeta: nil, - wantErr: ErrRedisNoListName, + wantErr: ErrRedisParse, }, { name: "invalid list length", @@ -179,7 +179,7 @@ func TestParseRedisClusterMetadata(t *testing.T) { "listLength": "invalid", }, wantMeta: nil, - wantErr: strconv.ErrSyntax, + wantErr: ErrRedisParse, }, { name: "address is defined in auth params", @@ -190,16 +190,16 @@ func TestParseRedisClusterMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, }, }, wantErr: nil, }, { - name: "hosts and ports given in auth params", + name: "Hosts and Ports given in auth params", metadata: map[string]string{ "listName": "mylist", }, @@ -208,18 +208,18 @@ func TestParseRedisClusterMetadata(t *testing.T) { "ports": "1, 2, 3", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, }, }, wantErr: nil, }, { - name: "username given in authParams", + name: "Username given in authParams", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -229,19 +229,19 @@ func TestParseRedisClusterMetadata(t *testing.T) { "username": "username", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, }, wantErr: nil, }, { - name: "username given in metadata", + name: "Username given in metadata", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -250,19 +250,19 @@ func TestParseRedisClusterMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, }, wantErr: nil, }, { - name: "username given in metadata from env", + name: "Username given in metadata from env", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -272,19 +272,19 @@ func TestParseRedisClusterMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "none", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "none", }, }, wantErr: nil, }, { - name: "password given in authParams", + name: "Password given in authParams", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -294,19 +294,19 @@ func TestParseRedisClusterMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", }, }, wantErr: nil, }, { - name: "password given in metadata from env", + name: "Password given in metadata from env", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -316,19 +316,19 @@ func TestParseRedisClusterMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "none", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "none", }, }, wantErr: nil, }, { - name: "tls enabled without setting unsafeSsl", + name: "tls enabled without setting UnsafeSsl", metadata: map[string]string{ "listName": "mylist", "enableTLS": "true", @@ -337,18 +337,18 @@ func TestParseRedisClusterMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, - enableTLS: true, - unsafeSsl: false, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, + EnableTLS: true, + UnsafeSsl: false, }, }, wantErr: nil, }, { - name: "tls enabled with unsafeSsl true", + name: "tls enabled with UnsafeSsl true", metadata: map[string]string{ "listName": "mylist", "enableTLS": "true", @@ -358,12 +358,12 @@ func TestParseRedisClusterMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, - enableTLS: true, - unsafeSsl: true, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, + EnableTLS: true, + UnsafeSsl: true, }, }, wantErr: nil, @@ -378,9 +378,9 @@ func TestParseRedisClusterMetadata(t *testing.T) { ResolvedEnv: c.resolvedEnv, AuthParams: c.authParams, } - meta, err := parseRedisMetadata(config, parseRedisClusterAddress) + meta, err := parseRedisMetadata(config) if c.wantErr != nil { - assert.ErrorIs(t, err, c.wantErr) + assert.ErrorContains(t, err, c.wantErr.Error()) } else { assert.NoError(t, err) } @@ -404,7 +404,7 @@ func TestParseRedisSentinelMetadata(t *testing.T) { wantErr: ErrRedisNoAddresses, }, { - name: "unequal number of hosts/ports", + name: "unequal number of Hosts/Ports", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2", @@ -420,7 +420,7 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "listLength": "5", }, wantMeta: nil, - wantErr: ErrRedisNoListName, + wantErr: ErrRedisParse, }, { name: "invalid list length", @@ -431,7 +431,7 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "listLength": "invalid", }, wantMeta: nil, - wantErr: strconv.ErrSyntax, + wantErr: ErrRedisParse, }, { name: "address is defined in auth params", @@ -442,16 +442,16 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, }, }, wantErr: nil, }, { - name: "hosts and ports given in auth params", + name: "Hosts and Ports given in auth params", metadata: map[string]string{ "listName": "mylist", }, @@ -460,18 +460,18 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "ports": "1, 2, 3", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, }, }, wantErr: nil, }, { - name: "hosts and ports given in auth params", + name: "Hosts and Ports given in auth params", metadata: map[string]string{ "listName": "mylist", }, @@ -480,18 +480,18 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "ports": "1, 2, 3", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, }, }, wantErr: nil, }, { - name: "username given in authParams", + name: "Username given in authParams", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -501,19 +501,19 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "username": "username", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, }, wantErr: nil, }, { - name: "username given in metadata", + name: "Username given in metadata", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -522,19 +522,19 @@ func TestParseRedisSentinelMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, }, wantErr: nil, }, { - name: "username given in metadata from env", + name: "Username given in metadata from env", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -544,19 +544,19 @@ func TestParseRedisSentinelMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "none", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "none", }, }, wantErr: nil, }, { - name: "password given in authParams", + name: "Password given in authParams", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -566,19 +566,19 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", }, }, wantErr: nil, }, { - name: "password given in metadata from env", + name: "Password given in metadata from env", metadata: map[string]string{ "hosts": "a, b, c", "ports": "1, 2, 3", @@ -588,13 +588,13 @@ func TestParseRedisSentinelMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "none", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "none", }, }, wantErr: nil, @@ -610,13 +610,13 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "sentinelUsername": "sentinelUsername", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "sentinelUsername", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "sentinelUsername", }, }, wantErr: nil, @@ -631,13 +631,13 @@ func TestParseRedisSentinelMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "sentinelUsername", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "sentinelUsername", }, }, wantErr: nil, @@ -653,13 +653,13 @@ func TestParseRedisSentinelMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "none", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "none", }, }, wantErr: nil, @@ -675,13 +675,13 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "sentinelPassword": "sentinelPassword", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelPassword: "sentinelPassword", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelPassword: "sentinelPassword", }, }, wantErr: nil, @@ -697,13 +697,13 @@ func TestParseRedisSentinelMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelPassword: "none", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelPassword: "none", }, }, wantErr: nil, @@ -719,13 +719,13 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "sentinelMaster": "sentinelMaster", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "sentinelMaster", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "sentinelMaster", }, }, wantErr: nil, @@ -740,13 +740,13 @@ func TestParseRedisSentinelMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "sentinelMaster", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "sentinelMaster", }, }, wantErr: nil, @@ -762,19 +762,19 @@ func TestParseRedisSentinelMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "none", + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "none", }, }, wantErr: nil, }, { - name: "tls enabled without setting unsafeSsl", + name: "tls enabled without setting UnsafeSsl", metadata: map[string]string{ "listName": "mylist", "enableTLS": "true", @@ -783,18 +783,18 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, - enableTLS: true, - unsafeSsl: false, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, + EnableTLS: true, + UnsafeSsl: false, }, }, wantErr: nil, }, { - name: "tls enabled with unsafeSsl true", + name: "tls enabled with UnsafeSsl true", metadata: map[string]string{ "listName": "mylist", "enableTLS": "true", @@ -804,12 +804,12 @@ func TestParseRedisSentinelMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisMetadata{ - listLength: 5, - listName: "mylist", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, - enableTLS: true, - unsafeSsl: true, + ListLength: 5, + ListName: "mylist", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, + EnableTLS: true, + UnsafeSsl: true, }, }, wantErr: nil, @@ -824,9 +824,9 @@ func TestParseRedisSentinelMetadata(t *testing.T) { ResolvedEnv: c.resolvedEnv, AuthParams: c.authParams, } - meta, err := parseRedisMetadata(config, parseRedisSentinelAddress) + meta, err := parseRedisMetadata(config) if c.wantErr != nil { - assert.ErrorIs(t, err, c.wantErr) + assert.ErrorContains(t, err, c.wantErr.Error()) } else { assert.NoError(t, err) } diff --git a/pkg/scalers/redis_streams_scaler.go b/pkg/scalers/redis_streams_scaler.go index fdb3f7bc085..9418ee5f85b 100644 --- a/pkg/scalers/redis_streams_scaler.go +++ b/pkg/scalers/redis_streams_scaler.go @@ -25,12 +25,6 @@ const ( ) const ( - // defaults - defaultDBIndex = 0 - defaultTargetEntries = 5 - defaultTargetLag = 5 - defaultActivationLagCount = 0 - // metadata names lagMetadata = "lagCount" pendingEntriesCountMetadata = "pendingEntriesCount" @@ -54,15 +48,54 @@ type redisStreamsScaler struct { type redisStreamsMetadata struct { scaleFactor scaleFactor - targetPendingEntriesCount int64 - targetStreamLength int64 - targetLag int64 - streamName string - consumerGroupName string - databaseIndex int - connectionInfo redisConnectionInfo triggerIndex int - activationLagCount int64 + TargetPendingEntriesCount int64 `keda:"name=pendingEntriesCount, order=triggerMetadata, optional, default=5"` + TargetStreamLength int64 `keda:"name=streamLength, order=triggerMetadata, optional, default=5"` + TargetLag int64 `keda:"name=lagCount, order=triggerMetadata, optional"` + StreamName string `keda:"name=stream, order=triggerMetadata"` + ConsumerGroupName string `keda:"name=consumerGroup, order=triggerMetadata, optional"` + DatabaseIndex int `keda:"name=databaseIndex, order=triggerMetadata, optional"` + ConnectionInfo redisConnectionInfo `keda:"optional"` + ActivationLagCount int64 `keda:"name=activationLagCount, order=triggerMetadata, optional"` + MetadataEnableTLS string `keda:"name=enableTLS, order=triggerMetadata, optional"` + AuthParamEnableTLS string `keda:"name=tls, order=authParams, optional"` +} + +func (r *redisStreamsMetadata) Validate() error { + err := validateRedisAddress(&r.ConnectionInfo) + if err != nil { + return err + } + + err = r.ConnectionInfo.SetEnableTLS(r.MetadataEnableTLS, r.AuthParamEnableTLS) + if err != nil { + return err + } + r.MetadataEnableTLS, r.AuthParamEnableTLS = "", "" + + if r.StreamName == "" { + return ErrRedisMissingStreamName + } + + if r.ConsumerGroupName != "" { + r.TargetStreamLength = 0 + if r.TargetLag != 0 { + r.scaleFactor = lagFactor + r.TargetPendingEntriesCount = 0 + + if r.ActivationLagCount == 0 { + err := errors.New("activationLagCount required for Redis lag") + return err + } + } else { + r.scaleFactor = xPendingFactor + } + } else { + r.scaleFactor = xLengthFactor + r.TargetPendingEntriesCount = 0 + } + + return nil } // NewRedisStreamsScaler creates a new redisStreamsScaler @@ -74,28 +107,21 @@ func NewRedisStreamsScaler(ctx context.Context, isClustered, isSentinel bool, co logger := InitializeLogger(config, "redis_streams_scaler") + meta, err := parseRedisStreamsMetadata(config) + if err != nil { + return nil, fmt.Errorf("error parsing redis streams metadata: %w", err) + } + if isClustered { - meta, err := parseRedisStreamsMetadata(config, parseRedisClusterAddress) - if err != nil { - return nil, fmt.Errorf("error parsing redis streams metadata: %w", err) - } return createClusteredRedisStreamsScaler(ctx, meta, metricType, logger) } else if isSentinel { - meta, err := parseRedisStreamsMetadata(config, parseRedisSentinelAddress) - if err != nil { - return nil, fmt.Errorf("error parsing redis streams metadata: %w", err) - } return createSentinelRedisStreamsScaler(ctx, meta, metricType, logger) } - meta, err := parseRedisStreamsMetadata(config, parseRedisAddress) - if err != nil { - return nil, fmt.Errorf("error parsing redis streams metadata: %w", err) - } return createRedisStreamsScaler(ctx, meta, metricType, logger) } func createClusteredRedisStreamsScaler(ctx context.Context, meta *redisStreamsMetadata, metricType v2.MetricTargetType, logger logr.Logger) (Scaler, error) { - client, err := getRedisClusterClient(ctx, meta.connectionInfo) + client, err := getRedisClusterClient(ctx, meta.ConnectionInfo) if err != nil { return nil, fmt.Errorf("connection to redis cluster failed: %w", err) @@ -121,7 +147,7 @@ func createClusteredRedisStreamsScaler(ctx context.Context, meta *redisStreamsMe } func createSentinelRedisStreamsScaler(ctx context.Context, meta *redisStreamsMetadata, metricType v2.MetricTargetType, logger logr.Logger) (Scaler, error) { - client, err := getRedisSentinelClient(ctx, meta.connectionInfo, meta.databaseIndex) + client, err := getRedisSentinelClient(ctx, meta.ConnectionInfo, meta.DatabaseIndex) if err != nil { return nil, fmt.Errorf("connection to redis sentinel failed: %w", err) } @@ -130,7 +156,7 @@ func createSentinelRedisStreamsScaler(ctx context.Context, meta *redisStreamsMet } func createRedisStreamsScaler(ctx context.Context, meta *redisStreamsMetadata, metricType v2.MetricTargetType, logger logr.Logger) (Scaler, error) { - client, err := getRedisClient(ctx, meta.connectionInfo, meta.databaseIndex) + client, err := getRedisClient(ctx, meta.ConnectionInfo, meta.DatabaseIndex) if err != nil { return nil, fmt.Errorf("connection to redis failed: %w", err) } @@ -162,7 +188,7 @@ func createEntriesCountFn(client redis.Cmdable, meta *redisStreamsMetadata) (ent switch meta.scaleFactor { case xPendingFactor: entriesCountFn = func(ctx context.Context) (int64, error) { - pendingEntries, err := client.XPending(ctx, meta.streamName, meta.consumerGroupName).Result() + pendingEntries, err := client.XPending(ctx, meta.StreamName, meta.ConsumerGroupName).Result() if err != nil { return -1, err } @@ -170,7 +196,7 @@ func createEntriesCountFn(client redis.Cmdable, meta *redisStreamsMetadata) (ent } case xLengthFactor: entriesCountFn = func(ctx context.Context) (int64, error) { - entriesLength, err := client.XLen(ctx, meta.streamName).Result() + entriesLength, err := client.XLen(ctx, meta.StreamName).Result() if err != nil { return -1, err } @@ -212,7 +238,7 @@ func createEntriesCountFn(client redis.Cmdable, meta *redisStreamsMetadata) (ent err := errors.New("could not find Redis version number") return -1, err } - groups, err := client.XInfoGroups(ctx, meta.streamName).Result() + groups, err := client.XInfoGroups(ctx, meta.StreamName).Result() // If XINFO GROUPS can't find the stream key, it hasn't been created // yet. In that case, we return a lag of 0. @@ -225,13 +251,13 @@ func createEntriesCountFn(client redis.Cmdable, meta *redisStreamsMetadata) (ent numGroups := len(groups) for i := 0; i < numGroups; i++ { group := groups[i] - if group.Name == meta.consumerGroupName { + if group.Name == meta.ConsumerGroupName { return group.Lag, nil } } // There is an edge case where the Redis producer has set up the - // stream [meta.streamName], but the consumer group [meta.consumerGroupName] + // stream [meta.StreamName], but the consumer group [meta.ConsumerGroupName] // for that stream isn't registered with Redis. In other words, the // producer has created messages for the stream, but the consumer group // hasn't yet registered itself on Redis because scaling starts with 0 @@ -240,7 +266,7 @@ func createEntriesCountFn(client redis.Cmdable, meta *redisStreamsMetadata) (ent // it's not possible to obtain the lag for a nonexistent consumer // group. From here, the consumer group gets instantiated, and scaling // again occurs according to XINFO GROUP lag. - entriesLength, err := client.XLen(ctx, meta.streamName).Result() + entriesLength, err := client.XLen(ctx, meta.StreamName).Result() if err != nil { return -1, err } @@ -255,84 +281,18 @@ func createEntriesCountFn(client redis.Cmdable, meta *redisStreamsMetadata) (ent var ( // ErrRedisMissingStreamName is returned when "stream" is missing. ErrRedisMissingStreamName = errors.New("missing redis stream name") -) - -func parseRedisStreamsMetadata(config *scalersconfig.ScalerConfig, parseFn redisAddressParser) (*redisStreamsMetadata, error) { - connInfo, err := parseFn(config.TriggerMetadata, config.ResolvedEnv, config.AuthParams) - if err != nil { - return nil, err - } - meta := redisStreamsMetadata{ - connectionInfo: connInfo, - } - - err = parseTLSConfigIntoConnectionInfo(config, &meta.connectionInfo) - if err != nil { - return nil, err - } - - if val, ok := config.TriggerMetadata[streamNameMetadata]; ok { - meta.streamName = val - } else { - return nil, ErrRedisMissingStreamName - } - - meta.activationLagCount = defaultActivationLagCount - - if val, ok := config.TriggerMetadata[consumerGroupNameMetadata]; ok { - meta.consumerGroupName = val - if val, ok := config.TriggerMetadata[lagMetadata]; ok { - meta.scaleFactor = lagFactor - lag, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("error parsing lag: %w", err) - } - meta.targetLag = lag - - if val, ok := config.TriggerMetadata[activationValueTriggerConfigName]; ok { - activationVal, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, errors.New("error while parsing activation lag value") - } - meta.activationLagCount = activationVal - } else { - err := errors.New("activationLagCount required for Redis lag") - return nil, err - } - } else { - meta.scaleFactor = xPendingFactor - meta.targetPendingEntriesCount = defaultTargetEntries - if val, ok := config.TriggerMetadata[pendingEntriesCountMetadata]; ok { - pendingEntriesCount, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("error parsing pending entries count: %w", err) - } - meta.targetPendingEntriesCount = pendingEntriesCount - } - } - } else { - meta.scaleFactor = xLengthFactor - meta.targetStreamLength = defaultTargetEntries - if val, ok := config.TriggerMetadata[streamLengthMetadata]; ok { - streamLength, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("error parsing stream length: %w", err) - } - meta.targetStreamLength = streamLength - } - } - meta.databaseIndex = defaultDBIndex - if val, ok := config.TriggerMetadata[databaseIndexMetadata]; ok { - dbIndex, err := strconv.ParseInt(val, 10, 32) - if err != nil { - return nil, fmt.Errorf("error parsing redis database index %w", err) - } - meta.databaseIndex = int(dbIndex) - } + // ErrRedisStreamParse is returned when missing parameters or parsing parameters error. + ErrRedisStreamParse = errors.New("error parsing redis stream metadata") +) +func parseRedisStreamsMetadata(config *scalersconfig.ScalerConfig) (*redisStreamsMetadata, error) { + meta := &redisStreamsMetadata{} meta.triggerIndex = config.TriggerIndex - return &meta, nil + if err := config.TypedConfig(meta); err != nil { + return nil, fmt.Errorf("error parsing redis stream metadata: %w", err) + } + return meta, nil } func (s *redisStreamsScaler) Close(context.Context) error { @@ -345,16 +305,16 @@ func (s *redisStreamsScaler) GetMetricSpecForScaling(context.Context) []v2.Metri switch s.metadata.scaleFactor { case xPendingFactor: - metricValue = s.metadata.targetPendingEntriesCount + metricValue = s.metadata.TargetPendingEntriesCount case xLengthFactor: - metricValue = s.metadata.targetStreamLength + metricValue = s.metadata.TargetStreamLength case lagFactor: - metricValue = s.metadata.targetLag + metricValue = s.metadata.TargetLag } externalMetric := &v2.ExternalMetricSource{ Metric: v2.MetricIdentifier{ - Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("redis-streams-%s", s.metadata.streamName))), + Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, kedautil.NormalizeString(fmt.Sprintf("redis-streams-%s", s.metadata.StreamName))), }, Target: GetMetricTarget(s.metricType, metricValue), } @@ -372,5 +332,5 @@ func (s *redisStreamsScaler) GetMetricsAndActivity(ctx context.Context, metricNa } metric := GenerateMetricInMili(metricName, float64(metricCount)) - return []external_metrics.ExternalMetricValue{metric}, metricCount > s.metadata.activationLagCount, nil + return []external_metrics.ExternalMetricValue{metric}, metricCount > s.metadata.ActivationLagCount, nil } diff --git a/pkg/scalers/redis_streams_scaler_test.go b/pkg/scalers/redis_streams_scaler_test.go index 0accba44f0b..205775b542b 100644 --- a/pkg/scalers/redis_streams_scaler_test.go +++ b/pkg/scalers/redis_streams_scaler_test.go @@ -49,25 +49,25 @@ func TestParseRedisStreamsMetadata(t *testing.T) { for _, tc := range testCasesPending { tc := tc t.Run(tc.name, func(te *testing.T) { - m, err := parseRedisStreamsMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: tc.metadata, ResolvedEnv: tc.resolvedEnv, AuthParams: tc.authParams}, parseRedisAddress) + m, err := parseRedisStreamsMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: tc.metadata, ResolvedEnv: tc.resolvedEnv, AuthParams: tc.authParams}) assert.Nil(t, err) - assert.Equal(t, tc.metadata[streamNameMetadata], m.streamName) - assert.Equal(t, tc.metadata[consumerGroupNameMetadata], m.consumerGroupName) - assert.Equal(t, tc.metadata[pendingEntriesCountMetadata], strconv.FormatInt(m.targetPendingEntriesCount, 10)) + assert.Equal(t, tc.metadata[streamNameMetadata], m.StreamName) + assert.Equal(t, tc.metadata[consumerGroupNameMetadata], m.ConsumerGroupName) + assert.Equal(t, tc.metadata[pendingEntriesCountMetadata], strconv.FormatInt(m.TargetPendingEntriesCount, 10)) if authParams != nil { // if authParam is used - assert.Equal(t, authParams[usernameMetadata], m.connectionInfo.username) - assert.Equal(t, authParams[passwordMetadata], m.connectionInfo.password) + assert.Equal(t, authParams[usernameMetadata], m.ConnectionInfo.Username) + assert.Equal(t, authParams[passwordMetadata], m.ConnectionInfo.Password) } else { // if metadata is used to pass credentials' env var names - assert.Equal(t, tc.resolvedEnv[tc.metadata[usernameMetadata]], m.connectionInfo.username) - assert.Equal(t, tc.resolvedEnv[tc.metadata[passwordMetadata]], m.connectionInfo.password) + assert.Equal(t, tc.resolvedEnv[tc.metadata[usernameMetadata]], m.ConnectionInfo.Username) + assert.Equal(t, tc.resolvedEnv[tc.metadata[passwordMetadata]], m.ConnectionInfo.Password) } - assert.Equal(t, tc.metadata[databaseIndexMetadata], strconv.Itoa(m.databaseIndex)) + assert.Equal(t, tc.metadata[databaseIndexMetadata], strconv.Itoa(m.DatabaseIndex)) b, err := strconv.ParseBool(tc.metadata[enableTLSMetadata]) assert.Nil(t, err) - assert.Equal(t, b, m.connectionInfo.enableTLS) + assert.Equal(t, b, m.ConnectionInfo.EnableTLS) }) } @@ -99,25 +99,25 @@ func TestParseRedisStreamsMetadata(t *testing.T) { for _, tc := range testCasesLag { tc := tc t.Run(tc.name, func(te *testing.T) { - m, err := parseRedisStreamsMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: tc.metadata, ResolvedEnv: tc.resolvedEnv, AuthParams: tc.authParams}, parseRedisAddress) + m, err := parseRedisStreamsMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: tc.metadata, ResolvedEnv: tc.resolvedEnv, AuthParams: tc.authParams}) assert.Nil(t, err) - assert.Equal(t, m.streamName, tc.metadata[streamNameMetadata]) - assert.Equal(t, m.consumerGroupName, tc.metadata[consumerGroupNameMetadata]) - assert.Equal(t, strconv.FormatInt(m.targetLag, 10), tc.metadata[lagMetadata]) + assert.Equal(t, m.StreamName, tc.metadata[streamNameMetadata]) + assert.Equal(t, m.ConsumerGroupName, tc.metadata[consumerGroupNameMetadata]) + assert.Equal(t, strconv.FormatInt(m.TargetLag, 10), tc.metadata[lagMetadata]) if authParams != nil { // if authParam is used - assert.Equal(t, m.connectionInfo.username, authParams[usernameMetadata]) - assert.Equal(t, m.connectionInfo.password, authParams[passwordMetadata]) + assert.Equal(t, m.ConnectionInfo.Username, authParams[usernameMetadata]) + assert.Equal(t, m.ConnectionInfo.Password, authParams[passwordMetadata]) } else { // if metadata is used to pass credentials' env var names - assert.Equal(t, m.connectionInfo.username, tc.resolvedEnv[tc.metadata[usernameMetadata]]) - assert.Equal(t, m.connectionInfo.password, tc.resolvedEnv[tc.metadata[passwordMetadata]]) + assert.Equal(t, m.ConnectionInfo.Username, tc.resolvedEnv[tc.metadata[usernameMetadata]]) + assert.Equal(t, m.ConnectionInfo.Password, tc.resolvedEnv[tc.metadata[passwordMetadata]]) } - assert.Equal(t, strconv.Itoa(m.databaseIndex), tc.metadata[databaseIndexMetadata]) + assert.Equal(t, strconv.Itoa(m.DatabaseIndex), tc.metadata[databaseIndexMetadata]) b, err := strconv.ParseBool(tc.metadata[enableTLSMetadata]) assert.Nil(t, err) - assert.Equal(t, m.connectionInfo.enableTLS, b) + assert.Equal(t, m.ConnectionInfo.EnableTLS, b) }) } } @@ -160,7 +160,7 @@ func TestParseRedisStreamsMetadataForInvalidCases(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.name, func(te *testing.T) { - _, err := parseRedisStreamsMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: tc.metadata, ResolvedEnv: tc.resolvedEnv, AuthParams: map[string]string{}}, parseRedisAddress) + _, err := parseRedisStreamsMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: tc.metadata, ResolvedEnv: tc.resolvedEnv, AuthParams: map[string]string{}}) assert.NotNil(t, err) }) } @@ -191,7 +191,7 @@ func TestRedisStreamsGetMetricSpecForScaling(t *testing.T) { } for _, testData := range redisStreamMetricIdentifiers { - meta, err := parseRedisStreamsMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, ResolvedEnv: map[string]string{"REDIS_SERVICE": "my-address"}, AuthParams: testData.metadataTestData.authParams, TriggerIndex: testData.triggerIndex}, parseRedisAddress) + meta, err := parseRedisStreamsMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, ResolvedEnv: map[string]string{"REDIS_SERVICE": "my-address"}, AuthParams: testData.metadataTestData.authParams, TriggerIndex: testData.triggerIndex}) if err != nil { t.Fatal("Could not parse metadata:", err) } @@ -250,7 +250,7 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "pendingEntriesCount": "invalid", }, wantMeta: nil, - wantErr: strconv.ErrSyntax, + wantErr: ErrRedisStreamParse, }, { name: "invalid lag", @@ -263,7 +263,7 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "lagCount": "junk", }, wantMeta: nil, - wantErr: strconv.ErrSyntax, + wantErr: ErrRedisStreamParse, }, { name: "address is defined in auth params", @@ -277,13 +277,13 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 6, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 6, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, }, scaleFactor: lagFactor, }, @@ -300,12 +300,12 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, }, scaleFactor: xPendingFactor, }, @@ -324,15 +324,15 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "ports": "1, 2, 3", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 6, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 6, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, }, scaleFactor: lagFactor, }, @@ -350,14 +350,14 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "ports": "1, 2, 3", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, }, scaleFactor: xPendingFactor, }, @@ -377,16 +377,16 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "username": "username", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, scaleFactor: lagFactor, }, @@ -405,15 +405,15 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "username": "username", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, scaleFactor: xPendingFactor, }, @@ -431,15 +431,15 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, scaleFactor: xPendingFactor, }, @@ -459,16 +459,16 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "none", }, scaleFactor: lagFactor, }, @@ -487,15 +487,15 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "none", }, scaleFactor: xPendingFactor, }, @@ -515,16 +515,16 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", }, scaleFactor: lagFactor, }, @@ -543,15 +543,15 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", }, scaleFactor: xPendingFactor, }, @@ -571,16 +571,16 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "none", }, scaleFactor: lagFactor, }, @@ -599,15 +599,15 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "none", }, scaleFactor: xPendingFactor, }, @@ -628,18 +628,18 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - unsafeSsl: false, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + UnsafeSsl: false, }, scaleFactor: lagFactor, }, @@ -659,17 +659,17 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - unsafeSsl: false, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + UnsafeSsl: false, }, scaleFactor: xPendingFactor, }, @@ -691,18 +691,18 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - unsafeSsl: true, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + UnsafeSsl: true, }, scaleFactor: lagFactor, }, @@ -723,17 +723,17 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - unsafeSsl: true, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + UnsafeSsl: true, }, scaleFactor: xPendingFactor, }, @@ -757,20 +757,20 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "keyPassword": "keeyPassword", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - ca: "caaa", - cert: "ceert", - key: "keey", - keyPassword: "keeyPassword", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + Ca: "caaa", + Cert: "ceert", + Key: "keey", + KeyPassword: "keeyPassword", }, scaleFactor: xPendingFactor, }, @@ -785,10 +785,10 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetStreamLength: 5, - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, + StreamName: "my-stream", + TargetStreamLength: 5, + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, }, scaleFactor: xLengthFactor, }, @@ -804,13 +804,13 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - targetLag: 0, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + TargetLag: 0, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, }, scaleFactor: xPendingFactor, }, @@ -826,9 +826,9 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { ResolvedEnv: c.resolvedEnv, AuthParams: c.authParams, } - meta, err := parseRedisStreamsMetadata(config, parseRedisClusterAddress) + meta, err := parseRedisStreamsMetadata(config) if c.wantErr != nil { - assert.ErrorIs(t, err, c.wantErr) + assert.ErrorContains(t, err, c.wantErr.Error()) } else { assert.NoError(t, err) } @@ -882,7 +882,7 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "activationLagCount": "3", }, wantMeta: nil, - wantErr: strconv.ErrSyntax, + wantErr: ErrRedisStreamParse, }, { name: "address is defined in auth params", @@ -896,13 +896,13 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, }, scaleFactor: lagFactor, }, @@ -919,12 +919,12 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "addresses": ":7001, :7002", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{":7001", ":7002"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{":7001", ":7002"}, }, scaleFactor: xPendingFactor, }, @@ -943,15 +943,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "ports": "1, 2, 3", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, }, scaleFactor: lagFactor, }, @@ -969,14 +969,14 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "ports": "1, 2, 3", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, }, scaleFactor: xPendingFactor, }, @@ -996,16 +996,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "username": "username", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, scaleFactor: lagFactor, }, @@ -1024,15 +1024,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "username": "username", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, scaleFactor: xPendingFactor, }, @@ -1051,15 +1051,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + StreamName: "my-stream", + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, scaleFactor: lagFactor, }, @@ -1077,15 +1077,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "username", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "username", }, scaleFactor: xPendingFactor, }, @@ -1105,16 +1105,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "none", }, scaleFactor: lagFactor, }, @@ -1133,15 +1133,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - username: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Username: "none", }, scaleFactor: xPendingFactor, }, @@ -1161,16 +1161,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", }, scaleFactor: lagFactor, }, @@ -1189,15 +1189,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", }, scaleFactor: xPendingFactor, }, @@ -1217,16 +1217,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "none", }, scaleFactor: lagFactor, }, @@ -1245,15 +1245,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "none", }, scaleFactor: xPendingFactor, }, @@ -1273,16 +1273,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "sentinelUsername": "sentinelUsername", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "sentinelUsername", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "sentinelUsername", }, scaleFactor: lagFactor, }, @@ -1301,15 +1301,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "sentinelUsername": "sentinelUsername", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "sentinelUsername", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "sentinelUsername", }, scaleFactor: xPendingFactor, }, @@ -1328,16 +1328,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "sentinelUsername", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "sentinelUsername", }, scaleFactor: lagFactor, }, @@ -1355,15 +1355,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "sentinelUsername", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "sentinelUsername", }, scaleFactor: xPendingFactor, }, @@ -1383,16 +1383,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "none", }, scaleFactor: lagFactor, }, @@ -1411,15 +1411,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelUsername: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelUsername: "none", }, scaleFactor: xPendingFactor, }, @@ -1439,16 +1439,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "sentinelPassword": "sentinelPassword", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelPassword: "sentinelPassword", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelPassword: "sentinelPassword", }, scaleFactor: lagFactor, }, @@ -1467,15 +1467,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "sentinelPassword": "sentinelPassword", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelPassword: "sentinelPassword", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelPassword: "sentinelPassword", }, scaleFactor: xPendingFactor, }, @@ -1495,16 +1495,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelPassword: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelPassword: "none", }, scaleFactor: lagFactor, }, @@ -1523,15 +1523,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelPassword: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelPassword: "none", }, scaleFactor: xPendingFactor, }, @@ -1551,16 +1551,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "sentinelMaster": "sentinelMaster", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "sentinelMaster", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "sentinelMaster", }, scaleFactor: lagFactor, }, @@ -1579,15 +1579,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "sentinelMaster": "sentinelMaster", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "sentinelMaster", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "sentinelMaster", }, scaleFactor: xPendingFactor, }, @@ -1606,16 +1606,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "sentinelMaster", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "sentinelMaster", }, scaleFactor: lagFactor, }, @@ -1633,15 +1633,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "sentinelMaster", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "sentinelMaster", }, scaleFactor: xPendingFactor, }, @@ -1661,16 +1661,16 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "none", }, scaleFactor: lagFactor, }, @@ -1689,15 +1689,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - sentinelMaster: "none", + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + SentinelMaster: "none", }, scaleFactor: xPendingFactor, }, @@ -1718,18 +1718,18 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - unsafeSsl: false, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + UnsafeSsl: false, }, scaleFactor: lagFactor, }, @@ -1749,17 +1749,17 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - unsafeSsl: false, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + UnsafeSsl: false, }, scaleFactor: xPendingFactor, }, @@ -1781,18 +1781,18 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - unsafeSsl: true, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + UnsafeSsl: true, }, scaleFactor: lagFactor, }, @@ -1813,17 +1813,17 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { "password": "password", }, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, - password: "password", - enableTLS: true, - unsafeSsl: true, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, + Password: "password", + EnableTLS: true, + UnsafeSsl: true, }, scaleFactor: xPendingFactor, }, @@ -1839,15 +1839,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetStreamLength: 15, - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1"}, - hosts: []string{"a"}, - ports: []string{"1"}, - password: "", - enableTLS: false, - unsafeSsl: false, + StreamName: "my-stream", + TargetStreamLength: 15, + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1"}, + Hosts: []string{"a"}, + Ports: []string{"1"}, + Password: "", + EnableTLS: false, + UnsafeSsl: false, }, scaleFactor: xLengthFactor, }, @@ -1866,18 +1866,18 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 70, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1"}, - hosts: []string{"a"}, - ports: []string{"1"}, - password: "", - enableTLS: false, - unsafeSsl: false, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 70, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1"}, + Hosts: []string{"a"}, + Ports: []string{"1"}, + Password: "", + EnableTLS: false, + UnsafeSsl: false, }, scaleFactor: lagFactor, }, @@ -1895,17 +1895,17 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 5, - activationLagCount: 0, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1"}, - hosts: []string{"a"}, - ports: []string{"1"}, - password: "", - enableTLS: false, - unsafeSsl: false, + StreamName: "my-stream", + TargetPendingEntriesCount: 5, + ActivationLagCount: 0, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1"}, + Hosts: []string{"a"}, + Ports: []string{"1"}, + Password: "", + EnableTLS: false, + UnsafeSsl: false, }, scaleFactor: xPendingFactor, }, @@ -1922,15 +1922,15 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { }, authParams: map[string]string{}, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetStreamLength: 15, - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1"}, - hosts: []string{"a"}, - ports: []string{"1"}, - password: "", - enableTLS: false, - unsafeSsl: false, + StreamName: "my-stream", + TargetStreamLength: 15, + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1"}, + Hosts: []string{"a"}, + Ports: []string{"1"}, + Password: "", + EnableTLS: false, + UnsafeSsl: false, }, scaleFactor: xLengthFactor, }, @@ -1946,9 +1946,9 @@ func TestParseRedisSentinelStreamsMetadata(t *testing.T) { ResolvedEnv: c.resolvedEnv, AuthParams: c.authParams, } - meta, err := parseRedisStreamsMetadata(config, parseRedisSentinelAddress) + meta, err := parseRedisStreamsMetadata(config) if c.wantErr != nil { - assert.ErrorIs(t, err, c.wantErr) + assert.ErrorContains(t, err, c.wantErr.Error()) } else { assert.NoError(t, err) } @@ -1982,15 +1982,15 @@ func TestActivityCount(t *testing.T) { authParams: map[string]string{}, resolvedEnv: testRedisResolvedEnv, wantMeta: &redisStreamsMetadata{ - streamName: "my-stream", - targetPendingEntriesCount: 0, - targetLag: 7, - activationLagCount: 3, - consumerGroupName: "consumer1", - connectionInfo: redisConnectionInfo{ - addresses: []string{"a:1", "b:2", "c:3"}, - hosts: []string{"a", "b", "c"}, - ports: []string{"1", "2", "3"}, + StreamName: "my-stream", + TargetPendingEntriesCount: 0, + TargetLag: 7, + ActivationLagCount: 3, + ConsumerGroupName: "consumer1", + ConnectionInfo: redisConnectionInfo{ + Addresses: []string{"a:1", "b:2", "c:3"}, + Hosts: []string{"a", "b", "c"}, + Ports: []string{"1", "2", "3"}, }, scaleFactor: lagFactor, }, @@ -2002,7 +2002,7 @@ func TestActivityCount(t *testing.T) { ResolvedEnv: c.resolvedEnv, AuthParams: c.authParams, } - meta, err := parseRedisStreamsMetadata(config, parseRedisClusterAddress) + meta, err := parseRedisStreamsMetadata(config) if c.wantErr != nil { assert.ErrorIs(t, err, c.wantErr) } else { diff --git a/pkg/scalers/scalersconfig/typed_config.go b/pkg/scalers/scalersconfig/typed_config.go index 1833c49d628..4e61f3e288d 100644 --- a/pkg/scalers/scalersconfig/typed_config.go +++ b/pkg/scalers/scalersconfig/typed_config.go @@ -62,8 +62,6 @@ const ( // separators for map and slice elements const ( - elemSeparator = "," - // TODO: support custom separator https://github.com/kedacore/keda/pull/5961/files#r1694991497 elemKeyValSeparator = "=" ) @@ -77,6 +75,7 @@ const ( enumTag = "enum" exclusiveSetTag = "exclusiveSet" rangeTag = "range" + separatorTag = "separator" ) // Params is a struct that represents the parameter list that can be used in the keda tag @@ -84,8 +83,8 @@ type Params struct { // FieldName is the name of the field in the struct FieldName string - // Name is the 'name' tag parameter defining the key in triggerMetadata, resolvedEnv or authParams - Name string + // Names is the 'name' tag parameter defining the key in triggerMetadata, resolvedEnv or authParams + Names []string // Optional is the 'optional' tag parameter defining if the parameter is optional Optional bool @@ -110,11 +109,19 @@ type Params struct { // RangeSeparator is the 'range' tag parameter defining the separator for range values RangeSeparator string + + // Separator is the tag parameter to define which separator will be used + Separator string +} + +// Name returns the name of the parameter (or comma separated list of names if it has multiple) +func (p Params) Name() string { + return strings.Join(p.Names, ",") } // IsNested is a function that returns true if the parameter is nested func (p Params) IsNested() bool { - return p.Name == "" + return len(p.Names) == 0 } // IsDeprecated is a function that returns true if the parameter is deprecated @@ -186,7 +193,7 @@ func (sc *ScalerConfig) parseTypedConfig(typedConfig any, parentOptional bool) e func (sc *ScalerConfig) setValue(field reflect.Value, params Params) error { valFromConfig, exists := sc.configParamValue(params) if exists && params.IsDeprecated() { - return fmt.Errorf("parameter %q is deprecated%v", params.Name, params.DeprecatedMessage()) + return fmt.Errorf("parameter %q is deprecated%v", params.Name(), params.DeprecatedMessage()) } if !exists && params.Default != "" { exists = true @@ -199,9 +206,9 @@ func (sc *ScalerConfig) setValue(field reflect.Value, params Params) error { if len(params.Order) == 0 { apo := maps.Keys(allowedParsingOrderMap) slices.Sort(apo) - return fmt.Errorf("missing required parameter %q, no 'order' tag, provide any from %v", params.Name, apo) + return fmt.Errorf("missing required parameter %q, no 'order' tag, provide any from %v", params.Name(), apo) } - return fmt.Errorf("missing required parameter %q in %v", params.Name, params.Order) + return fmt.Errorf("missing required parameter %q in %v", params.Name(), params.Order) } if params.Enum != nil { enumMap := make(map[string]bool) @@ -209,7 +216,7 @@ func (sc *ScalerConfig) setValue(field reflect.Value, params Params) error { enumMap[e] = true } missingMap := make(map[string]bool) - split := strings.Split(valFromConfig, elemSeparator) + split := splitWithSeparator(valFromConfig, params.Separator) for _, s := range split { s := strings.TrimSpace(s) if !enumMap[s] { @@ -217,7 +224,7 @@ func (sc *ScalerConfig) setValue(field reflect.Value, params Params) error { } } if len(missingMap) > 0 { - return fmt.Errorf("parameter %q value %q must be one of %v", params.Name, valFromConfig, params.Enum) + return fmt.Errorf("parameter %q value %q must be one of %v", params.Name(), valFromConfig, params.Enum) } } if params.ExclusiveSet != nil { @@ -225,7 +232,7 @@ func (sc *ScalerConfig) setValue(field reflect.Value, params Params) error { for _, e := range params.ExclusiveSet { exclusiveMap[e] = true } - split := strings.Split(valFromConfig, elemSeparator) + split := splitWithSeparator(valFromConfig, params.Separator) exclusiveCount := 0 for _, s := range split { s := strings.TrimSpace(s) @@ -234,7 +241,7 @@ func (sc *ScalerConfig) setValue(field reflect.Value, params Params) error { } } if exclusiveCount > 1 { - return fmt.Errorf("parameter %q value %q must contain only one of %v", params.Name, valFromConfig, params.ExclusiveSet) + return fmt.Errorf("parameter %q value %q must contain only one of %v", params.Name(), valFromConfig, params.ExclusiveSet) } } if params.IsNested() { @@ -248,7 +255,7 @@ func (sc *ScalerConfig) setValue(field reflect.Value, params Params) error { return sc.parseTypedConfig(field.Addr().Interface(), params.Optional) } if err := setConfigValueHelper(params, valFromConfig, field); err != nil { - return fmt.Errorf("unable to set param %q value %q: %w", params.Name, valFromConfig, err) + return fmt.Errorf("unable to set param %q value %q: %w", params.Name(), valFromConfig, err) } return nil } @@ -277,7 +284,7 @@ func setConfigValueURLParams(params Params, valFromConfig string, field reflect. // setConfigValueMap is a function that sets the value of the map field func setConfigValueMap(params Params, valFromConfig string, field reflect.Value) error { field.Set(reflect.MakeMap(reflect.MapOf(field.Type().Key(), field.Type().Elem()))) - split := strings.Split(valFromConfig, elemSeparator) + split := splitWithSeparator(valFromConfig, params.Separator) for _, s := range split { s := strings.TrimSpace(s) kv := strings.Split(s, elemKeyValSeparator) @@ -315,6 +322,15 @@ func canRange(valFromConfig, elemRangeSeparator string, field reflect.Value) boo return strings.Contains(valFromConfig, elemRangeSeparator) } +// splitWithSeparator is a function that splits on default or custom separator +func splitWithSeparator(valFromConfig, customSeparator string) []string { + separator := "," + if customSeparator != "" { + separator = customSeparator + } + return strings.Split(valFromConfig, separator) +} + // setConfigValueRange is a function that sets the value of the range field func setConfigValueRange(params Params, valFromConfig string, field reflect.Value) error { rangeSplit := strings.Split(valFromConfig, params.RangeSeparator) @@ -343,7 +359,7 @@ func setConfigValueRange(params Params, valFromConfig string, field reflect.Valu // setConfigValueSlice is a function that sets the value of the slice field func setConfigValueSlice(params Params, valFromConfig string, field reflect.Value) error { elemIfc := reflect.New(field.Type().Elem()).Interface() - split := strings.Split(valFromConfig, elemSeparator) + split := splitWithSeparator(valFromConfig, params.Separator) for i, s := range split { s := strings.TrimSpace(s) if canRange(s, params.RangeSeparator, field) { @@ -395,23 +411,24 @@ func setConfigValueHelper(params Params, valFromConfig string, field reflect.Val func (sc *ScalerConfig) configParamValue(params Params) (string, bool) { for _, po := range params.Order { var m map[string]string - key := params.Name - switch po { - case TriggerMetadata: - m = sc.TriggerMetadata - case AuthParams: - m = sc.AuthParams - case ResolvedEnv: - m = sc.ResolvedEnv - key = sc.TriggerMetadata[fmt.Sprintf("%sFromEnv", params.Name)] - default: - // this is checked when parsing the tags but adding as default case to avoid any potential future problems - return "", false - } - param, ok := m[key] - param = strings.TrimSpace(param) - if ok && param != "" { - return param, true + for _, key := range params.Names { + switch po { + case TriggerMetadata: + m = sc.TriggerMetadata + case AuthParams: + m = sc.AuthParams + case ResolvedEnv: + m = sc.ResolvedEnv + key = sc.TriggerMetadata[fmt.Sprintf("%sFromEnv", key)] + default: + // this is checked when parsing the tags but adding as default case to avoid any potential future problems + return "", false + } + param, ok := m[key] + param = strings.TrimSpace(param) + if ok && param != "" { + return param, true + } } } return "", params.IsNested() @@ -447,7 +464,7 @@ func paramsFromTag(tag string, field reflect.StructField) (Params, error) { } case nameTag: if len(tsplit) > 1 { - params.Name = strings.TrimSpace(tsplit[1]) + params.Names = strings.Split(strings.TrimSpace(tsplit[1]), tagValueSeparator) } case deprecatedTag: if len(tsplit) == 1 { @@ -474,6 +491,10 @@ func paramsFromTag(tag string, field reflect.StructField) (Params, error) { if len(tsplit) == 2 { params.RangeSeparator = strings.TrimSpace(tsplit[1]) } + case separatorTag: + if len(tsplit) > 1 { + params.Separator = strings.TrimSpace(tsplit[1]) + } case "": continue default: diff --git a/pkg/scalers/scalersconfig/typed_config_test.go b/pkg/scalers/scalersconfig/typed_config_test.go index 26b189c8dc5..866311f574a 100644 --- a/pkg/scalers/scalersconfig/typed_config_test.go +++ b/pkg/scalers/scalersconfig/typed_config_test.go @@ -128,7 +128,7 @@ func TestMissing(t *testing.T) { sc := &ScalerConfig{} type testStruct struct { - StringVal string `keda:"name=stringVal, order=triggerMetadata"` + StringVal string `keda:"name=stringVal, order=triggerMetadata"` } ts := testStruct{} @@ -216,14 +216,16 @@ func TestSlice(t *testing.T) { sc := &ScalerConfig{ TriggerMetadata: map[string]string{ - "sliceVal": "1,2,3", - "sliceValWithSpaces": "1, 2, 3", + "sliceVal": "1,2,3", + "sliceValWithSpaces": "1, 2, 3", + "sliceValWithOtherSeparator": "1;2;3", }, } type testStruct struct { - SliceVal []int `keda:"name=sliceVal, order=triggerMetadata"` - SliceValWithSpaces []int `keda:"name=sliceValWithSpaces, order=triggerMetadata"` + SliceVal []int `keda:"name=sliceVal, order=triggerMetadata"` + SliceValWithSpaces []int `keda:"name=sliceValWithSpaces, order=triggerMetadata"` + SliceValWithOtherSeparator []int `keda:"name=sliceValWithOtherSeparator, order=triggerMetadata, separator=;"` } ts := testStruct{} @@ -237,6 +239,10 @@ func TestSlice(t *testing.T) { Expect(ts.SliceValWithSpaces[0]).To(Equal(1)) Expect(ts.SliceValWithSpaces[1]).To(Equal(2)) Expect(ts.SliceValWithSpaces[2]).To(Equal(3)) + Expect(ts.SliceValWithOtherSeparator).To(HaveLen(3)) + Expect(ts.SliceValWithOtherSeparator[0]).To(Equal(1)) + Expect(ts.SliceValWithOtherSeparator[1]).To(Equal(2)) + Expect(ts.SliceValWithOtherSeparator[2]).To(Equal(3)) } // TestEnum tests the enum type @@ -497,7 +503,7 @@ func TestNoParsingOrder(t *testing.T) { } tsm := testStructMissing{} err := sc.TypedConfig(&tsm) - Expect(err).To(MatchError(`missing required parameter "strVal", no 'order' tag, provide any from [authParams resolvedEnv triggerMetadata]`)) + Expect(err).To(MatchError(ContainSubstring(`missing required parameter "strVal", no 'order' tag, provide any from [authParams resolvedEnv triggerMetadata]`))) type testStructDefault struct { DefaultVal string `keda:"name=defaultVal, default=dv"` @@ -547,3 +553,33 @@ func TestRange(t *testing.T) { Expect(ts.DottedRange).To(ConsistOf(2, 3, 4, 5, 6, 7)) Expect(ts.WrongRange).To(HaveLen(0)) } + +// TestMultiName tests the multi name param +func TestMultiName(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + TriggerMetadata: map[string]string{ + "property1": "aaa", + }, + } + + sc2 := &ScalerConfig{ + TriggerMetadata: map[string]string{ + "property2": "bbb", + }, + } + + type testStruct struct { + Property string `keda:"name=property1;property2, order=triggerMetadata"` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.Property).To(Equal("aaa")) + + err = sc2.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.Property).To(Equal("bbb")) +} diff --git a/pkg/scalers/selenium_grid_scaler.go b/pkg/scalers/selenium_grid_scaler.go index 4e1f75667ad..3cba72fbc9e 100644 --- a/pkg/scalers/selenium_grid_scaler.go +++ b/pkg/scalers/selenium_grid_scaler.go @@ -29,13 +29,20 @@ 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"` + Password string `keda:"name=password, order=authParams;resolvedEnv;triggerMetadata, optional"` TargetValue int64 } @@ -46,6 +53,7 @@ type seleniumResponse struct { type data struct { Grid grid `json:"grid"` + NodesInfo nodesInfo `json:"nodesInfo"` SessionsInfo sessionsInfo `json:"sessionsInfo"` } @@ -71,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" @@ -114,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 } @@ -152,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 { @@ -164,6 +188,9 @@ func (s *seleniumGridScaler) getSessionsCount(ctx context.Context, logger logr.L return -1, err } + // Add HTTP Auth + req.SetBasicAuth(s.metadata.Username, s.metadata.Password) + res, err := s.httpClient.Do(req) if err != nil { return -1, err @@ -179,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{} @@ -217,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++ @@ -231,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 95e24743c1e..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,43 @@ 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", + }, + }, + { + name: "valid username and password in AuthParams, url, browsername, and sessionbrowsername should return metadata", + args: args{ + config: &scalersconfig.ScalerConfig{ + AuthParams: map[string]string{ + "username": "username", + "password": "password", + }, + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "browserName": "MicrosoftEdge", + "sessionBrowserName": "msedge", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + URL: "http://selenium-hub:4444/graphql", + BrowserName: "MicrosoftEdge", + SessionBrowserName: "msedge", + TargetValue: 1, + BrowserVersion: "latest", + PlatformName: "linux", + Username: "username", + Password: "password", + SessionsPerNode: 1, + SessionBrowserVersion: "latest", }, }, { @@ -624,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", }, }, { @@ -648,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", }, }, { @@ -688,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", }, }, { @@ -714,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/pkg/scaling/resolver/aws_secretmanager_handler.go b/pkg/scaling/resolver/aws_secretmanager_handler.go index 3d10ba2e1e5..6c07281262a 100644 --- a/pkg/scaling/resolver/aws_secretmanager_handler.go +++ b/pkg/scaling/resolver/aws_secretmanager_handler.go @@ -86,6 +86,7 @@ func (ash *AwsSecretManagerHandler) Initialize(ctx context.Context, client clien return fmt.Errorf("AccessKeyID and AccessSecretKey are expected when not using a pod identity provider") } case kedav1alpha1.PodIdentityProviderAws: + ash.awsMetadata.UsingPodIdentity = true if ash.secretManager.PodIdentity.IsWorkloadIdentityOwner() { awsRoleArn, err := resolveServiceAccountAnnotation(ctx, client, podSpec.ServiceAccountName, triggerNamespace, kedav1alpha1.PodIdentityAnnotationEKS, true) if err != nil { diff --git a/tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go b/tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go index 55aaa463c5a..ae00ea8173d 100644 --- a/tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go +++ b/tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go @@ -42,6 +42,7 @@ var ( clusterName = "test-cluster" expectedSubject = fmt.Sprintf("/%s/%s/scaledobject/%s", clusterName, namespace, scaledObjectName) expectedSource = fmt.Sprintf("/%s/keda/keda", clusterName) + expectedType = "keda.scaledobject.ready.v1" monitoredDeploymentName = "monitored-deployment" sutDeploymentName = "sut-deployment" scaledObjectName = fmt.Sprintf("%s-so", testName) @@ -270,13 +271,17 @@ func checkMessage(t *testing.T, count int, client *azservicebus.Client) { assert.NoErrorf(t, err, "cannot receive messages - %s", err) assert.NotEmpty(t, messages) - var message = string(messages[0].Body) - - event := messaging.CloudEvent{} - err = json.Unmarshal([]byte(message), &event) - assert.NoErrorf(t, err, "cannot retrieve message - %s", err) + 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 + } + } - assert.Equal(t, expectedSubject, *event.Subject) - assert.Equal(t, expectedSource, event.Source) - assert.Equal(t, "keda.scaledobject.ready.v1", event.Type) + assert.True(t, found) } diff --git a/tests/internals/initial_delay_cooldownperiod/initial_delay_cooldownperiod_test.go b/tests/internals/initial_delay_cooldownperiod/initial_delay_cooldownperiod_test.go index bfc7f307b95..e70999ce996 100644 --- a/tests/internals/initial_delay_cooldownperiod/initial_delay_cooldownperiod_test.go +++ b/tests/internals/initial_delay_cooldownperiod/initial_delay_cooldownperiod_test.go @@ -78,13 +78,10 @@ spec: scaleDown: stabilizationWindowSeconds: 15 triggers: - - type: cron + - type: kubernetes-workload metadata: - timezone: Etc/UTC - start: {{.StartMin}} * * * * - end: {{.EndMin}} * * * * - desiredReplicas: '0' -` + podSelector: 'pod=no-matches' + value: '1'` ) func TestScaler(t *testing.T) { diff --git a/vendor/github.com/dennwc/varint/.gitignore b/vendor/github.com/dennwc/varint/.gitignore new file mode 100644 index 00000000000..9385b6db18b --- /dev/null +++ b/vendor/github.com/dennwc/varint/.gitignore @@ -0,0 +1,2 @@ +*.o +*.txt \ No newline at end of file diff --git a/vendor/github.com/dennwc/varint/.travis.yml b/vendor/github.com/dennwc/varint/.travis.yml new file mode 100644 index 00000000000..b3da258f520 --- /dev/null +++ b/vendor/github.com/dennwc/varint/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - 1.12.x + +env: + - GO111MODULE=on \ No newline at end of file diff --git a/vendor/github.com/dennwc/varint/LICENSE b/vendor/github.com/dennwc/varint/LICENSE new file mode 100644 index 00000000000..8b3f68715c1 --- /dev/null +++ b/vendor/github.com/dennwc/varint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Denys Smirnov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/dennwc/varint/README.md b/vendor/github.com/dennwc/varint/README.md new file mode 100644 index 00000000000..fe15b3b5003 --- /dev/null +++ b/vendor/github.com/dennwc/varint/README.md @@ -0,0 +1,47 @@ +# varint + +This package provides an optimized implementation of protobuf's varint encoding/decoding. +It has no dependencies. + +Benchmarks comparing to a `binary.Uvarint`: + +``` +benchmark old ns/op new ns/op delta +BenchmarkUvarint/1-8 4.13 2.85 -30.99% +BenchmarkUvarint/1_large-8 4.01 2.28 -43.14% +BenchmarkUvarint/2-8 6.23 2.87 -53.93% +BenchmarkUvarint/2_large-8 5.60 2.86 -48.93% +BenchmarkUvarint/3-8 6.55 3.44 -47.48% +BenchmarkUvarint/3_large-8 6.54 2.86 -56.27% +BenchmarkUvarint/4-8 7.30 3.71 -49.18% +BenchmarkUvarint/4_large-8 7.46 3.10 -58.45% +BenchmarkUvarint/5-8 8.31 4.12 -50.42% +BenchmarkUvarint/5_large-8 8.56 3.48 -59.35% +BenchmarkUvarint/6-8 9.42 4.66 -50.53% +BenchmarkUvarint/6_large-8 9.91 4.07 -58.93% +BenchmarkUvarint/7-8 10.6 5.28 -50.19% +BenchmarkUvarint/7_large-8 11.0 4.70 -57.27% +BenchmarkUvarint/8-8 11.7 6.02 -48.55% +BenchmarkUvarint/8_large-8 12.1 5.19 -57.11% +BenchmarkUvarint/9-8 12.9 6.83 -47.05% +BenchmarkUvarint/9_large-8 13.1 5.71 -56.41% +``` + +It also provides additional functionality like `UvarintSize` (similar to `sov*` in `gogo/protobuf`): + +``` +benchmark old ns/op new ns/op delta +BenchmarkUvarintSize/1-8 1.71 0.43 -74.85% +BenchmarkUvarintSize/2-8 2.56 0.57 -77.73% +BenchmarkUvarintSize/3-8 3.22 0.72 -77.64% +BenchmarkUvarintSize/4-8 3.74 0.72 -80.75% +BenchmarkUvarintSize/5-8 4.29 0.57 -86.71% +BenchmarkUvarintSize/6-8 4.85 0.58 -88.04% +BenchmarkUvarintSize/7-8 5.43 0.71 -86.92% +BenchmarkUvarintSize/8-8 6.01 0.86 -85.69% +BenchmarkUvarintSize/9-8 6.64 1.00 -84.94% +``` + +# License + +MIT \ No newline at end of file diff --git a/vendor/github.com/dennwc/varint/proto.go b/vendor/github.com/dennwc/varint/proto.go new file mode 100644 index 00000000000..e3b458547f9 --- /dev/null +++ b/vendor/github.com/dennwc/varint/proto.go @@ -0,0 +1,244 @@ +package varint + +// ProtoTag decodes a protobuf's field number and wire type pair +// from buf and returns that value and the number of bytes read (> 0). +// If an error occurred, n = 0 is returned. +func ProtoTag(buf []byte) (num int, typ byte, n int) { + // Same unrolled implementation as in Uvarint. + // + // But this time we can check if the wire type and field num + // are valid when reading the first byte. + // + // Also, the swifts are now different, because first 3 bits + // are for the wire type. + // + // The implementation will stop at 9 bytes, returning an error. + sz := len(buf) + if sz == 0 { + return 0, 0, 0 + } + const ( + bit = 1 << 7 + mask = bit - 1 + step = 7 + + // protobuf + typBits = 3 + typMask = 1<<3 - 1 + ) + if sz >= 9 { // no bound checks + // i == 0 + b := buf[0] + if b == 0 { + return 0, 0, 0 + } + typ = b & typMask + if typ > 5 { + return 0, 0, 0 + } + if b < bit { + num = int(b >> typBits) + if num == 0 { + return 0, 0, 0 + } + n = 1 + return + } + num = int((b & mask) >> typBits) + var s uint = step - typBits + + // i == 1 + b = buf[1] + if b < bit { + num |= int(b) << s + n = 2 + return + } + num |= int(b&mask) << s + s += step + + // i == 2 + b = buf[2] + if b < bit { + num |= int(b) << s + n = 3 + return + } + num |= int(b&mask) << s + s += step + + // i == 3 + b = buf[3] + if b < bit { + num |= int(b) << s + n = 4 + return + } + num |= int(b&mask) << s + s += step + + // i == 4 + b = buf[4] + if b < bit { + num |= int(b) << s + n = 5 + return + } + num |= int(b&mask) << s + s += step + + // i == 5 + b = buf[5] + if b < bit { + num |= int(b) << s + n = 6 + return + } + num |= int(b&mask) << s + s += step + + // i == 6 + b = buf[6] + if b < bit { + num |= int(b) << s + n = 7 + return + } + num |= int(b&mask) << s + s += step + + // i == 7 + b = buf[7] + if b < bit { + num |= int(b) << s + n = 8 + return + } + num |= int(b&mask) << s + s += step + + // i == 8 + b = buf[8] + if b < bit { + num |= int(b) << s + n = 9 + return + } + return 0, 0, 0 // too much + } + + // i == 0 + b := buf[0] + if b == 0 { + return 0, 0, 0 + } + typ = b & typMask + if typ > 5 { + return 0, 0, 0 + } + if b < bit { + num = int(b >> typBits) + if num == 0 { + return 0, 0, 0 + } + n = 1 + return + } else if sz == 1 { + return 0, 0, 0 + } + num = int((b & mask) >> typBits) + var s uint = step - typBits + + // i == 1 + b = buf[1] + if b < bit { + num |= int(b) << s + n = 2 + return + } else if sz == 2 { + return 0, 0, 0 + } + num |= int(b&mask) << s + s += step + + // i == 2 + b = buf[2] + if b < bit { + num |= int(b) << s + n = 3 + return + } else if sz == 3 { + return 0, 0, 0 + } + num |= int(b&mask) << s + s += step + + // i == 3 + b = buf[3] + if b < bit { + num |= int(b) << s + n = 4 + return + } else if sz == 4 { + return 0, 0, 0 + } + num |= int(b&mask) << s + s += step + + // i == 4 + b = buf[4] + if b < bit { + num |= int(b) << s + n = 5 + return + } else if sz == 5 { + return 0, 0, 0 + } + num |= int(b&mask) << s + s += step + + // i == 5 + b = buf[5] + if b < bit { + num |= int(b) << s + n = 6 + return + } else if sz == 6 { + return 0, 0, 0 + } + num |= int(b&mask) << s + s += step + + // i == 6 + b = buf[6] + if b < bit { + num |= int(b) << s + n = 7 + return + } else if sz == 7 { + return 0, 0, 0 + } + num |= int(b&mask) << s + s += step + + // i == 7 + b = buf[7] + if b < bit { + num |= int(b) << s + n = 8 + return + } else if sz == 8 { + return 0, 0, 0 + } + num |= int(b&mask) << s + s += step + + // i == 8 + b = buf[8] + if b < bit { + num |= int(b) << s + n = 9 + return + } + return 0, 0, 0 // too much +} diff --git a/vendor/github.com/dennwc/varint/varint.go b/vendor/github.com/dennwc/varint/varint.go new file mode 100644 index 00000000000..83278c2d7d2 --- /dev/null +++ b/vendor/github.com/dennwc/varint/varint.go @@ -0,0 +1,270 @@ +package varint + +const maxUint64 = uint64(1<<64 - 1) + +// MaxLenN is the maximum length of a varint-encoded N-bit integer. +const ( + MaxLen8 = 2 + MaxLen16 = 3 + MaxLen32 = 5 + MaxLen64 = 10 +) + +// MaxValN is the maximum varint-encoded integer that fits in N bytes. +const ( + MaxVal9 = maxUint64 >> (1 + iota*7) + MaxVal8 + MaxVal7 + MaxVal6 + MaxVal5 + MaxVal4 + MaxVal3 + MaxVal2 + MaxVal1 +) + +// UvarintSize returns the number of bytes necessary to encode a given uint. +func UvarintSize(x uint64) int { + if x <= MaxVal4 { + if x <= MaxVal1 { + return 1 + } else if x <= MaxVal2 { + return 2 + } else if x <= MaxVal3 { + return 3 + } + return 4 + } + if x <= MaxVal5 { + return 5 + } else if x <= MaxVal6 { + return 6 + } else if x <= MaxVal7 { + return 7 + } else if x <= MaxVal8 { + return 8 + } else if x <= MaxVal9 { + return 9 + } + return 10 +} + +// Uvarint decodes a uint64 from buf and returns that value and the +// number of bytes read (> 0). If an error occurred, the value is 0 +// and the number of bytes n is <= 0 meaning: +// +// n == 0: buf too small +// n < 0: value larger than 64 bits (overflow) +// and -n is the number of bytes read +// +func Uvarint(buf []byte) (uint64, int) { + // Fully unrolled implementation of binary.Uvarint. + // + // It will also eliminate bound checks for buffers larger than 9 bytes. + sz := len(buf) + if sz == 0 { + return 0, 0 + } + const ( + step = 7 + bit = 1 << 7 + mask = bit - 1 + ) + if sz >= 10 { // no bound checks + // i == 0 + b := buf[0] + if b < bit { + return uint64(b), 1 + } + x := uint64(b & mask) + var s uint = step + + // i == 1 + b = buf[1] + if b < bit { + return x | uint64(b)< 1 { + return 0, -10 // overflow + } + return x | uint64(b)< 1 { + return 0, -10 // overflow + } + return x | uint64(b)< 100 { +// level.Error(logger).Log("value", value) +// } +// +// NewFilter allows precise control over what happens when a log event is +// emitted without a level key, or if a squelched level is used. Check the +// Option functions for details. +package level diff --git a/vendor/github.com/go-kit/log/level/level.go b/vendor/github.com/go-kit/log/level/level.go new file mode 100644 index 00000000000..c641d985524 --- /dev/null +++ b/vendor/github.com/go-kit/log/level/level.go @@ -0,0 +1,256 @@ +package level + +import ( + "errors" + "strings" + + "github.com/go-kit/log" +) + +// ErrInvalidLevelString is returned whenever an invalid string is passed to Parse. +var ErrInvalidLevelString = errors.New("invalid level string") + +// Error returns a logger that includes a Key/ErrorValue pair. +func Error(logger log.Logger) log.Logger { + return log.WithPrefix(logger, Key(), ErrorValue()) +} + +// Warn returns a logger that includes a Key/WarnValue pair. +func Warn(logger log.Logger) log.Logger { + return log.WithPrefix(logger, Key(), WarnValue()) +} + +// Info returns a logger that includes a Key/InfoValue pair. +func Info(logger log.Logger) log.Logger { + return log.WithPrefix(logger, Key(), InfoValue()) +} + +// Debug returns a logger that includes a Key/DebugValue pair. +func Debug(logger log.Logger) log.Logger { + return log.WithPrefix(logger, Key(), DebugValue()) +} + +// NewFilter wraps next and implements level filtering. See the commentary on +// the Option functions for a detailed description of how to configure levels. +// If no options are provided, all leveled log events created with Debug, +// Info, Warn or Error helper methods are squelched and non-leveled log +// events are passed to next unmodified. +func NewFilter(next log.Logger, options ...Option) log.Logger { + l := &logger{ + next: next, + } + for _, option := range options { + option(l) + } + return l +} + +type logger struct { + next log.Logger + allowed level + squelchNoLevel bool + errNotAllowed error + errNoLevel error +} + +func (l *logger) Log(keyvals ...interface{}) error { + var hasLevel, levelAllowed bool + for i := 1; i < len(keyvals); i += 2 { + if v, ok := keyvals[i].(*levelValue); ok { + hasLevel = true + levelAllowed = l.allowed&v.level != 0 + break + } + } + if !hasLevel && l.squelchNoLevel { + return l.errNoLevel + } + if hasLevel && !levelAllowed { + return l.errNotAllowed + } + return l.next.Log(keyvals...) +} + +// Option sets a parameter for the leveled logger. +type Option func(*logger) + +// Allow the provided log level to pass. +func Allow(v Value) Option { + switch v { + case debugValue: + return AllowDebug() + case infoValue: + return AllowInfo() + case warnValue: + return AllowWarn() + case errorValue: + return AllowError() + default: + return AllowNone() + } +} + +// AllowAll is an alias for AllowDebug. +func AllowAll() Option { + return AllowDebug() +} + +// AllowDebug allows error, warn, info and debug level log events to pass. +func AllowDebug() Option { + return allowed(levelError | levelWarn | levelInfo | levelDebug) +} + +// AllowInfo allows error, warn and info level log events to pass. +func AllowInfo() Option { + return allowed(levelError | levelWarn | levelInfo) +} + +// AllowWarn allows error and warn level log events to pass. +func AllowWarn() Option { + return allowed(levelError | levelWarn) +} + +// AllowError allows only error level log events to pass. +func AllowError() Option { + return allowed(levelError) +} + +// AllowNone allows no leveled log events to pass. +func AllowNone() Option { + return allowed(0) +} + +func allowed(allowed level) Option { + return func(l *logger) { l.allowed = allowed } +} + +// Parse a string to its corresponding level value. Valid strings are "debug", +// "info", "warn", and "error". Strings are normalized via strings.TrimSpace and +// strings.ToLower. +func Parse(level string) (Value, error) { + switch strings.TrimSpace(strings.ToLower(level)) { + case debugValue.name: + return debugValue, nil + case infoValue.name: + return infoValue, nil + case warnValue.name: + return warnValue, nil + case errorValue.name: + return errorValue, nil + default: + return nil, ErrInvalidLevelString + } +} + +// ParseDefault calls Parse and returns the default Value on error. +func ParseDefault(level string, def Value) Value { + v, err := Parse(level) + if err != nil { + return def + } + return v +} + +// ErrNotAllowed sets the error to return from Log when it squelches a log +// event disallowed by the configured Allow[Level] option. By default, +// ErrNotAllowed is nil; in this case the log event is squelched with no +// error. +func ErrNotAllowed(err error) Option { + return func(l *logger) { l.errNotAllowed = err } +} + +// SquelchNoLevel instructs Log to squelch log events with no level, so that +// they don't proceed through to the wrapped logger. If SquelchNoLevel is set +// to true and a log event is squelched in this way, the error value +// configured with ErrNoLevel is returned to the caller. +func SquelchNoLevel(squelch bool) Option { + return func(l *logger) { l.squelchNoLevel = squelch } +} + +// ErrNoLevel sets the error to return from Log when it squelches a log event +// with no level. By default, ErrNoLevel is nil; in this case the log event is +// squelched with no error. +func ErrNoLevel(err error) Option { + return func(l *logger) { l.errNoLevel = err } +} + +// NewInjector wraps next and returns a logger that adds a Key/level pair to +// the beginning of log events that don't already contain a level. In effect, +// this gives a default level to logs without a level. +func NewInjector(next log.Logger, level Value) log.Logger { + return &injector{ + next: next, + level: level, + } +} + +type injector struct { + next log.Logger + level interface{} +} + +func (l *injector) Log(keyvals ...interface{}) error { + for i := 1; i < len(keyvals); i += 2 { + if _, ok := keyvals[i].(*levelValue); ok { + return l.next.Log(keyvals...) + } + } + kvs := make([]interface{}, len(keyvals)+2) + kvs[0], kvs[1] = key, l.level + copy(kvs[2:], keyvals) + return l.next.Log(kvs...) +} + +// Value is the interface that each of the canonical level values implement. +// It contains unexported methods that prevent types from other packages from +// implementing it and guaranteeing that NewFilter can distinguish the levels +// defined in this package from all other values. +type Value interface { + String() string + levelVal() +} + +// Key returns the unique key added to log events by the loggers in this +// package. +func Key() interface{} { return key } + +// ErrorValue returns the unique value added to log events by Error. +func ErrorValue() Value { return errorValue } + +// WarnValue returns the unique value added to log events by Warn. +func WarnValue() Value { return warnValue } + +// InfoValue returns the unique value added to log events by Info. +func InfoValue() Value { return infoValue } + +// DebugValue returns the unique value added to log events by Debug. +func DebugValue() Value { return debugValue } + +var ( + // key is of type interface{} so that it allocates once during package + // initialization and avoids allocating every time the value is added to a + // []interface{} later. + key interface{} = "level" + + errorValue = &levelValue{level: levelError, name: "error"} + warnValue = &levelValue{level: levelWarn, name: "warn"} + infoValue = &levelValue{level: levelInfo, name: "info"} + debugValue = &levelValue{level: levelDebug, name: "debug"} +) + +type level byte + +const ( + levelDebug level = 1 << iota + levelInfo + levelWarn + levelError +) + +type levelValue struct { + name string + level +} + +func (v *levelValue) String() string { return v.name } +func (v *levelValue) levelVal() {} diff --git a/vendor/github.com/go-kit/log/log.go b/vendor/github.com/go-kit/log/log.go new file mode 100644 index 00000000000..62e11adace5 --- /dev/null +++ b/vendor/github.com/go-kit/log/log.go @@ -0,0 +1,179 @@ +package log + +import "errors" + +// Logger is the fundamental interface for all log operations. Log creates a +// log event from keyvals, a variadic sequence of alternating keys and values. +// Implementations must be safe for concurrent use by multiple goroutines. In +// particular, any implementation of Logger that appends to keyvals or +// modifies or retains any of its elements must make a copy first. +type Logger interface { + Log(keyvals ...interface{}) error +} + +// ErrMissingValue is appended to keyvals slices with odd length to substitute +// the missing value. +var ErrMissingValue = errors.New("(MISSING)") + +// With returns a new contextual logger with keyvals prepended to those passed +// to calls to Log. If logger is also a contextual logger created by With, +// WithPrefix, or WithSuffix, keyvals is appended to the existing context. +// +// The returned Logger replaces all value elements (odd indexes) containing a +// Valuer with their generated value for each call to its Log method. +func With(logger Logger, keyvals ...interface{}) Logger { + if len(keyvals) == 0 { + return logger + } + l := newContext(logger) + kvs := append(l.keyvals, keyvals...) + if len(kvs)%2 != 0 { + kvs = append(kvs, ErrMissingValue) + } + return &context{ + logger: l.logger, + // Limiting the capacity of the stored keyvals ensures that a new + // backing array is created if the slice must grow in Log or With. + // Using the extra capacity without copying risks a data race that + // would violate the Logger interface contract. + keyvals: kvs[:len(kvs):len(kvs)], + hasValuer: l.hasValuer || containsValuer(keyvals), + sKeyvals: l.sKeyvals, + sHasValuer: l.sHasValuer, + } +} + +// WithPrefix returns a new contextual logger with keyvals prepended to those +// passed to calls to Log. If logger is also a contextual logger created by +// With, WithPrefix, or WithSuffix, keyvals is prepended to the existing context. +// +// The returned Logger replaces all value elements (odd indexes) containing a +// Valuer with their generated value for each call to its Log method. +func WithPrefix(logger Logger, keyvals ...interface{}) Logger { + if len(keyvals) == 0 { + return logger + } + l := newContext(logger) + // Limiting the capacity of the stored keyvals ensures that a new + // backing array is created if the slice must grow in Log or With. + // Using the extra capacity without copying risks a data race that + // would violate the Logger interface contract. + n := len(l.keyvals) + len(keyvals) + if len(keyvals)%2 != 0 { + n++ + } + kvs := make([]interface{}, 0, n) + kvs = append(kvs, keyvals...) + if len(kvs)%2 != 0 { + kvs = append(kvs, ErrMissingValue) + } + kvs = append(kvs, l.keyvals...) + return &context{ + logger: l.logger, + keyvals: kvs, + hasValuer: l.hasValuer || containsValuer(keyvals), + sKeyvals: l.sKeyvals, + sHasValuer: l.sHasValuer, + } +} + +// WithSuffix returns a new contextual logger with keyvals appended to those +// passed to calls to Log. If logger is also a contextual logger created by +// With, WithPrefix, or WithSuffix, keyvals is appended to the existing context. +// +// The returned Logger replaces all value elements (odd indexes) containing a +// Valuer with their generated value for each call to its Log method. +func WithSuffix(logger Logger, keyvals ...interface{}) Logger { + if len(keyvals) == 0 { + return logger + } + l := newContext(logger) + // Limiting the capacity of the stored keyvals ensures that a new + // backing array is created if the slice must grow in Log or With. + // Using the extra capacity without copying risks a data race that + // would violate the Logger interface contract. + n := len(l.sKeyvals) + len(keyvals) + if len(keyvals)%2 != 0 { + n++ + } + kvs := make([]interface{}, 0, n) + kvs = append(kvs, keyvals...) + if len(kvs)%2 != 0 { + kvs = append(kvs, ErrMissingValue) + } + kvs = append(l.sKeyvals, kvs...) + return &context{ + logger: l.logger, + keyvals: l.keyvals, + hasValuer: l.hasValuer, + sKeyvals: kvs, + sHasValuer: l.sHasValuer || containsValuer(keyvals), + } +} + +// context is the Logger implementation returned by With, WithPrefix, and +// WithSuffix. It wraps a Logger and holds keyvals that it includes in all +// log events. Its Log method calls bindValues to generate values for each +// Valuer in the context keyvals. +// +// A context must always have the same number of stack frames between calls to +// its Log method and the eventual binding of Valuers to their value. This +// requirement comes from the functional requirement to allow a context to +// resolve application call site information for a Caller stored in the +// context. To do this we must be able to predict the number of logging +// functions on the stack when bindValues is called. +// +// Two implementation details provide the needed stack depth consistency. +// +// 1. newContext avoids introducing an additional layer when asked to +// wrap another context. +// 2. With, WithPrefix, and WithSuffix avoid introducing an additional +// layer by returning a newly constructed context with a merged keyvals +// rather than simply wrapping the existing context. +type context struct { + logger Logger + keyvals []interface{} + sKeyvals []interface{} // suffixes + hasValuer bool + sHasValuer bool +} + +func newContext(logger Logger) *context { + if c, ok := logger.(*context); ok { + return c + } + return &context{logger: logger} +} + +// Log replaces all value elements (odd indexes) containing a Valuer in the +// stored context with their generated value, appends keyvals, and passes the +// result to the wrapped Logger. +func (l *context) Log(keyvals ...interface{}) error { + kvs := append(l.keyvals, keyvals...) + if len(kvs)%2 != 0 { + kvs = append(kvs, ErrMissingValue) + } + if l.hasValuer { + // If no keyvals were appended above then we must copy l.keyvals so + // that future log events will reevaluate the stored Valuers. + if len(keyvals) == 0 { + kvs = append([]interface{}{}, l.keyvals...) + } + bindValues(kvs[:(len(l.keyvals))]) + } + kvs = append(kvs, l.sKeyvals...) + if l.sHasValuer { + bindValues(kvs[len(kvs)-len(l.sKeyvals):]) + } + return l.logger.Log(kvs...) +} + +// LoggerFunc is an adapter to allow use of ordinary functions as Loggers. If +// f is a function with the appropriate signature, LoggerFunc(f) is a Logger +// object that calls f. +type LoggerFunc func(...interface{}) error + +// Log implements Logger by calling f(keyvals...). +func (f LoggerFunc) Log(keyvals ...interface{}) error { + return f(keyvals...) +} diff --git a/vendor/github.com/go-kit/log/logfmt_logger.go b/vendor/github.com/go-kit/log/logfmt_logger.go new file mode 100644 index 00000000000..a00305298b8 --- /dev/null +++ b/vendor/github.com/go-kit/log/logfmt_logger.go @@ -0,0 +1,62 @@ +package log + +import ( + "bytes" + "io" + "sync" + + "github.com/go-logfmt/logfmt" +) + +type logfmtEncoder struct { + *logfmt.Encoder + buf bytes.Buffer +} + +func (l *logfmtEncoder) Reset() { + l.Encoder.Reset() + l.buf.Reset() +} + +var logfmtEncoderPool = sync.Pool{ + New: func() interface{} { + var enc logfmtEncoder + enc.Encoder = logfmt.NewEncoder(&enc.buf) + return &enc + }, +} + +type logfmtLogger struct { + w io.Writer +} + +// NewLogfmtLogger returns a logger that encodes keyvals to the Writer in +// logfmt format. Each log event produces no more than one call to w.Write. +// The passed Writer must be safe for concurrent use by multiple goroutines if +// the returned Logger will be used concurrently. +func NewLogfmtLogger(w io.Writer) Logger { + return &logfmtLogger{w} +} + +func (l logfmtLogger) Log(keyvals ...interface{}) error { + enc := logfmtEncoderPool.Get().(*logfmtEncoder) + enc.Reset() + defer logfmtEncoderPool.Put(enc) + + if err := enc.EncodeKeyvals(keyvals...); err != nil { + return err + } + + // Add newline to the end of the buffer + if err := enc.EndRecord(); err != nil { + return err + } + + // The Logger interface requires implementations to be safe for concurrent + // use by multiple goroutines. For this implementation that means making + // only one call to l.w.Write() for each call to Log. + if _, err := l.w.Write(enc.buf.Bytes()); err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/go-kit/log/nop_logger.go b/vendor/github.com/go-kit/log/nop_logger.go new file mode 100644 index 00000000000..1047d626c43 --- /dev/null +++ b/vendor/github.com/go-kit/log/nop_logger.go @@ -0,0 +1,8 @@ +package log + +type nopLogger struct{} + +// NewNopLogger returns a logger that doesn't do anything. +func NewNopLogger() Logger { return nopLogger{} } + +func (nopLogger) Log(...interface{}) error { return nil } diff --git a/vendor/github.com/go-kit/log/staticcheck.conf b/vendor/github.com/go-kit/log/staticcheck.conf new file mode 100644 index 00000000000..528438b97d2 --- /dev/null +++ b/vendor/github.com/go-kit/log/staticcheck.conf @@ -0,0 +1 @@ +checks = ["all"] diff --git a/vendor/github.com/go-kit/log/stdlib.go b/vendor/github.com/go-kit/log/stdlib.go new file mode 100644 index 00000000000..0338edbe2ba --- /dev/null +++ b/vendor/github.com/go-kit/log/stdlib.go @@ -0,0 +1,151 @@ +package log + +import ( + "bytes" + "io" + "log" + "regexp" + "strings" +) + +// StdlibWriter implements io.Writer by invoking the stdlib log.Print. It's +// designed to be passed to a Go kit logger as the writer, for cases where +// it's necessary to redirect all Go kit log output to the stdlib logger. +// +// If you have any choice in the matter, you shouldn't use this. Prefer to +// redirect the stdlib log to the Go kit logger via NewStdlibAdapter. +type StdlibWriter struct{} + +// Write implements io.Writer. +func (w StdlibWriter) Write(p []byte) (int, error) { + log.Print(strings.TrimSpace(string(p))) + return len(p), nil +} + +// StdlibAdapter wraps a Logger and allows it to be passed to the stdlib +// logger's SetOutput. It will extract date/timestamps, filenames, and +// messages, and place them under relevant keys. +type StdlibAdapter struct { + Logger + timestampKey string + fileKey string + messageKey string + prefix string + joinPrefixToMsg bool +} + +// StdlibAdapterOption sets a parameter for the StdlibAdapter. +type StdlibAdapterOption func(*StdlibAdapter) + +// TimestampKey sets the key for the timestamp field. By default, it's "ts". +func TimestampKey(key string) StdlibAdapterOption { + return func(a *StdlibAdapter) { a.timestampKey = key } +} + +// FileKey sets the key for the file and line field. By default, it's "caller". +func FileKey(key string) StdlibAdapterOption { + return func(a *StdlibAdapter) { a.fileKey = key } +} + +// MessageKey sets the key for the actual log message. By default, it's "msg". +func MessageKey(key string) StdlibAdapterOption { + return func(a *StdlibAdapter) { a.messageKey = key } +} + +// Prefix configures the adapter to parse a prefix from stdlib log events. If +// you provide a non-empty prefix to the stdlib logger, then your should provide +// that same prefix to the adapter via this option. +// +// By default, the prefix isn't included in the msg key. Set joinPrefixToMsg to +// true if you want to include the parsed prefix in the msg. +func Prefix(prefix string, joinPrefixToMsg bool) StdlibAdapterOption { + return func(a *StdlibAdapter) { a.prefix = prefix; a.joinPrefixToMsg = joinPrefixToMsg } +} + +// NewStdlibAdapter returns a new StdlibAdapter wrapper around the passed +// logger. It's designed to be passed to log.SetOutput. +func NewStdlibAdapter(logger Logger, options ...StdlibAdapterOption) io.Writer { + a := StdlibAdapter{ + Logger: logger, + timestampKey: "ts", + fileKey: "caller", + messageKey: "msg", + } + for _, option := range options { + option(&a) + } + return a +} + +func (a StdlibAdapter) Write(p []byte) (int, error) { + p = a.handlePrefix(p) + + result := subexps(p) + keyvals := []interface{}{} + var timestamp string + if date, ok := result["date"]; ok && date != "" { + timestamp = date + } + if time, ok := result["time"]; ok && time != "" { + if timestamp != "" { + timestamp += " " + } + timestamp += time + } + if timestamp != "" { + keyvals = append(keyvals, a.timestampKey, timestamp) + } + if file, ok := result["file"]; ok && file != "" { + keyvals = append(keyvals, a.fileKey, file) + } + if msg, ok := result["msg"]; ok { + msg = a.handleMessagePrefix(msg) + keyvals = append(keyvals, a.messageKey, msg) + } + if err := a.Logger.Log(keyvals...); err != nil { + return 0, err + } + return len(p), nil +} + +func (a StdlibAdapter) handlePrefix(p []byte) []byte { + if a.prefix != "" { + p = bytes.TrimPrefix(p, []byte(a.prefix)) + } + return p +} + +func (a StdlibAdapter) handleMessagePrefix(msg string) string { + if a.prefix == "" { + return msg + } + + msg = strings.TrimPrefix(msg, a.prefix) + if a.joinPrefixToMsg { + msg = a.prefix + msg + } + return msg +} + +const ( + logRegexpDate = `(?P[0-9]{4}/[0-9]{2}/[0-9]{2})?[ ]?` + logRegexpTime = `(?P