├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 1_feature_request.yml │ ├── 2_bug_report.yml │ ├── 3_release_tracker.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── auto-add-issues-to-project.yml │ ├── build_canary.yml │ ├── build_release.yml │ ├── e2e-tests.yaml │ ├── images.yaml │ ├── linkinator.yaml │ └── tests.yaml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── .whitesource ├── ADOPTERS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── RELEASE-PROCESS.md ├── ROADMAP.md ├── cli └── README.md ├── config ├── crd │ ├── bases │ │ └── http.keda.sh_httpscaledobjects.yaml │ └── kustomization.yaml ├── default │ └── kustomization.yaml ├── interceptor │ ├── admin.service.yaml │ ├── deployment.yaml │ ├── e2e-test │ │ ├── otel │ │ │ ├── deployment.yaml │ │ │ ├── kustomization.yaml │ │ │ └── scaledobject.yaml │ │ └── tls │ │ │ ├── deployment.yaml │ │ │ ├── kustomization.yaml │ │ │ └── proxy.service.yaml │ ├── kustomization.yaml │ ├── metrics.service.yaml │ ├── proxy.service.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── scaledobject.yaml │ ├── service_account.yaml │ └── transformerconfig.yaml ├── operator │ ├── deployment.yaml │ ├── kustomization.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml └── scaler │ ├── deployment.yaml │ ├── e2e-test │ └── otel │ │ ├── deployment.yaml │ │ └── kustomization.yaml │ ├── kustomization.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── service.yaml │ └── service_account.yaml ├── docs ├── _config.yml ├── _includes │ └── head-custom.html ├── design.md ├── developing.md ├── faq.md ├── images │ ├── arch.excalidraw │ └── arch.png ├── install.md ├── integrations.md ├── operate.md ├── readme.md ├── ref │ ├── v0.1.0 │ │ └── http_scaled_object.md │ ├── v0.10.0 │ │ └── http_scaled_object.md │ ├── v0.2.0 │ │ └── http_scaled_object.md │ ├── v0.3.0 │ │ └── http_scaled_object.md │ ├── v0.6.0 │ │ └── http_scaled_object.md │ ├── v0.7.0 │ │ └── http_scaled_object.md │ ├── v0.8.0 │ │ └── http_scaled_object.md │ ├── v0.9.0 │ │ └── http_scaled_object.md │ └── vX.X.X │ │ └── http_scaled_object.md ├── scope.md ├── use_cases.md └── walkthrough.md ├── examples ├── v0.0.1 │ └── httpscaledobject.yaml ├── v0.0.2 │ └── httpscaledobject.yaml ├── v0.1.0 │ └── httpscaledobject.yaml ├── v0.10.0 │ └── httpscaledobject.yaml ├── v0.2.0 │ └── httpscaledobject.yaml ├── v0.3.0 │ └── httpscaledobject.yaml ├── v0.4.0 │ └── httpscaledobject.yaml ├── v0.5.0 │ └── httpscaledobject.yaml ├── v0.6.0 │ └── httpscaledobject.yaml ├── v0.7.0 │ └── httpscaledobject.yaml ├── v0.8.0 │ └── httpscaledobject.yaml ├── v0.9.0 │ └── httpscaledobject.yaml └── xkcd │ ├── .helmignore │ ├── Chart.yaml │ ├── NOTES.txt │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── externalservice.yaml │ ├── httproute.yaml │ ├── httpscaledobject.yaml │ ├── ingress.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt ├── tools.go ├── update-codegen.sh ├── update-mockgen.sh ├── validate-changelog.sh ├── verify-codegen.sh ├── verify-manifests.sh └── verify-mockgen.sh ├── interceptor ├── Dockerfile ├── config │ ├── metrics.go │ ├── serving.go │ ├── timeouts.go │ ├── tracing.go │ └── validate.go ├── forward_wait_func.go ├── forward_wait_func_test.go ├── handler │ ├── probe.go │ ├── probe_test.go │ ├── static.go │ ├── static_test.go │ ├── suite_test.go │ ├── upstream.go │ └── upstream_test.go ├── main.go ├── main_test.go ├── metrics │ ├── metricscollector.go │ ├── otelmetrics.go │ ├── otelmetrics_test.go │ ├── prommetrics.go │ └── prommetrics_test.go ├── middleware │ ├── counting.go │ ├── counting_test.go │ ├── logging.go │ ├── metrics.go │ ├── responsewriter.go │ ├── responsewriter_test.go │ ├── routing.go │ ├── routing_test.go │ └── suite_test.go ├── proxy_handlers.go ├── proxy_handlers_integration_test.go ├── proxy_handlers_test.go ├── suite_test.go └── tracing │ ├── tracing.go │ └── tracing_test.go ├── operator ├── .dockerignore ├── .gitignore ├── Dockerfile ├── PROJECT ├── apis │ └── http │ │ └── v1alpha1 │ │ ├── condition_types.go │ │ ├── groupversion_info.go │ │ ├── httpscaledobject_types.go │ │ └── zz_generated.deepcopy.go ├── controllers │ ├── http │ │ ├── app.go │ │ ├── condition_provider.go │ │ ├── config │ │ │ ├── config.go │ │ │ └── config_test.go │ │ ├── finalizer.go │ │ ├── httpscaledobject_controller.go │ │ ├── httpscaledobject_controller_test.go │ │ ├── ping.go │ │ ├── ping_test.go │ │ ├── scaled_object.go │ │ ├── scaled_object_test.go │ │ └── suite_test.go │ └── util │ │ └── predicate.go ├── generated │ ├── clientset │ │ └── versioned │ │ │ ├── clientset.go │ │ │ ├── fake │ │ │ ├── clientset_generated.go │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ ├── mock │ │ │ └── clientset.go │ │ │ ├── scheme │ │ │ ├── doc.go │ │ │ ├── mock │ │ │ │ ├── doc.go │ │ │ │ └── register.go │ │ │ └── register.go │ │ │ └── typed │ │ │ └── http │ │ │ └── v1alpha1 │ │ │ ├── doc.go │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── fake_http_client.go │ │ │ └── fake_httpscaledobject.go │ │ │ ├── generated_expansion.go │ │ │ ├── http_client.go │ │ │ ├── httpscaledobject.go │ │ │ └── mock │ │ │ ├── doc.go │ │ │ ├── generated_expansion.go │ │ │ ├── http_client.go │ │ │ └── httpscaledobject.go │ ├── informers │ │ └── externalversions │ │ │ ├── factory.go │ │ │ ├── generic.go │ │ │ ├── http │ │ │ ├── interface.go │ │ │ ├── mock │ │ │ │ └── interface.go │ │ │ └── v1alpha1 │ │ │ │ ├── httpscaledobject.go │ │ │ │ ├── interface.go │ │ │ │ └── mock │ │ │ │ ├── httpscaledobject.go │ │ │ │ └── interface.go │ │ │ ├── internalinterfaces │ │ │ ├── factory_interfaces.go │ │ │ └── mock │ │ │ │ └── factory_interfaces.go │ │ │ └── mock │ │ │ ├── factory.go │ │ │ └── generic.go │ └── listers │ │ └── http │ │ └── v1alpha1 │ │ ├── expansion_generated.go │ │ ├── httpscaledobject.go │ │ └── mock │ │ ├── expansion_generated.go │ │ └── httpscaledobject.go └── main.go ├── pkg ├── build │ └── version.go ├── env │ └── env.go ├── http │ ├── server.go │ ├── server_test.go │ └── test_utils.go ├── k8s │ ├── endpoints.go │ ├── endpoints_cache.go │ ├── endpoints_cache_fake.go │ ├── endpoints_cache_informer.go │ ├── endpoints_test.go │ ├── fake_endpoints.go │ ├── namespacedname.go │ ├── scaledobject.go │ ├── scheme.go │ ├── svc_cache.go │ └── val_ptrs.go ├── net │ ├── backoff.go │ ├── dial_context.go │ ├── dial_context_test.go │ └── mock_server.go ├── queue │ ├── bucketing.go │ ├── bucketing_test.go │ ├── queue.go │ ├── queue_counts.go │ ├── queue_counts_test.go │ ├── queue_fakes.go │ ├── queue_rpc.go │ ├── queue_rpc_test.go │ └── queue_test.go ├── routing │ ├── key.go │ ├── key_test.go │ ├── sharedindexinformer.go │ ├── suite_test.go │ ├── table.go │ ├── table_test.go │ ├── tablememory.go │ ├── tablememory_test.go │ └── test │ │ └── table.go └── util │ ├── async.go │ ├── atomicvalue.go │ ├── atomicvalue_test.go │ ├── context.go │ ├── contexthttp.go │ ├── env_resolver.go │ ├── env_resolver_test.go │ ├── errors.go │ ├── functional.go │ ├── healthcheck.go │ ├── reflect.go │ ├── signaler.go │ ├── signaler_test.go │ ├── stopwatch.go │ ├── stopwatch_test.go │ └── suite_test.go ├── scaler ├── Dockerfile ├── config.go ├── handlers.go ├── handlers_test.go ├── main.go ├── naming.go ├── queue_pinger.go └── queue_pinger_test.go └── tests ├── README.md ├── checks ├── httproute_in_app_namespace │ └── httproute_in_app_namespace_test.go ├── ingress_in_app_namespace │ └── ingress_in_app_namespace_test.go ├── ingress_in_keda_namespace │ └── ingress_in_keda_namespace_test.go ├── interceptor_otel_metrics │ └── interceptor_otel_metrics_test.go ├── interceptor_otel_tracing │ └── interceptor_otel_tracing_test.go ├── interceptor_prometheus_metrics │ └── interceptor_prometheus_metrics_test.go ├── interceptor_scaledobject │ └── interceptor_scaledobject_test.go ├── interceptor_tls │ └── interceptor_tls_test.go ├── internal_service │ └── internal_service_test.go ├── internal_service_port_name │ └── internal_service_port_name_test.go ├── multiple_hosts │ └── multiple_hosts_test.go ├── path_prefix │ └── path_prefix_test.go ├── scaling_phase_custom_resource │ └── scaling_phase_custom_resource_test.go ├── scaling_phase_deployment │ └── scaling_phase_deployment_test.go ├── scaling_phase_deployment_with_skip_so_creation │ └── scaling_phase_deployment_with_skip_so_creation_test.go └── scaling_phase_statefulset │ └── scaling_phase_statefulset_test.go ├── helper └── helper.go ├── run-all.go └── utils ├── cleanup_test.go └── setup_test.go /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at 2 | // https://github.com/microsoft/vscode-dev-containers/tree/master/containers/go 3 | { 4 | "name": "Go", 5 | "dockerFile": "Dockerfile", 6 | "runArgs": [ 7 | // Uncomment the next line to use a non-root user. On Linux, this will prevent 8 | // new files getting created as root, but you may need to update the USER_UID 9 | // and USER_GID in .devcontainer/Dockerfile to match your user if not 1000. 10 | // "-u", "vscode", 11 | 12 | // Mount go mod cache 13 | "-v", "keda-gomodcache:/go/pkg", 14 | // Cache vscode exentsions installs and homedir 15 | "-v", "keda-vscodecache:/root/.vscode-server", 16 | 17 | // Mount docker socket for docker builds 18 | "-v", "/var/run/docker.sock:/var/run/docker.sock", 19 | 20 | "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" 21 | ], 22 | 23 | // Use 'settings' to set *default* container specific settings.json values on container create. 24 | // You can edit these settings after create using File > Preferences > Settings > Remote. 25 | "settings": { 26 | "terminal.integrated.shell.linux": "/bin/bash", 27 | "go.gopath": "/go" 28 | }, 29 | 30 | // Add the IDs of extensions you want installed when the container is created in the array below. 31 | "extensions": [ 32 | "golang.go" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md 2 | *.yaml 3 | *.yml 4 | .* 5 | /LICENSE 6 | /bin 7 | /charts 8 | /cli 9 | /config 10 | /docs 11 | /examples 12 | /target 13 | /tests 14 | Dockerfile 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence 3 | * @kedacore/keda-http-contributors 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 🧭 2 | description: Suggest an idea for this project 3 | labels: "needs-discussion,feature-request" 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Proposal 8 | description: "What would you like to have as a feature" 9 | placeholder: "A clear and concise description of what you want to happen." 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Use-Case 15 | description: "How would this help you?" 16 | placeholder: "Tell us more what you'd like to achieve." 17 | validations: 18 | required: false 19 | - type: dropdown 20 | id: interested-in-implementing-the-feature 21 | attributes: 22 | label: Is this a feature you are interested in implementing yourself? 23 | options: 24 | - 'No' 25 | - 'Maybe' 26 | - 'Yes' 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: anything-else 31 | attributes: 32 | label: Anything else? 33 | description: "Let us know if you have anything else to share" 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Report a bug 🐛 2 | description: Create a report to help us improve 3 | labels: ["bug"] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Report 9 | description: "What bug have you encountered?" 10 | placeholder: "A clear and concise description of what the bug is." 11 | - type: textarea 12 | id: expected-behavior 13 | attributes: 14 | label: Expected Behavior 15 | description: What did you expect to happen? 16 | placeholder: What did you expect to happen? 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: actual-behavior 21 | attributes: 22 | label: Actual Behavior 23 | description: Also tell us, what did you see is happen? 24 | placeholder: Tell us what you see that is happening 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: repro-steps 29 | attributes: 30 | label: Steps to Reproduce the Problem 31 | description: "How can we reproduce this bug? Please walk us through it step by step." 32 | value: | 33 | 1. 34 | 2. 35 | 3. 36 | validations: 37 | required: true 38 | - type: textarea 39 | id: logs 40 | attributes: 41 | label: Logs from KEDA HTTP operator 42 | description: "Provide logs from the KEDA HTTP operator, if need be." 43 | value: | 44 | ``` 45 | example 46 | ``` 47 | validations: 48 | required: false 49 | - type: dropdown 50 | id: keda-http-version 51 | attributes: 52 | label: "HTTP Add-on Version" 53 | description: "What version of the KEDA HTTP Add-on are you running?" 54 | options: 55 | - "0.10.0" 56 | - "0.9.0" 57 | - "0.8.0" 58 | - "Other" 59 | validations: 60 | required: false 61 | - type: dropdown 62 | id: kubernetes-version 63 | attributes: 64 | label: Kubernetes Version 65 | description: What version of Kubernetes that are you running? 66 | options: 67 | - "1.32" 68 | - "1.31" 69 | - "1.30" 70 | - "< 1.30" 71 | - "Other" 72 | validations: 73 | required: false 74 | - type: dropdown 75 | id: cluster-type 76 | attributes: 77 | label: Platform 78 | description: Where is your cluster running? 79 | options: 80 | - Any 81 | - Alibaba Cloud 82 | - Amazon Web Services 83 | - Google Cloud 84 | - Microsoft Azure 85 | - Red Hat OpenShift 86 | - Other 87 | validations: 88 | required: false 89 | - type: textarea 90 | id: anything-else 91 | attributes: 92 | label: Anything else? 93 | description: "Let us know if you have anything else to share" 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_release_tracker.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: KEDA Release Tracker 3 | about: Template to keep track of the progress for a new KEDA HTTP add-on release. 4 | title: "Release: " 5 | labels: governance,release-management 6 | assignees: tomkerkhove,jorturfer 7 | --- 8 | 9 | This issue template is used to track the rollout of a new KEDA HTTP add-on version. 10 | 11 | For the full release process, we recommend reading [this document]([https://github.com/kedacore/keda/blob/main/RELEASE-PROCESS.md](https://github.com/kedacore/http-add-on/blob/main/RELEASE-PROCESS.md)). 12 | 13 | ## Required items 14 | 15 | - [ ] List items that are still open, but required for this release 16 | 17 | # Timeline 18 | 19 | We aim to release this release in the week of . 20 | 21 | ## Progress 22 | 23 | - [ ] Add the new version to GitHub Bug report template 24 | - [ ] Create KEDA release 25 | - [ ] Prepare & ship Helm chart 26 | - [ ] Publish on Artifact Hub ([repo](https://github.com/kedacore/external-scalers)) 27 | - [ ] Provide update in Slack 28 | - [ ] Tweet about new release 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask a question or get support 4 | url: https://github.com/kedacore/http-add-on/discussions/new 5 | about: Ask a question or request support for using KEDA 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | _Provide a description of what has been changed_ 8 | 9 | ### Checklist 10 | 11 | - [ ] Commits are signed with Developer Certificate of Origin (DCO) 12 | - [ ] Changelog has been updated and is aligned with our [changelog requirements](https://github.com/kedacore/keda/blob/main/CONTRIBUTING.md#Changelog) 13 | - [ ] Any necessary documentation is added, such as: 14 | - [`README.md`](/README.md) 15 | - [The `docs/` directory](./docs) 16 | - [The docs repo](https://github.com/kedacore/keda-docs) 17 | 18 | Fixes # 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | labels: 9 | - enhancement 10 | - dependency-management 11 | groups: 12 | all-updates: 13 | patterns: 14 | - "*" 15 | - package-ecosystem: gomod 16 | directory: "/" 17 | schedule: 18 | interval: weekly 19 | open-pull-requests-limit: 10 20 | labels: 21 | - enhancement 22 | - dependency-management 23 | groups: 24 | all-updates: 25 | patterns: 26 | - "*" 27 | - package-ecosystem: docker 28 | directory: "/" 29 | schedule: 30 | interval: weekly 31 | open-pull-requests-limit: 10 32 | labels: 33 | - enhancement 34 | - dependency-management 35 | groups: 36 | all-updates: 37 | patterns: 38 | - "*" 39 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - stale-bot-ignore 10 | - feature 11 | - security 12 | 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | 16 | # Set to true to ignore issues in a project (defaults to false) 17 | exemptProjects: false 18 | 19 | # Set to true to ignore issues in a milestone (defaults to false) 20 | exemptMilestones: false 21 | 22 | # Set to true to ignore issues with an assignee (defaults to false) 23 | exemptAssignees: false 24 | 25 | # Comment to post when marking an issue as stale. Set to `false` to disable 26 | markComment: > 27 | This issue has been automatically marked as stale because it has not had 28 | recent activity. It will be closed in 7 days if no further activity occurs. Thank you 29 | for your contributions. 30 | 31 | # Comment to post when removing the stale label. 32 | # unmarkComment: > 33 | # Your comment here. 34 | 35 | # Comment to post when closing a stale Issue or Pull Request. 36 | closeComment: > 37 | This issue has been automatically closed due to inactivity. 38 | 39 | # Limit the number of actions per hour, from 1-30. Default is 30 40 | limitPerRun: 30 41 | 42 | # Limit to only `issues` or `pulls` 43 | only: issues 44 | -------------------------------------------------------------------------------- /.github/workflows/auto-add-issues-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Automatically add new issue to backlog 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | track_issue: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Get project data 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GH_AUTOMATION_PAT }} 16 | ORGANIZATION: kedacore 17 | # This refers to our backlog project: https://github.com/orgs/kedacore/projects/6/views/1 18 | PROJECT_NUMBER: 6 19 | run: | 20 | gh api graphql -f query=' 21 | query($org: String!, $number: Int!) { 22 | organization(login: $org){ 23 | projectV2(number: $number) { 24 | id 25 | fields(first:20) { 26 | nodes { 27 | ... on ProjectV2Field { 28 | id 29 | name 30 | } 31 | } 32 | } 33 | } 34 | } 35 | }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json 36 | 37 | echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV 38 | - name: Add issue to project 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GH_AUTOMATION_PAT }} 41 | ISSUE_ID: ${{ github.event.issue.node_id }} 42 | run: | 43 | item_id="$( gh api graphql -f query=' 44 | mutation($project:ID!, $issue:ID!) { 45 | addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) { 46 | item { 47 | id 48 | } 49 | } 50 | }' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')" 51 | -------------------------------------------------------------------------------- /.github/workflows/build_canary.yml: -------------------------------------------------------------------------------- 1 | name: Publish canary image to GitHub Container Registry 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | id-token: write # needed for signing the images with GitHub OIDC Token **not production ready** 18 | 19 | container: ghcr.io/kedacore/keda-tools:1.23.6 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 22 | 23 | - name: Register workspace path 24 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 25 | 26 | - name: Login to GHCR 27 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 28 | with: 29 | # Username used to log in to a Docker registry. If not set then no login will occur 30 | username: ${{ github.repository_owner }} 31 | # Password or personal access token used to log in to a Docker registry. If not set then no login will occur 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | # Server address of Docker registry. If not set then will default to Docker Hub 34 | registry: ghcr.io 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 38 | 39 | - name: Publish on GitHub Container Registry 40 | run: make publish-multiarch 41 | env: 42 | VERSION: canary 43 | 44 | # https://github.com/sigstore/cosign-installer 45 | - name: Install Cosign 46 | uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 47 | 48 | - name: Check Cosign install! 49 | run: cosign version 50 | 51 | - name: Sign KEDA images published on GitHub Container Registry 52 | # This step uses the identity token to provision an ephemeral certificate 53 | # against the sigstore community Fulcio instance. 54 | run: make sign-images 55 | env: 56 | VERSION: canary 57 | -------------------------------------------------------------------------------- /.github/workflows/images.yaml: -------------------------------------------------------------------------------- 1 | name: Build Images 2 | on: 3 | pull_request: 4 | branches: [main] 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 8 | cancel-in-progress: true 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build_scaler: 15 | runs-on: ubuntu-latest 16 | container: ghcr.io/kedacore/keda-tools:1.23.6 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 19 | - name: Register workspace path 20 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 21 | - name: Build The Scaler 22 | run: | 23 | COMMIT=$(git rev-parse --short HEAD) 24 | VERSION=${COMMIT} make docker-build-scaler 25 | 26 | build_operator: 27 | runs-on: ubuntu-latest 28 | container: ghcr.io/kedacore/keda-tools:1.23.6 29 | steps: 30 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 31 | - name: Register workspace path 32 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 33 | - name: Build The Operator 34 | run: | 35 | COMMIT=$(git rev-parse --short=7 HEAD) 36 | VERSION=${COMMIT} make docker-build-operator 37 | 38 | build_interceptor: 39 | runs-on: ubuntu-latest 40 | container: ghcr.io/kedacore/keda-tools:1.23.6 41 | steps: 42 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 43 | - name: Register workspace path 44 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 45 | - name: Build The Interceptor 46 | run: | 47 | COMMIT=$(git rev-parse --short=7 HEAD) 48 | VERSION=${COMMIT} make docker-build-interceptor 49 | -------------------------------------------------------------------------------- /.github/workflows/linkinator.yaml: -------------------------------------------------------------------------------- 1 | name: Check links on all markdown documents 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | linkinator: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | - uses: JustinBeckwith/linkinator-action@3d5ba091319fa7b0ac14703761eebb7d100e6f6d # v1 21 | with: 22 | paths: "**/*.md" 23 | markdown: true 24 | retry: true 25 | linksToSkip: "https://github.com/kedacore/http-add-on/pkgs/container/http-add-on-interceptor, https://github.com/kedacore/http-add-on/pkgs/container/http-add-on-operator, https://github.com/kedacore/http-add-on/pkgs/container/http-add-on-scaler,http://opentelemetry-collector.open-telemetry-system:4318,http://opentelemetry-collector.open-telemetry-system:4318/v1/traces, https://www.gnu.org/software/make/" 26 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | validate: 17 | name: validate - ${{ matrix.name }} 18 | runs-on: ${{ matrix.runner }} 19 | container: ghcr.io/kedacore/keda-tools:1.23.6 20 | strategy: 21 | matrix: 22 | include: 23 | - runner: ARM64 24 | name: arm64 25 | - runner: ubuntu-latest 26 | name: amd64 27 | steps: 28 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 29 | 30 | - name: Register workspace path 31 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 32 | 33 | - name: Check go version 34 | run: go version 35 | 36 | - name: Set Go paths 37 | id: go-paths 38 | run: | 39 | echo ::set-output name=mod_cache::$(go env GOMODCACHE) 40 | echo ::set-output name=build_cache::$(go env GOCACHE) 41 | 42 | - name: Go modules cache 43 | uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 44 | with: 45 | path: ${{ steps.go-paths.outputs.mod_cache }} 46 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 47 | 48 | - name: Go build cache 49 | uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 50 | with: 51 | path: ${{ steps.go-paths.outputs.build_cache }} 52 | key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }} 53 | 54 | - name: Download Go Modules 55 | run: go mod download 56 | 57 | - name: Codegen 58 | run: make verify-codegen 59 | 60 | - name: Manifests 61 | run: make verify-manifests 62 | 63 | - name: Mockgen 64 | run: make verify-mockgen 65 | 66 | - name: Build 67 | run: ARCH=${{ matrix.name }} make build 68 | 69 | - name: Test 70 | run: ARCH=${{ matrix.name }} make test 71 | 72 | statics: 73 | permissions: 74 | contents: read # for actions/checkout to fetch code 75 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 76 | name: Static Checks 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 80 | - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 81 | with: 82 | go-version: "1.23" 83 | - uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea # v6.5.1 84 | with: 85 | version: v1.60 86 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # options for analysis running 2 | run: 3 | # default concurrency is a available CPU number 4 | concurrency: 4 5 | # add the build tags to include e2e tests files 6 | build-tags: 7 | - e2e 8 | # timeout for analysis, e.g. 30s, 5m, default is 1m 9 | timeout: 10m 10 | linters: 11 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 12 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 13 | disable-all: true 14 | enable: 15 | - typecheck 16 | - dupl 17 | - goprintffuncname 18 | - govet 19 | - nolintlint 20 | #- rowserrcheck 21 | - gofmt 22 | - revive 23 | - goimports 24 | - misspell 25 | - bodyclose 26 | - unconvert 27 | - ineffassign 28 | - staticcheck 29 | - exportloopref 30 | #- depguard #https://github.com/kedacore/keda/issues/4980 31 | - dogsled 32 | - errcheck 33 | #- funlen 34 | - gci 35 | - goconst 36 | - gocritic 37 | - gocyclo 38 | - gosimple 39 | - stylecheck 40 | - unused 41 | - unparam 42 | - unconvert 43 | - whitespace 44 | 45 | issues: 46 | include: 47 | - EXC0002 # disable excluding of issues about comments from golint 48 | # Excluding configuration per-path, per-linter, per-text and per-source 49 | exclude-rules: 50 | - path: _test\.go 51 | linters: 52 | - dupl 53 | - unparam 54 | - revive 55 | # Exclude gci check for //+kubebuilder:scaffold:imports comments. Waiting to 56 | # resolve https://github.com/kedacore/keda/issues/4379 57 | - path: operator/controllers/http/suite_test.go 58 | linters: 59 | - gci 60 | - path: operator/main.go 61 | linters: 62 | - gci 63 | # Exlude httpso.Spec.ScaleTargetRef.Deployment until we remove it in v0.9.0 64 | - linters: 65 | - staticcheck 66 | text: "SA1019: httpso.Spec.ScaleTargetRef.Deployment" 67 | 68 | linters-settings: 69 | funlen: 70 | lines: 80 71 | statements: 40 72 | gci: 73 | sections: 74 | - standard 75 | - default 76 | - prefix(github.com/kedacore/http-add-on) 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [commit, push] 2 | minimum_pre_commit_version: "1.20.0" 3 | repos: 4 | - repo: https://github.com/dnephin/pre-commit-golang 5 | rev: v0.3.5 6 | hooks: 7 | - id: go-fmt 8 | name: Run go fmt against the code 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v3.4.0 11 | hooks: 12 | - id: trailing-whitespace 13 | - id: detect-private-key 14 | - id: end-of-file-fixer 15 | - id: check-merge-conflict 16 | - id: mixed-line-ending 17 | - repo: https://github.com/thlorenz/doctoc.git 18 | rev: v2.0.0 19 | hooks: 20 | - id: doctoc 21 | name: Add TOC for md files 22 | files: ^README\.md$|^CONTRIBUTING\.md$ 23 | args: 24 | - "--maxlevel" 25 | - "3" 26 | - repo: local 27 | hooks: 28 | - id: language-matters 29 | language: pygrep 30 | name: Check for language that we do not accept as community 31 | description: Please use "deny_list" or "allow_list" instead. 32 | entry: "(?i)(black|white)[_-]?(list|List)" 33 | pass_filenames: true 34 | - id: golangci-lint 35 | language: golang 36 | name: Run golangci against the code 37 | entry: golangci-lint run 38 | types: [go] 39 | pass_filenames: false 40 | - id: validate-changelog 41 | name: Validate Changelog 42 | language: system 43 | entry: "bash hack/validate-changelog.sh" 44 | pass_filenames: false 45 | files: CHANGELOG\.md 46 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ADOPTERS.md: -------------------------------------------------------------------------------- 1 | # KEDA HTTP Add-on Adopters 2 | 3 | This page contains a list of organizations who are using KEDA's HTTP Add-on in production or at stages of testing. 4 | 5 | ## Adopters 6 | 7 | | Organization | Status | More Information (Blog post, etc.) | 8 | | ------------ | ---------| ---------------| 9 | | PropulsionAI |![testing](https://img.shields.io/badge/-development%20&%20testing-green?style=flat)|[PropulsionAI](https://propulsionhq.com) allows you to add AI to your apps, without writing code.| 10 | | REWE Digital |![testing](https://img.shields.io/badge/-development%20&%20testing-green?style=flat)|From delivery service to market — [REWE Digital](https://www.rewe-digital.com) strengthens leading technological position of REWE Group in food retail sector. | 11 | 12 | ## Become an adopter! 13 | 14 | You can easily become an adopter by sending a pull request to this file. 15 | 16 | These are the adoption statuses that you can use: 17 | 18 | - ![production](https://img.shields.io/badge/-production-blue?style=flat) 19 | - ![testing](https://img.shields.io/badge/-development%20&%20testing-green?style=flat) 20 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | KEDA uses [GitHub issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/about-issues) to track open work items with [GitHub Projects (beta)](https://docs.github.com/en/issues/trying-out-the-new-projects-experience/about-projects) to plan for upcoming releases. 4 | 5 | This document provides insights to the community on how we use it and what to expect. 6 | 7 | You can find our roadmap [here](https://github.com/orgs/kedacore/projects/6/views/1). 8 | 9 | ## Using our roadmap 10 | 11 | Here is some guidance on how to use our roadmap. 12 | 13 | ### Upcoming Release 14 | 15 | As we work towards our next release, we are planning and tracking work as part of the next release cycle. 16 | 17 | You can find an overview of the items in our upcoming release: 18 | 19 | - As list with the respective categories ([link](https://github.com/orgs/kedacore/projects/6/views/5)) 20 | - As list with the respective priorities ([link](https://github.com/orgs/kedacore/projects/6/views/2)) 21 | - As a board with the current status ([link](https://github.com/orgs/kedacore/projects/6/views/4)) 22 | 23 | ### Triaging 24 | 25 | All newly created issues are automatically added to the roadmap and waiting to be triaged by a maintainer. 26 | 27 | You can find an overview of all issues pending to be triaged [here](https://github.com/orgs/kedacore/projects/6/views/6). 28 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # KEDA-HTTP Command Line Interface (CLI) 2 | 3 | When finished, this CLI will enable a user to create a new KEDA-HTTP application with a command, without writing or submitting YAML to their Kubernetes cluster. 4 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - bases/http.keda.sh_httpscaledobjects.yaml 5 | #+kubebuilder:scaffold:crdkustomizeresource 6 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../crd 5 | - ../interceptor 6 | - ../operator 7 | - ../scaler 8 | namespace: keda 9 | namePrefix: keda-add-ons-http- 10 | labels: 11 | - includeSelectors: true 12 | includeTemplates: true 13 | pairs: 14 | app.kubernetes.io/name: http 15 | app.kubernetes.io/component: add-on 16 | app.kubernetes.io/part-of: keda 17 | - includeSelectors: false 18 | includeTemplates: false 19 | pairs: 20 | app.kubernetes.io/version: HEAD 21 | app.kubernetes.io/managed-by: kustomize 22 | -------------------------------------------------------------------------------- /config/interceptor/admin.service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: interceptor-admin 5 | spec: 6 | ports: 7 | - name: admin 8 | protocol: TCP 9 | port: 9090 10 | targetPort: admin 11 | -------------------------------------------------------------------------------- /config/interceptor/e2e-test/otel/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: interceptor 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - name: interceptor 11 | env: 12 | - name: OTEL_PROM_EXPORTER_ENABLED 13 | value: "true" 14 | - name: OTEL_PROM_EXPORTER_PORT 15 | value: "2223" 16 | - name: OTEL_EXPORTER_OTLP_METRICS_ENABLED 17 | value: "true" 18 | - name: OTEL_EXPORTER_OTLP_ENDPOINT 19 | value: "http://opentelemetry-collector.open-telemetry-system:4318" 20 | - name: OTEL_METRIC_EXPORT_INTERVAL 21 | value: "1" 22 | - name: OTEL_EXPORTER_OTLP_TRACES_ENABLED 23 | value: "true" 24 | - name: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL 25 | value: "http/protobuf" 26 | - name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT 27 | value: "http://opentelemetry-collector.open-telemetry-system:4318/v1/traces" 28 | - name: OTEL_EXPORTER_OTLP_TRACES_INSECURE 29 | value: "true" 30 | -------------------------------------------------------------------------------- /config/interceptor/e2e-test/otel/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - scaledobject.yaml 6 | -------------------------------------------------------------------------------- /config/interceptor/e2e-test/otel/scaledobject.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: keda.sh/v1alpha1 2 | kind: ScaledObject 3 | metadata: 4 | name: interceptor 5 | spec: 6 | minReplicaCount: 1 7 | -------------------------------------------------------------------------------- /config/interceptor/e2e-test/tls/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: interceptor 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - name: interceptor 11 | ports: 12 | - name: proxy-tls 13 | containerPort: 8443 14 | env: 15 | - name: KEDA_HTTP_PROXY_TLS_ENABLED 16 | value: "true" 17 | - name: KEDA_HTTP_PROXY_TLS_CERT_PATH 18 | value: "/certs/tls.crt" 19 | - name: KEDA_HTTP_PROXY_TLS_KEY_PATH 20 | value: "/certs/tls.key" 21 | - name: KEDA_HTTP_PROXY_TLS_CERT_STORE_PATHS 22 | value: "/additional-certs" 23 | - name: KEDA_HTTP_PROXY_TLS_PORT 24 | value: "8443" 25 | volumeMounts: 26 | - readOnly: true 27 | mountPath: "/certs" 28 | name: certs 29 | - readOnly: true 30 | mountPath: "/additional-certs/abc-certs" 31 | name: abc-certs 32 | volumes: 33 | - name: certs 34 | secret: 35 | secretName: keda-tls 36 | - name: abc-certs 37 | secret: 38 | secretName: abc-certs 39 | -------------------------------------------------------------------------------- /config/interceptor/e2e-test/tls/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - proxy.service.yaml 6 | -------------------------------------------------------------------------------- /config/interceptor/e2e-test/tls/proxy.service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: interceptor-proxy 5 | spec: 6 | type: ClusterIP 7 | ports: 8 | - name: proxy-tls 9 | protocol: TCP 10 | port: 8443 11 | targetPort: proxy-tls 12 | -------------------------------------------------------------------------------- /config/interceptor/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - role.yaml 6 | - role_binding.yaml 7 | - admin.service.yaml 8 | - proxy.service.yaml 9 | - metrics.service.yaml 10 | - service_account.yaml 11 | - scaledobject.yaml 12 | configurations: 13 | - transformerconfig.yaml 14 | labels: 15 | - includeSelectors: true 16 | includeTemplates: true 17 | pairs: 18 | app.kubernetes.io/instance: interceptor 19 | -------------------------------------------------------------------------------- /config/interceptor/metrics.service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: interceptor-metrics 5 | spec: 6 | type: ClusterIP 7 | ports: 8 | - name: metrics 9 | protocol: TCP 10 | port: 2223 11 | targetPort: metrics 12 | -------------------------------------------------------------------------------- /config/interceptor/proxy.service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: interceptor-proxy 5 | spec: 6 | type: ClusterIP 7 | ports: 8 | - name: proxy 9 | protocol: TCP 10 | port: 8080 11 | targetPort: proxy 12 | -------------------------------------------------------------------------------- /config/interceptor/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: interceptor 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - endpoints 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - services 19 | verbs: 20 | - get 21 | - list 22 | - watch 23 | - apiGroups: 24 | - http.keda.sh 25 | resources: 26 | - httpscaledobjects 27 | verbs: 28 | - get 29 | - list 30 | - watch 31 | -------------------------------------------------------------------------------- /config/interceptor/role_binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: interceptor 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: interceptor 10 | subjects: 11 | - kind: ServiceAccount 12 | name: interceptor 13 | -------------------------------------------------------------------------------- /config/interceptor/scaledobject.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: keda.sh/v1alpha1 2 | kind: ScaledObject 3 | metadata: 4 | name: interceptor 5 | spec: 6 | minReplicaCount: 3 7 | maxReplicaCount: 50 8 | pollingInterval: 1 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: interceptor 13 | triggers: 14 | - type: external 15 | metadata: 16 | scalerAddress: external-scaler:9090 17 | interceptorTargetPendingRequests: '200' 18 | -------------------------------------------------------------------------------- /config/interceptor/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: interceptor 5 | -------------------------------------------------------------------------------- /config/interceptor/transformerconfig.yaml: -------------------------------------------------------------------------------- 1 | namePrefix: 2 | - kind: ScaledObject 3 | path: spec/scaleTargetRef/name 4 | - kind: ScaledObject 5 | path: spec/triggers/metadata/scalerAddress 6 | -------------------------------------------------------------------------------- /config/operator/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: operator 5 | spec: 6 | template: 7 | spec: 8 | affinity: 9 | nodeAffinity: 10 | requiredDuringSchedulingIgnoredDuringExecution: 11 | nodeSelectorTerms: 12 | - matchExpressions: 13 | - key: kubernetes.io/os 14 | operator: In 15 | values: 16 | - linux 17 | - key: kubernetes.io/arch 18 | operator: In 19 | values: 20 | - amd64 21 | - arm64 22 | containers: 23 | - name: operator 24 | image: ghcr.io/kedacore/http-add-on-operator 25 | args: 26 | - --leader-elect 27 | - --zap-log-level=info 28 | - --zap-encoder=console 29 | - --zap-time-encoding=rfc3339 30 | env: 31 | - name: KEDAHTTP_OPERATOR_EXTERNAL_SCALER_SERVICE 32 | value: "keda-add-ons-http-external-scaler" 33 | - name: KEDAHTTP_OPERATOR_EXTERNAL_SCALER_PORT 34 | value: "9090" 35 | - name: KEDA_HTTP_OPERATOR_NAMESPACE 36 | value: "keda" 37 | - name: KEDA_HTTP_OPERATOR_WATCH_NAMESPACE 38 | value: "" 39 | ports: 40 | - name: metrics 41 | containerPort: 8080 42 | - name: probes 43 | containerPort: 8081 44 | livenessProbe: 45 | httpGet: 46 | path: /healthz 47 | port: probes 48 | readinessProbe: 49 | httpGet: 50 | path: /readyz 51 | port: probes 52 | # TODO(pedrotorres): set better default values avoiding overcommitment 53 | resources: 54 | requests: 55 | cpu: 100m 56 | memory: 100Mi 57 | limits: 58 | cpu: 1000m 59 | memory: 1000Mi 60 | securityContext: 61 | allowPrivilegeEscalation: false 62 | readOnlyRootFilesystem: true 63 | capabilities: 64 | drop: 65 | - ALL 66 | securityContext: 67 | runAsNonRoot: true 68 | seccompProfile: 69 | type: RuntimeDefault 70 | serviceAccountName: operator 71 | terminationGracePeriodSeconds: 10 72 | -------------------------------------------------------------------------------- /config/operator/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - role.yaml 6 | - role_binding.yaml 7 | - service_account.yaml 8 | labels: 9 | - includeSelectors: true 10 | includeTemplates: true 11 | pairs: 12 | app.kubernetes.io/instance: operator 13 | -------------------------------------------------------------------------------- /config/operator/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: operator 6 | rules: 7 | - apiGroups: 8 | - http.keda.sh 9 | resources: 10 | - httpscaledobjects 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - http.keda.sh 21 | resources: 22 | - httpscaledobjects/finalizers 23 | verbs: 24 | - update 25 | - apiGroups: 26 | - http.keda.sh 27 | resources: 28 | - httpscaledobjects/status 29 | verbs: 30 | - get 31 | - patch 32 | - update 33 | - apiGroups: 34 | - keda.sh 35 | resources: 36 | - scaledobjects 37 | verbs: 38 | - create 39 | - delete 40 | - get 41 | - list 42 | - patch 43 | - update 44 | - watch 45 | --- 46 | apiVersion: rbac.authorization.k8s.io/v1 47 | kind: Role 48 | metadata: 49 | name: operator 50 | namespace: keda 51 | rules: 52 | - apiGroups: 53 | - "" 54 | resources: 55 | - events 56 | verbs: 57 | - create 58 | - patch 59 | - apiGroups: 60 | - coordination.k8s.io 61 | resources: 62 | - leases 63 | verbs: 64 | - create 65 | - delete 66 | - get 67 | - list 68 | - patch 69 | - update 70 | - watch 71 | -------------------------------------------------------------------------------- /config/operator/role_binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: operator 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: operator 10 | subjects: 11 | - kind: ServiceAccount 12 | name: operator 13 | --- 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | kind: RoleBinding 16 | metadata: 17 | name: operator 18 | roleRef: 19 | apiGroup: rbac.authorization.k8s.io 20 | kind: Role 21 | name: operator 22 | subjects: 23 | - kind: ServiceAccount 24 | name: operator 25 | -------------------------------------------------------------------------------- /config/operator/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: operator 5 | -------------------------------------------------------------------------------- /config/scaler/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: scaler 5 | spec: 6 | replicas: 3 7 | template: 8 | spec: 9 | affinity: 10 | nodeAffinity: 11 | requiredDuringSchedulingIgnoredDuringExecution: 12 | nodeSelectorTerms: 13 | - matchExpressions: 14 | - key: kubernetes.io/os 15 | operator: In 16 | values: 17 | - linux 18 | - key: kubernetes.io/arch 19 | operator: In 20 | values: 21 | - amd64 22 | - arm64 23 | containers: 24 | - name: scaler 25 | image: ghcr.io/kedacore/http-add-on-scaler 26 | args: 27 | - --zap-log-level=info 28 | - --zap-encoder=console 29 | - --zap-time-encoding=rfc3339 30 | env: 31 | - name: KEDA_HTTP_SCALER_TARGET_ADMIN_DEPLOYMENT 32 | value: "keda-add-ons-http-interceptor" 33 | - name: KEDA_HTTP_SCALER_PORT 34 | value: "9090" 35 | - name: KEDA_HTTP_SCALER_TARGET_ADMIN_NAMESPACE 36 | value: "keda" 37 | - name: KEDA_HTTP_SCALER_TARGET_ADMIN_SERVICE 38 | value: "keda-add-ons-http-interceptor-admin" 39 | - name: KEDA_HTTP_SCALER_TARGET_ADMIN_PORT 40 | value: "9090" 41 | - name: KEDA_HTTP_SCALER_STREAM_INTERVAL_MS 42 | value: "200" 43 | ports: 44 | - name: grpc 45 | containerPort: 9090 46 | livenessProbe: 47 | grpc: 48 | port: 9090 49 | service: liveness 50 | timeoutSeconds: 1 51 | periodSeconds: 5 52 | successThreshold: 1 53 | failureThreshold: 3 54 | readinessProbe: 55 | grpc: 56 | port: 9090 57 | service: readiness 58 | timeoutSeconds: 1 59 | periodSeconds: 1 60 | successThreshold: 1 61 | failureThreshold: 3 62 | # TODO(pedrotorres): set better default values avoiding overcommitment 63 | resources: 64 | requests: 65 | cpu: 100m 66 | memory: 100Mi 67 | limits: 68 | cpu: 1000m 69 | memory: 1000Mi 70 | securityContext: 71 | allowPrivilegeEscalation: false 72 | readOnlyRootFilesystem: true 73 | capabilities: 74 | drop: 75 | - ALL 76 | securityContext: 77 | runAsNonRoot: true 78 | seccompProfile: 79 | type: RuntimeDefault 80 | serviceAccountName: scaler 81 | terminationGracePeriodSeconds: 10 82 | -------------------------------------------------------------------------------- /config/scaler/e2e-test/otel/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: scaler 5 | spec: 6 | replicas: 1 7 | -------------------------------------------------------------------------------- /config/scaler/e2e-test/otel/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | -------------------------------------------------------------------------------- /config/scaler/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - role.yaml 6 | - role_binding.yaml 7 | - service.yaml 8 | - service_account.yaml 9 | labels: 10 | - includeSelectors: true 11 | includeTemplates: true 12 | pairs: 13 | app.kubernetes.io/instance: external-scaler 14 | -------------------------------------------------------------------------------- /config/scaler/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: scaler 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - endpoints 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - http.keda.sh 17 | resources: 18 | - httpscaledobjects 19 | verbs: 20 | - get 21 | - list 22 | - watch 23 | -------------------------------------------------------------------------------- /config/scaler/role_binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: scaler 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: scaler 10 | subjects: 11 | - kind: ServiceAccount 12 | name: scaler 13 | --- 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | kind: RoleBinding 16 | metadata: 17 | name: scaler 18 | namespace: keda 19 | roleRef: 20 | apiGroup: rbac.authorization.k8s.io 21 | kind: Role 22 | name: scaler 23 | subjects: 24 | - kind: ServiceAccount 25 | name: scaler 26 | -------------------------------------------------------------------------------- /config/scaler/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: external-scaler 5 | spec: 6 | ports: 7 | - name: grpc 8 | protocol: TCP 9 | port: 9090 10 | targetPort: grpc 11 | -------------------------------------------------------------------------------- /config/scaler/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: scaler 5 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: KEDA HTTP Add-On 2 | description: Documentation for the KEDA HTTP add-on 3 | remote_theme: pages-themes/architect@v0.2.0 4 | plugins: 5 | - jekyll-remote-theme # add this line to the plugins list if you already have one 6 | -------------------------------------------------------------------------------- /docs/_includes/head-custom.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/images/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kedacore/http-add-on/6a6adfb7acbb3114b0d7221e2932a3a4645041f3/docs/images/arch.png -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # KEDA HTTP Add-On 2 | 3 | Here is an overview of detailed documentation: 4 | 5 | - [Why build an HTTP add-on?](scope.md) 6 | - [Install](install.md) 7 | - [Design](design.md) 8 | - [Use-Cases](use_cases.md) 9 | - [Walkthrough](walkthrough.md) 10 | - [Operate](operate.md) 11 | - [Developing](developing.md) 12 | - [Integrations](integrations.md) 13 | - [FAQ](faq.md) 14 | -------------------------------------------------------------------------------- /docs/ref/v0.1.0/http_scaled_object.md: -------------------------------------------------------------------------------- 1 | # The `HTTPScaledObject` 2 | 3 | >This document reflects the specification of the `HTTPScaledObject` resource for the `v0.1.0` version. 4 | 5 | Each `HTTPScaledObject` looks approximately like the below: 6 | 7 | ```yaml 8 | kind: HTTPScaledObject 9 | apiVersion: http.keda.sh/v1alpha1 10 | metadata: 11 | name: xkcd 12 | spec: 13 | scaleTargetRef: 14 | deployment: xkcd 15 | service: xkcd 16 | port: 8080 17 | ``` 18 | 19 | This document is a narrated reference guide for the `HTTPScaledObject`, and we'll focus on the `spec` field. 20 | 21 | ## `scaleTargetRef` 22 | 23 | This is the primary and most important part of the `spec` because it describes (1) what `Deployment` to scale and (2) where and how to route traffic. 24 | 25 | ### `deployment` 26 | 27 | This is the name of the `Deployment` to scale. It must exist in the same namespace as this `HTTPScaledObject` and shouldn't be managed by any other autoscaling system. This means that there should not be any `ScaledObject` already created for this `Deployment`. The HTTP Add-on will manage a `ScaledObject` internally. 28 | 29 | ### `service` 30 | 31 | This is the name of the service to route traffic to. The add-on will create autoscaling and routing components that route to this `Service`. It must exist in the same namespace as this `HTTPScaledObject` and should route to the same `Deployment` as you entered in the `deployment` field. 32 | 33 | ### `port` 34 | 35 | This is the port to route to on the service that you specified in the `service` field. It should be exposed on the service and should route to a valid `containerPort` on the `Deployment` you gave in the `deployment` field. 36 | -------------------------------------------------------------------------------- /docs/scope.md: -------------------------------------------------------------------------------- 1 | # Why build an HTTP add-on? 2 | 3 | Running production HTTP servers in Kubernetes is complicated and involves many pieces of infrastructure. The HTTP Add-on (called the "add-on" hereafter) aims to autoscale these HTTP servers, but does not aim to extend beyond that scope. Generally, this project only aims to do two things: 4 | 5 | 1. Autoscale arbitrary HTTP servers based on the volume of traffic incoming to it, including to zero. 6 | 2. Route HTTP traffic from a given source to an arbitrary HTTP server, as far as we need to efficiently accomplish (1). 7 | 8 | The add-on only provides this functionality to workloads that _opt in_ to it. We provide more detail below. 9 | 10 | ### Autoscaling HTTP 11 | 12 | To autoscale HTTP servers, the HTTP Add-on needs access to metrics that it can report to KEDA, so that KEDA itself can scale the target HTTP server. The mechanism by which the add-on does this is to use an [interceptor](../interceptor) and [external scaler](../scaler). An operator watches for a `HTTPScaledObject` resource and creates these components as necessary. 13 | 14 | The HTTP Add-on only includes the necessary infrastructure to respond to new, modified, or deleted `HTTPScaledObject`s, and when one is created, the add-on only creates the infrastructure needed specifically to accomplish autoscaling. 15 | 16 | >As stated above, the current architecture requires an "interceptor", which needs to proxy incoming HTTP requests in order to provide autoscaling metrics. That means the scope of the HTTP Add-on currently needs to include the app's network traffic routing system. 17 | 18 | To learn more, we recommend reading about our [design](design.md) or go through our [FAQ](faq.md). 19 | 20 | [Go back to landing page](./) 21 | -------------------------------------------------------------------------------- /docs/use_cases.md: -------------------------------------------------------------------------------- 1 | # Common Use Cases 2 | 3 | This document includes several common scenarios in which this project may be deployed along with descriptions on why and how it could be deployed in each case. 4 | 5 | ## Current Containerized HTTP Application In The Cloud, Migrating to Kubernetes 6 | 7 | In this use case, an application may be containerized running on a managed cloud platform that supports containers. Below is a non-exhaustive, alphabetically-ordered list of some examples: 8 | 9 | - [Amazon ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html) 10 | - [Azure App Services](https://docs.microsoft.com/en-us/azure/app-service/quickstart-custom-container?pivots=container-linux) 11 | - [Digital Ocean App Platform](https://www.digitalocean.com/products/app-platform/) 12 | - [Google App Engine Flexible Environment](https://cloud.google.com/appengine/docs/flexible/) 13 | 14 | The platform may or may not be autoscaling. 15 | 16 | Moving this application to Kubernetes may make sense for several reasons, but the pros and cons of that decision are out of scope of this document. 17 | 18 | ### How You'd Move This Application to KEDA-HTTP 19 | 20 | If the application _is_ being moved to Kubernetes, you would follow these steps to get it autoscaling and routing with KEDA-HTTP: 21 | 22 | - Create a workload and `Service` 23 | - [Install](./install.md) the HTTP Add-on 24 | - Create a single `HTTPScaledObject` in the same namespace as the workload and `Service` you created 25 | 26 | At that point, the operator will create the proper autoscaling and routing infrastructure behind the scenes and the application will be ready to scale. Any request received by the interceptor with the proper host will be routed to the proper backend. 27 | 28 | ## Current HTTP Server in Kubernetes 29 | 30 | In this use case, an HTTP application is already running in Kubernetes, possibly (but not necessarily) already serving in production to the public internet. 31 | 32 | In this case, the reasoning for adding the HTTP Add-on would be clear - adding autoscaling based on incoming HTTP traffic. 33 | 34 | ### How You'd Move This Application to KEDA-HTTP 35 | 36 | Getting the HTTP Add-on working can be done transparently and without downtime to the application: 37 | 38 | - [Install](./install.md) the add-on. This step will have no effect on the running application. 39 | - Create a new `HTTPScaledObject`. This step activates autoscaling for the workload that you specify and the application will immediately start scaling up and down based on incoming traffic through the interceptor that was created. 40 | 41 | [Go back to landing page](./) 42 | -------------------------------------------------------------------------------- /examples/v0.0.1/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | app_name: xkcd 7 | app_image: arschles/xkcd 8 | port: 8080 9 | -------------------------------------------------------------------------------- /examples/v0.0.2/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | scaleTargetRef: 7 | deployment: xkcd 8 | service: xkcd 9 | port: 8080 10 | replicas: 11 | min: 5 12 | max: 10 13 | -------------------------------------------------------------------------------- /examples/v0.1.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | scaleTargetRef: 7 | deployment: xkcd 8 | service: xkcd 9 | port: 8080 10 | replicas: 11 | min: 5 12 | max: 10 13 | -------------------------------------------------------------------------------- /examples/v0.10.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | hosts: 7 | - myhost.com 8 | pathPrefixes: 9 | - /test 10 | scaleTargetRef: 11 | name: xkcd 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | service: xkcd 15 | port: 8080 16 | replicas: 17 | min: 1 18 | max: 10 19 | scaledownPeriod: 300 20 | scalingMetric: 21 | requestRate: 22 | granularity: 1s 23 | targetValue: 100 24 | window: 1m 25 | -------------------------------------------------------------------------------- /examples/v0.2.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | host: myhost.com 7 | scaleTargetRef: 8 | deployment: xkcd 9 | service: xkcd 10 | port: 8080 11 | replicas: 12 | min: 5 13 | max: 10 14 | -------------------------------------------------------------------------------- /examples/v0.3.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | host: myhost.com 7 | scaleTargetRef: 8 | deployment: xkcd 9 | service: xkcd 10 | port: 8080 11 | replicas: 12 | min: 5 13 | max: 10 14 | -------------------------------------------------------------------------------- /examples/v0.4.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | host: myhost.com 7 | scaleTargetRef: 8 | deployment: xkcd 9 | service: xkcd 10 | port: 8080 11 | replicas: 12 | min: 5 13 | max: 10 14 | -------------------------------------------------------------------------------- /examples/v0.5.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | hosts: 7 | - myhost.com 8 | scaleTargetRef: 9 | deployment: xkcd 10 | service: xkcd 11 | port: 8080 12 | replicas: 13 | min: 5 14 | max: 10 15 | -------------------------------------------------------------------------------- /examples/v0.6.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | hosts: 7 | - myhost.com 8 | pathPrefixes: 9 | - /test 10 | scaleTargetRef: 11 | deployment: xkcd 12 | service: xkcd 13 | port: 8080 14 | replicas: 15 | min: 5 16 | max: 10 17 | -------------------------------------------------------------------------------- /examples/v0.7.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | hosts: 7 | - myhost.com 8 | pathPrefixes: 9 | - /test 10 | scaleTargetRef: 11 | name: xkcd 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | service: xkcd 15 | port: 8080 16 | replicas: 17 | min: 5 18 | max: 10 19 | -------------------------------------------------------------------------------- /examples/v0.8.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | hosts: 7 | - myhost.com 8 | pathPrefixes: 9 | - /test 10 | scaleTargetRef: 11 | name: xkcd 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | service: xkcd 15 | port: 8080 16 | replicas: 17 | min: 1 18 | max: 10 19 | scaledownPeriod: 300 20 | scalingMetric: 21 | requestRate: 22 | granularity: 1s 23 | targetValue: 100 24 | window: 1m 25 | -------------------------------------------------------------------------------- /examples/v0.9.0/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: xkcd 5 | spec: 6 | hosts: 7 | - myhost.com 8 | pathPrefixes: 9 | - /test 10 | scaleTargetRef: 11 | name: xkcd 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | service: xkcd 15 | port: 8080 16 | replicas: 17 | min: 1 18 | max: 10 19 | scaledownPeriod: 300 20 | scalingMetric: 21 | requestRate: 22 | granularity: 1s 23 | targetValue: 100 24 | window: 1m 25 | -------------------------------------------------------------------------------- /examples/xkcd/.helmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kedacore/http-add-on/6a6adfb7acbb3114b0d7221e2932a3a4645041f3/examples/xkcd/.helmignore -------------------------------------------------------------------------------- /examples/xkcd/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: xkcd 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /examples/xkcd/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "xkcd.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "xkcd.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "xkcd.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "xkcd.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /examples/xkcd/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "xkcd.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "xkcd.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "xkcd.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "xkcd.labels" -}} 37 | helm.sh/chart: {{ include "xkcd.chart" . }} 38 | {{ include "xkcd.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "xkcd.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "xkcd.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "xkcd.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "xkcd.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /examples/xkcd/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "xkcd.fullname" . }} 5 | labels: 6 | {{- include "xkcd.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | matchLabels: 10 | {{- include "xkcd.selectorLabels" . | nindent 6 }} 11 | template: 12 | metadata: 13 | {{- with .Values.podAnnotations }} 14 | annotations: 15 | {{- toYaml . | nindent 8 }} 16 | {{- end }} 17 | labels: 18 | {{- include "xkcd.selectorLabels" . | nindent 8 }} 19 | spec: 20 | {{- with .Values.imagePullSecrets }} 21 | imagePullSecrets: 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | serviceAccountName: {{ include "xkcd.serviceAccountName" . }} 25 | securityContext: 26 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 27 | containers: 28 | - name: {{ .Chart.Name }} 29 | securityContext: 30 | {{- toYaml .Values.securityContext | nindent 12 }} 31 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 32 | imagePullPolicy: {{ .Values.image.pullPolicy }} 33 | {{- if .Values.args }} 34 | args: 35 | {{- range $arg := .Values.args }} 36 | - {{ $arg }} 37 | {{- end }} 38 | {{- end }} 39 | ports: 40 | - name: http 41 | containerPort: 8080 42 | protocol: TCP 43 | livenessProbe: 44 | httpGet: 45 | path: / 46 | port: http 47 | readinessProbe: 48 | httpGet: 49 | path: / 50 | port: http 51 | -------------------------------------------------------------------------------- /examples/xkcd/templates/externalservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "xkcd.fullname" . }}-proxy 5 | labels: 6 | {{- include "xkcd.labels" . | nindent 4 }} 7 | spec: 8 | type: ExternalName 9 | externalName: keda-add-ons-http-interceptor-proxy.keda 10 | -------------------------------------------------------------------------------- /examples/xkcd/templates/httproute.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.httproute }} 2 | apiVersion: gateway.networking.k8s.io/v1 3 | kind: HTTPRoute 4 | metadata: 5 | name: {{ include "xkcd.fullname" . }} 6 | spec: 7 | parentRefs: 8 | - name: eg 9 | namespace: envoy-gateway-system 10 | hostnames: 11 | {{- range .Values.hosts }} 12 | - {{ . | toString }} 13 | {{- end }} 14 | rules: 15 | - backendRefs: 16 | - kind: Service 17 | name: keda-add-ons-http-interceptor-proxy 18 | namespace: keda 19 | port: 8080 20 | matches: 21 | - path: 22 | type: PathPrefix 23 | value: / 24 | --- 25 | apiVersion: gateway.networking.k8s.io/v1beta1 26 | kind: ReferenceGrant 27 | metadata: 28 | name: {{ include "xkcd.fullname" . }} 29 | namespace: keda 30 | spec: 31 | from: 32 | - group: gateway.networking.k8s.io 33 | kind: HTTPRoute 34 | namespace: {{ .Release.Namespace }} 35 | to: 36 | - group: "" 37 | kind: Service 38 | name: keda-add-ons-http-interceptor-proxy 39 | {{- end }} 40 | -------------------------------------------------------------------------------- /examples/xkcd/templates/httpscaledobject.yaml: -------------------------------------------------------------------------------- 1 | kind: HTTPScaledObject 2 | apiVersion: http.keda.sh/v1alpha1 3 | metadata: 4 | name: {{ include "xkcd.fullname" . }} 5 | spec: 6 | {{- with .Values.hosts }} 7 | hosts: 8 | {{- toYaml . | nindent 8 }} 9 | {{- end }} 10 | {{- with .Values.pathPrefixes }} 11 | pathPrefixes: 12 | {{- toYaml . | nindent 8 }} 13 | {{- end }} 14 | scalingMetric: 15 | concurrency: 16 | targetValue: {{ .Values.targetPendingRequests }} 17 | scaleTargetRef: 18 | name: {{ include "xkcd.fullname" . }} 19 | kind: Deployment 20 | apiVersion: apps/v1 21 | service: {{ include "xkcd.fullname" . }} 22 | port: 8080 23 | replicas: 24 | min: {{ .Values.autoscaling.http.minReplicas }} 25 | max: {{ .Values.autoscaling.http.maxReplicas }} 26 | -------------------------------------------------------------------------------- /examples/xkcd/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: {{ include "xkcd.fullname" . }} 5 | annotations: 6 | nginx.ingress.kubernetes.io/rewrite-target: / 7 | spec: 8 | ingressClassName: nginx 9 | rules: 10 | {{- range .Values.hosts }} 11 | - host: {{ . | toString }} 12 | http: 13 | paths: 14 | - path: / 15 | pathType: Prefix 16 | backend: 17 | service: 18 | name: {{ include "xkcd.fullname" $ }}-proxy 19 | port: 20 | number: 8080 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /examples/xkcd/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "xkcd.fullname" . }} 5 | labels: 6 | {{- include "xkcd.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "xkcd.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /examples/xkcd/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "xkcd.serviceAccountName" . }} 6 | labels: 7 | {{- include "xkcd.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /examples/xkcd/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | hosts: 3 | - "myhost.com" 4 | - "myhost2.com" 5 | pathPrefixes: 6 | - "/path1" 7 | - "/path2" 8 | targetPendingRequests: 200 9 | # This is the namespace that the ingress should be installed 10 | # into. It should be set to the same namespace as the 11 | # KEDA HTTP componentry is installed in. Defaults to the Helm 12 | # chart release namespace 13 | ingressNamespace: 14 | image: 15 | repository: registry.k8s.io/e2e-test-images/agnhost 16 | pullPolicy: Always 17 | # Overrides the image tag whose default is the chart appVersion. 18 | tag: "2.45" 19 | 20 | args: 21 | - netexec 22 | 23 | imagePullSecrets: [] 24 | nameOverride: "" 25 | fullnameOverride: "" 26 | 27 | serviceAccount: 28 | # Specifies whether a service account should be created 29 | create: true 30 | # Annotations to add to the service account 31 | annotations: {} 32 | # The name of the service account to use. 33 | # If not set and create is true, a name is generated using the fullname template 34 | name: "" 35 | 36 | podAnnotations: {} 37 | 38 | podSecurityContext: {} 39 | # fsGroup: 2000 40 | 41 | securityContext: {} 42 | # capabilities: 43 | # drop: 44 | # - ALL 45 | # readOnlyRootFilesystem: true 46 | # runAsNonRoot: true 47 | # runAsUser: 1000 48 | 49 | service: 50 | type: ClusterIP 51 | port: 8080 52 | 53 | autoscaling: 54 | http: 55 | minReplicas: 0 56 | maxReplicas: 10 57 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /hack/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | /* 5 | Copyright 2019 The Kubernetes Authors. 6 | Copyright 2023 The KEDA Authors. 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | // This package imports things required by build scripts, to force `go mod` to see them as dependencies 22 | package hack 23 | 24 | import ( 25 | _ "go.uber.org/mock/mockgen" 26 | _ "k8s.io/code-generator" 27 | _ "sigs.k8s.io/kustomize/kustomize/v5" 28 | ) 29 | -------------------------------------------------------------------------------- /hack/update-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # Copyright 2023 The KEDA Authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -o errexit 19 | set -o nounset 20 | set -o pipefail 21 | 22 | CODEGEN_PKG="${CODEGEN_PKG:-$(go list -f '{{ .Dir }}' -m k8s.io/code-generator 2>/dev/null)}" 23 | SCRIPT_ROOT="$(dirname "${BASH_SOURCE[0]}")/.." 24 | OUTPUT_BASE="$(mktemp -d)" 25 | 26 | GO_PACKAGE='github.com/kedacore/http-add-on' 27 | 28 | source "${CODEGEN_PKG}/kube_codegen.sh" 29 | 30 | kube::codegen::gen_helpers \ 31 | --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ 32 | "${SCRIPT_ROOT}/operator/apis" 33 | 34 | kube::codegen::gen_client \ 35 | --with-watch \ 36 | --output-dir "${SCRIPT_ROOT}/operator/generated" \ 37 | --output-pkg "github.com/kedacore/http-add-on/operator/generated" \ 38 | --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ 39 | "${SCRIPT_ROOT}/operator/apis" 40 | -------------------------------------------------------------------------------- /hack/update-mockgen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # Copyright 2023 The KEDA Authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -o errexit 19 | set -o nounset 20 | set -o pipefail 21 | 22 | OUTPUT="$(mktemp -d)" 23 | 24 | GEN='operator/generated' 25 | CPY='hack/boilerplate.go.txt' 26 | PKG='mock' 27 | 28 | MOCKGEN_PKG="${MOCKGEN_PKG:-$(go list -f '{{ .Dir }}' -m go.uber.org/mock 2>/dev/null)/mockgen}" 29 | MOCKGEN="${OUTPUT}/mockgen" 30 | go build -o "${MOCKGEN}" "${MOCKGEN_PKG}" 31 | 32 | for SRC in $(find "${GEN}" -type 'f' -name '*.go' | grep -v '/fake/' | grep -v "/${PKG}/") 33 | do 34 | DIR="$(dirname "${SRC}")/${PKG}" 35 | mkdir -p "${DIR}" 36 | DST="${DIR}/$(basename "${SRC}")" 37 | "${MOCKGEN}" -copyright_file="${CPY}" -destination="${DST}" -package="${PKG}" -source="${SRC}" 38 | done 39 | 40 | rm -fR "${OUTPUT}" 41 | -------------------------------------------------------------------------------- /hack/verify-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # Copyright 2023 The KEDA Authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -o errexit 19 | set -o nounset 20 | set -o pipefail 21 | 22 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 23 | 24 | DIFFROOT="${SCRIPT_ROOT}/operator" 25 | TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/operator" 26 | _tmp="${SCRIPT_ROOT}/_tmp" 27 | 28 | cleanup() { 29 | rm -rf "${_tmp}" 30 | } 31 | trap "cleanup" EXIT SIGINT 32 | 33 | cleanup 34 | 35 | mkdir -p "${TMP_DIFFROOT}" 36 | cp -a "${DIFFROOT}"/. "${TMP_DIFFROOT}" 37 | 38 | make codegen 39 | echo "diffing ${DIFFROOT} against freshly generated codegen" 40 | ret=0 41 | diff -Naprux 'mock' "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? 42 | cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" 43 | if [[ $ret -eq 0 ]] 44 | then 45 | echo "${DIFFROOT} up to date." 46 | else 47 | echo "${DIFFROOT} is out of date. Please run 'make codegen'" 48 | exit 1 49 | fi 50 | -------------------------------------------------------------------------------- /hack/verify-manifests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # Copyright 2023 The KEDA Authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -o errexit 19 | set -o nounset 20 | set -o pipefail 21 | 22 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 23 | 24 | DIFFROOT="${SCRIPT_ROOT}/config" 25 | TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/config" 26 | _tmp="${SCRIPT_ROOT}/_tmp" 27 | 28 | cleanup() { 29 | rm -rf "${_tmp}" 30 | } 31 | trap "cleanup" EXIT SIGINT 32 | 33 | cleanup 34 | 35 | mkdir -p "${TMP_DIFFROOT}" 36 | cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}" 37 | 38 | make manifests 39 | echo "diffing ${DIFFROOT} against freshly generated manifests" 40 | ret=0 41 | diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? 42 | cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" 43 | if [[ $ret -eq 0 ]] 44 | then 45 | echo "${DIFFROOT} up to date." 46 | else 47 | echo "${DIFFROOT} is out of date. Please run 'make manifests'" 48 | exit 1 49 | fi 50 | -------------------------------------------------------------------------------- /hack/verify-mockgen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # Copyright 2023 The KEDA Authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -o errexit 19 | set -o nounset 20 | set -o pipefail 21 | 22 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 23 | 24 | DIFFROOT="${SCRIPT_ROOT}/operator" 25 | TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/operator" 26 | _tmp="${SCRIPT_ROOT}/_tmp" 27 | 28 | cleanup() { 29 | rm -rf "${_tmp}" 30 | } 31 | trap "cleanup" EXIT SIGINT 32 | 33 | cleanup 34 | 35 | mkdir -p "${TMP_DIFFROOT}" 36 | cp -a "${DIFFROOT}"/. "${TMP_DIFFROOT}" 37 | 38 | make mockgen 39 | echo "diffing ${DIFFROOT} against freshly generated mockgen" 40 | ret=0 41 | diff -Napru "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? 42 | cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" 43 | if [[ $ret -eq 0 ]] 44 | then 45 | echo "${DIFFROOT} up to date." 46 | else 47 | echo "${DIFFROOT} is out of date. Please run '${SCRIPT_ROOT}/hack/update-mockgen.sh'" 48 | exit 1 49 | fi 50 | -------------------------------------------------------------------------------- /interceptor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.23.6 as builder 2 | WORKDIR /workspace 3 | COPY go.* . 4 | RUN go mod download 5 | COPY . . 6 | ARG VERSION=main 7 | ARG GIT_COMMIT=HEAD 8 | ARG TARGETOS 9 | ARG TARGETARCH 10 | RUN VERSION="${VERSION}" GIT_COMMIT="${GIT_COMMIT}" TARGET_OS="${TARGETOS}" ARCH="${TARGETARCH}" make build-interceptor 11 | 12 | FROM gcr.io/distroless/static:nonroot 13 | COPY --from=builder /workspace/bin/interceptor /sbin/init 14 | ENTRYPOINT ["/sbin/init"] 15 | -------------------------------------------------------------------------------- /interceptor/config/metrics.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kelseyhightower/envconfig" 5 | ) 6 | 7 | // Metrics is the configuration for configuring metrics in the interceptor. 8 | type Metrics struct { 9 | // Sets whether or not to enable the Prometheus metrics exporter 10 | OtelPrometheusExporterEnabled bool `envconfig:"OTEL_PROM_EXPORTER_ENABLED" default:"true"` 11 | // Sets the port which the Prometheus compatible metrics endpoint should be served on 12 | OtelPrometheusExporterPort int `envconfig:"OTEL_PROM_EXPORTER_PORT" default:"2223"` 13 | // Sets whether or not to enable the OTEL metrics exporter 14 | OtelHTTPExporterEnabled bool `envconfig:"OTEL_EXPORTER_OTLP_METRICS_ENABLED" default:"false"` 15 | } 16 | 17 | // Parse parses standard configs using envconfig and returns a pointer to the 18 | // newly created config. Returns nil and a non-nil error if parsing failed 19 | func MustParseMetrics() *Metrics { 20 | ret := new(Metrics) 21 | envconfig.MustProcess("", ret) 22 | return ret 23 | } 24 | -------------------------------------------------------------------------------- /interceptor/config/timeouts.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kelseyhightower/envconfig" 7 | "k8s.io/apimachinery/pkg/util/wait" 8 | ) 9 | 10 | // Timeouts is the configuration for connection and HTTP timeouts 11 | type Timeouts struct { 12 | // Connect is the connection timeout 13 | Connect time.Duration `envconfig:"KEDA_HTTP_CONNECT_TIMEOUT" default:"500ms"` 14 | // KeepAlive is the interval between keepalive probes 15 | KeepAlive time.Duration `envconfig:"KEDA_HTTP_KEEP_ALIVE" default:"1s"` 16 | // ResponseHeaderTimeout is how long to wait between when the HTTP request 17 | // is sent to the backing app and when response headers need to arrive 18 | ResponseHeader time.Duration `envconfig:"KEDA_RESPONSE_HEADER_TIMEOUT" default:"500ms"` 19 | // WorkloadReplicas is how long to wait for the backing workload 20 | // to have 1 or more replicas before connecting and sending the HTTP request. 21 | WorkloadReplicas time.Duration `envconfig:"KEDA_CONDITION_WAIT_TIMEOUT" default:"1500ms"` 22 | // ForceHTTP2 toggles whether to try to force HTTP2 for all requests 23 | ForceHTTP2 bool `envconfig:"KEDA_HTTP_FORCE_HTTP2" default:"false"` 24 | // MaxIdleConns is the max number of connections that can be idle in the 25 | // interceptor's internal connection pool 26 | MaxIdleConns int `envconfig:"KEDA_HTTP_MAX_IDLE_CONNS" default:"100"` 27 | // IdleConnTimeout is the timeout after which a connection in the interceptor's 28 | // internal connection pool will be closed 29 | IdleConnTimeout time.Duration `envconfig:"KEDA_HTTP_IDLE_CONN_TIMEOUT" default:"90s"` 30 | // TLSHandshakeTimeout is the max amount of time the interceptor will 31 | // wait to establish a TLS connection 32 | TLSHandshakeTimeout time.Duration `envconfig:"KEDA_HTTP_TLS_HANDSHAKE_TIMEOUT" default:"10s"` 33 | // ExpectContinueTimeout is the max amount of time the interceptor will wait 34 | // after sending request headers if the server returned an Expect: 100-continue 35 | // header 36 | ExpectContinueTimeout time.Duration `envconfig:"KEDA_HTTP_EXPECT_CONTINUE_TIMEOUT" default:"1s"` 37 | } 38 | 39 | // Backoff returns a wait.Backoff based on the timeouts in t 40 | func (t *Timeouts) Backoff(factor, jitter float64, steps int) wait.Backoff { 41 | return wait.Backoff{ 42 | Duration: t.Connect, 43 | Factor: factor, 44 | Jitter: jitter, 45 | Steps: steps, 46 | } 47 | } 48 | 49 | // DefaultBackoff calls t.Backoff with reasonable defaults and returns 50 | // the result 51 | func (t Timeouts) DefaultBackoff() wait.Backoff { 52 | return t.Backoff(2, 0.5, 5) 53 | } 54 | 55 | // Parse parses standard configs using envconfig and returns a pointer to the 56 | // newly created config. Returns nil and a non-nil error if parsing failed 57 | func MustParseTimeouts() *Timeouts { 58 | ret := new(Timeouts) 59 | envconfig.MustProcess("", ret) 60 | return ret 61 | } 62 | -------------------------------------------------------------------------------- /interceptor/config/tracing.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kelseyhightower/envconfig" 5 | ) 6 | 7 | // Tracing is the configuration for configuring tracing through the interceptor. 8 | type Tracing struct { 9 | // States whether tracing should be enabled, False by default 10 | Enabled bool `envconfig:"OTEL_EXPORTER_OTLP_TRACES_ENABLED" default:"false"` 11 | // Sets what tracing export to use, must be one of: console,http/protobuf, grpc 12 | Exporter string `envconfig:"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" default:"console"` 13 | } 14 | 15 | // Parse parses standard configs using envconfig and returns a pointer to the 16 | // newly created config. Returns nil and a non-nil error if parsing failed 17 | func MustParseTracing() *Tracing { 18 | ret := new(Tracing) 19 | envconfig.MustProcess("", ret) 20 | return ret 21 | } 22 | -------------------------------------------------------------------------------- /interceptor/config/validate.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/go-logr/logr" 9 | ) 10 | 11 | func Validate(srvCfg *Serving, timeoutsCfg Timeouts, lggr logr.Logger) error { 12 | // TODO(jorturfer): delete this for v0.9.0 13 | _, deploymentEnvExist := os.LookupEnv("KEDA_HTTP_DEPLOYMENT_CACHE_POLLING_INTERVAL_MS") 14 | _, endpointsEnvExist := os.LookupEnv("KEDA_HTTP_ENDPOINTS_CACHE_POLLING_INTERVAL_MS") 15 | if deploymentEnvExist && endpointsEnvExist { 16 | return fmt.Errorf( 17 | "%s and %s are mutual exclusive", 18 | "KEDA_HTTP_DEPLOYMENT_CACHE_POLLING_INTERVAL_MS", 19 | "KEDA_HTTP_ENDPOINTS_CACHE_POLLING_INTERVAL_MS", 20 | ) 21 | } 22 | if deploymentEnvExist && !endpointsEnvExist { 23 | srvCfg.EndpointsCachePollIntervalMS = srvCfg.DeploymentCachePollIntervalMS 24 | srvCfg.DeploymentCachePollIntervalMS = 0 25 | lggr.Info("WARNING: KEDA_HTTP_DEPLOYMENT_CACHE_POLLING_INTERVAL_MS has been deprecated in favor of KEDA_HTTP_ENDPOINTS_CACHE_POLLING_INTERVAL_MS and wil be removed for v0.9.0") 26 | } 27 | // END TODO 28 | 29 | endpointsCachePollInterval := time.Duration(srvCfg.EndpointsCachePollIntervalMS) * time.Millisecond 30 | if timeoutsCfg.WorkloadReplicas < endpointsCachePollInterval { 31 | return fmt.Errorf( 32 | "workload replicas timeout (%s) should not be less than the Endpoints Cache Poll Interval (%s)", 33 | timeoutsCfg.WorkloadReplicas, 34 | endpointsCachePollInterval, 35 | ) 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /interceptor/forward_wait_func.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-logr/logr" 8 | v1 "k8s.io/api/core/v1" 9 | 10 | "github.com/kedacore/http-add-on/pkg/k8s" 11 | ) 12 | 13 | // forwardWaitFunc is a function that waits for a condition 14 | // before proceeding to serve the request. 15 | type forwardWaitFunc func(context.Context, string, string) (bool, error) 16 | 17 | func workloadActiveEndpoints(endpoints v1.Endpoints) int { 18 | total := 0 19 | for _, subset := range endpoints.Subsets { 20 | total += len(subset.Addresses) 21 | } 22 | return total 23 | } 24 | 25 | func newWorkloadReplicasForwardWaitFunc( 26 | lggr logr.Logger, 27 | endpointCache k8s.EndpointsCache, 28 | ) forwardWaitFunc { 29 | return func(ctx context.Context, endpointNS, endpointName string) (bool, error) { 30 | // get a watcher & its result channel before querying the 31 | // endpoints cache, to ensure we don't miss events 32 | watcher, err := endpointCache.Watch(endpointNS, endpointName) 33 | if err != nil { 34 | return false, err 35 | } 36 | eventCh := watcher.ResultChan() 37 | defer watcher.Stop() 38 | 39 | endpoints, err := endpointCache.Get(endpointNS, endpointName) 40 | if err != nil { 41 | // if we didn't get the initial endpoints state, bail out 42 | return false, fmt.Errorf( 43 | "error getting state for endpoints %s/%s: %w", 44 | endpointNS, 45 | endpointName, 46 | err, 47 | ) 48 | } 49 | // if there is 1 or more active endpoints, we're done waiting 50 | activeEndpoints := workloadActiveEndpoints(endpoints) 51 | if activeEndpoints > 0 { 52 | return false, nil 53 | } 54 | 55 | for { 56 | select { 57 | case event := <-eventCh: 58 | endpoints, ok := event.Object.(*v1.Endpoints) 59 | if !ok { 60 | lggr.Info( 61 | "Didn't get a endpoints back in event", 62 | ) 63 | } else if activeEndpoints := workloadActiveEndpoints(*endpoints); activeEndpoints > 0 { 64 | return true, nil 65 | } 66 | case <-ctx.Done(): 67 | // otherwise, if the context is marked done before 68 | // we're done waiting, fail. 69 | return false, fmt.Errorf( 70 | "context marked done while waiting for workload reach > 0 replicas: %w", 71 | ctx.Err(), 72 | ) 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /interceptor/handler/probe.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/kedacore/http-add-on/pkg/util" 10 | ) 11 | 12 | type Probe struct { 13 | healthCheckers []util.HealthChecker 14 | healthy atomic.Bool 15 | } 16 | 17 | func NewProbe(healthChecks []util.HealthChecker) *Probe { 18 | return &Probe{ 19 | healthCheckers: healthChecks, 20 | } 21 | } 22 | 23 | var _ http.Handler = (*Probe)(nil) 24 | 25 | func (ph *Probe) ServeHTTP(w http.ResponseWriter, r *http.Request) { 26 | r = util.RequestWithLoggerWithName(r, "ProbeHandler") 27 | ctx := r.Context() 28 | 29 | sc := http.StatusOK 30 | if !ph.healthy.Load() { 31 | sc = http.StatusServiceUnavailable 32 | } 33 | w.WriteHeader(sc) 34 | 35 | st := http.StatusText(sc) 36 | if _, err := w.Write([]byte(st)); err != nil { 37 | logger := util.LoggerFromContext(ctx) 38 | logger.Error(err, "write failed") 39 | } 40 | } 41 | 42 | func (ph *Probe) Start(ctx context.Context) { 43 | for { 44 | ph.check(ctx) 45 | 46 | select { 47 | case <-ctx.Done(): 48 | return 49 | case <-time.After(time.Second): 50 | continue 51 | } 52 | } 53 | } 54 | 55 | func (ph *Probe) check(ctx context.Context) { 56 | logger := util.LoggerFromContext(ctx) 57 | logger = logger.WithName("Probe") 58 | 59 | for _, hc := range ph.healthCheckers { 60 | if err := hc.HealthCheck(ctx); err != nil { 61 | ph.healthy.Store(false) 62 | 63 | logger.Error(err, "health check function failed") 64 | return 65 | } 66 | } 67 | 68 | ph.healthy.Store(true) 69 | } 70 | -------------------------------------------------------------------------------- /interceptor/handler/static.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kedacore/http-add-on/pkg/k8s" 7 | "github.com/kedacore/http-add-on/pkg/routing" 8 | "github.com/kedacore/http-add-on/pkg/util" 9 | ) 10 | 11 | type Static struct { 12 | statusCode int 13 | err error 14 | } 15 | 16 | func NewStatic(statusCode int, err error) *Static { 17 | return &Static{ 18 | statusCode: statusCode, 19 | err: err, 20 | } 21 | } 22 | 23 | var _ http.Handler = (*Static)(nil) 24 | 25 | func (sh *Static) ServeHTTP(w http.ResponseWriter, r *http.Request) { 26 | r = util.RequestWithLoggerWithName(r, "StaticHandler") 27 | ctx := r.Context() 28 | 29 | logger := util.LoggerFromContext(ctx) 30 | httpso := util.HTTPSOFromContext(ctx) 31 | stream := util.StreamFromContext(ctx) 32 | 33 | statusText := http.StatusText(sh.statusCode) 34 | routingKey := routing.NewKeyFromRequest(r) 35 | namespacedName := k8s.NamespacedNameFromObject(httpso) 36 | logger.Error(sh.err, statusText, "routingKey", routingKey, "namespacedName", namespacedName, "stream", stream) 37 | 38 | w.WriteHeader(sh.statusCode) 39 | if _, err := w.Write([]byte(statusText)); err != nil { 40 | logger.Error(err, "write failed") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /interceptor/handler/static_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | 9 | "github.com/go-logr/logr/funcr" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/kedacore/http-add-on/pkg/routing" 14 | "github.com/kedacore/http-add-on/pkg/util" 15 | ) 16 | 17 | var _ = Describe("ServeHTTP", func() { 18 | var ( 19 | w *httptest.ResponseRecorder 20 | r *http.Request 21 | 22 | sc = http.StatusTeapot 23 | st = http.StatusText(sc) 24 | 25 | se = errors.New("test error") 26 | ) 27 | 28 | BeforeEach(func() { 29 | w = httptest.NewRecorder() 30 | r = httptest.NewRequest(http.MethodGet, "/", nil) 31 | }) 32 | 33 | It("serves expected status code and body", func() { 34 | sh := NewStatic(sc, nil) 35 | sh.ServeHTTP(w, r) 36 | 37 | Expect(w.Code).To(Equal(sc)) 38 | Expect(w.Body.String()).To(Equal(st)) 39 | }) 40 | 41 | It("logs the failed request", func() { 42 | var b bool 43 | r = r.WithContext(util.ContextWithLogger(r.Context(), funcr.NewJSON( 44 | func(obj string) { 45 | var m map[string]interface{} 46 | 47 | err := json.Unmarshal([]byte(obj), &m) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | rk := routing.NewKeyFromRequest(r) 51 | Expect(m).To(HaveKeyWithValue("error", se.Error())) 52 | Expect(m).To(HaveKeyWithValue("msg", st)) 53 | Expect(m).To(HaveKeyWithValue("routingKey", rk.String())) 54 | 55 | b = true 56 | }, 57 | funcr.Options{}, 58 | ))) 59 | 60 | sh := NewStatic(sc, se) 61 | sh.ServeHTTP(w, r) 62 | 63 | Expect(b).To(BeTrue()) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /interceptor/handler/suite_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestHandler(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | 13 | RunSpecs(t, "Handler Suite") 14 | } 15 | -------------------------------------------------------------------------------- /interceptor/handler/upstream.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/http/httputil" 7 | 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/attribute" 10 | "go.opentelemetry.io/otel/propagation" 11 | "go.opentelemetry.io/otel/trace" 12 | 13 | "github.com/kedacore/http-add-on/interceptor/config" 14 | "github.com/kedacore/http-add-on/pkg/util" 15 | ) 16 | 17 | var ( 18 | errNilStream = errors.New("context stream is nil") 19 | ) 20 | 21 | type Upstream struct { 22 | roundTripper http.RoundTripper 23 | tracingCfg *config.Tracing 24 | } 25 | 26 | func NewUpstream(roundTripper http.RoundTripper, tracingCfg *config.Tracing) *Upstream { 27 | return &Upstream{ 28 | roundTripper: roundTripper, 29 | tracingCfg: tracingCfg, 30 | } 31 | } 32 | 33 | var _ http.Handler = (*Upstream)(nil) 34 | 35 | func (uh *Upstream) ServeHTTP(w http.ResponseWriter, r *http.Request) { 36 | r = util.RequestWithLoggerWithName(r, "UpstreamHandler") 37 | ctx := r.Context() 38 | 39 | if uh.tracingCfg.Enabled { 40 | p := otel.GetTextMapPropagator() 41 | ctx = p.Extract(ctx, propagation.HeaderCarrier(r.Header)) 42 | 43 | p.Inject(ctx, propagation.HeaderCarrier(w.Header())) 44 | 45 | span := trace.SpanFromContext(ctx) 46 | defer span.End() 47 | 48 | serviceValAttr := attribute.String("service", "keda-http-interceptor-proxy-upstream") 49 | coldStartValAttr := attribute.String("cold-start", w.Header().Get("X-KEDA-HTTP-Cold-Start")) 50 | 51 | span.SetAttributes(serviceValAttr, coldStartValAttr) 52 | } 53 | 54 | stream := util.StreamFromContext(ctx) 55 | if stream == nil { 56 | sh := NewStatic(http.StatusInternalServerError, errNilStream) 57 | sh.ServeHTTP(w, r) 58 | 59 | return 60 | } 61 | 62 | proxy := httputil.NewSingleHostReverseProxy(stream) 63 | superDirector := proxy.Director 64 | proxy.Transport = uh.roundTripper 65 | proxy.Director = func(req *http.Request) { 66 | superDirector(req) 67 | req.URL = stream 68 | req.URL.Path = r.URL.Path 69 | req.URL.RawPath = r.URL.RawPath 70 | req.URL.RawQuery = r.URL.RawQuery 71 | // delete the incoming X-Forwarded-For header so the proxy 72 | // puts its own in. This is also important to prevent IP spoofing 73 | req.Header.Del("X-Forwarded-For ") 74 | } 75 | proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { 76 | sh := NewStatic(http.StatusBadGateway, err) 77 | sh.ServeHTTP(w, r) 78 | } 79 | 80 | proxy.ServeHTTP(w, r) 81 | } 82 | -------------------------------------------------------------------------------- /interceptor/metrics/metricscollector.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/kedacore/http-add-on/interceptor/config" 5 | ) 6 | 7 | var ( 8 | collectors []Collector 9 | ) 10 | 11 | const meterName = "keda-interceptor-proxy" 12 | 13 | type Collector interface { 14 | RecordRequestCount(method string, path string, responseCode int, host string) 15 | RecordPendingRequestCount(host string, value int64) 16 | } 17 | 18 | func NewMetricsCollectors(metricsConfig *config.Metrics) { 19 | if metricsConfig.OtelPrometheusExporterEnabled { 20 | promometrics := NewPrometheusMetrics() 21 | collectors = append(collectors, promometrics) 22 | } 23 | 24 | if metricsConfig.OtelHTTPExporterEnabled { 25 | otelhttpmetrics := NewOtelMetrics() 26 | collectors = append(collectors, otelhttpmetrics) 27 | } 28 | } 29 | 30 | func RecordRequestCount(method string, path string, responseCode int, host string) { 31 | for _, collector := range collectors { 32 | collector.RecordRequestCount(method, path, responseCode, host) 33 | } 34 | } 35 | 36 | func RecordPendingRequestCount(host string, value int64) { 37 | for _, collector := range collectors { 38 | collector.RecordPendingRequestCount(host, value) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /interceptor/metrics/otelmetrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" 9 | api "go.opentelemetry.io/otel/metric" 10 | "go.opentelemetry.io/otel/sdk/metric" 11 | "go.opentelemetry.io/otel/sdk/resource" 12 | semconv "go.opentelemetry.io/otel/semconv/v1.4.0" 13 | 14 | "github.com/kedacore/http-add-on/pkg/build" 15 | ) 16 | 17 | type OtelMetrics struct { 18 | meter api.Meter 19 | requestCounter api.Int64Counter 20 | pendingRequestCounter api.Int64UpDownCounter 21 | } 22 | 23 | func NewOtelMetrics(options ...metric.Option) *OtelMetrics { 24 | ctx := context.Background() 25 | 26 | exporter, err := otlpmetrichttp.New(ctx) 27 | if err != nil { 28 | log.Fatalf("could not create otelmetrichttp exporter: %v", err) 29 | } 30 | 31 | if options == nil { 32 | res := resource.NewWithAttributes( 33 | semconv.SchemaURL, 34 | semconv.ServiceNameKey.String("interceptor-proxy"), 35 | semconv.ServiceVersionKey.String(build.Version()), 36 | ) 37 | 38 | options = []metric.Option{ 39 | metric.WithReader(metric.NewPeriodicReader(exporter)), 40 | metric.WithResource(res), 41 | } 42 | } 43 | 44 | provider := metric.NewMeterProvider(options...) 45 | meter := provider.Meter(meterName) 46 | 47 | reqCounter, err := meter.Int64Counter("interceptor_request_count", api.WithDescription("a counter of requests processed by the interceptor proxy")) 48 | if err != nil { 49 | log.Fatalf("could not create new otelhttpmetric request counter: %v", err) 50 | } 51 | 52 | pendingRequestCounter, err := meter.Int64UpDownCounter("interceptor_pending_request_count", api.WithDescription("a count of requests pending forwarding by the interceptor proxy")) 53 | if err != nil { 54 | log.Fatalf("could not create new otelhttpmetric pending request counter: %v", err) 55 | } 56 | 57 | return &OtelMetrics{ 58 | meter: meter, 59 | requestCounter: reqCounter, 60 | pendingRequestCounter: pendingRequestCounter, 61 | } 62 | } 63 | 64 | func (om *OtelMetrics) RecordRequestCount(method string, path string, responseCode int, host string) { 65 | ctx := context.Background() 66 | opt := api.WithAttributeSet( 67 | attribute.NewSet( 68 | attribute.Key("method").String(method), 69 | attribute.Key("path").String(path), 70 | attribute.Key("code").Int(responseCode), 71 | attribute.Key("host").String(host), 72 | ), 73 | ) 74 | om.requestCounter.Add(ctx, 1, opt) 75 | } 76 | 77 | func (om *OtelMetrics) RecordPendingRequestCount(host string, value int64) { 78 | ctx := context.Background() 79 | opt := api.WithAttributeSet( 80 | attribute.NewSet( 81 | attribute.Key("host").String(host), 82 | ), 83 | ) 84 | 85 | om.pendingRequestCounter.Add(ctx, value, opt) 86 | } 87 | -------------------------------------------------------------------------------- /interceptor/metrics/otelmetrics_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "go.opentelemetry.io/otel/sdk/metric" 9 | "go.opentelemetry.io/otel/sdk/metric/metricdata" 10 | ) 11 | 12 | var ( 13 | testOtel *OtelMetrics 14 | testReader metric.Reader 15 | ) 16 | 17 | func init() { 18 | testReader = metric.NewManualReader() 19 | options := metric.WithReader(testReader) 20 | testOtel = NewOtelMetrics(options) 21 | } 22 | 23 | func TestRequestCounter(t *testing.T) { 24 | testOtel.RecordRequestCount("GET", "/test", 200, "test-host-1") 25 | got := metricdata.ResourceMetrics{} 26 | err := testReader.Collect(context.Background(), &got) 27 | 28 | assert.Nil(t, err) 29 | scopeMetrics := got.ScopeMetrics[0] 30 | assert.NotEqual(t, len(scopeMetrics.Metrics), 0) 31 | 32 | metricInfo := retrieveMetric(scopeMetrics.Metrics, "interceptor_request_count") 33 | data := metricInfo.Data.(metricdata.Sum[int64]).DataPoints[0] 34 | assert.Equal(t, data.Value, int64(1)) 35 | } 36 | 37 | func TestPendingRequestCounter(t *testing.T) { 38 | testOtel.RecordPendingRequestCount("test-host", 5) 39 | got := metricdata.ResourceMetrics{} 40 | err := testReader.Collect(context.Background(), &got) 41 | 42 | assert.Nil(t, err) 43 | scopeMetrics := got.ScopeMetrics[0] 44 | assert.NotEqual(t, len(scopeMetrics.Metrics), 0) 45 | 46 | metricInfo := retrieveMetric(scopeMetrics.Metrics, "interceptor_pending_request_count") 47 | data := metricInfo.Data.(metricdata.Sum[int64]).DataPoints[0] 48 | assert.Equal(t, data.Value, int64(5)) 49 | } 50 | 51 | func retrieveMetric(metrics []metricdata.Metrics, metricname string) *metricdata.Metrics { 52 | for _, m := range metrics { 53 | if m.Name == metricname { 54 | return &m 55 | } 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /interceptor/metrics/prommetrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/exporters/prometheus" 9 | api "go.opentelemetry.io/otel/metric" 10 | "go.opentelemetry.io/otel/sdk/metric" 11 | "go.opentelemetry.io/otel/sdk/resource" 12 | semconv "go.opentelemetry.io/otel/semconv/v1.4.0" 13 | 14 | "github.com/kedacore/http-add-on/pkg/build" 15 | ) 16 | 17 | type PrometheusMetrics struct { 18 | meter api.Meter 19 | requestCounter api.Int64Counter 20 | pendingRequestCounter api.Int64UpDownCounter 21 | } 22 | 23 | func NewPrometheusMetrics(options ...prometheus.Option) *PrometheusMetrics { 24 | var exporter *prometheus.Exporter 25 | var err error 26 | if options == nil { 27 | exporter, err = prometheus.New() 28 | } else { 29 | exporter, err = prometheus.New(options...) 30 | } 31 | if err != nil { 32 | log.Fatalf("could not create Prometheus exporter: %v", err) 33 | } 34 | 35 | res := resource.NewWithAttributes( 36 | semconv.SchemaURL, 37 | semconv.ServiceNameKey.String("interceptor-proxy"), 38 | semconv.ServiceVersionKey.String(build.Version()), 39 | ) 40 | 41 | provider := metric.NewMeterProvider( 42 | metric.WithReader(exporter), 43 | metric.WithResource(res), 44 | ) 45 | meter := provider.Meter(meterName) 46 | 47 | reqCounter, err := meter.Int64Counter("interceptor_request_count", api.WithDescription("a counter of requests processed by the interceptor proxy")) 48 | if err != nil { 49 | log.Fatalf("could not create new Prometheus request counter: %v", err) 50 | } 51 | 52 | pendingRequestCounter, err := meter.Int64UpDownCounter("interceptor_pending_request_count", api.WithDescription("a count of requests pending forwarding by the interceptor proxy")) 53 | if err != nil { 54 | log.Fatalf("could not create new Prometheus pending request counter: %v", err) 55 | } 56 | 57 | return &PrometheusMetrics{ 58 | meter: meter, 59 | requestCounter: reqCounter, 60 | pendingRequestCounter: pendingRequestCounter, 61 | } 62 | } 63 | 64 | func (p *PrometheusMetrics) RecordRequestCount(method string, path string, responseCode int, host string) { 65 | ctx := context.Background() 66 | opt := api.WithAttributeSet( 67 | attribute.NewSet( 68 | attribute.Key("method").String(method), 69 | attribute.Key("path").String(path), 70 | attribute.Key("code").Int(responseCode), 71 | attribute.Key("host").String(host), 72 | ), 73 | ) 74 | p.requestCounter.Add(ctx, 1, opt) 75 | } 76 | 77 | func (p *PrometheusMetrics) RecordPendingRequestCount(host string, value int64) { 78 | ctx := context.Background() 79 | opt := api.WithAttributeSet( 80 | attribute.NewSet( 81 | attribute.Key("host").String(host), 82 | ), 83 | ) 84 | 85 | p.pendingRequestCounter.Add(ctx, value, opt) 86 | } 87 | -------------------------------------------------------------------------------- /interceptor/metrics/prommetrics_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/testutil" 9 | "github.com/stretchr/testify/assert" 10 | promexporter "go.opentelemetry.io/otel/exporters/prometheus" 11 | ) 12 | 13 | func TestPromRequestCountMetric(t *testing.T) { 14 | testRegistry := prometheus.NewRegistry() 15 | options := []promexporter.Option{promexporter.WithRegisterer(testRegistry)} 16 | testPrometheus := NewPrometheusMetrics(options...) 17 | expectedOutput := ` 18 | # HELP interceptor_request_count_total a counter of requests processed by the interceptor proxy 19 | # TYPE interceptor_request_count_total counter 20 | interceptor_request_count_total{code="500",host="test-host",method="post",otel_scope_name="keda-interceptor-proxy",otel_scope_version="",path="/test"} 1 21 | interceptor_request_count_total{code="200",host="test-host",method="post",otel_scope_name="keda-interceptor-proxy",otel_scope_version="",path="/test"} 1 22 | # HELP otel_scope_info Instrumentation Scope metadata 23 | # TYPE otel_scope_info gauge 24 | otel_scope_info{otel_scope_name="keda-interceptor-proxy",otel_scope_version=""} 1 25 | # HELP target_info Target metadata 26 | # TYPE target_info gauge 27 | target_info{"service.name"="interceptor-proxy","service.version"="main"} 1 28 | ` 29 | expectedOutputReader := strings.NewReader(expectedOutput) 30 | testPrometheus.RecordRequestCount("post", "/test", 500, "test-host") 31 | testPrometheus.RecordRequestCount("post", "/test", 200, "test-host") 32 | err := testutil.CollectAndCompare(testRegistry, expectedOutputReader) 33 | assert.Nil(t, err) 34 | } 35 | 36 | func TestPromPendingRequestCountMetric(t *testing.T) { 37 | testRegistry := prometheus.NewRegistry() 38 | options := []promexporter.Option{promexporter.WithRegisterer(testRegistry)} 39 | testPrometheus := NewPrometheusMetrics(options...) 40 | expectedOutput := ` 41 | # HELP interceptor_pending_request_count a count of requests pending forwarding by the interceptor proxy 42 | # TYPE interceptor_pending_request_count gauge 43 | interceptor_pending_request_count{host="test-host",otel_scope_name="keda-interceptor-proxy",otel_scope_version=""} 10 44 | # HELP otel_scope_info Instrumentation Scope metadata 45 | # TYPE otel_scope_info gauge 46 | otel_scope_info{otel_scope_name="keda-interceptor-proxy",otel_scope_version=""} 1 47 | # HELP target_info Target metadata 48 | # TYPE target_info gauge 49 | target_info{"service.name"="interceptor-proxy","service.version"="main"} 1 50 | ` 51 | expectedOutputReader := strings.NewReader(expectedOutput) 52 | testPrometheus.RecordPendingRequestCount("test-host", 10) 53 | err := testutil.CollectAndCompare(testRegistry, expectedOutputReader) 54 | assert.Nil(t, err) 55 | } 56 | -------------------------------------------------------------------------------- /interceptor/middleware/counting.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/go-logr/logr" 8 | 9 | "github.com/kedacore/http-add-on/interceptor/metrics" 10 | "github.com/kedacore/http-add-on/pkg/k8s" 11 | "github.com/kedacore/http-add-on/pkg/queue" 12 | "github.com/kedacore/http-add-on/pkg/util" 13 | ) 14 | 15 | type Counting struct { 16 | queueCounter queue.Counter 17 | upstreamHandler http.Handler 18 | } 19 | 20 | func NewCountingMiddleware(queueCounter queue.Counter, upstreamHandler http.Handler) *Counting { 21 | return &Counting{ 22 | queueCounter: queueCounter, 23 | upstreamHandler: upstreamHandler, 24 | } 25 | } 26 | 27 | var _ http.Handler = (*Counting)(nil) 28 | 29 | func (cm *Counting) ServeHTTP(w http.ResponseWriter, r *http.Request) { 30 | r = util.RequestWithLoggerWithName(r, "CountingMiddleware") 31 | ctx := r.Context() 32 | 33 | defer cm.countAsync(ctx)() 34 | 35 | cm.upstreamHandler.ServeHTTP(w, r) 36 | } 37 | 38 | func (cm *Counting) countAsync(ctx context.Context) func() { 39 | signaler := util.NewSignaler() 40 | 41 | go cm.count(ctx, signaler) 42 | 43 | return func() { 44 | go signaler.Signal() 45 | } 46 | } 47 | 48 | func (cm *Counting) count(ctx context.Context, signaler util.Signaler) { 49 | logger := util.LoggerFromContext(ctx) 50 | httpso := util.HTTPSOFromContext(ctx) 51 | 52 | key := k8s.NamespacedNameFromObject(httpso).String() 53 | 54 | if !cm.inc(logger, key) { 55 | return 56 | } 57 | 58 | if err := signaler.Wait(ctx); err != nil && err != context.Canceled { 59 | logger.Error(err, "failed to wait signal") 60 | } 61 | 62 | cm.dec(logger, key) 63 | } 64 | 65 | func (cm *Counting) inc(logger logr.Logger, key string) bool { 66 | if err := cm.queueCounter.Increase(key, 1); err != nil { 67 | logger.Error(err, "error incrementing queue counter", "key", key) 68 | 69 | return false 70 | } 71 | 72 | metrics.RecordPendingRequestCount(key, int64(1)) 73 | 74 | return true 75 | } 76 | 77 | func (cm *Counting) dec(logger logr.Logger, key string) bool { 78 | if err := cm.queueCounter.Decrease(key, 1); err != nil { 79 | logger.Error(err, "error decrementing queue counter", "key", key) 80 | 81 | return false 82 | } 83 | 84 | metrics.RecordPendingRequestCount(key, int64(-1)) 85 | 86 | return true 87 | } 88 | -------------------------------------------------------------------------------- /interceptor/middleware/logging.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-logr/logr" 8 | 9 | "github.com/kedacore/http-add-on/pkg/util" 10 | ) 11 | 12 | const ( 13 | CombinedLogFormat = `%s %s %s [%s] "%s %s %s" %d %d "%s" "%s"` 14 | CombinedLogTimeFormat = "02/Jan/2006:15:04:05 -0700" 15 | CombinedLogBlankValue = "-" 16 | ) 17 | 18 | type Logging struct { 19 | logger logr.Logger 20 | upstreamHandler http.Handler 21 | } 22 | 23 | func NewLogging(logger logr.Logger, upstreamHandler http.Handler) *Logging { 24 | return &Logging{ 25 | logger: logger, 26 | upstreamHandler: upstreamHandler, 27 | } 28 | } 29 | 30 | var _ http.Handler = (*Logging)(nil) 31 | 32 | func (lm *Logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { 33 | r = util.RequestWithLogger(r, lm.logger.WithName("LoggingMiddleware")) 34 | w = newResponseWriter(w) 35 | 36 | var sw util.Stopwatch 37 | defer lm.logAsync(w, r, &sw) 38 | 39 | sw.Start() 40 | defer sw.Stop() 41 | 42 | lm.upstreamHandler.ServeHTTP(w, r) 43 | } 44 | 45 | func (lm *Logging) logAsync(w http.ResponseWriter, r *http.Request, sw *util.Stopwatch) { 46 | go lm.log(w, r, sw) 47 | } 48 | 49 | func (lm *Logging) log(w http.ResponseWriter, r *http.Request, sw *util.Stopwatch) { 50 | ctx := r.Context() 51 | logger := util.LoggerFromContext(ctx) 52 | 53 | lrw := w.(*responseWriter) 54 | if lrw == nil { 55 | lrw = newResponseWriter(w) 56 | } 57 | 58 | timestamp := sw.StartTime().Format(CombinedLogTimeFormat) 59 | log := fmt.Sprintf( 60 | CombinedLogFormat, 61 | r.RemoteAddr, 62 | CombinedLogBlankValue, 63 | CombinedLogBlankValue, 64 | timestamp, 65 | r.Method, 66 | r.URL.Path, 67 | r.Proto, 68 | lrw.StatusCode(), 69 | lrw.BytesWritten(), 70 | r.Referer(), 71 | r.UserAgent(), 72 | ) 73 | logger.Info(log) 74 | } 75 | -------------------------------------------------------------------------------- /interceptor/middleware/metrics.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kedacore/http-add-on/interceptor/metrics" 7 | ) 8 | 9 | type Metrics struct { 10 | upstreamHandler http.Handler 11 | } 12 | 13 | func NewMetrics(upstreamHandler http.Handler) *Metrics { 14 | return &Metrics{ 15 | upstreamHandler: upstreamHandler, 16 | } 17 | } 18 | 19 | func (m *Metrics) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | w = newResponseWriter(w) 21 | 22 | defer m.metrics(w, r) 23 | 24 | m.upstreamHandler.ServeHTTP(w, r) 25 | } 26 | 27 | func (m *Metrics) metrics(w http.ResponseWriter, r *http.Request) { 28 | mrw := w.(*responseWriter) 29 | if mrw == nil { 30 | mrw = newResponseWriter(w) 31 | } 32 | 33 | // exclude readiness & liveness probes from the emitted metrics 34 | if r.URL.Path != "/livez" && r.URL.Path != "/readyz" { 35 | metrics.RecordRequestCount(r.Method, r.URL.Path, mrw.statusCode, r.Host) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /interceptor/middleware/responsewriter.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type responseWriter struct { 8 | downstreamResponseWriter http.ResponseWriter 9 | bytesWritten int 10 | statusCode int 11 | } 12 | 13 | func newResponseWriter(downstreamResponseWriter http.ResponseWriter) *responseWriter { 14 | return &responseWriter{ 15 | downstreamResponseWriter: downstreamResponseWriter, 16 | } 17 | } 18 | 19 | func (rw *responseWriter) BytesWritten() int { 20 | return rw.bytesWritten 21 | } 22 | 23 | func (rw *responseWriter) StatusCode() int { 24 | return rw.statusCode 25 | } 26 | 27 | var _ http.ResponseWriter = (*responseWriter)(nil) 28 | 29 | func (rw *responseWriter) Header() http.Header { 30 | return rw.downstreamResponseWriter.Header() 31 | } 32 | 33 | func (rw *responseWriter) Write(bytes []byte) (int, error) { 34 | n, err := rw.downstreamResponseWriter.Write(bytes) 35 | if f, ok := rw.downstreamResponseWriter.(http.Flusher); ok { 36 | f.Flush() 37 | } 38 | 39 | rw.bytesWritten += n 40 | 41 | return n, err 42 | } 43 | 44 | func (rw *responseWriter) WriteHeader(statusCode int) { 45 | rw.downstreamResponseWriter.WriteHeader(statusCode) 46 | 47 | rw.statusCode = statusCode 48 | } 49 | -------------------------------------------------------------------------------- /interceptor/middleware/suite_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMiddleware(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | 13 | RunSpecs(t, "Middleware Suite") 14 | } 15 | -------------------------------------------------------------------------------- /interceptor/suite_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestInterceptor(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | 13 | RunSpecs(t, "Interceptor Suite") 14 | } 15 | -------------------------------------------------------------------------------- /interceptor/tracing/tracing_test.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/kedacore/http-add-on/interceptor/config" 9 | ) 10 | 11 | func TestTracingConfig(t *testing.T) { 12 | tracingCfg := config.MustParseTracing() 13 | tracingCfg.Enabled = true 14 | 15 | // check defaults are set correctly 16 | assert.Equal(t, "console", tracingCfg.Exporter) 17 | assert.Equal(t, true, tracingCfg.Enabled) 18 | } 19 | -------------------------------------------------------------------------------- /operator/.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /operator/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | Dockerfile.cross 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Kubernetes Generated files - skip generated files, except for vendored files 19 | 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | -------------------------------------------------------------------------------- /operator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.23.6 as builder 2 | WORKDIR /workspace 3 | COPY go.* . 4 | RUN go mod download 5 | COPY . . 6 | ARG VERSION=main 7 | ARG GIT_COMMIT=HEAD 8 | ARG TARGETOS 9 | ARG TARGETARCH 10 | RUN VERSION="${VERSION}" GIT_COMMIT="${GIT_COMMIT}" TARGET_OS="${TARGETOS}" ARCH="${TARGETARCH}" make build-operator 11 | 12 | FROM gcr.io/distroless/static:nonroot 13 | COPY --from=builder /workspace/bin/operator /sbin/init 14 | ENTRYPOINT ["/sbin/init"] 15 | -------------------------------------------------------------------------------- /operator/PROJECT: -------------------------------------------------------------------------------- 1 | domain: keda.sh 2 | layout: 3 | - go.kubebuilder.io/v3 4 | multigroup: true 5 | plugins: 6 | manifests.sdk.operatorframework.io/v2: {} 7 | scorecard.sdk.operatorframework.io/v2: {} 8 | projectName: http-addon 9 | repo: github.com/kedacore/http-add-on/operator 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: keda.sh 16 | group: http 17 | kind: HTTPScaledObject 18 | path: github.com/kedacore/http-add-on/operator/apis/http/v1alpha1 19 | version: v1alpha1 20 | version: "3" 21 | -------------------------------------------------------------------------------- /operator/apis/http/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the http v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=http.keda.sh 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // SchemeGroupVersion is group version used to register these objects 29 | SchemeGroupVersion = schema.GroupVersion{Group: "http.keda.sh", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | 38 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 39 | func Resource(resource string) schema.GroupResource { 40 | return SchemeGroupVersion.WithResource(resource).GroupResource() 41 | } 42 | -------------------------------------------------------------------------------- /operator/controllers/http/condition_provider.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-logr/logr" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 12 | ) 13 | 14 | // SaveStatus will trigger an object update to save the current status 15 | // conditions 16 | func SaveStatus( 17 | ctx context.Context, 18 | logger logr.Logger, 19 | cl client.Client, 20 | httpso *httpv1alpha1.HTTPScaledObject, 21 | ) { 22 | logger.Info("Updating status on HTTPScaledObject", "resource version", httpso.ResourceVersion) 23 | 24 | err := cl.Status().Update(ctx, httpso) 25 | if err != nil { 26 | logger.Error(err, "failed to update status on HTTPScaledObject", "httpso", httpso) 27 | } else { 28 | logger.Info("Updated status on HTTPScaledObject", "resource version", httpso.ResourceVersion) 29 | } 30 | } 31 | 32 | // AddOrUpdateCondition adds or update a condition to the HTTPScaledObject 33 | func AddOrUpdateCondition(httpso *httpv1alpha1.HTTPScaledObject, condition httpv1alpha1.HTTPScaledObjectCondition) *httpv1alpha1.HTTPScaledObject { 34 | found := false 35 | for i := range httpso.Status.Conditions { 36 | if httpso.Status.Conditions[i].Reason == condition.Reason { 37 | found = true 38 | httpso.Status.Conditions[i] = condition 39 | } 40 | } 41 | if !found { 42 | httpso.Status.Conditions = append(httpso.Status.Conditions, condition) 43 | } 44 | return httpso 45 | } 46 | 47 | // CreateCondition initializes a new status condition 48 | func CreateCondition( 49 | condType httpv1alpha1.HTTPScaledObjectCreationStatus, 50 | status metav1.ConditionStatus, 51 | reason httpv1alpha1.HTTPScaledObjectConditionReason, 52 | ) *httpv1alpha1.HTTPScaledObjectCondition { 53 | cond := httpv1alpha1.HTTPScaledObjectCondition{ 54 | Timestamp: time.Now().Format(time.RFC3339), 55 | Type: condType, 56 | Status: status, 57 | Reason: reason, 58 | } 59 | return &cond 60 | } 61 | 62 | // SetMessage sets the optional reason for the condition 63 | func SetMessage(c *httpv1alpha1.HTTPScaledObjectCondition, message string) *httpv1alpha1.HTTPScaledObjectCondition { 64 | c.Message = message 65 | return c 66 | } 67 | -------------------------------------------------------------------------------- /operator/controllers/http/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestExternalScalerHostName(t *testing.T) { 12 | r := require.New(t) 13 | sc := ExternalScaler{ 14 | ServiceName: "TestExternalScalerHostNameSvc", 15 | Port: int32(8098), 16 | } 17 | const ns = "testns" 18 | hst := sc.HostName(ns) 19 | spl := strings.Split(hst, ".") 20 | r.Equal(2, len(spl), "HostName should return a hostname with 2 parts") 21 | r.Equal(sc.ServiceName, spl[0]) 22 | r.Equal(fmt.Sprintf("%s:%d", ns, sc.Port), spl[1]) 23 | } 24 | -------------------------------------------------------------------------------- /operator/controllers/http/finalizer.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-logr/logr" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | 9 | httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 10 | ) 11 | 12 | const ( 13 | httpScaledObjectFinalizer = "httpscaledobject.http.keda.sh" 14 | ) 15 | 16 | // ensureFinalizer check there is finalizer present on the ScaledObject, if not it adds one 17 | func ensureFinalizer( 18 | ctx context.Context, 19 | logger logr.Logger, 20 | client client.Client, 21 | httpso *httpv1alpha1.HTTPScaledObject, 22 | ) error { 23 | if !contains(httpso.GetFinalizers(), httpScaledObjectFinalizer) { 24 | logger.Info("Adding Finalizer for the ScaledObject") 25 | httpso.SetFinalizers(append(httpso.GetFinalizers(), httpScaledObjectFinalizer)) 26 | 27 | // Update CR 28 | err := client.Update(ctx, httpso) 29 | if err != nil { 30 | logger.Error( 31 | err, 32 | "Failed to update HTTPScaledObject with a finalizer", 33 | "finalizer", 34 | httpScaledObjectFinalizer, 35 | ) 36 | return err 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | func finalizeScaledObject( 43 | ctx context.Context, 44 | logger logr.Logger, 45 | client client.Client, 46 | httpso *httpv1alpha1.HTTPScaledObject) error { 47 | if contains(httpso.GetFinalizers(), httpScaledObjectFinalizer) { 48 | httpso.SetFinalizers(remove(httpso.GetFinalizers(), httpScaledObjectFinalizer)) 49 | if err := client.Update(ctx, httpso); err != nil { 50 | logger.Error( 51 | err, 52 | "Failed to update ScaledObject after removing a finalizer", 53 | "finalizer", 54 | httpScaledObjectFinalizer, 55 | ) 56 | return err 57 | } 58 | } 59 | 60 | logger.Info("Successfully finalized HTTPScaledObject") 61 | return nil 62 | } 63 | 64 | // contains checks if the passed string is present in the given slice of strings. 65 | // This is taken from github.com/kedacore/keda 66 | func contains(list []string, s string) bool { 67 | for _, v := range list { 68 | if v == s { 69 | return true 70 | } 71 | } 72 | return false 73 | } 74 | 75 | // remove deletes the passed string from the given slice of strings. 76 | // This is taken from github.com/kedacore/keda 77 | func remove(list []string, s string) []string { 78 | for i, v := range list { 79 | if v == s { 80 | list = append(list[:i], list[i+1:]...) 81 | } 82 | } 83 | return list 84 | } 85 | -------------------------------------------------------------------------------- /operator/controllers/http/ping.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "golang.org/x/sync/errgroup" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/kedacore/http-add-on/pkg/k8s" 12 | ) 13 | 14 | func pingInterceptors( 15 | ctx context.Context, 16 | cl client.Client, 17 | httpCl *http.Client, 18 | ns, 19 | interceptorSvcName, 20 | interceptorPort string, 21 | ) error { 22 | endpointURLs, err := k8s.EndpointsForService( 23 | ctx, 24 | ns, 25 | interceptorSvcName, 26 | interceptorPort, 27 | k8s.EndpointsFuncForControllerClient(cl), 28 | ) 29 | if err != nil { 30 | return fmt.Errorf("pingInterceptors: %w", err) 31 | } 32 | errGrp, _ := errgroup.WithContext(ctx) 33 | for _, endpointURL := range endpointURLs { 34 | endpointStr := endpointURL.String() 35 | errGrp.Go(func() error { 36 | fullAddr := fmt.Sprintf("%s/routing_ping", endpointStr) 37 | resp, err := httpCl.Get(fullAddr) 38 | if err != nil { 39 | return err 40 | } 41 | resp.Body.Close() 42 | return nil 43 | }) 44 | } 45 | return errGrp.Wait() 46 | } 47 | -------------------------------------------------------------------------------- /operator/controllers/http/ping_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 10 | 11 | "github.com/kedacore/http-add-on/pkg/k8s" 12 | kedanet "github.com/kedacore/http-add-on/pkg/net" 13 | ) 14 | 15 | func TestPingInterceptors(t *testing.T) { 16 | const ( 17 | ns = "testns" 18 | svcName = "testsvc" 19 | ) 20 | r := require.New(t) 21 | // create a new server (that we can introspect later on) to act 22 | // like a fake interceptor. we expect that pingInterceptors() 23 | // will make requests to this server 24 | hdl := kedanet.NewTestHTTPHandlerWrapper( 25 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | w.WriteHeader(200) 27 | }), 28 | ) 29 | srv, url, err := kedanet.StartTestServer(hdl) 30 | r.NoError(err) 31 | defer srv.Close() 32 | ctx := context.Background() 33 | endpoints, err := k8s.FakeEndpointsForURL(url, ns, svcName, 2) 34 | r.NoError(err) 35 | cl := fake.NewClientBuilder().WithObjects(endpoints).Build() 36 | r.NoError(pingInterceptors( 37 | ctx, 38 | cl, 39 | srv.Client(), 40 | ns, 41 | svcName, 42 | url.Port(), 43 | )) 44 | reqs := hdl.IncomingRequests() 45 | r.Equal(len(endpoints.Subsets[0].Addresses), len(reqs)) 46 | } 47 | -------------------------------------------------------------------------------- /operator/controllers/util/predicate.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" 5 | "k8s.io/apimachinery/pkg/api/equality" 6 | "sigs.k8s.io/controller-runtime/pkg/event" 7 | "sigs.k8s.io/controller-runtime/pkg/predicate" 8 | 9 | "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 10 | ) 11 | 12 | type HTTPScaledObjectReadyConditionPredicate struct { 13 | predicate.Funcs 14 | } 15 | 16 | func (HTTPScaledObjectReadyConditionPredicate) Update(e event.UpdateEvent) bool { 17 | if e.ObjectOld == nil || e.ObjectNew == nil { 18 | return false 19 | } 20 | 21 | var newReadyCondition, oldReadyCondition v1alpha1.HTTPScaledObjectCondition 22 | 23 | oldObj, ok := e.ObjectOld.(*v1alpha1.HTTPScaledObject) 24 | if !ok { 25 | return false 26 | } 27 | oldReadyCondition = oldObj.Status.Conditions.GetReadyCondition() 28 | 29 | newObj, ok := e.ObjectNew.(*v1alpha1.HTTPScaledObject) 30 | if !ok { 31 | return false 32 | } 33 | newReadyCondition = newObj.Status.Conditions.GetReadyCondition() 34 | 35 | // False/Unknown -> True 36 | if !oldReadyCondition.IsTrue() && newReadyCondition.IsTrue() { 37 | return true 38 | } 39 | 40 | return false 41 | } 42 | 43 | type ScaledObjectSpecChangedPredicate struct { 44 | predicate.Funcs 45 | } 46 | 47 | func (ScaledObjectSpecChangedPredicate) Update(e event.UpdateEvent) bool { 48 | newObj := e.ObjectNew.(*kedav1alpha1.ScaledObject) 49 | oldObj := e.ObjectOld.(*kedav1alpha1.ScaledObject) 50 | 51 | return !equality.Semantic.DeepDerivative(newObj.Spec, oldObj.Spec) 52 | } 53 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated fake clientset. 20 | package fake 21 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | ) 29 | 30 | var scheme = runtime.NewScheme() 31 | var codecs = serializer.NewCodecFactory(scheme) 32 | 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | httpv1alpha1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package contains the scheme of the automatically generated clientset. 20 | package scheme 21 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/scheme/mock/doc.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Copyright 2023 The KEDA Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // */ 16 | // 17 | 18 | // Code generated by MockGen. DO NOT EDIT. 19 | // Source: operator/generated/clientset/versioned/scheme/doc.go 20 | // 21 | // Generated by this command: 22 | // 23 | // mockgen -copyright_file=hack/boilerplate.go.txt -destination=operator/generated/clientset/versioned/scheme/mock/doc.go -package=mock -source=operator/generated/clientset/versioned/scheme/doc.go 24 | // 25 | 26 | // Package mock is a generated GoMock package. 27 | package mock 28 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/scheme/mock/register.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Copyright 2023 The KEDA Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // */ 16 | // 17 | 18 | // Code generated by MockGen. DO NOT EDIT. 19 | // Source: operator/generated/clientset/versioned/scheme/register.go 20 | // 21 | // Generated by this command: 22 | // 23 | // mockgen -copyright_file=hack/boilerplate.go.txt -destination=operator/generated/clientset/versioned/scheme/mock/register.go -package=mock -source=operator/generated/clientset/versioned/scheme/register.go 24 | // 25 | 26 | // Package mock is a generated GoMock package. 27 | package mock 28 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package scheme 20 | 21 | import ( 22 | httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | ) 29 | 30 | var Scheme = runtime.NewScheme() 31 | var Codecs = serializer.NewCodecFactory(Scheme) 32 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | httpv1alpha1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(Scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/typed/http/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated typed clients. 20 | package v1alpha1 21 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/typed/http/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // Package fake has the automatically generated clients. 20 | package fake 21 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/typed/http/v1alpha1/fake/fake_http_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | v1alpha1 "github.com/kedacore/http-add-on/operator/generated/clientset/versioned/typed/http/v1alpha1" 23 | rest "k8s.io/client-go/rest" 24 | testing "k8s.io/client-go/testing" 25 | ) 26 | 27 | type FakeHttpV1alpha1 struct { 28 | *testing.Fake 29 | } 30 | 31 | func (c *FakeHttpV1alpha1) HTTPScaledObjects(namespace string) v1alpha1.HTTPScaledObjectInterface { 32 | return &FakeHTTPScaledObjects{c, namespace} 33 | } 34 | 35 | // RESTClient returns a RESTClient that is used to communicate 36 | // with API server by this client implementation. 37 | func (c *FakeHttpV1alpha1) RESTClient() rest.Interface { 38 | var ret *rest.RESTClient 39 | return ret 40 | } 41 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/typed/http/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | type HTTPScaledObjectExpansion interface{} 22 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/typed/http/v1alpha1/mock/doc.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Copyright 2023 The KEDA Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // */ 16 | // 17 | 18 | // Code generated by MockGen. DO NOT EDIT. 19 | // Source: operator/generated/clientset/versioned/typed/http/v1alpha1/doc.go 20 | // 21 | // Generated by this command: 22 | // 23 | // mockgen -copyright_file=hack/boilerplate.go.txt -destination=operator/generated/clientset/versioned/typed/http/v1alpha1/mock/doc.go -package=mock -source=operator/generated/clientset/versioned/typed/http/v1alpha1/doc.go 24 | // 25 | 26 | // Package mock is a generated GoMock package. 27 | package mock 28 | -------------------------------------------------------------------------------- /operator/generated/clientset/versioned/typed/http/v1alpha1/mock/generated_expansion.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Copyright 2023 The KEDA Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // */ 16 | // 17 | 18 | // Code generated by MockGen. DO NOT EDIT. 19 | // Source: operator/generated/clientset/versioned/typed/http/v1alpha1/generated_expansion.go 20 | // 21 | // Generated by this command: 22 | // 23 | // mockgen -copyright_file=hack/boilerplate.go.txt -destination=operator/generated/clientset/versioned/typed/http/v1alpha1/mock/generated_expansion.go -package=mock -source=operator/generated/clientset/versioned/typed/http/v1alpha1/generated_expansion.go 24 | // 25 | 26 | // Package mock is a generated GoMock package. 27 | package mock 28 | 29 | import ( 30 | gomock "go.uber.org/mock/gomock" 31 | ) 32 | 33 | // MockHTTPScaledObjectExpansion is a mock of HTTPScaledObjectExpansion interface. 34 | type MockHTTPScaledObjectExpansion struct { 35 | ctrl *gomock.Controller 36 | recorder *MockHTTPScaledObjectExpansionMockRecorder 37 | isgomock struct{} 38 | } 39 | 40 | // MockHTTPScaledObjectExpansionMockRecorder is the mock recorder for MockHTTPScaledObjectExpansion. 41 | type MockHTTPScaledObjectExpansionMockRecorder struct { 42 | mock *MockHTTPScaledObjectExpansion 43 | } 44 | 45 | // NewMockHTTPScaledObjectExpansion creates a new mock instance. 46 | func NewMockHTTPScaledObjectExpansion(ctrl *gomock.Controller) *MockHTTPScaledObjectExpansion { 47 | mock := &MockHTTPScaledObjectExpansion{ctrl: ctrl} 48 | mock.recorder = &MockHTTPScaledObjectExpansionMockRecorder{mock} 49 | return mock 50 | } 51 | 52 | // EXPECT returns an object that allows the caller to indicate expected use. 53 | func (m *MockHTTPScaledObjectExpansion) EXPECT() *MockHTTPScaledObjectExpansionMockRecorder { 54 | return m.recorder 55 | } 56 | -------------------------------------------------------------------------------- /operator/generated/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package externalversions 20 | 21 | import ( 22 | "fmt" 23 | 24 | v1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | cache "k8s.io/client-go/tools/cache" 27 | ) 28 | 29 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 30 | // sharedInformers based on type 31 | type GenericInformer interface { 32 | Informer() cache.SharedIndexInformer 33 | Lister() cache.GenericLister 34 | } 35 | 36 | type genericInformer struct { 37 | informer cache.SharedIndexInformer 38 | resource schema.GroupResource 39 | } 40 | 41 | // Informer returns the SharedIndexInformer. 42 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 43 | return f.informer 44 | } 45 | 46 | // Lister returns the GenericLister. 47 | func (f *genericInformer) Lister() cache.GenericLister { 48 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 49 | } 50 | 51 | // ForResource gives generic access to a shared informer of the matching type 52 | // TODO extend this to unknown resources with a client pool 53 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 54 | switch resource { 55 | // Group=http, Version=v1alpha1 56 | case v1alpha1.SchemeGroupVersion.WithResource("httpscaledobjects"): 57 | return &genericInformer{resource: resource.GroupResource(), informer: f.Http().V1alpha1().HTTPScaledObjects().Informer()}, nil 58 | 59 | } 60 | 61 | return nil, fmt.Errorf("no informer found for %v", resource) 62 | } 63 | -------------------------------------------------------------------------------- /operator/generated/informers/externalversions/http/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package http 20 | 21 | import ( 22 | v1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1" 23 | internalinterfaces "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/internalinterfaces" 24 | ) 25 | 26 | // Interface provides access to each of this group's versions. 27 | type Interface interface { 28 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 29 | V1alpha1() v1alpha1.Interface 30 | } 31 | 32 | type group struct { 33 | factory internalinterfaces.SharedInformerFactory 34 | namespace string 35 | tweakListOptions internalinterfaces.TweakListOptionsFunc 36 | } 37 | 38 | // New returns a new Interface. 39 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 40 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 41 | } 42 | 43 | // V1alpha1 returns a new v1alpha1.Interface. 44 | func (g *group) V1alpha1() v1alpha1.Interface { 45 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 46 | } 47 | -------------------------------------------------------------------------------- /operator/generated/informers/externalversions/http/mock/interface.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Copyright 2023 The KEDA Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // */ 16 | // 17 | 18 | // Code generated by MockGen. DO NOT EDIT. 19 | // Source: operator/generated/informers/externalversions/http/interface.go 20 | // 21 | // Generated by this command: 22 | // 23 | // mockgen -copyright_file=hack/boilerplate.go.txt -destination=operator/generated/informers/externalversions/http/mock/interface.go -package=mock -source=operator/generated/informers/externalversions/http/interface.go 24 | // 25 | 26 | // Package mock is a generated GoMock package. 27 | package mock 28 | 29 | import ( 30 | reflect "reflect" 31 | 32 | v1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1" 33 | gomock "go.uber.org/mock/gomock" 34 | ) 35 | 36 | // MockInterface is a mock of Interface interface. 37 | type MockInterface struct { 38 | ctrl *gomock.Controller 39 | recorder *MockInterfaceMockRecorder 40 | isgomock struct{} 41 | } 42 | 43 | // MockInterfaceMockRecorder is the mock recorder for MockInterface. 44 | type MockInterfaceMockRecorder struct { 45 | mock *MockInterface 46 | } 47 | 48 | // NewMockInterface creates a new mock instance. 49 | func NewMockInterface(ctrl *gomock.Controller) *MockInterface { 50 | mock := &MockInterface{ctrl: ctrl} 51 | mock.recorder = &MockInterfaceMockRecorder{mock} 52 | return mock 53 | } 54 | 55 | // EXPECT returns an object that allows the caller to indicate expected use. 56 | func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { 57 | return m.recorder 58 | } 59 | 60 | // V1alpha1 mocks base method. 61 | func (m *MockInterface) V1alpha1() v1alpha1.Interface { 62 | m.ctrl.T.Helper() 63 | ret := m.ctrl.Call(m, "V1alpha1") 64 | ret0, _ := ret[0].(v1alpha1.Interface) 65 | return ret0 66 | } 67 | 68 | // V1alpha1 indicates an expected call of V1alpha1. 69 | func (mr *MockInterfaceMockRecorder) V1alpha1() *gomock.Call { 70 | mr.mock.ctrl.T.Helper() 71 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "V1alpha1", reflect.TypeOf((*MockInterface)(nil).V1alpha1)) 72 | } 73 | -------------------------------------------------------------------------------- /operator/generated/informers/externalversions/http/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | internalinterfaces "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/internalinterfaces" 23 | ) 24 | 25 | // Interface provides access to all the informers in this group version. 26 | type Interface interface { 27 | // HTTPScaledObjects returns a HTTPScaledObjectInformer. 28 | HTTPScaledObjects() HTTPScaledObjectInformer 29 | } 30 | 31 | type version struct { 32 | factory internalinterfaces.SharedInformerFactory 33 | namespace string 34 | tweakListOptions internalinterfaces.TweakListOptionsFunc 35 | } 36 | 37 | // New returns a new Interface. 38 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 39 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 40 | } 41 | 42 | // HTTPScaledObjects returns a HTTPScaledObjectInformer. 43 | func (v *version) HTTPScaledObjects() HTTPScaledObjectInformer { 44 | return &hTTPScaledObjectInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 45 | } 46 | -------------------------------------------------------------------------------- /operator/generated/informers/externalversions/http/v1alpha1/mock/interface.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Copyright 2023 The KEDA Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // */ 16 | // 17 | 18 | // Code generated by MockGen. DO NOT EDIT. 19 | // Source: operator/generated/informers/externalversions/http/v1alpha1/interface.go 20 | // 21 | // Generated by this command: 22 | // 23 | // mockgen -copyright_file=hack/boilerplate.go.txt -destination=operator/generated/informers/externalversions/http/v1alpha1/mock/interface.go -package=mock -source=operator/generated/informers/externalversions/http/v1alpha1/interface.go 24 | // 25 | 26 | // Package mock is a generated GoMock package. 27 | package mock 28 | 29 | import ( 30 | reflect "reflect" 31 | 32 | v1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1" 33 | gomock "go.uber.org/mock/gomock" 34 | ) 35 | 36 | // MockInterface is a mock of Interface interface. 37 | type MockInterface struct { 38 | ctrl *gomock.Controller 39 | recorder *MockInterfaceMockRecorder 40 | isgomock struct{} 41 | } 42 | 43 | // MockInterfaceMockRecorder is the mock recorder for MockInterface. 44 | type MockInterfaceMockRecorder struct { 45 | mock *MockInterface 46 | } 47 | 48 | // NewMockInterface creates a new mock instance. 49 | func NewMockInterface(ctrl *gomock.Controller) *MockInterface { 50 | mock := &MockInterface{ctrl: ctrl} 51 | mock.recorder = &MockInterfaceMockRecorder{mock} 52 | return mock 53 | } 54 | 55 | // EXPECT returns an object that allows the caller to indicate expected use. 56 | func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { 57 | return m.recorder 58 | } 59 | 60 | // HTTPScaledObjects mocks base method. 61 | func (m *MockInterface) HTTPScaledObjects() v1alpha1.HTTPScaledObjectInformer { 62 | m.ctrl.T.Helper() 63 | ret := m.ctrl.Call(m, "HTTPScaledObjects") 64 | ret0, _ := ret[0].(v1alpha1.HTTPScaledObjectInformer) 65 | return ret0 66 | } 67 | 68 | // HTTPScaledObjects indicates an expected call of HTTPScaledObjects. 69 | func (mr *MockInterfaceMockRecorder) HTTPScaledObjects() *gomock.Call { 70 | mr.mock.ctrl.T.Helper() 71 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HTTPScaledObjects", reflect.TypeOf((*MockInterface)(nil).HTTPScaledObjects)) 72 | } 73 | -------------------------------------------------------------------------------- /operator/generated/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package internalinterfaces 20 | 21 | import ( 22 | time "time" 23 | 24 | versioned "github.com/kedacore/http-add-on/operator/generated/clientset/versioned" 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | cache "k8s.io/client-go/tools/cache" 28 | ) 29 | 30 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 31 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 32 | 33 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 34 | type SharedInformerFactory interface { 35 | Start(stopCh <-chan struct{}) 36 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 37 | } 38 | 39 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 40 | type TweakListOptionsFunc func(*v1.ListOptions) 41 | -------------------------------------------------------------------------------- /operator/generated/listers/http/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | // HTTPScaledObjectListerExpansion allows custom methods to be added to 22 | // HTTPScaledObjectLister. 23 | type HTTPScaledObjectListerExpansion interface{} 24 | 25 | // HTTPScaledObjectNamespaceListerExpansion allows custom methods to be added to 26 | // HTTPScaledObjectNamespaceLister. 27 | type HTTPScaledObjectNamespaceListerExpansion interface{} 28 | -------------------------------------------------------------------------------- /pkg/build/version.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/go-logr/logr" 8 | ) 9 | 10 | var ( 11 | version = "main" 12 | gitCommit string 13 | ) 14 | 15 | // Version returns the current git SHA of commit the binary was built from 16 | func Version() string { 17 | return version 18 | } 19 | 20 | // GitCommit stores the current commit hash 21 | func GitCommit() string { 22 | return gitCommit 23 | } 24 | 25 | func PrintComponentInfo(logger logr.Logger, component string) { 26 | logger.Info(fmt.Sprintf("%s Version: %s", component, Version())) 27 | logger.Info(fmt.Sprintf("%s Commit: %s", component, GitCommit())) 28 | logger.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) 29 | logger.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | // GetOr gets the value of the environment variable called envName. If that variable 10 | // is not set, returns otherwise 11 | func GetOr(envName, otherwise string) string { 12 | fromEnv, err := Get(envName) 13 | if err != nil { 14 | return otherwise 15 | } 16 | return fromEnv 17 | } 18 | 19 | // Get gets the value of the environment variable called envName. If that variable 20 | // is not set, returns a non-nil error 21 | func Get(envName string) (string, error) { 22 | fromEnv := os.Getenv(envName) 23 | if fromEnv == "" { 24 | return "", fmt.Errorf("environnment variable %s not found", envName) 25 | } 26 | return fromEnv, nil 27 | } 28 | 29 | // GetInt32Or returns the int32 value of the environment variable called envName. 30 | // If the environment variable is missing or it's not a valid int32, returns otherwise 31 | func GetInt32Or(envName string, otherwise int32) int32 { 32 | strVal, err := Get(envName) 33 | if err != nil { 34 | return otherwise 35 | } 36 | val, err := strconv.ParseInt(strVal, 10, 32) 37 | if err != nil { 38 | return otherwise 39 | } 40 | return int32(val) 41 | } 42 | 43 | func GetIntOr(envName string, otherwise int) int { 44 | strVal, err := Get(envName) 45 | if err != nil { 46 | return otherwise 47 | } 48 | val, err := strconv.Atoi(strVal) 49 | if err != nil { 50 | return otherwise 51 | } 52 | return val 53 | } 54 | -------------------------------------------------------------------------------- /pkg/http/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | 8 | "github.com/kedacore/http-add-on/pkg/util" 9 | ) 10 | 11 | func ServeContext(ctx context.Context, addr string, hdl http.Handler, tlsConfig *tls.Config) error { 12 | srv := &http.Server{ 13 | Handler: hdl, 14 | Addr: addr, 15 | TLSConfig: tlsConfig, 16 | } 17 | 18 | go func() { 19 | <-ctx.Done() 20 | 21 | if err := srv.Shutdown(context.Background()); err != nil { 22 | logger := util.LoggerFromContext(ctx) 23 | logger.Error(err, "failed shutting down server") 24 | } 25 | }() 26 | 27 | if tlsConfig != nil { 28 | return srv.ListenAndServeTLS("", "") 29 | } 30 | 31 | return srv.ListenAndServe() 32 | } 33 | -------------------------------------------------------------------------------- /pkg/http/server_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestServeContext(t *testing.T) { 15 | r := require.New(t) 16 | ctx, done := context.WithCancel( 17 | context.Background(), 18 | ) 19 | hdl := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | w.Header().Set("foo", "bar") 21 | _, err := w.Write([]byte("hello world")) 22 | if err != nil { 23 | t.Fatalf("error writing message to client from handler") 24 | } 25 | }) 26 | addr := "localhost:1234" 27 | const cancelDur = 500 * time.Millisecond 28 | go func() { 29 | time.Sleep(cancelDur) 30 | done() 31 | }() 32 | start := time.Now() 33 | err := ServeContext(ctx, addr, hdl, nil) 34 | elapsed := time.Since(start) 35 | 36 | r.Error(err) 37 | r.True(errors.Is(err, http.ErrServerClosed), "error is not a http.ErrServerClosed (%w)", err) 38 | r.Greater(elapsed, cancelDur) 39 | r.Less(elapsed, cancelDur*4) 40 | } 41 | 42 | func TestServeContextWithTLS(t *testing.T) { 43 | r := require.New(t) 44 | ctx, done := context.WithCancel( 45 | context.Background(), 46 | ) 47 | hdl := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 | w.Header().Set("foo", "bar") 49 | _, err := w.Write([]byte("hello world")) 50 | if err != nil { 51 | t.Fatalf("error writing message to client from handler") 52 | } 53 | }) 54 | addr := "localhost:1234" 55 | const cancelDur = 500 * time.Millisecond 56 | go func() { 57 | time.Sleep(cancelDur) 58 | done() 59 | }() 60 | start := time.Now() 61 | tlsConfig := tls.Config{ 62 | GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 63 | cert, err := tls.LoadX509KeyPair("../../certs/tls.crt", "../../certs/tls.key") 64 | return &cert, err 65 | }, 66 | } 67 | err := ServeContext(ctx, addr, hdl, &tlsConfig) 68 | elapsed := time.Since(start) 69 | 70 | r.Error(err) 71 | r.True(errors.Is(err, http.ErrServerClosed), "error is not a http.ErrServerClosed (%w)", err) 72 | r.Greater(elapsed, cancelDur) 73 | r.Less(elapsed, cancelDur*4) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/http/test_utils.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | nethttp "net/http" 5 | "net/http/httptest" 6 | ) 7 | 8 | func NewTestCtx( 9 | method, 10 | path string, 11 | ) (*nethttp.Request, *httptest.ResponseRecorder) { 12 | req := httptest.NewRequest(method, path, nil) 13 | rec := httptest.NewRecorder() 14 | return req, rec 15 | } 16 | -------------------------------------------------------------------------------- /pkg/k8s/endpoints.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | 8 | v1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/kubernetes" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | ) 13 | 14 | // GetEndpointsFunc is a type that represents a function that can 15 | // fetch endpoints 16 | type GetEndpointsFunc func( 17 | ctx context.Context, 18 | namespace, 19 | serviceName string, 20 | ) (*v1.Endpoints, error) 21 | 22 | func EndpointsForService( 23 | ctx context.Context, 24 | ns, 25 | serviceName, 26 | servicePort string, 27 | endpointsFn GetEndpointsFunc, 28 | ) ([]*url.URL, error) { 29 | endpoints, err := endpointsFn(ctx, ns, serviceName) 30 | if err != nil { 31 | return nil, fmt.Errorf("pkg.k8s.EndpointsForService: %w", err) 32 | } 33 | ret := []*url.URL{} 34 | for _, subset := range endpoints.Subsets { 35 | for _, addr := range subset.Addresses { 36 | u, err := url.Parse( 37 | fmt.Sprintf("http://%s:%s", addr.IP, servicePort), 38 | ) 39 | if err != nil { 40 | return nil, err 41 | } 42 | ret = append(ret, u) 43 | } 44 | } 45 | 46 | return ret, nil 47 | } 48 | 49 | // EndpointsFuncForControllerClient returns a new GetEndpointsFunc 50 | // that uses the controller-runtime client.Client to fetch endpoints 51 | func EndpointsFuncForControllerClient( 52 | cl client.Client, 53 | ) GetEndpointsFunc { 54 | return func( 55 | ctx context.Context, 56 | namespace, 57 | serviceName string, 58 | ) (*v1.Endpoints, error) { 59 | endpts := &v1.Endpoints{} 60 | if err := cl.Get(ctx, client.ObjectKey{ 61 | Namespace: namespace, 62 | Name: serviceName, 63 | }, endpts); err != nil { 64 | return nil, err 65 | } 66 | return endpts, nil 67 | } 68 | } 69 | 70 | func EndpointsFuncForK8sClientset( 71 | cl *kubernetes.Clientset, 72 | ) GetEndpointsFunc { 73 | return func( 74 | ctx context.Context, 75 | namespace, 76 | serviceName string, 77 | ) (*v1.Endpoints, error) { 78 | endpointsCl := cl.CoreV1().Endpoints(namespace) 79 | return endpointsCl.Get(ctx, serviceName, metav1.GetOptions{}) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/k8s/endpoints_cache.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | v1 "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/watch" 8 | ) 9 | 10 | // EndpointsCache is a simple cache of endpoints. 11 | // It allows callers to quickly get given endpoints in a given 12 | // namespace, or watch for changes to specific endpoints, all 13 | // without incurring the cost of issuing a network request 14 | // to the Kubernetes API 15 | type EndpointsCache interface { 16 | json.Marshaler 17 | // Get gets the endpoints with the given name 18 | // in the given namespace from the cache. 19 | // 20 | // If the endpoints doesn't exist in the cache, it 21 | // will be requested from the backing store (most commonly 22 | // the Kubernetes API server) 23 | Get(namespace, name string) (v1.Endpoints, error) 24 | // Watch opens a watch stream for the endpoints with 25 | // the given name in the given namespace from the cache. 26 | // 27 | // If the endpoints don't exist in the cache, it 28 | // will be requested from the backing store (most commonly 29 | // the Kubernetes API server) 30 | Watch(namespace, name string) (watch.Interface, error) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/k8s/fake_endpoints.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | 7 | v1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // FakeEndpointsForURL creates and returns a new *v1.Endpoints with a 12 | // single v1.EndpointSubset in it, which has num v1.EndpointAddresses 13 | // in it. Each of those EndpointAddresses has a Hostname and IP both 14 | // equal to u.Hostname() 15 | func FakeEndpointsForURL( 16 | u *url.URL, 17 | namespace, 18 | name string, 19 | num int, 20 | ) (*v1.Endpoints, error) { 21 | urls := make([]*url.URL, num) 22 | for i := 0; i < num; i++ { 23 | urls[i] = u 24 | } 25 | return FakeEndpointsForURLs(urls, namespace, name) 26 | } 27 | 28 | // FakeEndpointsForURLs creates and returns a new 29 | // *v1.Endpoints with a single v1.EndpointSubset in it 30 | // that has each url in the urls parameter in it. 31 | func FakeEndpointsForURLs( 32 | urls []*url.URL, 33 | namespace, 34 | name string, 35 | ) (*v1.Endpoints, error) { 36 | addrs := make([]v1.EndpointAddress, len(urls)) 37 | ports := make([]v1.EndpointPort, len(urls)) 38 | for i, u := range urls { 39 | addrs[i] = v1.EndpointAddress{ 40 | Hostname: u.Hostname(), 41 | IP: u.Hostname(), 42 | } 43 | portInt, err := strconv.Atoi(u.Port()) 44 | if err != nil { 45 | return nil, err 46 | } 47 | ports[i] = v1.EndpointPort{ 48 | Port: int32(portInt), 49 | } 50 | } 51 | return &v1.Endpoints{ 52 | ObjectMeta: metav1.ObjectMeta{ 53 | Name: name, 54 | Namespace: namespace, 55 | }, 56 | Subsets: []v1.EndpointSubset{ 57 | { 58 | Addresses: addrs, 59 | Ports: ports, 60 | }, 61 | }, 62 | }, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/k8s/namespacedname.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "github.com/kedacore/keda/v2/pkg/scalers/externalscaler" 5 | "k8s.io/apimachinery/pkg/types" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | 8 | "github.com/kedacore/http-add-on/pkg/util" 9 | ) 10 | 11 | func NamespacedNameFromObject(obj client.Object) *types.NamespacedName { 12 | if util.IsNil(obj) { 13 | return nil 14 | } 15 | 16 | return &types.NamespacedName{ 17 | Namespace: obj.GetNamespace(), 18 | Name: obj.GetName(), 19 | } 20 | } 21 | 22 | func NamespacedNameFromScaledObjectRef(sor *externalscaler.ScaledObjectRef) *types.NamespacedName { 23 | if sor == nil { 24 | return nil 25 | } 26 | 27 | return &types.NamespacedName{ 28 | Namespace: sor.GetNamespace(), 29 | Name: sor.GetName(), 30 | } 31 | } 32 | 33 | func NamespacedNameFromNameAndNamespace(name, namespace string) *types.NamespacedName { 34 | if name == "" || namespace == "" { 35 | return nil 36 | } 37 | 38 | return &types.NamespacedName{ 39 | Name: name, 40 | Namespace: namespace, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/k8s/scaledobject.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/utils/ptr" 7 | 8 | "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 9 | ) 10 | 11 | const ( 12 | soPollingInterval = 15 13 | soTriggerType = "external-push" 14 | 15 | ScalerAddressKey = "scalerAddress" 16 | HTTPScaledObjectKey = "httpScaledObject" 17 | ) 18 | 19 | // NewScaledObject creates a new ScaledObject in memory 20 | func NewScaledObject( 21 | namespace string, 22 | name string, 23 | labels map[string]string, 24 | annotations map[string]string, 25 | workloadRef v1alpha1.ScaleTargetRef, 26 | scalerAddress string, 27 | minReplicas *int32, 28 | maxReplicas *int32, 29 | cooldownPeriod *int32, 30 | initialCooldownPeriod *int32, 31 | ) *kedav1alpha1.ScaledObject { 32 | so := &kedav1alpha1.ScaledObject{ 33 | TypeMeta: metav1.TypeMeta{ 34 | APIVersion: kedav1alpha1.SchemeGroupVersion.Identifier(), 35 | Kind: ObjectKind(&kedav1alpha1.ScaledObject{}), 36 | }, 37 | ObjectMeta: metav1.ObjectMeta{ 38 | Namespace: namespace, 39 | Name: name, 40 | Labels: labels, 41 | Annotations: annotations, 42 | }, 43 | Spec: kedav1alpha1.ScaledObjectSpec{ 44 | ScaleTargetRef: &kedav1alpha1.ScaleTarget{ 45 | APIVersion: workloadRef.APIVersion, 46 | Kind: workloadRef.Kind, 47 | Name: workloadRef.Name, 48 | }, 49 | PollingInterval: ptr.To[int32](soPollingInterval), 50 | CooldownPeriod: cooldownPeriod, 51 | MinReplicaCount: minReplicas, 52 | MaxReplicaCount: maxReplicas, 53 | Advanced: &kedav1alpha1.AdvancedConfig{ 54 | RestoreToOriginalReplicaCount: true, 55 | }, 56 | Triggers: []kedav1alpha1.ScaleTriggers{ 57 | { 58 | Type: soTriggerType, 59 | Metadata: map[string]string{ 60 | ScalerAddressKey: scalerAddress, 61 | HTTPScaledObjectKey: name, 62 | }, 63 | }, 64 | }, 65 | }, 66 | } 67 | if initialCooldownPeriod != nil { 68 | so.Spec.InitialCooldownPeriod = *initialCooldownPeriod 69 | } 70 | return so 71 | } 72 | -------------------------------------------------------------------------------- /pkg/k8s/scheme.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "reflect" 5 | 6 | "k8s.io/apimachinery/pkg/runtime" 7 | ) 8 | 9 | func ObjectKind(obj runtime.Object) string { 10 | t := reflect.TypeOf(obj) 11 | if t.Kind() == reflect.Pointer { 12 | t = t.Elem() 13 | } 14 | return t.Name() 15 | } 16 | -------------------------------------------------------------------------------- /pkg/k8s/val_ptrs.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | // Int32P converts an int32 into an *int32. It's a convenience function 4 | // for various values in Kubernetes API types 5 | func Int32P(i int32) *int32 { 6 | return &i 7 | } 8 | -------------------------------------------------------------------------------- /pkg/net/backoff.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "time" 5 | 6 | "k8s.io/apimachinery/pkg/util/wait" 7 | ) 8 | 9 | // MinTotalBackoffDuration returns the minimum duration that backoff 10 | // would wait, including all steps, not including any jitter 11 | func MinTotalBackoffDuration(backoff wait.Backoff) time.Duration { 12 | initial := backoff.Duration.Milliseconds() 13 | retMS := backoff.Duration.Milliseconds() 14 | numSteps := backoff.Steps 15 | for i := 2; i <= numSteps; i++ { 16 | retMS += initial * int64(i) 17 | } 18 | return time.Duration(retMS) * time.Millisecond 19 | } 20 | -------------------------------------------------------------------------------- /pkg/net/dial_context.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "k8s.io/apimachinery/pkg/util/wait" 10 | ) 11 | 12 | // DialContextFunc is a function that matches the (net).Dialer.DialContext functions's 13 | // signature 14 | type DialContextFunc func(ctx context.Context, network, addr string) (net.Conn, error) 15 | 16 | // DialContextWithRetry creates a new DialContextFunc -- 17 | // which has the same signature as the net.Conn.DialContext function -- 18 | // that calls coreDialer.DialContext multiple times with an exponential backoff. 19 | // The timeout of the first dial will be coreDialer.Timeout, and it will 20 | // multiply by a factor of two from there. 21 | // 22 | // This function is mainly used in the interceptor so that, once it sees that the target deployment has 23 | // 1 or more replicas, it can forward the request. If the deployment's state changed 24 | // in the time slice between detecting >=1 replicas and the network send, the connection 25 | // will be retried a few times. 26 | // 27 | // Thanks to Knative for inspiring this code. See GitHub link below 28 | // https://github.com/knative/serving/blob/20815258c92d0f26100031c71a91d0bef930a475/vendor/knative.dev/pkg/network/transports.go#L70 29 | func DialContextWithRetry(coreDialer *net.Dialer, backoff wait.Backoff) DialContextFunc { 30 | numDialTries := backoff.Steps 31 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 32 | // We need to copy the backoff struct because Step() mutates it but the 33 | // number of steps is never reset. 34 | backoff := backoff 35 | 36 | // note that we could test for backoff.Steps >= 0 here, but every call to backoff.Step() 37 | // (below) decrements the backoff.Steps value. If you accidentally call that function 38 | // more than once inside the loop, you will reduce the number of times the loop 39 | // executes. Using a standard counter makes this algorithm less likely to introduce 40 | // a bug 41 | var lastError error 42 | for i := 0; i < numDialTries; i++ { 43 | conn, err := coreDialer.DialContext(ctx, network, addr) 44 | if err == nil { 45 | return conn, nil 46 | } 47 | lastError = err 48 | sleepDur := backoff.Step() 49 | t := time.NewTimer(sleepDur) 50 | select { 51 | case <-ctx.Done(): 52 | t.Stop() 53 | return nil, fmt.Errorf("context timed out: %w", ctx.Err()) 54 | case <-t.C: 55 | t.Stop() 56 | } 57 | } 58 | return nil, lastError 59 | } 60 | } 61 | 62 | // NewNetDialer creates a new (net).Dialer with the given connection timeout and 63 | // keep alive duration. 64 | func NewNetDialer(connectTimeout, keepAlive time.Duration) *net.Dialer { 65 | return &net.Dialer{ 66 | Timeout: connectTimeout, 67 | KeepAlive: keepAlive, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/net/dial_context_test.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | "k8s.io/apimachinery/pkg/util/wait" 10 | ) 11 | 12 | func TestDialContextWithRetry(t *testing.T) { 13 | r := require.New(t) 14 | 15 | const ( 16 | connTimeout = 10 * time.Millisecond 17 | keepAlive = 10 * time.Millisecond 18 | network = "tcp" 19 | addr = "localhost:60001" 20 | ) 21 | backoff := wait.Backoff{ 22 | Duration: connTimeout, 23 | Factor: 2, 24 | Jitter: 0.5, 25 | Steps: 5, 26 | } 27 | 28 | ctx := context.Background() 29 | dialer := NewNetDialer(connTimeout, keepAlive) 30 | dRetry := DialContextWithRetry(dialer, backoff) 31 | minTotalWaitDur := MinTotalBackoffDuration(backoff) 32 | 33 | start := time.Now() 34 | _, err := dRetry(ctx, network, addr) 35 | r.Error(err, "error was not found") 36 | 37 | elapsed := time.Since(start) 38 | r.GreaterOrEqual( 39 | elapsed, 40 | minTotalWaitDur, 41 | "total elapsed (%s) was not >= than the minimum expected (%s)", 42 | elapsed, 43 | minTotalWaitDur, 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/net/mock_server.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "sync" 8 | ) 9 | 10 | type TestHTTPHandlerWrapper struct { 11 | rwm *sync.RWMutex 12 | hdl http.Handler 13 | incomingRequests []http.Request 14 | } 15 | 16 | func (t *TestHTTPHandlerWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { 17 | t.hdl.ServeHTTP(w, r) 18 | // log a request after the handler has been run 19 | t.rwm.Lock() 20 | t.incomingRequests = append(t.incomingRequests, *r) 21 | // do not put this unlock in a defer or after the ServeHTTP line below, 22 | // or you risk deadlock if the handler waits for something or otherwise 23 | // hangs 24 | t.rwm.Unlock() 25 | } 26 | 27 | // IncomingRequests returns a copy slice of all the requests that have been received before 28 | // this function was called. 29 | func (t *TestHTTPHandlerWrapper) IncomingRequests() []http.Request { 30 | t.rwm.RLock() 31 | defer t.rwm.RUnlock() 32 | retSlice := make([]http.Request, len(t.incomingRequests)) 33 | copy(retSlice, t.incomingRequests) 34 | return retSlice 35 | } 36 | 37 | func NewTestHTTPHandlerWrapper(hdl http.Handler) *TestHTTPHandlerWrapper { 38 | return &TestHTTPHandlerWrapper{ 39 | rwm: new(sync.RWMutex), 40 | hdl: hdl, 41 | incomingRequests: nil, 42 | } 43 | } 44 | 45 | // StartTestServer creates and starts an *httptest.Server 46 | // in the background, then parses its URL and returns both 47 | // values. 48 | // 49 | // If this function returns a nil error, the caller is 50 | // responsible for closing the returned server. If it 51 | // returns a non-nil error, the server and URL 52 | // will be nil. 53 | func StartTestServer(hdl http.Handler) (*httptest.Server, *url.URL, error) { 54 | srv := httptest.NewServer(hdl) 55 | u, err := url.Parse(srv.URL) 56 | if err != nil { 57 | return nil, nil, err 58 | } 59 | return srv, u, nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/queue/queue_counts.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Count is a snapshot of the HTTP pending request (Concurrency) 9 | // count and RPS 10 | 11 | type Count struct { 12 | Concurrency int 13 | RPS float64 14 | } 15 | 16 | // Count is a snapshot of the HTTP pending request (Concurrency) 17 | // count and RPS for each host. 18 | // This is a json.Marshaler, json.Unmarshaler, and fmt.Stringer 19 | // implementation. 20 | // 21 | // Use NewQueueCounts to create a new one of these. 22 | type Counts struct { 23 | json.Marshaler 24 | json.Unmarshaler 25 | fmt.Stringer 26 | Counts map[string]Count 27 | } 28 | 29 | // NewQueueCounts creates a new empty QueueCounts struct 30 | func NewCounts() *Counts { 31 | return &Counts{ 32 | Counts: map[string]Count{}, 33 | } 34 | } 35 | 36 | // Aggregate returns the total count across all hosts 37 | func (q *Counts) Aggregate() Count { 38 | res := Count{} 39 | for _, count := range q.Counts { 40 | res.Concurrency += count.Concurrency 41 | res.RPS += count.RPS 42 | } 43 | return res 44 | } 45 | 46 | // MarshalJSON implements json.Marshaler 47 | func (q *Counts) MarshalJSON() ([]byte, error) { 48 | return json.Marshal(q.Counts) 49 | } 50 | 51 | // UnmarshalJSON implements json.Unmarshaler 52 | func (q *Counts) UnmarshalJSON(data []byte) error { 53 | return json.Unmarshal(data, &q.Counts) 54 | } 55 | 56 | // String implements fmt.Stringer 57 | func (q *Counts) String() string { 58 | return fmt.Sprintf("%v", q.Counts) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/queue/queue_counts_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestAggregate(t *testing.T) { 10 | r := require.New(t) 11 | counts := NewCounts() 12 | counts.Counts = map[string]Count{ 13 | "host1": { 14 | Concurrency: 123, 15 | RPS: 123, 16 | }, 17 | "host2": { 18 | Concurrency: 234, 19 | RPS: 234, 20 | }, 21 | "host3": { 22 | Concurrency: 345, 23 | RPS: 345, 24 | }, 25 | "host4": { 26 | Concurrency: 456, 27 | RPS: 456, 28 | }, 29 | } 30 | expectedConcurrency := 0 31 | expectedRPS := 0. 32 | for _, v := range counts.Counts { 33 | expectedConcurrency += v.Concurrency 34 | expectedRPS += v.RPS 35 | } 36 | agg := counts.Aggregate() 37 | r.Equal(expectedConcurrency, agg.Concurrency) 38 | r.Equal(expectedRPS, agg.RPS) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/queue/queue_fakes.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | var _ Counter = (*FakeCounter)(nil) 10 | 11 | type HostAndCount struct { 12 | Host string 13 | Count int 14 | } 15 | type FakeCounter struct { 16 | mapMut *sync.RWMutex 17 | RetMap map[string]Count 18 | ResizedCh chan HostAndCount 19 | ResizeTimeout time.Duration 20 | } 21 | 22 | func NewFakeCounter() *FakeCounter { 23 | return &FakeCounter{ 24 | mapMut: new(sync.RWMutex), 25 | RetMap: map[string]Count{}, 26 | ResizedCh: make(chan HostAndCount), 27 | ResizeTimeout: 1 * time.Second, 28 | } 29 | } 30 | 31 | func (f *FakeCounter) Increase(host string, i int) error { 32 | f.mapMut.Lock() 33 | count := f.RetMap[host] 34 | count.Concurrency += i 35 | count.RPS += float64(i) 36 | f.RetMap[host] = count 37 | f.mapMut.Unlock() 38 | select { 39 | case f.ResizedCh <- HostAndCount{Host: host, Count: i}: 40 | case <-time.After(f.ResizeTimeout): 41 | return fmt.Errorf( 42 | "FakeCounter.Increase timeout after %s", 43 | f.ResizeTimeout, 44 | ) 45 | } 46 | return nil 47 | } 48 | 49 | func (f *FakeCounter) Decrease(host string, i int) error { 50 | f.mapMut.Lock() 51 | count := f.RetMap[host] 52 | count.Concurrency -= i 53 | f.RetMap[host] = count 54 | f.mapMut.Unlock() 55 | select { 56 | case f.ResizedCh <- HostAndCount{Host: host, Count: i}: 57 | case <-time.After(f.ResizeTimeout): 58 | return fmt.Errorf( 59 | "FakeCounter.Decrease timeout after %s", 60 | f.ResizeTimeout, 61 | ) 62 | } 63 | return nil 64 | } 65 | 66 | func (f *FakeCounter) EnsureKey(host string, _, _ time.Duration) { 67 | f.mapMut.Lock() 68 | defer f.mapMut.Unlock() 69 | f.RetMap[host] = Count{ 70 | Concurrency: 0, 71 | } 72 | } 73 | 74 | func (f *FakeCounter) UpdateBuckets(_ string, _, _ time.Duration) {} 75 | 76 | func (f *FakeCounter) RemoveKey(host string) bool { 77 | f.mapMut.Lock() 78 | defer f.mapMut.Unlock() 79 | _, ok := f.RetMap[host] 80 | delete(f.RetMap, host) 81 | return ok 82 | } 83 | 84 | func (f *FakeCounter) Current() (*Counts, error) { 85 | ret := NewCounts() 86 | f.mapMut.RLock() 87 | defer f.mapMut.RUnlock() 88 | retMap := f.RetMap 89 | ret.Counts = retMap 90 | return ret, nil 91 | } 92 | 93 | var _ CountReader = &FakeCountReader{} 94 | 95 | type FakeCountReader struct { 96 | concurrency int 97 | rps float64 98 | err error 99 | } 100 | 101 | func (f *FakeCountReader) Current() (*Counts, error) { 102 | ret := NewCounts() 103 | ret.Counts = map[string]Count{ 104 | "sample.com": { 105 | Concurrency: f.concurrency, 106 | RPS: f.rps, 107 | }, 108 | } 109 | return ret, f.err 110 | } 111 | -------------------------------------------------------------------------------- /pkg/queue/queue_rpc.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/go-logr/logr" 10 | ) 11 | 12 | const countsPath = "/queue" 13 | 14 | func AddCountsRoute(lggr logr.Logger, mux *http.ServeMux, q CountReader) { 15 | lggr = lggr.WithName("pkg.queue.AddCountsRoute") 16 | lggr.Info("adding queue counts route", "path", countsPath) 17 | mux.Handle(countsPath, newSizeHandler(lggr, q)) 18 | } 19 | 20 | // newForwardingHandler takes in the service URL for the app backend 21 | // and forwards incoming requests to it. Note that it isn't multitenant. 22 | // It's intended to be deployed and scaled alongside the application itself 23 | func newSizeHandler( 24 | lggr logr.Logger, 25 | q CountReader, 26 | ) http.Handler { 27 | return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 28 | cur, err := q.Current() 29 | if err != nil { 30 | lggr.Error(err, "getting queue size") 31 | w.WriteHeader(500) 32 | if _, err := w.Write([]byte( 33 | "error getting queue size", 34 | )); err != nil { 35 | lggr.Error( 36 | err, 37 | "could not send error message to client", 38 | ) 39 | } 40 | return 41 | } 42 | if err := json.NewEncoder(w).Encode(cur); err != nil { 43 | lggr.Error(err, "encoding QueueCounts") 44 | w.WriteHeader(500) 45 | if _, err := w.Write([]byte( 46 | "error encoding queue counts", 47 | )); err != nil { 48 | lggr.Error( 49 | err, 50 | "could not send error message to client", 51 | ) 52 | } 53 | return 54 | } 55 | }) 56 | } 57 | 58 | // GetQueueCounts issues an RPC call to get the queue counts 59 | // from the given hostAndPort. Note that the hostAndPort should 60 | // not end with a "/" and shouldn't include a path. 61 | func GetCounts( 62 | httpCl *http.Client, 63 | interceptorURL url.URL, 64 | ) (*Counts, error) { 65 | interceptorURL.Path = countsPath 66 | resp, err := httpCl.Get(interceptorURL.String()) 67 | if err != nil { 68 | return nil, fmt.Errorf("requesting the queue counts from %s: %w", interceptorURL.String(), err) 69 | } 70 | defer resp.Body.Close() 71 | counts := NewCounts() 72 | if err := json.NewDecoder(resp.Body).Decode(counts); err != nil { 73 | return nil, fmt.Errorf("decoding response from the interceptor at %s: %w", interceptorURL.String(), err) 74 | } 75 | 76 | return counts, nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/queue/queue_rpc_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/go-logr/logr" 9 | "github.com/stretchr/testify/require" 10 | 11 | pkghttp "github.com/kedacore/http-add-on/pkg/http" 12 | kedanet "github.com/kedacore/http-add-on/pkg/net" 13 | ) 14 | 15 | func TestQueueSizeHandlerSuccess(t *testing.T) { 16 | lggr := logr.Discard() 17 | r := require.New(t) 18 | reader := &FakeCountReader{ 19 | concurrency: 123, 20 | rps: 100, 21 | err: nil, 22 | } 23 | 24 | handler := newSizeHandler(lggr, reader) 25 | req, rec := pkghttp.NewTestCtx("GET", "/queue") 26 | handler.ServeHTTP(rec, req) 27 | r.Equal(200, rec.Code, "response code") 28 | respMap := map[string]Count{} 29 | decodeErr := json.NewDecoder(rec.Body).Decode(&respMap) 30 | r.NoError(decodeErr) 31 | r.Equalf(1, len(respMap), "response JSON length was not 1") 32 | sizeVal, ok := respMap["sample.com"] 33 | r.Truef(ok, "'sample.com' entry not available in return JSON") 34 | r.Equalf(reader.concurrency, sizeVal.Concurrency, "returned JSON concurrent size was wrong") 35 | r.Equalf(reader.rps, sizeVal.RPS, "returned JSON rps size was wrong") 36 | 37 | reader.err = errors.New("test error") 38 | req, rec = pkghttp.NewTestCtx("GET", "/queue") 39 | handler.ServeHTTP(rec, req) 40 | r.Equal(500, rec.Code, "response code was not expected") 41 | } 42 | 43 | func TestQueueSizeHandlerFail(t *testing.T) { 44 | lggr := logr.Discard() 45 | r := require.New(t) 46 | reader := &FakeCountReader{ 47 | concurrency: 0, 48 | rps: 0, 49 | err: errors.New("test error"), 50 | } 51 | 52 | handler := newSizeHandler(lggr, reader) 53 | req, rec := pkghttp.NewTestCtx("GET", "/queue") 54 | handler.ServeHTTP(rec, req) 55 | r.Equal(500, rec.Code, "response code") 56 | } 57 | 58 | func TestQueueSizeHandlerIntegration(t *testing.T) { 59 | lggr := logr.Discard() 60 | r := require.New(t) 61 | reader := &FakeCountReader{ 62 | concurrency: 50, 63 | rps: 60, 64 | err: nil, 65 | } 66 | 67 | hdl := kedanet.NewTestHTTPHandlerWrapper(newSizeHandler(lggr, reader)) 68 | srv, url, err := kedanet.StartTestServer(hdl) 69 | r.NoError(err) 70 | defer srv.Close() 71 | httpCl := srv.Client() 72 | counts, err := GetCounts(httpCl, *url) 73 | r.NoError(err) 74 | r.Equal(1, len(counts.Counts)) 75 | for _, val := range counts.Counts { 76 | r.Equal(reader.concurrency, val.Concurrency) 77 | r.Equal(reader.rps, val.RPS) 78 | } 79 | reqs := hdl.IncomingRequests() 80 | r.Equal(1, len(reqs)) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/queue/queue_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCurrent(t *testing.T) { 11 | r := require.New(t) 12 | memory := NewMemory() 13 | now := time.Now() 14 | host := "host1" 15 | memory.EnsureKey(host, time.Minute, time.Second) 16 | err := memory.Increase(host, 1) 17 | r.NoError(err) 18 | current, err := memory.Current() 19 | r.NoError(err) 20 | r.Equal(current.Counts[host].Concurrency, memory.concurrentMap[host]) 21 | r.Equal(current.Counts[host].RPS, memory.rpsMap[host].WindowAverage(now)) 22 | 23 | err = memory.Increase(host, 1) 24 | r.NoError(err) 25 | err = memory.Increase(host, 1) 26 | r.NoError(err) 27 | r.NotEqual(current.Counts[host].Concurrency, memory.concurrentMap[host]) 28 | r.NotEqual(current.Counts[host].RPS, memory.rpsMap[host].WindowAverage(now)) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/routing/key.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | 9 | httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 10 | ) 11 | 12 | type Key []byte 13 | 14 | func NewKey(host string, path string) Key { 15 | if i := strings.LastIndex(host, ":"); i != -1 { 16 | host = host[:i] 17 | } 18 | 19 | path = strings.Trim(path, "/") 20 | if path != "" { 21 | path += "/" 22 | } 23 | 24 | key := fmt.Sprintf("//%s/%s", host, path) 25 | return []byte(key) 26 | } 27 | 28 | func NewKeyFromURL(url *url.URL) Key { 29 | if url == nil { 30 | return nil 31 | } 32 | 33 | return NewKey(url.Host, url.Path) 34 | } 35 | 36 | func NewKeyFromRequest(req *http.Request) Key { 37 | if req == nil { 38 | return nil 39 | } 40 | 41 | reqURL := req.URL 42 | if reqURL == nil { 43 | return nil 44 | } 45 | 46 | keyURL := *reqURL 47 | if reqHost := req.Host; reqHost != "" { 48 | keyURL.Host = reqHost 49 | } 50 | 51 | return NewKeyFromURL(&keyURL) 52 | } 53 | 54 | var _ fmt.Stringer = (*Key)(nil) 55 | 56 | func (k Key) String() string { 57 | return string(k) 58 | } 59 | 60 | type Keys []Key 61 | 62 | func NewKeysFromHTTPSO(httpso *httpv1alpha1.HTTPScaledObject) Keys { 63 | if httpso == nil { 64 | return nil 65 | } 66 | spec := httpso.Spec 67 | 68 | hosts := spec.Hosts 69 | if hosts == nil { 70 | hosts = []string{""} 71 | } 72 | hostsSize := len(hosts) 73 | 74 | pathPrefixes := spec.PathPrefixes 75 | if pathPrefixes == nil { 76 | pathPrefixes = []string{""} 77 | } 78 | pathPrefixesSize := len(pathPrefixes) 79 | 80 | keysSize := hostsSize * pathPrefixesSize 81 | keys := make([]Key, 0, keysSize) 82 | for _, host := range hosts { 83 | for _, pathPrefix := range pathPrefixes { 84 | key := NewKey(host, pathPrefix) 85 | keys = append(keys, key) 86 | } 87 | } 88 | 89 | return keys 90 | } 91 | -------------------------------------------------------------------------------- /pkg/routing/sharedindexinformer.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "k8s.io/client-go/tools/cache" 5 | ) 6 | 7 | type sharedIndexInformer interface { 8 | cache.SharedIndexInformer 9 | HasStarted() bool 10 | } 11 | -------------------------------------------------------------------------------- /pkg/routing/suite_test.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestRouting(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | 13 | RunSpecs(t, "Routing Suite") 14 | } 15 | -------------------------------------------------------------------------------- /pkg/routing/test/table.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 8 | "github.com/kedacore/http-add-on/pkg/routing" 9 | "github.com/kedacore/http-add-on/pkg/util" 10 | ) 11 | 12 | type Table struct { 13 | Memory map[string]*httpv1alpha1.HTTPScaledObject 14 | } 15 | 16 | func NewTable() *Table { 17 | return &Table{ 18 | Memory: make(map[string]*httpv1alpha1.HTTPScaledObject), 19 | } 20 | } 21 | 22 | var _ routing.Table = (*Table)(nil) 23 | 24 | func (t Table) Start(_ context.Context) error { 25 | return nil 26 | } 27 | 28 | func (t Table) Route(req *http.Request) *httpv1alpha1.HTTPScaledObject { 29 | return t.Memory[req.Host] 30 | } 31 | 32 | func (t Table) HasSynced() bool { 33 | return true 34 | } 35 | 36 | var _ util.HealthChecker = (*Table)(nil) 37 | 38 | func (t Table) HealthCheck(_ context.Context) error { 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/util/async.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func WithTimeout(d time.Duration, f func() error) error { 9 | errs := make(chan error) 10 | defer close(errs) 11 | 12 | go func() { 13 | errs <- f() 14 | }() 15 | 16 | select { 17 | case err := <-errs: 18 | return err 19 | case <-time.After(d): 20 | return fmt.Errorf("timed out after %v", d) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/util/atomicvalue.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | type AtomicValue[V any] struct { 8 | atomicValue atomic.Value 9 | } 10 | 11 | func NewAtomicValue[V any](v V) *AtomicValue[V] { 12 | var av AtomicValue[V] 13 | av.Set(v) 14 | 15 | return &av 16 | } 17 | 18 | func (av *AtomicValue[V]) Get() V { 19 | if v, ok := av.atomicValue.Load().(V); ok { 20 | return v 21 | } 22 | 23 | return *new(V) 24 | } 25 | 26 | func (av *AtomicValue[V]) Set(v V) { 27 | av.atomicValue.Store(v) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/util/atomicvalue_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "sync/atomic" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("AtomicValue", func() { 12 | var ( 13 | i int 14 | ) 15 | 16 | BeforeEach(func() { 17 | i = rand.Int() 18 | }) 19 | 20 | Context("New", func() { 21 | It("returns an AtomicValue with the expected value", func() { 22 | v := NewAtomicValue[int](i) 23 | 24 | out, ok := v.atomicValue.Load().(int) 25 | Expect(ok).To(BeTrue()) 26 | Expect(out).To(Equal(i)) 27 | }) 28 | }) 29 | 30 | Context("Get", func() { 31 | It("returns the stored value", func() { 32 | var a atomic.Value 33 | a.Store(i) 34 | 35 | v := AtomicValue[int]{ 36 | atomicValue: a, 37 | } 38 | 39 | out := v.Get() 40 | Expect(out).To(Equal(i)) 41 | }) 42 | }) 43 | 44 | Context("Set", func() { 45 | It("stores the expected value", func() { 46 | var v AtomicValue[int] 47 | v.Set(i) 48 | 49 | out, ok := v.atomicValue.Load().(int) 50 | Expect(ok).To(BeTrue()) 51 | Expect(out).To(Equal(i)) 52 | }) 53 | }) 54 | 55 | Context("E2E", func() { 56 | It("succeeds", func() { 57 | v := NewAtomicValue[int](i) 58 | 59 | out0 := v.Get() 60 | Expect(out0).To(Equal(i)) 61 | 62 | i = rand.Int() 63 | v.Set(i) 64 | 65 | out1 := v.Get() 66 | Expect(out1).To(Equal(i)) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /pkg/util/context.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | 7 | "github.com/go-logr/logr" 8 | 9 | httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 10 | ) 11 | 12 | type contextKey int 13 | 14 | const ( 15 | ckLogger contextKey = iota 16 | ckHTTPSO 17 | ckStream 18 | ) 19 | 20 | func ContextWithLogger(ctx context.Context, logger logr.Logger) context.Context { 21 | return context.WithValue(ctx, ckLogger, logger) 22 | } 23 | 24 | func LoggerFromContext(ctx context.Context) logr.Logger { 25 | cv, _ := ctx.Value(ckLogger).(logr.Logger) 26 | return cv 27 | } 28 | 29 | func ContextWithHTTPSO(ctx context.Context, httpso *httpv1alpha1.HTTPScaledObject) context.Context { 30 | return context.WithValue(ctx, ckHTTPSO, httpso) 31 | } 32 | 33 | func HTTPSOFromContext(ctx context.Context) *httpv1alpha1.HTTPScaledObject { 34 | cv, _ := ctx.Value(ckHTTPSO).(*httpv1alpha1.HTTPScaledObject) 35 | return cv 36 | } 37 | 38 | func ContextWithStream(ctx context.Context, url *url.URL) context.Context { 39 | return context.WithValue(ctx, ckStream, url) 40 | } 41 | 42 | func StreamFromContext(ctx context.Context) *url.URL { 43 | cv, _ := ctx.Value(ckStream).(*url.URL) 44 | return cv 45 | } 46 | -------------------------------------------------------------------------------- /pkg/util/contexthttp.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | 7 | "github.com/go-logr/logr" 8 | 9 | httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" 10 | ) 11 | 12 | func RequestWithLoggerWithName(r *http.Request, name string) *http.Request { 13 | logger := LoggerFromContext(r.Context()) 14 | logger = logger.WithName(name) 15 | 16 | return RequestWithLogger(r, logger) 17 | } 18 | 19 | func RequestWithLogger(r *http.Request, logger logr.Logger) *http.Request { 20 | ctx := r.Context() 21 | ctx = ContextWithLogger(ctx, logger) 22 | 23 | return r.WithContext(ctx) 24 | } 25 | 26 | func RequestWithHTTPSO(r *http.Request, httpso *httpv1alpha1.HTTPScaledObject) *http.Request { 27 | ctx := r.Context() 28 | ctx = ContextWithHTTPSO(ctx, httpso) 29 | 30 | return r.WithContext(ctx) 31 | } 32 | 33 | func RequestWithStream(r *http.Request, stream *url.URL) *http.Request { 34 | ctx := r.Context() 35 | ctx = ContextWithStream(ctx, stream) 36 | 37 | return r.WithContext(ctx) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/util/env_resolver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The KEDA Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "os" 21 | "strconv" 22 | "time" 23 | ) 24 | 25 | func ResolveOsEnvBool(envName string, defaultValue bool) (bool, error) { 26 | valueStr, found := os.LookupEnv(envName) 27 | 28 | if found && valueStr != "" { 29 | return strconv.ParseBool(valueStr) 30 | } 31 | 32 | return defaultValue, nil 33 | } 34 | 35 | func ResolveOsEnvInt(envName string, defaultValue int) (int, error) { 36 | valueStr, found := os.LookupEnv(envName) 37 | 38 | if found && valueStr != "" { 39 | return strconv.Atoi(valueStr) 40 | } 41 | 42 | return defaultValue, nil 43 | } 44 | 45 | func ResolveOsEnvDuration(envName string) (*time.Duration, error) { 46 | valueStr, found := os.LookupEnv(envName) 47 | 48 | if found && valueStr != "" { 49 | value, err := time.ParseDuration(valueStr) 50 | return &value, err 51 | } 52 | 53 | return nil, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/util/errors.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | ) 8 | 9 | var ( 10 | ignoredErrs = []error{ 11 | nil, 12 | context.Canceled, 13 | http.ErrServerClosed, 14 | } 15 | ) 16 | 17 | func IsIgnoredErr(err error) bool { 18 | for _, ignoredErr := range ignoredErrs { 19 | if errors.Is(err, ignoredErr) { 20 | return true 21 | } 22 | } 23 | 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /pkg/util/functional.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | //revive:disable:context-as-argument 8 | func ApplyContext(f func(ctx context.Context) error, ctx context.Context) func() error { 9 | //revive:enable:context-as-argument 10 | return func() error { 11 | return f(ctx) 12 | } 13 | } 14 | 15 | func DeapplyError(f func(), err error) func() error { 16 | return func() error { 17 | f() 18 | return err 19 | } 20 | } 21 | 22 | func IgnoringError(f func() error) { 23 | _ = f() 24 | } 25 | -------------------------------------------------------------------------------- /pkg/util/healthcheck.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type HealthChecker interface { 8 | HealthCheck(ctx context.Context) error 9 | } 10 | 11 | type HealthCheckerFunc func(ctx context.Context) error 12 | 13 | var _ HealthChecker = (*HealthCheckerFunc)(nil) 14 | 15 | func (f HealthCheckerFunc) HealthCheck(ctx context.Context) error { 16 | return f(ctx) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/util/reflect.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func IsNil(i interface{}) bool { 8 | if i == nil { 9 | return true 10 | } 11 | 12 | switch v := reflect.ValueOf(i); v.Kind() { 13 | case 14 | reflect.Chan, 15 | reflect.Func, 16 | reflect.Interface, 17 | reflect.Map, 18 | reflect.Pointer, 19 | reflect.Slice, 20 | reflect.UnsafePointer: 21 | return v.IsNil() 22 | } 23 | 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /pkg/util/signaler.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Signaler interface { 8 | Signal() 9 | Wait(ctx context.Context) error 10 | } 11 | 12 | type signaler chan struct{} 13 | 14 | func NewSignaler() Signaler { 15 | return make(signaler, 1) 16 | } 17 | 18 | var _ Signaler = (*signaler)(nil) 19 | 20 | func (s signaler) Signal() { 21 | select { 22 | case s <- struct{}{}: 23 | default: 24 | } 25 | } 26 | 27 | func (s signaler) Wait(ctx context.Context) error { 28 | select { 29 | case <-s: 30 | return nil 31 | case <-ctx.Done(): 32 | return ctx.Err() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/util/signaler_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("Signaler", func() { 12 | Context("New", func() { 13 | It("returns a signaler of capacity 1", func() { 14 | i := NewSignaler() 15 | 16 | s, ok := i.(signaler) 17 | Expect(ok).To(BeTrue()) 18 | 19 | c := cap(s) 20 | Expect(c).To(Equal(1)) 21 | }) 22 | }) 23 | 24 | Context("Signal", func() { 25 | It("produces on channel", func() { 26 | s := make(signaler, 1) 27 | 28 | err := WithTimeout(time.Second, DeapplyError(s.Signal, nil)) 29 | Expect(err).NotTo(HaveOccurred()) 30 | 31 | select { 32 | case <-s: 33 | default: 34 | Fail("channel should not be empty") 35 | } 36 | }) 37 | 38 | It("does not block when channel is full", func() { 39 | s := make(signaler, 1) 40 | s <- struct{}{} 41 | 42 | err := WithTimeout(time.Second, DeapplyError(s.Signal, nil)) 43 | Expect(err).NotTo(HaveOccurred()) 44 | }) 45 | }) 46 | 47 | Context("Wait", func() { 48 | It("returns nil when channel is not empty", func() { 49 | ctx := context.TODO() 50 | 51 | s := make(signaler, 1) 52 | s <- struct{}{} 53 | 54 | err := WithTimeout(time.Second, ApplyContext(s.Wait, ctx)) 55 | Expect(err).NotTo(HaveOccurred()) 56 | }) 57 | 58 | It("returns err when context is done", func() { 59 | ctx, cancel := context.WithCancel(context.TODO()) 60 | cancel() 61 | 62 | s := make(signaler, 1) 63 | 64 | err := WithTimeout(time.Second, ApplyContext(s.Wait, ctx)) 65 | Expect(err).To(MatchError(context.Canceled)) 66 | }) 67 | }) 68 | 69 | Context("E2E", func() { 70 | It("succeeds", func() { 71 | ctx, cancel := context.WithCancel(context.TODO()) 72 | defer cancel() 73 | 74 | s := NewSignaler() 75 | 76 | err0 := WithTimeout(time.Second, DeapplyError(s.Signal, nil)) 77 | Expect(err0).NotTo(HaveOccurred()) 78 | 79 | err1 := WithTimeout(time.Second, DeapplyError(s.Signal, nil)) 80 | Expect(err1).NotTo(HaveOccurred()) 81 | 82 | err2 := WithTimeout(time.Second, ApplyContext(s.Wait, ctx)) 83 | Expect(err2).NotTo(HaveOccurred()) 84 | 85 | cancel() 86 | 87 | err3 := WithTimeout(time.Second, ApplyContext(s.Wait, ctx)) 88 | Expect(err3).To(MatchError(context.Canceled)) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /pkg/util/stopwatch.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Stopwatch struct { 8 | startTime time.Time 9 | stopTime time.Time 10 | } 11 | 12 | func (sw *Stopwatch) Start() { 13 | sw.startTime = time.Now() 14 | } 15 | 16 | func (sw *Stopwatch) Stop() { 17 | sw.stopTime = time.Now() 18 | } 19 | 20 | func (sw *Stopwatch) StartTime() time.Time { 21 | return sw.startTime 22 | } 23 | 24 | func (sw *Stopwatch) StopTime() time.Time { 25 | return sw.stopTime 26 | } 27 | 28 | func (sw *Stopwatch) ElapsedTime() time.Duration { 29 | return sw.stopTime.Sub(sw.startTime) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/util/stopwatch_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Stopwatch", func() { 11 | Context("Start", func() { 12 | It("records current time on startTime", func() { 13 | var sw Stopwatch 14 | 15 | sw.Start() 16 | Expect(sw.startTime).To(BeTemporally("~", time.Now(), time.Millisecond)) 17 | }) 18 | }) 19 | 20 | Context("Stop", func() { 21 | It("records current time on stopTime", func() { 22 | var sw Stopwatch 23 | 24 | sw.Stop() 25 | Expect(sw.stopTime).To(BeTemporally("~", time.Now(), time.Millisecond)) 26 | }) 27 | }) 28 | 29 | Context("StartTime", func() { 30 | It("returns the expected value", func() { 31 | var ( 32 | st = time.Now().Add(-time.Minute) 33 | ) 34 | 35 | sw := Stopwatch{ 36 | startTime: st, 37 | } 38 | 39 | ret := sw.StartTime() 40 | Expect(ret).To(Equal(st)) 41 | }) 42 | }) 43 | 44 | Context("StopTime", func() { 45 | It("returns the expected value", func() { 46 | var ( 47 | st = time.Now().Add(+time.Minute) 48 | ) 49 | 50 | sw := Stopwatch{ 51 | stopTime: st, 52 | } 53 | 54 | ret := sw.StopTime() 55 | Expect(ret).To(Equal(st)) 56 | }) 57 | }) 58 | 59 | Context("ElapsedTime", func() { 60 | It("returns the difference between startTime and stopTime", func() { 61 | var ( 62 | at = time.Now().Add(-time.Minute) 63 | ot = time.Now().Add(+time.Minute) 64 | du = ot.Sub(at) 65 | ) 66 | 67 | sw := &Stopwatch{ 68 | startTime: at, 69 | stopTime: ot, 70 | } 71 | 72 | ret := sw.ElapsedTime() 73 | Expect(ret).To(Equal(du)) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /pkg/util/suite_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestUtil(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | 13 | RunSpecs(t, "Util Suite") 14 | } 15 | -------------------------------------------------------------------------------- /scaler/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/keda-tools:1.23.6 as builder 2 | WORKDIR /workspace 3 | COPY go.* . 4 | RUN go mod download 5 | COPY . . 6 | ARG VERSION=main 7 | ARG GIT_COMMIT=HEAD 8 | ARG TARGETOS 9 | ARG TARGETARCH 10 | RUN VERSION="${VERSION}" GIT_COMMIT="${GIT_COMMIT}" TARGET_OS="${TARGETOS}" ARCH="${TARGETARCH}" make build-scaler 11 | 12 | FROM gcr.io/distroless/static:nonroot 13 | COPY --from=builder /workspace/bin/scaler /sbin/init 14 | ENTRYPOINT ["/sbin/init"] 15 | -------------------------------------------------------------------------------- /scaler/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kelseyhightower/envconfig" 7 | ) 8 | 9 | type config struct { 10 | // GRPCPort is what port to serve the KEDA-compatible gRPC external scaler interface 11 | // on 12 | GRPCPort int `envconfig:"KEDA_HTTP_SCALER_PORT" default:"8080"` 13 | // TargetNamespace is the namespace in which this scaler is running, and the namespace 14 | // that the target interceptors are running in. This scaler and all the interceptors 15 | // must be running in the same namespace 16 | TargetNamespace string `envconfig:"KEDA_HTTP_SCALER_TARGET_ADMIN_NAMESPACE" required:"true"` 17 | // TargetService is the name of the service to issue metrics RPC requests to interceptors 18 | TargetService string `envconfig:"KEDA_HTTP_SCALER_TARGET_ADMIN_SERVICE" required:"true"` 19 | // TargetDeployment is the name of the deployment to issue metrics RPC requests to interceptors 20 | TargetDeployment string `envconfig:"KEDA_HTTP_SCALER_TARGET_ADMIN_DEPLOYMENT" required:"true"` 21 | // TargetPort is the port on TargetService to which to issue metrics RPC requests to 22 | // interceptors 23 | TargetPort int `envconfig:"KEDA_HTTP_SCALER_TARGET_ADMIN_PORT" required:"true"` 24 | // TargetPendingRequests is the default value for the 25 | // pending requests value that the scaler will return to 26 | // KEDA, if that value is not set on an incoming 27 | // `HTTPScaledObject` 28 | TargetPendingRequests int `envconfig:"KEDA_HTTP_SCALER_TARGET_PENDING_REQUESTS" default:"100"` 29 | // ConfigMapCacheRsyncPeriod is the time interval 30 | // for the configmap informer to rsync the local cache. 31 | ConfigMapCacheRsyncPeriod time.Duration `envconfig:"KEDA_HTTP_SCALER_CONFIG_MAP_INFORMER_RSYNC_PERIOD" default:"60m"` 32 | // DeploymentCacheRsyncPeriod is the time interval 33 | // for the deployment informer to rsync the local cache. 34 | DeploymentCacheRsyncPeriod time.Duration `envconfig:"KEDA_HTTP_SCALER_DEPLOYMENT_INFORMER_RSYNC_PERIOD" default:"60m"` 35 | // QueueTickDuration is the duration between queue requests 36 | QueueTickDuration time.Duration `envconfig:"KEDA_HTTP_QUEUE_TICK_DURATION" default:"500ms"` 37 | } 38 | 39 | func mustParseConfig() *config { 40 | ret := new(config) 41 | envconfig.MustProcess("", ret) 42 | return ret 43 | } 44 | -------------------------------------------------------------------------------- /scaler/naming.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "k8s.io/apimachinery/pkg/types" 8 | ) 9 | 10 | var ( 11 | unsafeChars = regexp.MustCompile(`[^-.0-9A-Za-z]`) 12 | ) 13 | 14 | func escapeRune(r string) string { 15 | return fmt.Sprintf("_%04X", r) 16 | } 17 | 18 | func escapeString(s string) string { 19 | return unsafeChars.ReplaceAllStringFunc(s, escapeRune) 20 | } 21 | 22 | func MetricName(namespacedName *types.NamespacedName) string { 23 | mn := fmt.Sprintf("http-%v", namespacedName) 24 | return escapeString(mn) 25 | } 26 | -------------------------------------------------------------------------------- /tests/checks/interceptor_scaledobject/interceptor_scaledobject_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | // +build e2e 3 | 4 | package interceptor_scaledobject_test 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | . "github.com/kedacore/http-add-on/tests/helper" 13 | ) 14 | 15 | const ( 16 | kedaNamespace = "keda" 17 | interceptorScaledObjectName = "keda-add-ons-http-interceptor" 18 | scaledObject = "scaledobject" 19 | ) 20 | 21 | func TestCheck(t *testing.T) { 22 | predicate := `-o jsonpath="{.status.conditions[?(@.type=="Ready")].status}"` 23 | expectedResult := "True" 24 | result := "False" 25 | for i := 0; i < 4; i++ { 26 | result = KubectlGetResult(t, scaledObject, interceptorScaledObjectName, kedaNamespace, predicate) 27 | if result == expectedResult { 28 | break 29 | } 30 | time.Sleep(15 * time.Second) 31 | } 32 | assert.Equal(t, expectedResult, result) 33 | } 34 | -------------------------------------------------------------------------------- /tests/utils/cleanup_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | // +build e2e 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | 12 | . "github.com/kedacore/http-add-on/tests/helper" 13 | ) 14 | 15 | func TestRemoveKEDAHttpAddOn(t *testing.T) { 16 | out, err := ExecuteCommandWithDir("make undeploy", "../..") 17 | require.NoErrorf(t, err, "error removing KEDA Http Add-on - %s", err) 18 | 19 | t.Log(string(out)) 20 | t.Log("KEDA Http Add-on removed successfully using 'make undeploy' command") 21 | } 22 | 23 | func TestRemoveKEDA(t *testing.T) { 24 | _, err := ExecuteCommand(fmt.Sprintf("helm uninstall keda --namespace %s", KEDANamespace)) 25 | require.NoErrorf(t, err, "cannot uninstall KEDA - %s", err) 26 | } 27 | 28 | func TestRemoveIngress(t *testing.T) { 29 | _, err := ExecuteCommand(fmt.Sprintf("helm uninstall %s --namespace %s", IngressReleaseName, IngressNamespace)) 30 | require.NoErrorf(t, err, "cannot uninstall ingress - %s", err) 31 | DeleteNamespace(t, IngressNamespace) 32 | } 33 | 34 | func TestRemoveArgoRollouts(t *testing.T) { 35 | _, err := ExecuteCommand(fmt.Sprintf("helm uninstall %s --namespace %s", ArgoRolloutsName, ArgoRolloutsNamespace)) 36 | require.NoErrorf(t, err, "cannot uninstall ingress - %s", err) 37 | DeleteNamespace(t, ArgoRolloutsNamespace) 38 | } 39 | 40 | func TestRemoveKEDANamespace(t *testing.T) { 41 | DeleteNamespace(t, KEDANamespace) 42 | } 43 | 44 | func TestRemoveOpentelemetryComponents(t *testing.T) { 45 | OpentelemetryNamespace := "open-telemetry-system" 46 | _, err := ExecuteCommand(fmt.Sprintf("helm uninstall opentelemetry-collector --namespace %s", OpentelemetryNamespace)) 47 | require.NoErrorf(t, err, "cannot uninstall opentelemetry-collector - %s", err) 48 | DeleteNamespace(t, OpentelemetryNamespace) 49 | } 50 | 51 | func TestCleanUpCerts(t *testing.T) { 52 | out, err := ExecuteCommandWithDir("make clean-test-certs", "../..") 53 | require.NoErrorf(t, err, "error cleaning up test certs - %s", err) 54 | t.Log(string(out)) 55 | t.Log("test certificates successfully cleaned up") 56 | } 57 | 58 | func TestRemoveEnvoyGateway(t *testing.T) { 59 | gatewayClass := ` 60 | apiVersion: gateway.networking.k8s.io/v1 61 | kind: GatewayClass 62 | metadata: 63 | name: eg 64 | ` 65 | 66 | gateway := ` 67 | apiVersion: gateway.networking.k8s.io/v1 68 | kind: Gateway 69 | metadata: 70 | name: eg 71 | namespace: envoy-gateway-system 72 | ` 73 | 74 | KubectlDeleteWithTemplate(t, nil, "gateway", gateway) 75 | KubectlDeleteWithTemplate(t, nil, "gatewayClass", gatewayClass) 76 | _, err := ExecuteCommand(fmt.Sprintf("helm uninstall %s --namespace %s", EnvoyReleaseName, EnvoyNamespace)) 77 | require.NoErrorf(t, err, "cannot uninstall envoy gateway - %s", err) 78 | DeleteNamespace(t, EnvoyNamespace) 79 | } 80 | --------------------------------------------------------------------------------