├── .github ├── pull_request_template.md └── workflows │ ├── codeql.yml │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENCE ├── Makefile ├── README.md ├── assets ├── diagrams │ └── readme.drawio └── images │ └── overview.png ├── charts └── container-startup-autoscaler │ ├── .helmignore │ ├── CHANGELOG.md │ ├── Chart.yaml │ ├── LICENCE │ ├── README.md │ ├── releasenotes.md │ ├── templates │ ├── _annotation.tpl │ ├── _container.tpl │ ├── _deployment.tpl │ ├── _label.tpl │ ├── _name.tpl │ ├── _pod.tpl │ ├── _selector.tpl │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── deployment.yaml │ └── serviceaccount.yaml │ ├── tests │ ├── clusterrole_test.yaml │ ├── clusterrolebinding_test.yaml │ ├── deployment_test.yaml │ └── serviceaccount_test.yaml │ └── values.yaml ├── cmd └── container-startup-autoscaler │ └── main.go ├── go.mod ├── go.sum ├── internal ├── common │ ├── error.go │ ├── error_test.go │ ├── string.go │ ├── string_test.go │ ├── struct.go │ ├── struct_test.go │ └── timeout.go ├── context │ ├── const.go │ ├── contexttest │ │ ├── contextbuilder.go │ │ └── contextconfig.go │ ├── helper.go │ └── helper_test.go ├── controller │ ├── controller.go │ ├── controller_test.go │ ├── controllercommon │ │ ├── config.go │ │ └── config_test.go │ ├── predicatefunc.go │ ├── predicatefunc_test.go │ ├── reconciler.go │ └── reconciler_test.go ├── event │ ├── eventcommon │ │ ├── interfaces.go │ │ ├── podevent.go │ │ └── podevent_test.go │ ├── eventtest │ │ └── mockpodeventpublisher.go │ ├── podeventpublisher.go │ └── podeventpublisher_test.go ├── kube │ ├── containerhelper.go │ ├── containerhelper_test.go │ ├── error.go │ ├── error_test.go │ ├── kubecommon │ │ ├── datatype.go │ │ ├── interfaces.go │ │ └── metadataconst.go │ ├── kubetest │ │ ├── containerbuilder.go │ │ ├── containerconsts.go │ │ ├── controllerruntime.go │ │ ├── mockcontainerhelper.go │ │ ├── mockpodhelper.go │ │ ├── podbuilder.go │ │ ├── podconsts.go │ │ └── podinterceptor.go │ ├── podhelper.go │ ├── podhelper_test.go │ └── retry.go ├── logging │ ├── keys.go │ ├── log.go │ └── log_test.go ├── metrics │ ├── informercache │ │ ├── informercache.go │ │ └── informercache_test.go │ ├── metricscommon │ │ ├── const.go │ │ └── metric.go │ ├── reconciler │ │ ├── reconciler.go │ │ └── reconciler_test.go │ ├── registry.go │ ├── registry_test.go │ ├── retry │ │ ├── kubeapi.go │ │ └── kubeapi_test.go │ └── scale │ │ ├── scale.go │ │ └── scale_test.go ├── pod │ ├── configuration.go │ ├── configuration_test.go │ ├── error.go │ ├── error_test.go │ ├── pod.go │ ├── pod_test.go │ ├── podcommon │ │ ├── interfaces.go │ │ ├── stateconst.go │ │ ├── stateconst_test.go │ │ ├── states.go │ │ ├── states_test.go │ │ ├── statusannotation.go │ │ ├── statusannotation_test.go │ │ ├── statusconst.go │ │ └── statusconst_test.go │ ├── podtest │ │ ├── mockconfiguration.go │ │ ├── mockstatus.go │ │ ├── mocktargetcontaineraction.go │ │ ├── mocktargetcontainerstate.go │ │ └── mockvalidation.go │ ├── status.go │ ├── status_test.go │ ├── targetcontaineraction.go │ ├── targetcontaineraction_test.go │ ├── targetcontainerstate.go │ ├── targetcontainerstate_test.go │ ├── validation.go │ ├── validation_test.go │ └── vpaconst.go ├── retry │ ├── retry.go │ └── retry_test.go └── scale │ ├── configuration.go │ ├── configuration_test.go │ ├── configurations.go │ ├── configurations_test.go │ ├── scalecommon │ ├── interfaces.go │ ├── metadataconst.go │ ├── resources.go │ └── resources_test.go │ ├── scaletest │ ├── mockconfiguration.go │ ├── mockconfigurations.go │ ├── mockstate.go │ ├── mockstates.go │ └── resourcesconsts.go │ ├── state.go │ ├── state_test.go │ ├── states.go │ ├── states_test.go │ ├── update.go │ ├── update_test.go │ ├── updates.go │ └── updates_test.go ├── scripts └── sandbox │ ├── config │ ├── kind.yaml │ ├── metricsserver │ │ └── kustomization.yaml │ └── vars.sh │ ├── csa-get-metrics.sh │ ├── csa-install.sh │ ├── csa-tail-logs.sh │ ├── csa-uninstall.sh │ ├── echo-cause-container-restart.sh │ ├── echo-delete.sh │ ├── echo-reinstall.sh │ ├── echo-watch.sh │ ├── echo │ ├── failure-infeasible │ │ └── cpu.yaml │ ├── failure-validation │ │ └── cpu-config.yaml │ ├── post-startup-resources │ │ ├── cpu-and-memory │ │ │ ├── both-probes.yaml │ │ │ ├── readiness-probe.yaml │ │ │ └── startup-probe.yaml │ │ ├── cpu-only │ │ │ ├── both-probes.yaml │ │ │ ├── readiness-probe.yaml │ │ │ └── startup-probe.yaml │ │ └── memory-only │ │ │ ├── both-probes.yaml │ │ │ ├── readiness-probe.yaml │ │ │ └── startup-probe.yaml │ └── startup-resources │ │ ├── cpu-and-memory │ │ ├── both-probes.yaml │ │ ├── readiness-probe.yaml │ │ └── startup-probe.yaml │ │ ├── cpu-only │ │ ├── both-probes.yaml │ │ ├── readiness-probe.yaml │ │ └── startup-probe.yaml │ │ └── memory-only │ │ ├── both-probes.yaml │ │ ├── readiness-probe.yaml │ │ └── startup-probe.yaml │ └── extracacert │ └── Dockerfile └── test └── integration ├── assert.go ├── command.go ├── config ├── kind.yaml └── metricsserver │ └── kustomization.yaml ├── consts.go ├── csa.go ├── echoserver.go ├── extracacert └── Dockerfile ├── integration_test.go ├── kind.go ├── kube.go ├── log.go ├── path.go ├── suppliedconfig.go └── workload.go /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thank you for contributing to CSA! Please provide all details requested below prior to submitting your pull request 2 | (PR). 3 | 4 | # Important Information 5 | - We don't formalize coding standards for this project, but please try to use the existing code as a convention guide. 6 | - CSA and the Helm chart adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) - please be aware of this 7 | when incrementing versions. 8 | - Releases of CSA and the Helm chart are managed independently of merging to `main` - a maintainer will prepare 9 | release(s) for you post-merge. 10 | 11 | # Areas Affected by PR 12 | Please indicate what areas are affected by your PR (check all that apply): 13 | 14 | - [ ] CSA 15 | - [ ] Helm chart 16 | 17 | # Nature of PR 18 | Please indicate the nature of your PR (check all that apply): 19 | 20 | - [ ] New feature 21 | - [ ] Bug fix 22 | - [ ] Security 23 | - [ ] Other (please provide details within the description) 24 | 25 | # Issue Links 26 | Please link to any issues related to your PR, or indicate if not applicable. 27 | 28 | # Description 29 | Please provide a description of the change(s) contained within your PR here. 30 | 31 | # Checklist 32 | Please ensure you work through any of **applicable** items below prior to submitting your PR: 33 | 34 | ## Commits 35 | - [ ] Commits are of logical units (to help us work through your changes) 36 | 37 | ## Tests 38 | - [ ] New unit/integration tests are implemented 39 | - [ ] Existing unit/integration tests are updated 40 | - [ ] All unit/integration tests are passing 41 | 42 | ## Sandbox Scripts 43 | - [ ] Sandbox scripts are updated 44 | 45 | ## Docs 46 | - [ ] `README.md` is updated 47 | - [ ] `CHANGELOG.md` is updated 48 | 49 | ## Helm Chart 50 | - [ ] New tests are implemented 51 | - [ ] Existing tests are updated 52 | - [ ] All tests are passing 53 | - [ ] `version` within `Chart.yaml` is incremented (only if changes made to CSA or the chart itself) 54 | - [ ] `appVersion` within `Chart.yaml` is incremented (only if changes made to CSA) 55 | - [ ] `README.md` is updated 56 | - [ ] `CHANGELOG.md` is updated 57 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | pull_request: 5 | branches: ["main"] 6 | push: 7 | branches: 8 | - main 9 | schedule: 10 | - cron: '0 0 * * 6' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Initialize CodeQL 24 | uses: github/codeql-action/init@v2 25 | with: 26 | languages: go 27 | queries: security-extended 28 | 29 | - name: Autobuild 30 | uses: github/codeql-action/autobuild@v2 31 | 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v2 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'csa-*' 7 | 8 | jobs: 9 | validate-tag: 10 | name: Validate tag 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Validate tag 15 | run: | 16 | if ! [[ "$GITHUB_REF_NAME" =~ ^csa-[0-9]+.[0-9]+.[0-9]+$ ]]; then 17 | echo "Tag does not match expected pattern - got $GITHUB_REF_NAME" 18 | exit 1 19 | fi 20 | 21 | publish-docker-image: 22 | name: Publish Docker image 23 | runs-on: ubuntu-latest 24 | needs: validate-tag 25 | 26 | steps: 27 | - name: Docker metadata 28 | id: meta 29 | uses: docker/metadata-action@v5 30 | with: 31 | images: expediagroup/container-startup-autoscaler 32 | tags: | 33 | type=match,pattern=csa-(.*),group=1 34 | 35 | - name: Docker setup QEMU 36 | uses: docker/setup-qemu-action@v3 37 | 38 | - name: Docker setup buildx 39 | uses: docker/setup-buildx-action@v3 40 | 41 | - name: Docker Hub login 42 | uses: docker/login-action@v3 43 | with: 44 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 45 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 46 | 47 | - name: Docker build and push 48 | uses: docker/build-push-action@v5 49 | with: 50 | platforms: linux/amd64,linux/arm64 51 | push: true 52 | tags: ${{ steps.meta.outputs.tags }} 53 | labels: ${{ steps.meta.outputs.labels }} 54 | 55 | publish-helm-chart: 56 | name: Publish Helm chart 57 | runs-on: ubuntu-latest 58 | needs: publish-docker-image 59 | 60 | steps: 61 | - name: Checkout repository 62 | uses: actions/checkout@v3 63 | with: 64 | fetch-depth: 0 65 | 66 | - name: Install Helm 67 | uses: azure/setup-helm@v3 68 | with: 69 | version: v3.13.1 70 | 71 | - name: Install chart-releaser 72 | uses: helm/chart-releaser-action@v1.5.0 73 | with: 74 | install_only: true 75 | 76 | - name: Run chart-releaser 77 | env: 78 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 79 | CR_RELEASE_NAME_TEMPLATE: "chart-{{ .Version }}" 80 | run: | 81 | owner=$(cut -d '/' -f 1 <<< "$GITHUB_REPOSITORY") 82 | repo=$(cut -d '/' -f 2 <<< "$GITHUB_REPOSITORY") 83 | 84 | git config user.name "$GITHUB_ACTOR" 85 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 86 | 87 | rm -rf .cr-release-packages 88 | rm -rf .cr-index 89 | mkdir -p .cr-release-packages 90 | mkdir -p .cr-index 91 | 92 | cr package charts/container-startup-autoscaler 93 | 94 | # https://github.com/helm/chart-releaser#create-github-releases-from-helm-chart-packages 95 | cr upload -o "$owner" -r "$repo" -c "$(git rev-parse HEAD)" --skip-existing \ 96 | --release-notes-file=releasenotes.md --make-release-latest=false 97 | 98 | # https://github.com/helm/chart-releaser#create-the-repository-index-from-github-releases 99 | cr index -o "$owner" -r "$repo" --push 100 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | unit: 9 | name: Run unit tests 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: '1.24.2' 22 | 23 | - name: Test 24 | run: make test-run-unit 25 | 26 | integration: 27 | name: Run integration tests 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | arg: 32 | - '1.33' 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v3 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Set up Go 41 | uses: actions/setup-go@v4 42 | with: 43 | go-version: '1.24.2' 44 | 45 | - name: Set up Kind 46 | uses: helm/kind-action@v1.10.0 47 | with: 48 | install_only: true 49 | version: v0.27.0 50 | 51 | - name: Test with Kubernetes ${{ matrix.arg }} 52 | env: 53 | MAX_PARALLELISM: 2 # Constrained to 2 CPUs on ubuntu-latest 54 | run: make test-run-int-verbose KUBE_VERSION=${{ matrix.arg }} 55 | 56 | helm: 57 | name: Run Helm tests 58 | runs-on: ubuntu-latest 59 | 60 | steps: 61 | - name: Checkout repository 62 | uses: actions/checkout@v3 63 | with: 64 | fetch-depth: 0 65 | 66 | - name: Test 67 | run: make test-run-helm 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | If you'd like to contribute code, please fork this GitHub repository, create a branch, and open a pull request. Please 3 | provide all details requested within the pull request template. A maintainer will review your submission. 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM --platform=$BUILDPLATFORM golang:1.24.2 AS build 16 | ARG BUILDPLATFORM 17 | ARG TARGETPLATFORM 18 | ARG TARGETOS 19 | ARG TARGETARCH 20 | RUN echo "Build platform: $BUILDPLATFORM, target platform: $TARGETPLATFORM, target OS: $TARGETOS, target arch: $TARGETARCH" 21 | RUN openssl s_client -showcerts -connect proxy.golang.org:443 /dev/null|openssl x509 -outform PEM > /usr/local/share/ca-certificates/goproxy.crt 22 | RUN update-ca-certificates 23 | WORKDIR /csa 24 | COPY go.mod go.sum ./ 25 | RUN go mod download && go mod verify 26 | COPY cmd ./cmd 27 | COPY internal ./internal 28 | RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -o ./csa ./cmd/container-startup-autoscaler 29 | 30 | FROM scratch 31 | COPY --from=build /csa/csa /csa/csa 32 | EXPOSE 8080/tcp 33 | EXPOSE 8081/tcp 34 | EXPOSE 8082/tcp 35 | ENTRYPOINT ["/csa/csa"] 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Based on https://www.thapaliya.com/en/writings/well-documented-makefiles/ 16 | 17 | .DEFAULT_GOAL:=help 18 | SHELL:=/bin/bash 19 | ROOT_DIR:=$(dir $(realpath $(lastword $(MAKEFILE_LIST)))) 20 | 21 | INT_TESTS_TIMEOUT=30m 22 | HELM_TESTS_SNAPSHOT_DIR=${ROOT_DIR}charts/container-startup-autoscaler/tests/__snapshot__ 23 | 24 | .PHONY: help 25 | help: ## Displays this help 26 | @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) 27 | 28 | ## ------------------ 29 | ## Test 30 | ## ------------------ 31 | 32 | .PHONY: test-run-unit 33 | test-run-unit: ## Runs unit tests 34 | go test -count=1 ./internal/... 35 | 36 | .PHONY: test-run-int 37 | test-run-int: ## Runs integration tests for a specific major.minor version of Kube 38 | @if [ -z "${KUBE_VERSION}" ]; then \ 39 | echo "KUBE_VERSION is required - run 'make test-run-int KUBE_VERSION=x.y'"; \ 40 | exit 1; \ 41 | fi 42 | go test -count=1 -timeout ${INT_TESTS_TIMEOUT} ./test/integration/... 43 | 44 | .PHONY: test-run-int-verbose 45 | test-run-int-verbose: ## Runs integration tests for a specific major.minor version of Kube, with verbose logging 46 | @if [ -z "${KUBE_VERSION}" ]; then \ 47 | echo "KUBE_VERSION is required - run 'make test-run-int KUBE_VERSION=x.y'"; \ 48 | exit 1; \ 49 | fi 50 | go test -count=1 -timeout ${INT_TESTS_TIMEOUT} -v ./test/integration/... 51 | 52 | .PHONY: test-run-helm 53 | test-run-helm: ## Runs Helm tests 54 | @rm -rf ${HELM_TESTS_SNAPSHOT_DIR} 55 | @mkdir ${HELM_TESTS_SNAPSHOT_DIR} 56 | @chmod 777 ${HELM_TESTS_SNAPSHOT_DIR} 57 | docker run -t --rm -v ${ROOT_DIR}charts:/apps helmunittest/helm-unittest:3.12.3-0.3.5 container-startup-autoscaler 58 | @rm -rf ${HELM_TESTS_SNAPSHOT_DIR} 59 | 60 | ## ------------------ 61 | ## Go Modules 62 | ## ------------------ 63 | 64 | .PHONY: go-modules-update 65 | go-modules-update: ## Gets latest versions of all Go modules and updates go.mod 66 | go get -u ./... 67 | go mod tidy 68 | -------------------------------------------------------------------------------- /assets/diagrams/readme.drawio: -------------------------------------------------------------------------------- 1 | 7Vpbc+I2FP41PC5jyxfMI5Bk2+nuNFPS2Zl9E7awVYzlkUVC+ut7JMvYlk3SBAjZBIYB+UjW5XyfzkX2wJmtt185zpPvLCLpAFnRduBcDRCybQvBn5Q8lpLA90pBzGmkG9WCOf2XaKGlpRsakaLVUDCWCpq3hSHLMhKKlgxzzh7azZYsbY+a45h0BPMQp13pDxqJpFqXP64rfiM0TvTQARqVFQscrmLONpkeL2MZKWvWuOpGr7FIcMQeGiLneuDMOGOiLK23M5JKtVYaK++72VO7mzInmfg/N/w9/+7+8ZPgzc9V8udK3C1ut94XR0/uHqcbrQs9W/FYKQcmnstiQbM4JROp7oEzfUioIPMch7LqAWgBskSsU7iyobhbrbxYskzc4DVNJT3+YgsmmJbO2YarHhIhAGvkORP4gTXIH9mgGMaMwbA4p8UwZGtVERaq6c2y7BOKRq8lwWxXXtM0nbGUcbUYZ7kkfhjKCQrOVqRRE43GC8uCGkWoigbW0PVbn1HVQg9iDS3HdYEQXuAjyw/GvufJ7ldEhKoDuIg5jijAdEU5sJeyTKqMFADbtIuiBvaecEG2DZFG9SthayI4rNrSta7GUO+93aZ6qJkc+FqWNEhcYY/15ol3PdcsgoIm0gtIhTqcmkRrKgSJQHrLog7D1AYikWbLM8ySiqGwdScpjaUiBcv7YJfXeoQ3p2CbchEmwbKXcn4YkMXS3C1xiotCM+cI9LBRmx920OXHTtbkx+kI4nQIcod5TATIZqBwTDPCDyNJkw7OOSxQg3otcI8BqGdseKcHUNQDqH8qPN0Onh8AxlPD5qBzw2Z3YOv6/qYbawAT4SJRIFoViEUbRMMGomDkXaM+G7hUH6jJGQWUpNCbwhf8qgVB5Mwa+o78G3jQeqYqbCVGhnTcK1VdmC3HezoeqbuhvqcT25ChwGgLX0UaFSutt7GMlIerzYLwjIB6hjSUfn+a899V4SqvHOFBrEKBYd1HXVY5b+n8vQ6p5gKDx87iyvkfy8Z/+EDgCPRwTHqc3/n7F+d/SLBvvzfnP7o4/5fDdn7nH/Tb6TpHu5jptzPTvvvuzDRyOrCTKCYVRoyLhMUsw+l1LZ22iVG3+cYk6Er4DxHiUSOPN0r/xp4/10nRLqatAtxbLAREb0qCrCehlqp5EmhOUizoffu8sQ80feutjIZrggSWYfZNu1Ao9ei7mqeARkeu2dHIa3cklDPudDThHD82muloff+E/f4J75uX2d71nm7vuIe1r9Zdb5FyhfWG2YH3+j00voQ6hxjF0XsLdSonfol1XoLb+WMd+3LS8eufdHhmjHTukw4bXVj1y7PKDKzOz6rgE8Xd6MlnXKcOqj3DUY1eGVP7e2LdY8fUvtU7373T2pMznDTk7bGBH5e+Km2Uhptm8Z2aqlULvpGlaEumTAgY4Jknu6dmvXMk1ncySfc0maSZuT3D+k6i575Bomd3n2jP5pPDUgLz3GxNo0jtFtN+fu6jM/MJh9uTbPSdnJ0s2agym+brL3kuFQdUwFxs8gHyU/nuzwKSRj+WJU7KXVd0KANaEG1etFWrXjhr46BFWPOGl2vecw7bR8KaptaZbesR+GHvMXdNfvS9HeWdjB/dZLTiR84K8eUsJEmVs/qsHPHt5zkyflOOPPlGDUQAiiQn4EEISoQhPi0T9h0rN6ng9Z1dvSIfhMv67d8yEqnfrnau/wM= -------------------------------------------------------------------------------- /assets/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpediaGroup/container-startup-autoscaler/25025ef93dcad8f946e343c1641bcc5fadba8c33/assets/images/overview.png -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/.helmignore: -------------------------------------------------------------------------------- 1 | tests 2 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | - Based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 3 | - This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## 1.7.0 6 | 2025-04-29 7 | 8 | ### Changed 9 | - CSA version only. 10 | 11 | ### CSA Version 12 | [0.8.0](../../CHANGELOG.md#080) 13 | 14 | ## 1.6.0 15 | 2025-04-25 16 | 17 | ### Changed 18 | - CSA version only. 19 | 20 | ### CSA Version 21 | [0.7.0](../../CHANGELOG.md#070) 22 | 23 | ## 1.5.0 24 | 2025-03-07 25 | 26 | ### Changed 27 | - CSA version only. 28 | 29 | ### CSA Version 30 | [0.6.0](../../CHANGELOG.md#060) 31 | 32 | ## 1.4.0 33 | 2024-12-12 34 | 35 | ### Added 36 | - Support for Kubernetes 1.32. 37 | - Cluster role includes `pods/resize` rule. 38 | 39 | ### Changed 40 | - CSA version. 41 | 42 | ### CSA Version 43 | [0.5.0](../../CHANGELOG.md#050) 44 | 45 | ## 1.3.0 46 | 2024-11-29 47 | 48 | ### Changed 49 | - CSA version only. 50 | 51 | ### CSA Version 52 | [0.4.0](../../CHANGELOG.md#040) 53 | 54 | ## 1.2.0 55 | 2024-02-01 56 | 57 | ### Changed 58 | - CSA version only. 59 | 60 | ### CSA Version 61 | [0.3.0](../../CHANGELOG.md#030) 62 | 63 | ## 1.1.0 64 | 2024-02-01 65 | 66 | ### Changed 67 | - CSA version only. 68 | 69 | ### CSA Version 70 | [0.2.0](../../CHANGELOG.md#020) 71 | 72 | ## 1.0.0 73 | 2024-01-05 74 | 75 | ### Added 76 | - Initial version. 77 | 78 | ### CSA Version 79 | [0.1.0](../../CHANGELOG.md#010) 80 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: container-startup-autoscaler 3 | description: > 4 | container-startup-autoscaler is a Kubernetes controller that modifies the CPU and/or memory resources of containers 5 | depending on whether they're starting up. 6 | version: 1.7.0 7 | appVersion: "0.8.0" 8 | home: https://github.com/ExpediaGroup/container-startup-autoscaler/README.md 9 | sources: 10 | - https://github.com/ExpediaGroup/container-startup-autoscaler 11 | keywords: 12 | - kubernetes 13 | - startup 14 | - scale 15 | - cpu 16 | - memory 17 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/README.md: -------------------------------------------------------------------------------- 1 | # container-startup-autoscaler 2 | A Helm chart for [container-startup-autoscaler](../../README.md) (CSA). 3 | 4 | ## Versioning 5 | To promote immutability, a new version of the chart is produced for each new version of CSA. The CSA version that each 6 | chart version ships is specified within the `appVersion` key of [Chart.yaml](Chart.yaml). If you're looking for a 7 | specific version of CSA, `appVersion` is included within the [output](#repository) of `helm search repo` (or see 8 | [CHANGELOG.md](CHANGELOG.md)) 9 | 10 | Chart versions are maintained separately to CSA versions; the chart version does not indicate CSA version. 11 | 12 | ## Repository 13 | To add the chart repository, use: 14 | 15 | `helm repo add container-startup-autoscaler https://ExpediaGroup.github.io/container-startup-autoscaler` 16 | 17 | To locally update versions of the chart, use: 18 | 19 | `helm repo update container-startup-autoscaler` 20 | 21 | To view available versions of the chart, use: 22 | 23 | `helm search repo -l container-startup-autoscaler` 24 | 25 | ## Configuration 26 | See [values.yaml](values.yaml) for a commented list of all configuration items. 27 | 28 | ## Tests 29 | Chart tests are implemented using [helm-unittest](https://github.com/helm-unittest/helm-unittest) - see the 30 | [tests](tests) directory. Unit tests can be run by executing `make test-run-helm` from the CSA root directory. 31 | 32 | ## Requirements 33 | - Helm 3. 34 | - See [corresponding CSA version](CHANGELOG.md) for Kubernetes compatibility. 35 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/releasenotes.md: -------------------------------------------------------------------------------- 1 | See [CHANGELOG.md](https://github.com/ExpediaGroup/container-startup-autoscaler/tree/main/charts/container-startup-autoscaler/CHANGELOG.md) for details on this chart release. -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/_annotation.tpl: -------------------------------------------------------------------------------- 1 | {{ define "csa.annotation.deployment" }} 2 | {{- end }} 3 | 4 | {{ define "csa.annotation.pod" }} 5 | {{- if .Values.pod.extraAnnotations }} 6 | annotations: {{- toYaml .Values.pod.extraAnnotations | nindent 2 }} 7 | {{- end }} 8 | {{- end }} 9 | 10 | {{ define "csa.annotation.serviceaccount" }} 11 | {{- end}} 12 | 13 | {{ define "csa.annotation.clusterrole" }} 14 | {{- end }} 15 | 16 | {{ define "csa.annotation.clusterrolebinding" }} 17 | {{- end }} 18 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/_container.tpl: -------------------------------------------------------------------------------- 1 | {{- define "csa.container.tag" -}} 2 | {{- if .Values.container.tag }} 3 | {{- .Values.container.tag}} 4 | {{- else }} 5 | {{- .Chart.AppVersion }} 6 | {{- end }} 7 | {{- end }} 8 | 9 | {{- define "csa.container.imageTag" -}} 10 | {{ .Values.container.image }}:{{ include "csa.container.tag" . }} 11 | {{- end }} 12 | 13 | {{ define "csa.container.args" }} 14 | args: 15 | - --leader-election-enabled 16 | - "{{ .Values.pod.leaderElectionEnabled }}" 17 | - --leader-election-resource-namespace 18 | - "{{ include "csa.name.namespace" . }}" 19 | {{- if .Values.csa.cacheSyncPeriodMins }} 20 | - --cache-sync-period-mins 21 | - "{{ .Values.csa.cacheSyncPeriodMins }}" 22 | {{- end }} 23 | {{- if .Values.csa.gracefulShutdownTimeoutSecs }} 24 | - --graceful-shutdown-timeout-secs 25 | - "{{ .Values.csa.gracefulShutdownTimeoutSecs }}" 26 | {{- end }} 27 | {{- if .Values.csa.requeueDurationSecs }} 28 | - --requeue-duration-secs 29 | - "{{ .Values.csa.requeueDurationSecs }}" 30 | {{- end }} 31 | {{- if .Values.csa.maxConcurrentReconciles }} 32 | - --max-concurrent-reconciles 33 | - "{{ .Values.csa.maxConcurrentReconciles }}" 34 | {{- end }} 35 | {{- if .Values.csa.standardRetryAttempts }} 36 | - --standard-retry-attempts 37 | - "{{ .Values.csa.standardRetryAttempts }}" 38 | {{- end }} 39 | {{- if .Values.csa.standardRetryDelaySecs }} 40 | - --standard-retry-delay-secs 41 | - "{{ .Values.csa.standardRetryDelaySecs }}" 42 | {{- end }} 43 | {{- if .Values.csa.scaleWhenUnknownResources }} 44 | - --scale-when-unknown-resources 45 | - "{{ .Values.csa.scaleWhenUnknownResources }}" 46 | {{- end }} 47 | {{- if .Values.csa.logV }} 48 | - --log-v 49 | - "{{ .Values.csa.logV }}" 50 | {{- end }} 51 | {{- if .Values.csa.logAddCaller }} 52 | - --log-add-caller 53 | - "{{ .Values.csa.logAddCaller }}" 54 | {{- end }} 55 | {{- end }} 56 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/_deployment.tpl: -------------------------------------------------------------------------------- 1 | {{- define "csa.deployment.replicas" -}} 2 | {{- if .Values.pod.leaderElectionEnabled -}} 3 | 2 4 | {{- else -}} 5 | 1 6 | {{- end }} 7 | {{- end }} 8 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/_label.tpl: -------------------------------------------------------------------------------- 1 | {{ define "csa.label.deployment" }} 2 | labels: {{- include "csa.label.core" . | nindent 2 }} 3 | {{- end }} 4 | 5 | {{ define "csa.label.pod" }} 6 | labels: {{- include "csa.label.core" . | nindent 2 }} 7 | {{- if .Values.pod.extraLabels }} 8 | {{ toYaml .Values.pod.extraLabels | indent 2 }} 9 | {{- end }} 10 | {{- end }} 11 | 12 | {{ define "csa.label.serviceaccount" }} 13 | labels: {{- include "csa.label.core" . | nindent 2 }} 14 | {{- end}} 15 | 16 | {{ define "csa.label.clusterrole" }} 17 | labels: {{- include "csa.label.core" . | nindent 2 }} 18 | {{- end }} 19 | 20 | {{ define "csa.label.clusterrolebinding" }} 21 | labels: {{- include "csa.label.core" . | nindent 2 }} 22 | {{- end }} 23 | 24 | {{- define "csa.label.core" -}} 25 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 26 | app.kubernetes.io/managed-by: "{{ .Release.Service }}" 27 | {{ include "csa.label.kubeName" . }} 28 | {{ include "csa.label.kubeInstance" . }} 29 | app.kubernetes.io/version: "{{ include "csa.container.tag" . }}" 30 | {{- end }} 31 | 32 | {{- define "csa.label.kubeName" -}} 33 | app.kubernetes.io/name: "{{ include "csa.name.app" . }}" 34 | {{- end }} 35 | 36 | {{- define "csa.label.kubeInstance" -}} 37 | app.kubernetes.io/instance: "{{ include "csa.name.release" . }}" 38 | {{- end }} 39 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/_name.tpl: -------------------------------------------------------------------------------- 1 | {{- define "csa.name.app" -}} 2 | {{ .Chart.Name }} 3 | {{- end }} 4 | 5 | {{- define "csa.name.release" -}} 6 | {{ .Release.Name }} 7 | {{- end }} 8 | 9 | {{- define "csa.name.namespace" -}} 10 | {{ .Release.Namespace }} 11 | {{- end }} 12 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/_pod.tpl: -------------------------------------------------------------------------------- 1 | {{- define "csa.pod.terminationGracePeriodSeconds" -}} 2 | {{- if .Values.csa.gracefulShutdownTimeoutSecs -}} 3 | {{ add .Values.csa.gracefulShutdownTimeoutSecs 5 }} 4 | {{- else -}} 5 | {{- /* 6 | Must be graceful-shutdown-timeout-secs default + 5s (i.e. some margin) 7 | */ -}} 8 | 15 9 | {{- end }} 10 | {{- end }} 11 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/_selector.tpl: -------------------------------------------------------------------------------- 1 | {{ define "csa.selector.deployment" }} 2 | matchLabels: 3 | {{ include "csa.label.kubeName" . }} 4 | {{ include "csa.label.kubeInstance" . }} 5 | {{- end }} 6 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | namespace: "{{ include "csa.name.namespace" . }}" 5 | name: "{{ include "csa.name.release" . }}" 6 | {{- include "csa.label.clusterrole" . | indent 2 }} 7 | {{- include "csa.annotation.clusterrole" . | indent 2 }} 8 | rules: 9 | - apiGroups: [""] 10 | resources: ["pods"] 11 | verbs: ["get", "list", "patch", "update", "watch"] 12 | - apiGroups: [""] 13 | resources: ["pods/resize"] 14 | verbs: ["patch"] 15 | - apiGroups: [""] 16 | resources: ["events"] 17 | verbs: ["create", "patch", "update"] 18 | - apiGroups: ["coordination.k8s.io"] 19 | resources: ["leases"] 20 | verbs: ["get", "create", "patch", "update"] 21 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | namespace: "{{ include "csa.name.namespace" . }}" 5 | name: "{{ include "csa.name.release" . }}" 6 | {{- include "csa.label.clusterrolebinding" . | indent 2 }} 7 | {{- include "csa.annotation.clusterrolebinding" . | indent 2 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: "{{ include "csa.name.release" . }}" 12 | subjects: 13 | - kind: ServiceAccount 14 | namespace: "{{ include "csa.name.namespace" . }}" 15 | name: "{{ include "csa.name.release" . }}" 16 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | namespace: "{{ include "csa.name.namespace" . }}" 5 | name: "{{ include "csa.name.release" . }}" 6 | {{- include "csa.label.deployment" . | indent 2 }} 7 | {{- include "csa.annotation.deployment" . | indent 2 }} 8 | spec: 9 | replicas: {{ include "csa.deployment.replicas" . }} 10 | strategy: 11 | type: Recreate 12 | selector: 13 | {{- include "csa.selector.deployment" . | indent 4 }} 14 | template: 15 | metadata: 16 | {{- include "csa.label.pod" . | indent 6 }} 17 | {{- include "csa.annotation.pod" . | indent 6 }} 18 | spec: 19 | serviceAccountName: "{{ include "csa.name.release" . }}" 20 | terminationGracePeriodSeconds: {{ include "csa.pod.terminationGracePeriodSeconds" . }} 21 | {{- if .Values.pod.imagePullSecrets }} 22 | imagePullSecrets: {{- toYaml .Values.pod.imagePullSecrets | nindent 8 }} 23 | {{- end }} 24 | containers: 25 | - name: "{{ include "csa.name.app" . }}" 26 | image: "{{ include "csa.container.imageTag" . }}" 27 | imagePullPolicy: IfNotPresent 28 | ports: 29 | - containerPort: 8080 # Metrics 30 | - containerPort: 8081 # Probes 31 | - containerPort: 8082 # pprof 32 | {{- include "csa.container.args" . | indent 10}} 33 | resources: 34 | requests: 35 | cpu: {{ .Values.container.cpu | quote }} 36 | memory: {{ .Values.container.memory | quote }} 37 | limits: 38 | cpu: {{ .Values.container.cpu | quote }} 39 | memory: {{ .Values.container.memory | quote }} 40 | livenessProbe: 41 | httpGet: 42 | path: /healthz 43 | port: 8081 44 | scheme: HTTP 45 | initialDelaySeconds: 5 46 | periodSeconds: 5 47 | timeoutSeconds: 5 48 | successThreshold: 1 49 | failureThreshold: 3 50 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | namespace: "{{ include "csa.name.namespace" . }}" 5 | name: "{{ include "csa.name.release" . }}" 6 | {{- include "csa.label.serviceaccount" . | indent 2 }} 7 | {{- include "csa.annotation.serviceaccount" . | indent 2 }} 8 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/tests/clusterrole_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test clusterrole 2 | templates: 3 | - clusterrole.yaml 4 | release: 5 | namespace: release-namespace 6 | name: release-name 7 | chart: 8 | version: 1.2.3 9 | appVersion: 3.2.1 10 | 11 | tests: 12 | - it: defaults correct 13 | asserts: 14 | - hasDocuments: 15 | count: 1 16 | - containsDocument: 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: ClusterRole 19 | namespace: release-namespace 20 | name: release-name 21 | - equal: 22 | path: metadata.labels 23 | value: 24 | helm.sh/chart: container-startup-autoscaler-1.2.3 25 | app.kubernetes.io/managed-by: Helm 26 | app.kubernetes.io/name: container-startup-autoscaler 27 | app.kubernetes.io/instance: release-name 28 | app.kubernetes.io/version: 3.2.1 29 | - notExists: 30 | path: metadata.annotations 31 | - contains: 32 | path: rules 33 | any: true 34 | content: 35 | apiGroups: [ "" ] 36 | resources: [ pods ] 37 | - contains: 38 | path: rules 39 | any: true 40 | content: 41 | apiGroups: [ "" ] 42 | resources: [ events ] 43 | 44 | - it: container tag overridden 45 | set: 46 | container.tag: 9.9.9 47 | asserts: 48 | - equal: 49 | path: metadata.labels["app.kubernetes.io/version"] 50 | value: 9.9.9 51 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/tests/clusterrolebinding_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test clusterrolebinding 2 | templates: 3 | - clusterrolebinding.yaml 4 | release: 5 | namespace: release-namespace 6 | name: release-name 7 | chart: 8 | version: 1.2.3 9 | appVersion: 3.2.1 10 | 11 | tests: 12 | - it: defaults correct 13 | asserts: 14 | - hasDocuments: 15 | count: 1 16 | - containsDocument: 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: ClusterRoleBinding 19 | namespace: release-namespace 20 | name: release-name 21 | - equal: 22 | path: metadata.labels 23 | value: 24 | helm.sh/chart: container-startup-autoscaler-1.2.3 25 | app.kubernetes.io/managed-by: Helm 26 | app.kubernetes.io/name: container-startup-autoscaler 27 | app.kubernetes.io/instance: release-name 28 | app.kubernetes.io/version: 3.2.1 29 | - notExists: 30 | path: metadata.annotations 31 | - equal: 32 | path: roleRef 33 | value: 34 | apiGroup: rbac.authorization.k8s.io 35 | kind: ClusterRole 36 | name: release-name 37 | - contains: 38 | path: subjects 39 | content: 40 | kind: ServiceAccount 41 | namespace: release-namespace 42 | name: release-name 43 | 44 | - it: container tag overridden 45 | set: 46 | container.tag: 9.9.9 47 | asserts: 48 | - equal: 49 | path: metadata.labels["app.kubernetes.io/version"] 50 | value: 9.9.9 51 | -------------------------------------------------------------------------------- /charts/container-startup-autoscaler/tests/serviceaccount_test.yaml: -------------------------------------------------------------------------------- 1 | suite: test serviceaccount 2 | templates: 3 | - serviceaccount.yaml 4 | release: 5 | namespace: release-namespace 6 | name: release-name 7 | chart: 8 | version: 1.2.3 9 | appVersion: 3.2.1 10 | 11 | tests: 12 | - it: defaults correct 13 | asserts: 14 | - hasDocuments: 15 | count: 1 16 | - containsDocument: 17 | apiVersion: v1 18 | kind: ServiceAccount 19 | namespace: release-namespace 20 | name: release-name 21 | - equal: 22 | path: metadata.labels 23 | value: 24 | helm.sh/chart: container-startup-autoscaler-1.2.3 25 | app.kubernetes.io/managed-by: Helm 26 | app.kubernetes.io/name: container-startup-autoscaler 27 | app.kubernetes.io/instance: release-name 28 | app.kubernetes.io/version: 3.2.1 29 | - notExists: 30 | path: metadata.annotations 31 | 32 | - it: container tag overridden 33 | set: 34 | container.tag: 9.9.9 35 | asserts: 36 | - equal: 37 | path: metadata.labels["app.kubernetes.io/version"] 38 | value: 9.9.9 39 | -------------------------------------------------------------------------------- /internal/common/error.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 common 18 | 19 | import "fmt" 20 | 21 | // WrapErrorf returns an error with the supplied format that wraps err. The supplied format is appended with ': %w', 22 | // with %w as err. 23 | func WrapErrorf(err error, format string, a ...any) error { 24 | wrapFormat := fmt.Sprintf("%s: %%w", format) 25 | return fmt.Errorf(wrapFormat, append(a, err)...) 26 | } 27 | -------------------------------------------------------------------------------- /internal/common/error_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 common 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestWrapErrorf(t *testing.T) { 27 | t.Run("WithFormatString", func(t *testing.T) { 28 | err := WrapErrorf(errors.New("non-wrapped err message"), "format (%s)", "test") 29 | assert.Equal(t, "format (test): non-wrapped err message", err.Error()) 30 | }) 31 | 32 | t.Run("WithoutFormatString", func(t *testing.T) { 33 | err := WrapErrorf(errors.New("non-wrapped err message"), "format") 34 | assert.Equal(t, "format: non-wrapped err message", err.Error()) 35 | }) 36 | 37 | t.Run("ChainWithFormatString", func(t *testing.T) { 38 | errInner := WrapErrorf(errors.New("non-wrapped err message"), "errInner format (%s)", "errInner test") 39 | errOuter := WrapErrorf(errInner, "errOuter format (%s)", "errOuter test") 40 | assert.Equal(t, "errOuter format (errOuter test): errInner format (errInner test): non-wrapped err message", errOuter.Error()) 41 | }) 42 | 43 | t.Run("ChainWithoutFormatString", func(t *testing.T) { 44 | errInner := WrapErrorf(errors.New("non-wrapped err message"), "errInner format") 45 | errOuter := WrapErrorf(errInner, "errOuter format") 46 | assert.Equal(t, "errOuter format: errInner format: non-wrapped err message", errOuter.Error()) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /internal/common/string.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 common 18 | 19 | import ( 20 | "strings" 21 | "unicode" 22 | ) 23 | 24 | // IsStringEmpty returns whether s is empty. All-spaces are reported empty. 25 | func IsStringEmpty(s string) bool { 26 | return strings.ReplaceAll(s, " ", "") == "" 27 | } 28 | 29 | // CapitalizeFirstChar returns s with the first character capitalized. 30 | func CapitalizeFirstChar(s string) string { 31 | if IsStringEmpty(s) { 32 | return s 33 | } 34 | 35 | out := []rune(s) 36 | out[0] = unicode.ToUpper(out[0]) 37 | return string(out) 38 | } 39 | -------------------------------------------------------------------------------- /internal/common/string_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 common 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestIsStringEmpty(t *testing.T) { 26 | type args struct { 27 | s string 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want bool 33 | }{ 34 | {"TrueEmpty", args{s: ""}, true}, 35 | {"TrueWhitespace", args{s: " "}, true}, 36 | {"False", args{s: "test"}, false}, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | assert.Equal(t, tt.want, IsStringEmpty(tt.args.s)) 41 | }) 42 | } 43 | } 44 | 45 | func TestCapitalizeFirstChar(t *testing.T) { 46 | type args struct { 47 | s string 48 | } 49 | tests := []struct { 50 | name string 51 | args args 52 | want string 53 | }{ 54 | {"OneWord", args{s: "test"}, "Test"}, 55 | {"TwoWords", args{s: "test test"}, "Test test"}, 56 | {"Empty", args{s: ""}, ""}, 57 | {"EmptyWhitespace", args{s: " "}, " "}, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | assert.Equal(t, tt.want, CapitalizeFirstChar(tt.args.s)) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/common/struct.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 common 18 | 19 | import "reflect" 20 | 21 | // IsStructEmpty returns whether s is (deeply) empty. Supports pointers. Returns false if s is not a struct. 22 | func IsStructEmpty(s any) bool { 23 | value := reflect.ValueOf(s) 24 | 25 | if value.Kind() == reflect.Ptr { 26 | value = reflect.Indirect(reflect.ValueOf(s)) 27 | s = value.Interface() 28 | } 29 | 30 | if value.Kind() == reflect.Struct { 31 | // Deep compare against a new empty version of the struct. 32 | return reflect.DeepEqual(s, reflect.New(reflect.TypeOf(s)).Elem().Interface()) 33 | } 34 | 35 | return false 36 | } 37 | 38 | // AreStructsEqual returns whether s1 and s2 are (deeply) equal. Supports pointers. Returns false if s1 or s2 are not 39 | // structs. 40 | func AreStructsEqual(s1 any, s2 any) bool { 41 | value1 := reflect.ValueOf(s1) 42 | 43 | if value1.Kind() == reflect.Ptr { 44 | value1 = reflect.Indirect(reflect.ValueOf(s1)) 45 | s1 = value1.Interface() 46 | } 47 | 48 | value2 := reflect.ValueOf(s2) 49 | 50 | if value2.Kind() == reflect.Ptr { 51 | value2 = reflect.Indirect(reflect.ValueOf(s2)) 52 | s2 = value2.Interface() 53 | } 54 | 55 | if value1.Kind() != reflect.Struct || value2.Kind() != reflect.Struct { 56 | return false 57 | } 58 | 59 | return reflect.DeepEqual(s1, s2) 60 | } 61 | -------------------------------------------------------------------------------- /internal/common/struct_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 common 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | type testStruct1 struct { 26 | structField testStruct2 27 | } 28 | 29 | type testStruct2 struct { 30 | stringField string 31 | } 32 | 33 | func TestIsStructEmpty(t *testing.T) { 34 | tests := []struct { 35 | name string 36 | s any 37 | want bool 38 | }{ 39 | {"NonPointerStructTrue", testStruct1{}, true}, 40 | {"NonPointerStructFalse", testStruct1{structField: testStruct2{stringField: "test"}}, false}, 41 | {"PointerStructTrue", &testStruct1{}, true}, 42 | {"PointerStructFalse", &testStruct1{structField: testStruct2{stringField: "test"}}, false}, 43 | {"NotStructFalse", 5, false}, 44 | {"NilFalse", nil, false}, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | assert.Equal(t, tt.want, IsStructEmpty(tt.s)) 49 | }) 50 | } 51 | } 52 | 53 | func TestAreStructsEqual(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | s1 any 57 | s2 any 58 | want bool 59 | }{ 60 | { 61 | "NonPointerStructTrue", 62 | testStruct1{structField: testStruct2{stringField: "test"}}, 63 | testStruct1{structField: testStruct2{stringField: "test"}}, 64 | true, 65 | }, 66 | { 67 | "NonPointerStructFalse", 68 | testStruct1{structField: testStruct2{stringField: "test1"}}, 69 | testStruct1{structField: testStruct2{stringField: "test2"}}, 70 | false, 71 | }, 72 | { 73 | "PointerStructTrue", 74 | &testStruct1{structField: testStruct2{stringField: "test"}}, 75 | &testStruct1{structField: testStruct2{stringField: "test"}}, 76 | true, 77 | }, 78 | { 79 | "PointerStructFalse", 80 | &testStruct1{structField: testStruct2{stringField: "test1"}}, 81 | &testStruct1{structField: testStruct2{stringField: "test2"}}, 82 | false, 83 | }, 84 | { 85 | "NotStructFalse", 86 | 5, 87 | 5, 88 | false, 89 | }, 90 | { 91 | "NilFalse", 92 | nil, 93 | nil, 94 | false, 95 | }, 96 | } 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | assert.Equal(t, tt.want, AreStructsEqual(tt.s1, tt.s2)) 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /internal/common/timeout.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 common 18 | 19 | const ( 20 | WaitForCacheUpdateMaxWaitSecs = 3 21 | SubscriberChannelWriteTimeoutSecs = 3 22 | ) 23 | -------------------------------------------------------------------------------- /internal/context/const.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 context 18 | 19 | const ( 20 | KeyStandardRetryAttempts = "rattempts" 21 | KeyStandardRetryDelaySecs = "rdelaysecs" 22 | KeyTargetContainerName = "cname" 23 | KeyTargetContainerStates = "cstates" 24 | KeyTimeoutOverride = "toverride" 25 | ) 26 | -------------------------------------------------------------------------------- /internal/context/contexttest/contextbuilder.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 contexttest 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "time" 23 | 24 | context2 "github.com/ExpediaGroup/container-startup-autoscaler/internal/context" 25 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/logging" 26 | "github.com/go-logr/logr" 27 | "github.com/google/uuid" 28 | "github.com/tonglil/buflogr" 29 | ) 30 | 31 | type CtxBuilder struct { 32 | config CtxConfig 33 | } 34 | 35 | func NewCtxBuilder(config CtxConfig) *CtxBuilder { 36 | return &CtxBuilder{config: config} 37 | } 38 | 39 | func (b *CtxBuilder) LogBuffer(logBuffer *bytes.Buffer) *CtxBuilder { 40 | b.config.logBuffer = logBuffer 41 | return b 42 | } 43 | 44 | func (b *CtxBuilder) StandardRetryAttempts(standardRetryAttempts int) *CtxBuilder { 45 | b.config.standardRetryAttempts = standardRetryAttempts 46 | return b 47 | } 48 | 49 | func (b *CtxBuilder) StandardRetryDelaySecs(standardRetryDelaySecs int) *CtxBuilder { 50 | b.config.standardRetryDelaySecs = standardRetryDelaySecs 51 | return b 52 | } 53 | 54 | func (b *CtxBuilder) TimeoutOverride(timeoutOverride time.Duration) *CtxBuilder { 55 | b.config.timeoutOverride = timeoutOverride 56 | return b 57 | } 58 | 59 | func (b *CtxBuilder) Build() context.Context { 60 | var c context.Context 61 | 62 | if b.config.logBuffer == nil { 63 | logging.Init(logging.DefaultW, logging.DefaultV, logging.DefaultAddCaller) 64 | c = logr.NewContext(context.TODO(), logging.Logger) 65 | } else { 66 | c = logr.NewContext(context.TODO(), buflogr.NewWithBuffer(b.config.logBuffer)) 67 | } 68 | 69 | c = context.WithValue(c, KeyUuid, uuid.New().String()) 70 | if b.config.standardRetryAttempts == 0 { 71 | b.config.standardRetryAttempts = 1 72 | } 73 | c = context.WithValue(c, context2.KeyStandardRetryAttempts, b.config.standardRetryAttempts) 74 | c = context.WithValue(c, context2.KeyStandardRetryDelaySecs, b.config.standardRetryDelaySecs) 75 | if b.config.timeoutOverride != 0 { 76 | c = context.WithValue(c, context2.KeyTimeoutOverride, b.config.timeoutOverride) 77 | } 78 | return c 79 | } 80 | -------------------------------------------------------------------------------- /internal/context/contexttest/contextconfig.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 contexttest 18 | 19 | import ( 20 | "bytes" 21 | "time" 22 | ) 23 | 24 | const KeyUuid = "uuid" 25 | 26 | type CtxConfig struct { 27 | logBuffer *bytes.Buffer 28 | standardRetryAttempts int 29 | standardRetryDelaySecs int 30 | timeoutOverride time.Duration 31 | } 32 | 33 | func NewCtxConfig() CtxConfig { 34 | return CtxConfig{} 35 | } 36 | 37 | func NewNoRetryCtxConfig(logBuffer *bytes.Buffer) CtxConfig { 38 | return CtxConfig{ 39 | logBuffer: logBuffer, 40 | standardRetryAttempts: 1, 41 | standardRetryDelaySecs: 0, 42 | } 43 | } 44 | 45 | func NewOneRetryCtxConfig(logBuffer *bytes.Buffer) CtxConfig { 46 | return CtxConfig{ 47 | logBuffer: logBuffer, 48 | standardRetryAttempts: 2, 49 | standardRetryDelaySecs: 0, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/event/eventcommon/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 eventcommon 18 | 19 | import "context" 20 | 21 | // PodEventPublisher publishes pod events to subscribers. 22 | type PodEventPublisher interface { 23 | Subscribe( 24 | namespace string, 25 | name string, 26 | eventTypes []PodEventType, 27 | ) <-chan PodEvent 28 | 29 | Unsubscribe( 30 | ch <-chan PodEvent, 31 | ) 32 | 33 | Publish( 34 | ctx context.Context, 35 | event PodEvent, 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /internal/event/eventcommon/podevent.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 eventcommon 18 | 19 | import "k8s.io/api/core/v1" 20 | 21 | // PodEventType indicates the type of pod event. 22 | type PodEventType string 23 | 24 | const ( 25 | PodEventTypeCreate PodEventType = "create" 26 | PodEventTypeDelete PodEventType = "delete" 27 | PodEventTypeUpdate PodEventType = "update" 28 | ) 29 | 30 | // PodEvent represents a pod event triggered via a Kubernetes watch. 31 | type PodEvent struct { 32 | EventType PodEventType 33 | Pod *v1.Pod 34 | } 35 | 36 | func NewPodEvent( 37 | eventType PodEventType, 38 | pod *v1.Pod, 39 | ) PodEvent { 40 | return PodEvent{ 41 | EventType: eventType, 42 | Pod: pod, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/event/eventcommon/podevent_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 eventcommon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | v1 "k8s.io/api/core/v1" 24 | ) 25 | 26 | func TestNewPodEvent(t *testing.T) { 27 | pod := &v1.Pod{} 28 | pe := NewPodEvent(PodEventTypeUpdate, pod) 29 | 30 | expected := PodEvent{ 31 | EventType: PodEventTypeUpdate, 32 | Pod: pod, 33 | } 34 | assert.Equal(t, expected, pe) 35 | } 36 | -------------------------------------------------------------------------------- /internal/event/eventtest/mockpodeventpublisher.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 eventtest 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/event/eventcommon" 23 | "github.com/stretchr/testify/mock" 24 | ) 25 | 26 | type MockPodEventPublisher struct { 27 | mock.Mock 28 | } 29 | 30 | func NewMockPodEventPublisher(configFunc func(*MockPodEventPublisher)) *MockPodEventPublisher { 31 | m := &MockPodEventPublisher{} 32 | if configFunc != nil { 33 | configFunc(m) 34 | } else { 35 | m.AllDefaults() 36 | } 37 | 38 | return m 39 | } 40 | 41 | func (m *MockPodEventPublisher) Subscribe( 42 | namespace string, 43 | name string, 44 | eventTypes []eventcommon.PodEventType, 45 | ) <-chan eventcommon.PodEvent { 46 | args := m.Called(namespace, name, eventTypes) 47 | return args.Get(0).(chan eventcommon.PodEvent) 48 | } 49 | 50 | func (m *MockPodEventPublisher) Unsubscribe(ch <-chan eventcommon.PodEvent) { 51 | m.Called(ch) 52 | } 53 | 54 | func (m *MockPodEventPublisher) Publish(ctx context.Context, event eventcommon.PodEvent) { 55 | m.Called(ctx, event) 56 | } 57 | 58 | func (m *MockPodEventPublisher) SubscribeDefault() { 59 | m.On("Subscribe", mock.Anything, mock.Anything, mock.Anything).Return(make(chan eventcommon.PodEvent, 1)) 60 | } 61 | 62 | func (m *MockPodEventPublisher) UnsubscribeDefault() { 63 | m.On("Unsubscribe", mock.Anything).Return() 64 | } 65 | 66 | func (m *MockPodEventPublisher) PublishDefault() { 67 | m.On("Publish", mock.Anything).Return() 68 | } 69 | 70 | func (m *MockPodEventPublisher) AllDefaults() { 71 | m.SubscribeDefault() 72 | m.UnsubscribeDefault() 73 | m.PublishDefault() 74 | } 75 | -------------------------------------------------------------------------------- /internal/kube/error.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kube 18 | 19 | // ContainerStatusNotPresentError is an error that indicates container status is not present. 20 | type ContainerStatusNotPresentError struct{} 21 | 22 | func NewContainerStatusNotPresentError() error { 23 | return ContainerStatusNotPresentError{} 24 | } 25 | 26 | func (e ContainerStatusNotPresentError) Error() string { 27 | return "container status not present" 28 | } 29 | 30 | // ContainerStatusResourcesNotPresentError is an error that indicates container status resources is not present. 31 | type ContainerStatusResourcesNotPresentError struct{} 32 | 33 | func NewContainerStatusResourcesNotPresentError() error { 34 | return ContainerStatusResourcesNotPresentError{} 35 | } 36 | 37 | func (e ContainerStatusResourcesNotPresentError) Error() string { 38 | return "container status resources not present" 39 | } 40 | -------------------------------------------------------------------------------- /internal/kube/error_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kube 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestNewContainerStatusNotPresentError(t *testing.T) { 26 | assert.Equal(t, ContainerStatusNotPresentError{}, NewContainerStatusNotPresentError()) 27 | } 28 | 29 | func TestContainerStatusNotPresentErrorError(t *testing.T) { 30 | e := NewContainerStatusNotPresentError() 31 | assert.Equal(t, "container status not present", e.Error()) 32 | } 33 | 34 | func TestNewContainerStatusResourcesNotPresentError(t *testing.T) { 35 | assert.Equal(t, ContainerStatusResourcesNotPresentError{}, NewContainerStatusResourcesNotPresentError()) 36 | } 37 | 38 | func TestContainerStatusResourcesNotPresentErrorError(t *testing.T) { 39 | e := NewContainerStatusResourcesNotPresentError() 40 | assert.Equal(t, "container status resources not present", e.Error()) 41 | } 42 | -------------------------------------------------------------------------------- /internal/kube/kubecommon/datatype.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kubecommon 18 | 19 | // DataType indicates a data type. 20 | type DataType string 21 | 22 | const ( 23 | DataTypeString DataType = "string" 24 | DataTypeBool DataType = "bool" 25 | ) 26 | -------------------------------------------------------------------------------- /internal/kube/kubecommon/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kubecommon 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/event/eventcommon" 23 | v1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/api/resource" 25 | "k8s.io/apimachinery/pkg/types" 26 | ) 27 | 28 | // PodHelper performs operations relating to Kube pods. 29 | type PodHelper interface { 30 | Get( 31 | ctx context.Context, 32 | name types.NamespacedName, 33 | ) (bool, *v1.Pod, error) 34 | 35 | Patch( 36 | ctx context.Context, 37 | podEventPublisher eventcommon.PodEventPublisher, 38 | pod *v1.Pod, 39 | podMutationFuncs []func(podToMutate *v1.Pod) (bool, func(currentPod *v1.Pod) bool, error), 40 | patchResize bool, 41 | ) (*v1.Pod, error) 42 | 43 | HasAnnotation( 44 | pod *v1.Pod, 45 | name string, 46 | ) (bool, string) 47 | 48 | ExpectedLabelValueAs( 49 | pod *v1.Pod, 50 | name string, 51 | as DataType, 52 | ) (any, error) 53 | 54 | ExpectedAnnotationValueAs( 55 | pod *v1.Pod, 56 | name string, 57 | as DataType, 58 | ) (any, error) 59 | 60 | IsContainerInSpec( 61 | pod *v1.Pod, 62 | containerName string, 63 | ) bool 64 | 65 | ResizeConditions( 66 | pod *v1.Pod, 67 | ) []v1.PodCondition 68 | 69 | QOSClass( 70 | pod *v1.Pod, 71 | ) (v1.PodQOSClass, error) 72 | } 73 | 74 | // ContainerHelper performs operations relating to Kube containers. 75 | type ContainerHelper interface { 76 | Get( 77 | pod *v1.Pod, 78 | containerName string, 79 | ) (*v1.Container, error) 80 | 81 | HasStartupProbe( 82 | container *v1.Container, 83 | ) bool 84 | 85 | HasReadinessProbe( 86 | container *v1.Container, 87 | ) bool 88 | 89 | State( 90 | pod *v1.Pod, 91 | container *v1.Container, 92 | ) (v1.ContainerState, error) 93 | 94 | IsStarted( 95 | pod *v1.Pod, 96 | container *v1.Container, 97 | ) (bool, error) 98 | 99 | IsReady( 100 | pod *v1.Pod, 101 | container *v1.Container, 102 | ) (bool, error) 103 | 104 | Requests( 105 | container *v1.Container, 106 | resourceName v1.ResourceName, 107 | ) resource.Quantity 108 | 109 | Limits( 110 | container *v1.Container, 111 | resourceName v1.ResourceName, 112 | ) resource.Quantity 113 | 114 | ResizePolicy( 115 | container *v1.Container, 116 | resourceName v1.ResourceName, 117 | ) (v1.ResourceResizeRestartPolicy, error) 118 | 119 | CurrentRequests( 120 | pod *v1.Pod, 121 | container *v1.Container, 122 | resourceName v1.ResourceName, 123 | ) (resource.Quantity, error) 124 | 125 | CurrentLimits( 126 | pod *v1.Pod, 127 | container *v1.Container, 128 | resourceName v1.ResourceName, 129 | ) (resource.Quantity, error) 130 | } 131 | -------------------------------------------------------------------------------- /internal/kube/kubecommon/metadataconst.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kubecommon 18 | 19 | const Namespace = "csa.expediagroup.com" 20 | 21 | const ( 22 | LabelEnabled = Namespace + "/enabled" 23 | ) 24 | 25 | const ( 26 | AnnotationStatus = Namespace + "/status" 27 | ) 28 | -------------------------------------------------------------------------------- /internal/kube/kubetest/containerconsts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kubetest 18 | 19 | import ( 20 | v1 "k8s.io/api/core/v1" 21 | "k8s.io/apimachinery/pkg/api/resource" 22 | ) 23 | 24 | const ( 25 | DefaultContainerName = "container" 26 | ) 27 | 28 | var ( 29 | PodCpuStartupEnabled = resource.MustParse(PodAnnotationCpuStartup) 30 | PodCpuPostStartupRequestsEnabled = resource.MustParse(PodAnnotationCpuPostStartupRequests) 31 | PodCpuPostStartupLimitsEnabled = resource.MustParse(PodAnnotationCpuPostStartupLimits) 32 | 33 | PodCpuStartupDisabled = PodCpuStartupEnabled 34 | PodCpuPostStartupRequestsDisabled = PodCpuStartupEnabled 35 | PodCpuPostStartupLimitsDisabled = PodCpuStartupEnabled 36 | 37 | PodMemoryStartupEnabled = resource.MustParse(PodAnnotationMemoryStartup) 38 | PodMemoryPostStartupRequestsEnabled = resource.MustParse(PodAnnotationMemoryPostStartupRequests) 39 | PodMemoryPostStartupLimitsEnabled = resource.MustParse(PodAnnotationMemoryPostStartupLimits) 40 | 41 | PodMemoryStartupDisabled = PodMemoryStartupEnabled 42 | PodMemoryPostStartupRequestsDisabled = PodMemoryStartupEnabled 43 | PodMemoryPostStartupLimitsDisabled = PodMemoryStartupEnabled 44 | 45 | PodCpuUnknown = resource.MustParse(PodAnnotationCpuUnknown) 46 | PodMemoryUnknown = resource.MustParse(PodAnnotationMemoryUnknown) 47 | ) 48 | 49 | var ( 50 | DefaultContainerCpuResizePolicy = v1.NotRequired 51 | DefaultContainerMemoryResizePolicy = v1.NotRequired 52 | ) 53 | -------------------------------------------------------------------------------- /internal/kube/kubetest/controllerruntime.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kubetest 18 | 19 | import ( 20 | kubefake "k8s.io/client-go/kubernetes/fake" 21 | "sigs.k8s.io/controller-runtime/pkg/client" 22 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 23 | "sigs.k8s.io/controller-runtime/pkg/client/interceptor" 24 | ) 25 | 26 | func ControllerRuntimeFakeClientWithKubeFake( 27 | fakeClientFunc func() *kubefake.Clientset, 28 | interceptorFuncsFunc func() interceptor.Funcs, 29 | ) client.WithWatch { 30 | return fake.NewClientBuilder(). 31 | WithObjectTracker(fakeClientFunc().Tracker()). 32 | WithInterceptorFuncs(interceptorFuncsFunc()). 33 | Build() 34 | } 35 | -------------------------------------------------------------------------------- /internal/kube/kubetest/podconsts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kubetest 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | "k8s.io/apimachinery/pkg/types" 22 | ) 23 | 24 | const ( 25 | PodAnnotationCpuStartup = "3m" 26 | PodAnnotationCpuPostStartupRequests = "1m" 27 | PodAnnotationCpuPostStartupLimits = "2m" 28 | 29 | PodAnnotationMemoryStartup = "3M" 30 | PodAnnotationMemoryPostStartupRequests = "1M" 31 | PodAnnotationMemoryPostStartupLimits = "2M" 32 | 33 | PodAnnotationCpuUnknown = "999m" 34 | PodAnnotationMemoryUnknown = "999M" 35 | ) 36 | 37 | var ( 38 | PodResizeConditionsNotStartedOrCompletedNoConditions []corev1.PodCondition 39 | PodResizeConditionsNotStartedOrCompletedResizeInProgressTrue = []corev1.PodCondition{ 40 | { 41 | Type: corev1.PodResizeInProgress, 42 | Status: corev1.ConditionTrue, 43 | }, 44 | } 45 | PodResizeConditionsInProgress = []corev1.PodCondition{ 46 | { 47 | Type: corev1.PodResizeInProgress, 48 | Status: corev1.ConditionFalse, 49 | }, 50 | } 51 | PodResizeConditionsDeferred = []corev1.PodCondition{ 52 | { 53 | Type: corev1.PodResizePending, 54 | Reason: corev1.PodReasonDeferred, 55 | Message: "message", 56 | }, 57 | } 58 | PodResizeConditionsInfeasible = []corev1.PodCondition{ 59 | { 60 | Type: corev1.PodResizePending, 61 | Reason: corev1.PodReasonInfeasible, 62 | Message: "message", 63 | }, 64 | } 65 | PodResizeConditionsError = []corev1.PodCondition{ 66 | { 67 | Type: corev1.PodResizeInProgress, 68 | Status: corev1.ConditionFalse, 69 | Reason: corev1.PodReasonError, 70 | Message: "message", 71 | }, 72 | } 73 | PodResizeConditionsUnknownPending = []corev1.PodCondition{ 74 | { 75 | Type: corev1.PodResizePending, 76 | Reason: "unknownreason", 77 | }, 78 | } 79 | PodResizeConditionsUnknownInProgress = []corev1.PodCondition{ 80 | { 81 | Type: corev1.PodResizeInProgress, 82 | Status: corev1.ConditionUnknown, 83 | }, 84 | } 85 | PodResizeConditionsUnknownConditions = []corev1.PodCondition{ 86 | { 87 | Type: "unknowntype", 88 | }, 89 | } 90 | ) 91 | 92 | const ( 93 | DefaultPodNamespace = "namespace" 94 | DefaultPodName = "name" 95 | DefaultPodResourceVersion = "1" 96 | DefaultLabelEnabledValue = "true" 97 | DefaultAnnotationTargetContainerName = DefaultContainerName 98 | DefaultStatusContainerName = DefaultContainerName 99 | ) 100 | 101 | var ( 102 | DefaultPodNamespacedName = types.NamespacedName{ 103 | Namespace: DefaultPodNamespace, 104 | Name: DefaultPodName, 105 | } 106 | ) 107 | -------------------------------------------------------------------------------- /internal/kube/retry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 kube 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "strings" 23 | 24 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/logging" 25 | metricsretry "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/retry" 26 | "github.com/avast/retry-go/v4" 27 | kerrors "k8s.io/apimachinery/pkg/api/errors" 28 | "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | ) 30 | 31 | // kubeApiRetryOptions returns the configuration necessary to perform a retry for a Kube API invocation. 32 | func kubeApiRetryOptions(ctx context.Context) []retry.Option { 33 | var opts []retry.Option 34 | 35 | // Don't retry if it's a 'not found' error or not recoverable. 36 | opts = append(opts, retry.RetryIf(func(err error) bool { 37 | return !kerrors.IsNotFound(err) && retry.IsRecoverable(err) 38 | })) 39 | 40 | // Log retry. 41 | opts = append(opts, retry.OnRetry(func(n uint, err error) { 42 | reason := "unknown" 43 | 44 | var stat kerrors.APIStatus 45 | var ok bool 46 | if stat, ok = err.(kerrors.APIStatus); !ok { 47 | stat, _ = errors.Unwrap(err).(kerrors.APIStatus) 48 | } 49 | 50 | if stat != nil && stat.Status().Reason != v1.StatusReasonUnknown { 51 | reason = strings.ToLower(string(stat.Status().Reason)) 52 | } 53 | 54 | logging.Errorf(ctx, err, "(attempt %d) kube api call failed (reason: %s)", n+1, reason) 55 | metricsretry.Retry(reason).Inc() 56 | })) 57 | 58 | return opts 59 | } 60 | -------------------------------------------------------------------------------- /internal/logging/keys.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 logging 18 | 19 | const ( 20 | keyTargetContainerName = "targetname" 21 | keyTargetContainerStates = "targetstates" 22 | ) 23 | -------------------------------------------------------------------------------- /internal/metrics/informercache/informercache.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 informercache 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "sigs.k8s.io/controller-runtime/pkg/metrics" 23 | ) 24 | 25 | const ( 26 | Subsystem = "informercache" 27 | ) 28 | 29 | const ( 30 | syncTimeoutName = "sync_timeout" 31 | ) 32 | 33 | var ( 34 | syncTimeout = prometheus.NewCounterVec(prometheus.CounterOpts{ 35 | Namespace: metricscommon.Namespace, 36 | Subsystem: Subsystem, 37 | Name: syncTimeoutName, 38 | Help: "Number of informer cache sync timeouts after a pod mutation was performed via the Kubernetes API (may result in CSA status inaccuracies)", 39 | }, []string{}) 40 | ) 41 | 42 | // allMetrics must include all metrics defined above. 43 | var allMetrics = []prometheus.Collector{ 44 | syncTimeout, 45 | } 46 | 47 | func RegisterMetrics(registry metrics.RegistererGatherer) { 48 | registry.MustRegister(allMetrics...) 49 | } 50 | 51 | func ResetMetrics() { 52 | metricscommon.ResetMetrics(allMetrics) 53 | } 54 | 55 | func SyncTimeout() prometheus.Counter { 56 | return syncTimeout.WithLabelValues() 57 | } 58 | -------------------------------------------------------------------------------- /internal/metrics/informercache/informercache_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 informercache 18 | 19 | import ( 20 | "fmt" 21 | "sync" 22 | "testing" 23 | 24 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 25 | "github.com/prometheus/client_golang/prometheus" 26 | "github.com/stretchr/testify/assert" 27 | "k8s.io/component-base/metrics/testutil" 28 | ) 29 | 30 | func TestRegisterMetrics(t *testing.T) { 31 | registry := prometheus.NewRegistry() 32 | RegisterMetrics(registry) 33 | assert.Equal(t, len(allMetrics), len(descs(registry))) 34 | } 35 | 36 | func TestResetMetrics(t *testing.T) { 37 | SyncTimeout().Inc() 38 | value, _ := testutil.GetCounterMetricValue(SyncTimeout()) 39 | assert.Equal(t, float64(1), value) 40 | ResetMetrics() 41 | 42 | value, _ = testutil.GetCounterMetricValue(SyncTimeout()) 43 | assert.Equal(t, float64(0), value) 44 | } 45 | 46 | func TestPatchSyncTimeout(t *testing.T) { 47 | m := SyncTimeout() 48 | assert.Contains( 49 | t, 50 | m.Desc().String(), 51 | fmt.Sprintf("%s_%s_%s", metricscommon.Namespace, Subsystem, syncTimeoutName), 52 | ) 53 | } 54 | 55 | func descs(registry *prometheus.Registry) []string { 56 | ch := make(chan *prometheus.Desc) 57 | done := make(chan struct{}) 58 | var ret []string 59 | 60 | var wg sync.WaitGroup 61 | wg.Add(1) 62 | go func() { 63 | defer wg.Done() 64 | 65 | for { 66 | select { 67 | case desc := <-ch: 68 | ret = append(ret, desc.String()) 69 | case <-done: 70 | return 71 | } 72 | } 73 | }() 74 | 75 | registry.Describe(ch) 76 | done <- struct{}{} 77 | wg.Wait() 78 | return ret 79 | } 80 | -------------------------------------------------------------------------------- /internal/metrics/metricscommon/const.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 metricscommon 18 | 19 | const ( 20 | Namespace = "csa" 21 | 22 | DirectionLabelName = "direction" 23 | OutcomeLabelName = "outcome" 24 | ReasonLabelName = "reason" 25 | ) 26 | 27 | // Direction indicates the direction of a scale. 28 | type Direction string 29 | 30 | var ( 31 | DirectionUp Direction = "up" 32 | DirectionDown Direction = "down" 33 | ) 34 | 35 | // Outcome indicates the outcome of something. 36 | type Outcome string 37 | 38 | var ( 39 | OutcomeSuccess Outcome = "success" 40 | OutcomeFailure Outcome = "failure" 41 | ) 42 | -------------------------------------------------------------------------------- /internal/metrics/metricscommon/metric.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 metricscommon 18 | 19 | import ( 20 | "github.com/prometheus/client_golang/prometheus" 21 | ) 22 | 23 | // ResetMetrics resets all supplied metrics. 24 | func ResetMetrics(metrics []prometheus.Collector) { 25 | for _, metric := range metrics { 26 | switch metric.(type) { 27 | case *prometheus.CounterVec: 28 | metric.(*prometheus.CounterVec).Reset() 29 | case *prometheus.GaugeVec: 30 | metric.(*prometheus.GaugeVec).Reset() 31 | case *prometheus.HistogramVec: 32 | metric.(*prometheus.HistogramVec).Reset() 33 | case *prometheus.SummaryVec: 34 | metric.(*prometheus.SummaryVec).Reset() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/metrics/reconciler/reconciler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 reconciler 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "sigs.k8s.io/controller-runtime/pkg/metrics" 23 | ) 24 | 25 | const ( 26 | Subsystem = "reconciler" 27 | ) 28 | 29 | const ( 30 | skippedOnlyStatusChangeName = "skipped_only_status_change" 31 | existingInProgressName = "existing_in_progress" 32 | failureName = "failure" 33 | ) 34 | 35 | type FailureReason string 36 | 37 | const ( 38 | FailureReasonUnableToGetPod = FailureReason("unable_to_get_pod") 39 | FailureReasonPodDoesNotExist = FailureReason("pod_does_not_exist") 40 | FailureReasonConfiguration = FailureReason("configuration") 41 | FailureReasonValidation = FailureReason("validation") 42 | FailureReasonStatesDetermination = FailureReason("states_determination") 43 | FailureReasonStatesAction = FailureReason("states_action") 44 | ) 45 | 46 | var ( 47 | skippedOnlyStatusChange = prometheus.NewCounterVec(prometheus.CounterOpts{ 48 | Namespace: metricscommon.Namespace, 49 | Subsystem: Subsystem, 50 | Name: skippedOnlyStatusChangeName, 51 | Help: "Number of reconciles that were skipped because only the status changed", 52 | }, []string{}) 53 | 54 | existingInProgress = prometheus.NewCounterVec(prometheus.CounterOpts{ 55 | Namespace: metricscommon.Namespace, 56 | Subsystem: Subsystem, 57 | Name: existingInProgressName, 58 | Help: "Number of attempted reconciles where one was already in progress for the same namespace/name (results in a requeue)", 59 | }, []string{}) 60 | 61 | failure = prometheus.NewCounterVec(prometheus.CounterOpts{ 62 | Namespace: metricscommon.Namespace, 63 | Subsystem: Subsystem, 64 | Name: failureName, 65 | Help: "Number of reconciles where there was a failure", 66 | }, []string{metricscommon.ReasonLabelName}) 67 | ) 68 | 69 | // allMetrics must include all metrics defined above. 70 | var allMetrics = []prometheus.Collector{ 71 | skippedOnlyStatusChange, existingInProgress, failure, 72 | } 73 | 74 | func RegisterMetrics(registry metrics.RegistererGatherer) { 75 | registry.MustRegister(allMetrics...) 76 | } 77 | 78 | func ResetMetrics() { 79 | metricscommon.ResetMetrics(allMetrics) 80 | } 81 | 82 | func SkippedOnlyStatusChange() prometheus.Counter { 83 | return skippedOnlyStatusChange.WithLabelValues() 84 | } 85 | 86 | func ExistingInProgress() prometheus.Counter { 87 | return existingInProgress.WithLabelValues() 88 | } 89 | 90 | func Failure(reason FailureReason) prometheus.Counter { 91 | return failure.WithLabelValues(string(reason)) 92 | } 93 | -------------------------------------------------------------------------------- /internal/metrics/reconciler/reconciler_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 reconciler 18 | 19 | import ( 20 | "fmt" 21 | "sync" 22 | "testing" 23 | 24 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 25 | "github.com/prometheus/client_golang/prometheus" 26 | "github.com/stretchr/testify/assert" 27 | "k8s.io/component-base/metrics/testutil" 28 | ) 29 | 30 | func TestRegisterMetrics(t *testing.T) { 31 | registry := prometheus.NewRegistry() 32 | RegisterMetrics(registry) 33 | assert.Equal(t, len(allMetrics), len(descs(registry))) 34 | } 35 | 36 | func TestResetMetrics(t *testing.T) { 37 | SkippedOnlyStatusChange().Inc() 38 | value, _ := testutil.GetCounterMetricValue(SkippedOnlyStatusChange()) 39 | assert.Equal(t, float64(1), value) 40 | ResetMetrics() 41 | 42 | value, _ = testutil.GetCounterMetricValue(SkippedOnlyStatusChange()) 43 | assert.Equal(t, float64(0), value) 44 | } 45 | 46 | func TestSkippedOnlyStatusChange(t *testing.T) { 47 | m := SkippedOnlyStatusChange() 48 | assert.Contains( 49 | t, 50 | m.Desc().String(), 51 | fmt.Sprintf("%s_%s_%s", metricscommon.Namespace, Subsystem, skippedOnlyStatusChangeName), 52 | ) 53 | } 54 | 55 | func TestExistingInProgress(t *testing.T) { 56 | m := ExistingInProgress() 57 | assert.Contains( 58 | t, 59 | m.Desc().String(), 60 | fmt.Sprintf("%s_%s_%s", metricscommon.Namespace, Subsystem, existingInProgressName), 61 | ) 62 | } 63 | 64 | func TestFailureUnableToGetPod(t *testing.T) { 65 | m := Failure("") 66 | assert.Contains( 67 | t, 68 | m.Desc().String(), 69 | fmt.Sprintf("%s_%s_%s", metricscommon.Namespace, Subsystem, failureName), 70 | ) 71 | } 72 | 73 | func descs(registry *prometheus.Registry) []string { 74 | ch := make(chan *prometheus.Desc) 75 | done := make(chan struct{}) 76 | var ret []string 77 | 78 | var wg sync.WaitGroup 79 | wg.Add(1) 80 | go func() { 81 | defer wg.Done() 82 | 83 | for { 84 | select { 85 | case desc := <-ch: 86 | ret = append(ret, desc.String()) 87 | case <-done: 88 | return 89 | } 90 | } 91 | }() 92 | 93 | registry.Describe(ch) 94 | done <- struct{}{} 95 | wg.Wait() 96 | return ret 97 | } 98 | -------------------------------------------------------------------------------- /internal/metrics/registry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 metrics 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/informercache" 21 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/reconciler" 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/retry" 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/scale" 24 | "sigs.k8s.io/controller-runtime/pkg/metrics" 25 | ) 26 | 27 | // RegisterAllMetrics registers all metrics with the supplied registry. 28 | func RegisterAllMetrics(registry metrics.RegistererGatherer) { 29 | reconciler.RegisterMetrics(registry) 30 | retry.RegisterKubeApiMetrics(registry) 31 | scale.RegisterMetrics(registry) 32 | informercache.RegisterMetrics(registry) 33 | } 34 | -------------------------------------------------------------------------------- /internal/metrics/registry_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 metrics 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | "sync" 23 | "testing" 24 | 25 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/informercache" 26 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 27 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/reconciler" 28 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/retry" 29 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/scale" 30 | "github.com/prometheus/client_golang/prometheus" 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func TestRegisterAllMetrics(t *testing.T) { 35 | registry := prometheus.NewRegistry() 36 | RegisterAllMetrics(registry) 37 | 38 | gotReconciler, gotRetryKubeapi, gotScale, gotInformerCache := gotSubsystems(registry) 39 | assert.True(t, gotReconciler) 40 | assert.True(t, gotScale) 41 | assert.True(t, gotRetryKubeapi) 42 | assert.True(t, gotInformerCache) 43 | } 44 | 45 | func gotSubsystems(registry *prometheus.Registry) (bool, bool, bool, bool) { 46 | descCh := make(chan *prometheus.Desc) 47 | doneCh := make(chan struct{}) 48 | gotReconciler, gotRetryKubeapi, gotScale, gotInformerCache := false, false, false, false 49 | 50 | var wg sync.WaitGroup 51 | wg.Add(1) 52 | go func() { 53 | defer wg.Done() 54 | 55 | for { 56 | select { 57 | case desc := <-descCh: 58 | if strings.Contains(desc.String(), fmt.Sprintf("%s_%s", metricscommon.Namespace, reconciler.Subsystem)) { 59 | gotReconciler = true 60 | } 61 | 62 | if strings.Contains(desc.String(), fmt.Sprintf("%s_%s", metricscommon.Namespace, retry.SubsystemKubeApi)) { 63 | gotRetryKubeapi = true 64 | } 65 | 66 | if strings.Contains(desc.String(), fmt.Sprintf("%s_%s", metricscommon.Namespace, scale.Subsystem)) { 67 | gotScale = true 68 | } 69 | 70 | if strings.Contains(desc.String(), fmt.Sprintf("%s_%s", metricscommon.Namespace, informercache.Subsystem)) { 71 | gotInformerCache = true 72 | } 73 | 74 | case <-doneCh: 75 | return 76 | } 77 | } 78 | }() 79 | 80 | registry.Describe(descCh) 81 | doneCh <- struct{}{} 82 | wg.Wait() 83 | return gotReconciler, gotRetryKubeapi, gotScale, gotInformerCache 84 | } 85 | -------------------------------------------------------------------------------- /internal/metrics/retry/kubeapi.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 retry 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "sigs.k8s.io/controller-runtime/pkg/metrics" 23 | ) 24 | 25 | const ( 26 | SubsystemKubeApi = "retrykubeapi" 27 | ) 28 | 29 | const ( 30 | retryName = "retry" 31 | ) 32 | 33 | var ( 34 | retry = prometheus.NewCounterVec(prometheus.CounterOpts{ 35 | Namespace: metricscommon.Namespace, 36 | Subsystem: SubsystemKubeApi, 37 | Name: retryName, 38 | Help: "Number of Kube API retries (by reason)", 39 | }, []string{metricscommon.ReasonLabelName}) 40 | ) 41 | 42 | // allMetrics must include all metrics defined above. 43 | var allMetrics = []prometheus.Collector{ 44 | retry, 45 | } 46 | 47 | func RegisterKubeApiMetrics(registry metrics.RegistererGatherer) { 48 | registry.MustRegister(allMetrics...) 49 | } 50 | 51 | func ResetKubeApiMetrics() { 52 | metricscommon.ResetMetrics(allMetrics) 53 | } 54 | 55 | func Retry(reason string) prometheus.Counter { 56 | return retry.WithLabelValues(reason) 57 | } 58 | -------------------------------------------------------------------------------- /internal/metrics/retry/kubeapi_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 retry 18 | 19 | import ( 20 | "fmt" 21 | "sync" 22 | "testing" 23 | 24 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 25 | "github.com/prometheus/client_golang/prometheus" 26 | "github.com/stretchr/testify/assert" 27 | "k8s.io/component-base/metrics/testutil" 28 | ) 29 | 30 | func TestRegisterKubeApiMetrics(t *testing.T) { 31 | registry := prometheus.NewRegistry() 32 | RegisterKubeApiMetrics(registry) 33 | assert.Equal(t, len(allMetrics), len(descs(registry))) 34 | } 35 | 36 | func TestResetKubeApiMetrics(t *testing.T) { 37 | Retry("").Inc() 38 | value, _ := testutil.GetCounterMetricValue(Retry("")) 39 | assert.Equal(t, float64(1), value) 40 | ResetKubeApiMetrics() 41 | 42 | value, _ = testutil.GetCounterMetricValue(Retry("")) 43 | assert.Equal(t, float64(0), value) 44 | } 45 | 46 | func TestRetry(t *testing.T) { 47 | m := Retry("") 48 | assert.Contains( 49 | t, 50 | m.Desc().String(), 51 | fmt.Sprintf("%s_%s_%s", metricscommon.Namespace, SubsystemKubeApi, retryName), 52 | ) 53 | } 54 | 55 | func descs(registry *prometheus.Registry) []string { 56 | ch := make(chan *prometheus.Desc) 57 | done := make(chan struct{}) 58 | var ret []string 59 | 60 | var wg sync.WaitGroup 61 | wg.Add(1) 62 | go func() { 63 | defer wg.Done() 64 | 65 | for { 66 | select { 67 | case desc := <-ch: 68 | ret = append(ret, desc.String()) 69 | case <-done: 70 | return 71 | } 72 | } 73 | }() 74 | 75 | registry.Describe(ch) 76 | done <- struct{}{} 77 | wg.Wait() 78 | return ret 79 | } 80 | -------------------------------------------------------------------------------- /internal/metrics/scale/scale.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 scale 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "sigs.k8s.io/controller-runtime/pkg/metrics" 23 | ) 24 | 25 | const ( 26 | Subsystem = "scale" 27 | ) 28 | 29 | const ( 30 | failureName = "failure" 31 | commandedUnknownResName = "commanded_unknown_resources" 32 | durationName = "duration_seconds" 33 | ) 34 | 35 | var ( 36 | failure = prometheus.NewCounterVec(prometheus.CounterOpts{ 37 | Namespace: metricscommon.Namespace, 38 | Subsystem: Subsystem, 39 | Name: failureName, 40 | Help: "Number of scale failures (by scale direction, reason)", 41 | }, []string{metricscommon.DirectionLabelName, metricscommon.ReasonLabelName}) 42 | 43 | commandedUnknownRes = prometheus.NewCounterVec(prometheus.CounterOpts{ 44 | Namespace: metricscommon.Namespace, 45 | Subsystem: Subsystem, 46 | Name: commandedUnknownResName, 47 | Help: "Number of scales commanded upon encountering unknown resources", 48 | }, []string{}) 49 | 50 | duration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 51 | Namespace: metricscommon.Namespace, 52 | Subsystem: Subsystem, 53 | Name: durationName, 54 | Help: "Scale duration (from commanded to enacted) in seconds (by scale direction, outcome)", 55 | Buckets: []float64{1, 2, 4, 8, 16, 32, 64, 128}, 56 | }, []string{metricscommon.DirectionLabelName, metricscommon.OutcomeLabelName}) 57 | ) 58 | 59 | // allMetrics must include all metrics defined above. 60 | var allMetrics = []prometheus.Collector{ 61 | failure, commandedUnknownRes, duration, 62 | } 63 | 64 | func RegisterMetrics(registry metrics.RegistererGatherer) { 65 | registry.MustRegister(allMetrics...) 66 | } 67 | 68 | func ResetMetrics() { 69 | metricscommon.ResetMetrics(allMetrics) 70 | } 71 | 72 | func Failure(direction metricscommon.Direction, reason string) prometheus.Counter { 73 | return failure.WithLabelValues(string(direction), reason) 74 | } 75 | 76 | func CommandedUnknownRes() prometheus.Counter { 77 | return commandedUnknownRes.WithLabelValues() 78 | } 79 | 80 | func Duration(direction metricscommon.Direction, outcome metricscommon.Outcome) prometheus.Observer { 81 | return duration.WithLabelValues(string(direction), string(outcome)) 82 | } 83 | -------------------------------------------------------------------------------- /internal/metrics/scale/scale_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 scale 18 | 19 | import ( 20 | "fmt" 21 | "sync" 22 | "testing" 23 | 24 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 25 | "github.com/prometheus/client_golang/prometheus" 26 | "github.com/stretchr/testify/assert" 27 | "k8s.io/component-base/metrics/testutil" 28 | ) 29 | 30 | func TestRegisterMetrics(t *testing.T) { 31 | registry := prometheus.NewRegistry() 32 | RegisterMetrics(registry) 33 | assert.Equal(t, len(allMetrics), len(descs(registry))) 34 | } 35 | 36 | func TestResetMetrics(t *testing.T) { 37 | Failure("", "").Inc() 38 | value, _ := testutil.GetCounterMetricValue(Failure("", "")) 39 | assert.Equal(t, float64(1), value) 40 | ResetMetrics() 41 | 42 | value, _ = testutil.GetCounterMetricValue(Failure("", "")) 43 | assert.Equal(t, float64(0), value) 44 | } 45 | 46 | func TestFailure(t *testing.T) { 47 | m := Failure("", "") 48 | assert.Contains( 49 | t, 50 | m.Desc().String(), 51 | fmt.Sprintf("%s_%s_%s", metricscommon.Namespace, Subsystem, failureName), 52 | ) 53 | } 54 | 55 | func TestCommandedUnknownRes(t *testing.T) { 56 | m := CommandedUnknownRes() 57 | assert.Contains( 58 | t, 59 | m.Desc().String(), 60 | fmt.Sprintf("%s_%s_%s", metricscommon.Namespace, Subsystem, commandedUnknownResName), 61 | ) 62 | } 63 | 64 | func TestDuration(t *testing.T) { 65 | m := Duration("", "").(prometheus.Metric) 66 | assert.Contains( 67 | t, 68 | m.Desc().String(), 69 | fmt.Sprintf("%s_%s_%s", metricscommon.Namespace, Subsystem, durationName), 70 | ) 71 | } 72 | 73 | func descs(registry *prometheus.Registry) []string { 74 | ch := make(chan *prometheus.Desc) 75 | done := make(chan struct{}) 76 | var ret []string 77 | 78 | var wg sync.WaitGroup 79 | wg.Add(1) 80 | go func() { 81 | defer wg.Done() 82 | 83 | for { 84 | select { 85 | case desc := <-ch: 86 | ret = append(ret, desc.String()) 87 | case <-done: 88 | return 89 | } 90 | } 91 | }() 92 | 93 | registry.Describe(ch) 94 | done <- struct{}{} 95 | wg.Wait() 96 | return ret 97 | } 98 | -------------------------------------------------------------------------------- /internal/pod/configuration.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 pod 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/common" 21 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/kube/kubecommon" 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale" 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 24 | v1 "k8s.io/api/core/v1" 25 | ) 26 | 27 | // configuration is the default implementation of podcommon.Configuration. 28 | type configuration struct { 29 | podHelper kubecommon.PodHelper 30 | containerHelper kubecommon.ContainerHelper 31 | } 32 | 33 | func newConfiguration( 34 | podHelper kubecommon.PodHelper, 35 | containerHelper kubecommon.ContainerHelper, 36 | ) *configuration { 37 | return &configuration{ 38 | podHelper: podHelper, 39 | containerHelper: containerHelper, 40 | } 41 | } 42 | 43 | // Configure performs configuration tasks using the supplied pod. 44 | func (c *configuration) Configure(pod *v1.Pod) (scalecommon.Configurations, error) { 45 | configs := scale.NewConfigurations(c.podHelper, c.containerHelper) 46 | 47 | if err := configs.StoreFromAnnotationsAll(pod); err != nil { 48 | return nil, common.WrapErrorf(err, "unable to store configuration from annotations") 49 | } 50 | 51 | return configs, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/pod/configuration_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 pod 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/kube" 24 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/kube/kubetest" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/mock" 27 | v1 "k8s.io/api/core/v1" 28 | ) 29 | 30 | func TestNewConfiguration(t *testing.T) { 31 | podHelper := kube.NewPodHelper(nil) 32 | containerHelper := kube.NewContainerHelper() 33 | config := newConfiguration(podHelper, containerHelper) 34 | expected := &configuration{ 35 | podHelper: podHelper, 36 | containerHelper: containerHelper, 37 | } 38 | assert.Equal(t, expected, config) 39 | } 40 | 41 | func TestConfigurationConfigure(t *testing.T) { 42 | t.Run("UnableToStoreConfigurationFromAnnotations", func(t *testing.T) { 43 | mockPodHelper := kubetest.NewMockPodHelper(func(m *kubetest.MockPodHelper) { 44 | m.On("ExpectedAnnotationValueAs", mock.Anything, mock.Anything, mock.Anything). 45 | Return("", errors.New("")) 46 | m.HasAnnotationDefault() 47 | }) 48 | 49 | configuration := newConfiguration(mockPodHelper, nil) 50 | configs, err := configuration.Configure(&v1.Pod{}) 51 | assert.ErrorContains(t, err, "unable to store configuration from annotations") 52 | assert.Nil(t, configs) 53 | }) 54 | 55 | t.Run("Ok", func(t *testing.T) { 56 | mockPodHelper := kubetest.NewMockPodHelper(nil) 57 | mockContainerHelper := kubetest.NewMockContainerHelper(nil) 58 | 59 | configuration := newConfiguration(mockPodHelper, mockContainerHelper) 60 | configs, err := configuration.Configure(&v1.Pod{}) 61 | assert.NoError(t, err) 62 | assert.NotNil(t, configs) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /internal/pod/error.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 pod 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | // validationError is an error that indicates validation failed. It wraps another error. 24 | type validationError struct { 25 | message string 26 | wrapped error 27 | } 28 | 29 | func newValidationError(message string, toWrap error) error { 30 | if toWrap == nil { 31 | return validationError{message: message} 32 | } 33 | 34 | return validationError{ 35 | message: message, 36 | wrapped: toWrap, 37 | } 38 | } 39 | 40 | func (e validationError) Error() string { 41 | if e.wrapped == nil { 42 | return "validation error: " + e.message 43 | } 44 | 45 | return fmt.Errorf("validation error: %s: %w", e.message, e.wrapped).Error() 46 | } 47 | 48 | func (e validationError) Unwrap() error { 49 | return e.wrapped 50 | } 51 | -------------------------------------------------------------------------------- /internal/pod/error_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 pod 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/common" 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestNewValidationError(t *testing.T) { 28 | err := newValidationError("test", errors.New("")) 29 | expected := validationError{ 30 | message: "test", 31 | wrapped: errors.New(""), 32 | } 33 | assert.Equal(t, expected, err) 34 | } 35 | 36 | func TestValidationErrorError(t *testing.T) { 37 | t.Run("Wrapped", func(t *testing.T) { 38 | err1 := errors.New("err1") 39 | err2 := common.WrapErrorf(err1, "err2") 40 | e := newValidationError("err3", err2) 41 | assert.Equal(t, "validation error: err3: err2: err1", e.Error()) 42 | }) 43 | 44 | t.Run("NotWrapped", func(t *testing.T) { 45 | e := newValidationError("test", nil) 46 | assert.Equal(t, "validation error: test", e.Error()) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /internal/pod/pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 pod 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/controller/controllercommon" 21 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/event" 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/kube" 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/kube/kubecommon" 24 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/pod/podcommon" 25 | "k8s.io/client-go/tools/record" 26 | "sigs.k8s.io/controller-runtime/pkg/client" 27 | ) 28 | 29 | // Pod is a facade (and the only package-external entry point) for pod interaction and contains a number of services. 30 | // It only exposes exported services methods via their corresponding interfaces. 31 | type Pod struct { 32 | Configuration podcommon.Configuration 33 | Validation podcommon.Validation 34 | TargetContainerState podcommon.TargetContainerState 35 | TargetContainerAction podcommon.TargetContainerAction 36 | Status podcommon.Status 37 | PodHelper kubecommon.PodHelper 38 | ContainerHelper kubecommon.ContainerHelper 39 | } 40 | 41 | func NewPod( 42 | controllerConfig controllercommon.ControllerConfig, 43 | client client.Client, 44 | recorder record.EventRecorder, 45 | ) *Pod { 46 | podHelper := kube.NewPodHelper(client) 47 | containerHelper := kube.NewContainerHelper() 48 | stat := newStatus(recorder, podHelper) 49 | 50 | return &Pod{ 51 | Configuration: newConfiguration(podHelper, containerHelper), 52 | Validation: newValidation(stat, podHelper, containerHelper, event.DefaultPodEventPublisher), 53 | TargetContainerState: newTargetContainerState(podHelper, containerHelper), 54 | TargetContainerAction: newTargetContainerAction(controllerConfig, stat, podHelper, event.DefaultPodEventPublisher), 55 | Status: stat, 56 | PodHelper: podHelper, 57 | ContainerHelper: containerHelper, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/pod/pod_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 pod 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/controller/controllercommon" 23 | "github.com/stretchr/testify/assert" 24 | "k8s.io/client-go/tools/record" 25 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 26 | ) 27 | 28 | func TestNewPod(t *testing.T) { 29 | pod := NewPod(controllercommon.ControllerConfig{}, fake.NewClientBuilder().Build(), &record.FakeRecorder{}) 30 | assert.NotNil(t, pod.Configuration) 31 | assert.NotNil(t, pod.Validation) 32 | assert.NotNil(t, pod.TargetContainerState) 33 | assert.NotNil(t, pod.TargetContainerAction) 34 | assert.NotNil(t, pod.Status) 35 | assert.NotNil(t, pod.PodHelper) 36 | assert.NotNil(t, pod.ContainerHelper) 37 | } 38 | -------------------------------------------------------------------------------- /internal/pod/podcommon/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podcommon 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/event/eventcommon" 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 24 | v1 "k8s.io/api/core/v1" 25 | ) 26 | 27 | // Configuration performs operations relating to configuration. 28 | type Configuration interface { 29 | Configure( 30 | pod *v1.Pod, 31 | ) (scalecommon.Configurations, error) 32 | } 33 | 34 | // Validation performs operations relating to validation. 35 | type Validation interface { 36 | Validate( 37 | ctx context.Context, 38 | pod *v1.Pod, 39 | targetContainerName string, 40 | scaleConfigs scalecommon.Configurations, 41 | ) (*v1.Container, error) 42 | } 43 | 44 | // TargetContainerState performs operations relating to determining target container state. 45 | type TargetContainerState interface { 46 | States( 47 | ctx context.Context, 48 | pod *v1.Pod, 49 | targetContainer *v1.Container, 50 | scaleConfigs scalecommon.Configurations, 51 | ) (States, error) 52 | } 53 | 54 | // TargetContainerAction performs actions based on target container state. 55 | type TargetContainerAction interface { 56 | Execute( 57 | ctx context.Context, 58 | states States, 59 | pod *v1.Pod, 60 | targetContainer *v1.Container, 61 | scaleConfigs scalecommon.Configurations, 62 | ) error 63 | } 64 | 65 | // Status performs operations relating to controller status. 66 | type Status interface { 67 | Update( 68 | ctx context.Context, 69 | podEventPublisher eventcommon.PodEventPublisher, 70 | pod *v1.Pod, 71 | status string, 72 | states States, 73 | statusScaleState StatusScaleState, 74 | scaleConfigs scalecommon.Configurations, 75 | failReason string, 76 | ) (*v1.Pod, error) 77 | } 78 | -------------------------------------------------------------------------------- /internal/pod/podcommon/stateconst_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podcommon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestStateBoolBool(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | s StateBool 30 | want bool 31 | }{ 32 | { 33 | string(StateBoolTrue), 34 | StateBoolTrue, 35 | true, 36 | }, 37 | { 38 | string(StateBoolFalse), 39 | StateBoolFalse, 40 | false, 41 | }, 42 | { 43 | string(StateBoolUnknown), 44 | StateBoolFalse, 45 | false, 46 | }, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | assert.Equal(t, tt.want, tt.s.Bool()) 51 | }) 52 | } 53 | } 54 | 55 | func TestStateResourcesDirection(t *testing.T) { 56 | tests := []struct { 57 | name string 58 | s StateResources 59 | wantPanicErrMsg string 60 | want metricscommon.Direction 61 | }{ 62 | { 63 | string(StateResourcesStartup), 64 | StateResourcesStartup, 65 | "", 66 | metricscommon.DirectionUp, 67 | }, 68 | { 69 | string(StateResourcesPostStartup), 70 | StateResourcesPostStartup, 71 | "", 72 | metricscommon.DirectionDown, 73 | }, 74 | { 75 | "NotSupported", 76 | StateResources("test"), 77 | "'test' not supported", 78 | metricscommon.Direction(""), 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | if tt.wantPanicErrMsg != "" { 84 | assert.PanicsWithError(t, tt.wantPanicErrMsg, func() { tt.s.Direction() }) 85 | return 86 | } 87 | 88 | assert.Equal(t, tt.want, tt.s.Direction()) 89 | }) 90 | } 91 | } 92 | 93 | func TestStateResourcesHumanReadable(t *testing.T) { 94 | tests := []struct { 95 | name string 96 | s StateResources 97 | want string 98 | }{ 99 | { 100 | string(StateResourcesPostStartup), 101 | StateResourcesPostStartup, 102 | "post-startup", 103 | }, 104 | { 105 | string(StateResourcesStartup), 106 | StateResourcesStartup, 107 | string(StateResourcesStartup), 108 | }, 109 | } 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | assert.Equal(t, tt.want, tt.s.HumanReadable()) 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /internal/pod/podcommon/states.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podcommon 18 | 19 | // States holds information related to the current state of the target container. 20 | type States struct { 21 | StartupProbe StateBool `json:"startupProbe"` 22 | ReadinessProbe StateBool `json:"readinessProbe"` 23 | Container StateContainer `json:"container"` 24 | Started StateBool `json:"started"` 25 | Ready StateBool `json:"ready"` 26 | Resources StateResources `json:"resources"` 27 | StatusResources StateStatusResources `json:"statusResources"` 28 | Resize ResizeState `json:"resize"` 29 | } 30 | 31 | func NewStates( 32 | startupProbe StateBool, 33 | readinessProbe StateBool, 34 | stateContainer StateContainer, 35 | started StateBool, 36 | ready StateBool, 37 | stateResources StateResources, 38 | stateStatusResources StateStatusResources, 39 | resize ResizeState, 40 | ) States { 41 | return States{ 42 | StartupProbe: startupProbe, 43 | ReadinessProbe: readinessProbe, 44 | Container: stateContainer, 45 | Started: started, 46 | Ready: ready, 47 | Resources: stateResources, 48 | StatusResources: stateStatusResources, 49 | Resize: resize, 50 | } 51 | } 52 | 53 | func NewStatesAllUnknown() States { 54 | return States{ 55 | StartupProbe: StateBoolUnknown, 56 | ReadinessProbe: StateBoolUnknown, 57 | Container: StateContainerUnknown, 58 | Started: StateBoolUnknown, 59 | Ready: StateBoolUnknown, 60 | Resources: StateResourcesUnknown, 61 | StatusResources: StateStatusResourcesUnknown, 62 | Resize: NewResizeState(StateResizeUnknown, ""), 63 | } 64 | } 65 | 66 | // ResizeState holds information related to the current state of a pod resize. 67 | type ResizeState struct { 68 | State StateResize `json:"state"` 69 | Message string `json:"message"` 70 | } 71 | 72 | func NewResizeState( 73 | state StateResize, 74 | message string, 75 | ) ResizeState { 76 | return ResizeState{ 77 | State: state, 78 | Message: message, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/pod/podcommon/states_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podcommon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestNewStates(t *testing.T) { 26 | s := NewStates( 27 | StateBoolUnknown, 28 | StateBoolUnknown, 29 | StateContainerUnknown, 30 | StateBoolUnknown, 31 | StateBoolUnknown, 32 | StateResourcesUnknown, 33 | StateStatusResourcesUnknown, 34 | NewResizeState(StateResizeNotStartedOrCompleted, ""), 35 | ) 36 | expected := States{ 37 | StartupProbe: StateBoolUnknown, 38 | ReadinessProbe: StateBoolUnknown, 39 | Container: StateContainerUnknown, 40 | Started: StateBoolUnknown, 41 | Ready: StateBoolUnknown, 42 | Resources: StateResourcesUnknown, 43 | StatusResources: StateStatusResourcesUnknown, 44 | Resize: NewResizeState(StateResizeNotStartedOrCompleted, ""), 45 | } 46 | assert.Equal(t, expected, s) 47 | } 48 | 49 | func TestNewStatesAllUnknown(t *testing.T) { 50 | s := NewStatesAllUnknown() 51 | expected := States{ 52 | StartupProbe: StateBoolUnknown, 53 | ReadinessProbe: StateBoolUnknown, 54 | Container: StateContainerUnknown, 55 | Started: StateBoolUnknown, 56 | Ready: StateBoolUnknown, 57 | Resources: StateResourcesUnknown, 58 | StatusResources: StateStatusResourcesUnknown, 59 | Resize: NewResizeState(StateResizeUnknown, ""), 60 | } 61 | assert.Equal(t, expected, s) 62 | } 63 | 64 | func TestNewResizeState(t *testing.T) { 65 | rs := NewResizeState( 66 | StateResizeUnknown, 67 | "test", 68 | ) 69 | expected := ResizeState{ 70 | State: StateResizeUnknown, 71 | Message: "test", 72 | } 73 | assert.Equal(t, expected, rs) 74 | } 75 | -------------------------------------------------------------------------------- /internal/pod/podcommon/statusconst.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podcommon 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 23 | ) 24 | 25 | // StatusScaleState indicates the scale state for status purposes. 26 | type StatusScaleState string 27 | 28 | const ( 29 | // StatusScaleStateNotApplicable indicates the scale state is not applicable. 30 | StatusScaleStateNotApplicable StatusScaleState = "notapplicable" 31 | 32 | // StatusScaleStateUpCommanded indicates scaling up commanded. 33 | StatusScaleStateUpCommanded StatusScaleState = "upcommanded" 34 | 35 | // StatusScaleStateUpEnacted indicates scaling up enacted. 36 | StatusScaleStateUpEnacted StatusScaleState = "upenacted" 37 | 38 | // StatusScaleStateUpFailed indicates scaling up failed. 39 | StatusScaleStateUpFailed StatusScaleState = "upfailed" 40 | 41 | // StatusScaleStateDownCommanded indicates scaling down commanded. 42 | StatusScaleStateDownCommanded StatusScaleState = "downcommanded" 43 | 44 | // StatusScaleStateDownEnacted indicates scaling down enacted. 45 | StatusScaleStateDownEnacted StatusScaleState = "downenacted" 46 | 47 | // StatusScaleStateDownFailed indicates scaling down failed. 48 | StatusScaleStateDownFailed StatusScaleState = "downfailed" 49 | 50 | // StatusScaleStateUnknownCommanded indicates scaling in an unknown direction commanded. 51 | StatusScaleStateUnknownCommanded StatusScaleState = "unknowncommanded" 52 | ) 53 | 54 | // Direction returns the scale direction. 55 | func (s StatusScaleState) Direction() metricscommon.Direction { 56 | switch s { 57 | case StatusScaleStateUpCommanded, StatusScaleStateUpEnacted, StatusScaleStateUpFailed: 58 | return metricscommon.DirectionUp 59 | case StatusScaleStateDownCommanded, StatusScaleStateDownEnacted, StatusScaleStateDownFailed: 60 | return metricscommon.DirectionDown 61 | } 62 | 63 | panic(fmt.Errorf("'%s' not supported", s)) 64 | } 65 | -------------------------------------------------------------------------------- /internal/pod/podcommon/statusconst_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podcommon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/metrics/metricscommon" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestStatusScaleStateDirection(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | s StatusScaleState 30 | wantPanicErrMsg string 31 | want metricscommon.Direction 32 | }{ 33 | { 34 | string(StatusScaleStateUpCommanded), 35 | StatusScaleStateUpCommanded, 36 | "", 37 | metricscommon.DirectionUp, 38 | }, 39 | { 40 | string(StatusScaleStateUpEnacted), 41 | StatusScaleStateUpEnacted, 42 | "", 43 | metricscommon.DirectionUp, 44 | }, 45 | { 46 | string(StatusScaleStateUpFailed), 47 | StatusScaleStateUpFailed, 48 | "", 49 | metricscommon.DirectionUp, 50 | }, 51 | { 52 | string(StatusScaleStateDownCommanded), 53 | StatusScaleStateDownCommanded, 54 | "", 55 | metricscommon.DirectionDown, 56 | }, 57 | { 58 | string(StatusScaleStateDownEnacted), 59 | StatusScaleStateDownEnacted, 60 | "", 61 | metricscommon.DirectionDown, 62 | }, 63 | { 64 | string(StatusScaleStateDownFailed), 65 | StatusScaleStateDownFailed, 66 | "", 67 | metricscommon.DirectionDown, 68 | }, 69 | { 70 | "NotSupported", 71 | StatusScaleStateNotApplicable, 72 | "'notapplicable' not supported", 73 | metricscommon.Direction(""), 74 | }, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | if tt.wantPanicErrMsg != "" { 79 | assert.PanicsWithError(t, tt.wantPanicErrMsg, func() { tt.s.Direction() }) 80 | return 81 | } 82 | 83 | assert.Equal(t, tt.want, tt.s.Direction()) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/pod/podtest/mockconfiguration.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podtest 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 21 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scaletest" 22 | "github.com/stretchr/testify/mock" 23 | "k8s.io/api/core/v1" 24 | ) 25 | 26 | type MockConfiguration struct { 27 | mock.Mock 28 | } 29 | 30 | func NewMockConfiguration(configFunc func(*MockConfiguration)) *MockConfiguration { 31 | m := &MockConfiguration{} 32 | if configFunc != nil { 33 | configFunc(m) 34 | } else { 35 | m.AllDefaults() 36 | } 37 | 38 | return m 39 | } 40 | 41 | func (m *MockConfiguration) Configure(pod *v1.Pod) (scalecommon.Configurations, error) { 42 | args := m.Called(pod) 43 | return args.Get(0).(scalecommon.Configurations), args.Error(1) 44 | } 45 | 46 | func (m *MockConfiguration) ConfigureDefault() { 47 | m.On("Configure", mock.Anything).Return(scaletest.NewMockConfigurations(nil), nil) 48 | } 49 | 50 | func (m *MockConfiguration) AllDefaults() { 51 | m.ConfigureDefault() 52 | } 53 | -------------------------------------------------------------------------------- /internal/pod/podtest/mockstatus.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podtest 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/event/eventcommon" 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/pod/podcommon" 24 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 25 | "github.com/stretchr/testify/mock" 26 | v1 "k8s.io/api/core/v1" 27 | ) 28 | 29 | type MockStatus struct { 30 | mock.Mock 31 | } 32 | 33 | func NewMockStatus(configFunc func(*MockStatus)) *MockStatus { 34 | m := &MockStatus{} 35 | if configFunc != nil { 36 | configFunc(m) 37 | } else { 38 | m.AllDefaults() 39 | } 40 | 41 | return m 42 | } 43 | 44 | func NewMockStatusWithRun(configFunc func(*MockStatus, func()), run func()) *MockStatus { 45 | m := &MockStatus{} 46 | if configFunc != nil { 47 | configFunc(m, run) 48 | } else { 49 | m.AllDefaults() 50 | } 51 | 52 | return m 53 | } 54 | 55 | func (m *MockStatus) Update( 56 | ctx context.Context, 57 | podEventPublisher eventcommon.PodEventPublisher, 58 | pod *v1.Pod, 59 | status string, 60 | states podcommon.States, 61 | statusScaleState podcommon.StatusScaleState, 62 | scaleConfigs scalecommon.Configurations, 63 | failReason string, 64 | ) (*v1.Pod, error) { 65 | args := m.Called(ctx, podEventPublisher, pod, status, states, statusScaleState, scaleConfigs, failReason) 66 | return args.Get(0).(*v1.Pod), args.Error(1) 67 | } 68 | 69 | func (m *MockStatus) UpdateDefault() { 70 | m.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). 71 | Return(&v1.Pod{}, nil) 72 | } 73 | 74 | func (m *MockStatus) UpdateDefaultAndRun(run func()) { 75 | m.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). 76 | Return(&v1.Pod{}, nil). 77 | Run(func(args mock.Arguments) { run() }) 78 | } 79 | 80 | func (m *MockStatus) AllDefaults() { 81 | m.UpdateDefault() 82 | } 83 | -------------------------------------------------------------------------------- /internal/pod/podtest/mocktargetcontaineraction.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podtest 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/pod/podcommon" 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 24 | "github.com/stretchr/testify/mock" 25 | "k8s.io/api/core/v1" 26 | ) 27 | 28 | type MockTargetContainerAction struct { 29 | mock.Mock 30 | } 31 | 32 | func NewMockTargetContainerAction(configFunc func(*MockTargetContainerAction)) *MockTargetContainerAction { 33 | m := &MockTargetContainerAction{} 34 | if configFunc != nil { 35 | configFunc(m) 36 | } else { 37 | m.AllDefaults() 38 | } 39 | 40 | return m 41 | } 42 | 43 | func (m *MockTargetContainerAction) Execute( 44 | ctx context.Context, 45 | states podcommon.States, 46 | pod *v1.Pod, 47 | targetContainer *v1.Container, 48 | scaleConfigs scalecommon.Configurations, 49 | ) error { 50 | args := m.Called(ctx, states, pod, targetContainer, scaleConfigs) 51 | return args.Error(0) 52 | } 53 | 54 | func (m *MockTargetContainerAction) ExecuteDefault() { 55 | m.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) 56 | } 57 | 58 | func (m *MockTargetContainerAction) AllDefaults() { 59 | m.ExecuteDefault() 60 | } 61 | -------------------------------------------------------------------------------- /internal/pod/podtest/mocktargetcontainerstate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podtest 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/pod/podcommon" 23 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 24 | "github.com/stretchr/testify/mock" 25 | "k8s.io/api/core/v1" 26 | ) 27 | 28 | type MockTargetContainerState struct { 29 | mock.Mock 30 | } 31 | 32 | func NewMockTargetContainerState(configFunc func(*MockTargetContainerState)) *MockTargetContainerState { 33 | m := &MockTargetContainerState{} 34 | if configFunc != nil { 35 | configFunc(m) 36 | } else { 37 | m.AllDefaults() 38 | } 39 | 40 | return m 41 | } 42 | 43 | func (m *MockTargetContainerState) States( 44 | ctx context.Context, 45 | pod *v1.Pod, 46 | targetContainer *v1.Container, 47 | scaleConfigs scalecommon.Configurations, 48 | ) (podcommon.States, error) { 49 | args := m.Called(ctx, pod, targetContainer, scaleConfigs) 50 | return args.Get(0).(podcommon.States), args.Error(1) 51 | } 52 | 53 | func (m *MockTargetContainerState) StatesDefault() { 54 | m.On("States", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( 55 | podcommon.NewStates( 56 | podcommon.StateBoolTrue, 57 | podcommon.StateBoolTrue, 58 | podcommon.StateContainerRunning, 59 | podcommon.StateBoolTrue, 60 | podcommon.StateBoolTrue, 61 | podcommon.StateResourcesStartup, 62 | podcommon.StateStatusResourcesContainerResourcesMatch, 63 | podcommon.NewResizeState(podcommon.StateResizeNotStartedOrCompleted, ""), 64 | ), 65 | nil, 66 | ) 67 | } 68 | 69 | func (m *MockTargetContainerState) AllDefaults() { 70 | m.StatesDefault() 71 | } 72 | -------------------------------------------------------------------------------- /internal/pod/podtest/mockvalidation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 podtest 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 23 | "github.com/stretchr/testify/mock" 24 | "k8s.io/api/core/v1" 25 | ) 26 | 27 | type MockValidation struct { 28 | mock.Mock 29 | } 30 | 31 | func NewMockValidation(configFunc func(*MockValidation)) *MockValidation { 32 | m := &MockValidation{} 33 | if configFunc != nil { 34 | configFunc(m) 35 | } else { 36 | m.AllDefaults() 37 | } 38 | 39 | return m 40 | } 41 | 42 | func (m *MockValidation) Validate( 43 | ctx context.Context, 44 | pod *v1.Pod, 45 | targetContainerName string, 46 | scaleConfigs scalecommon.Configurations, 47 | ) (*v1.Container, error) { 48 | args := m.Called(ctx, pod, targetContainerName, scaleConfigs) 49 | return args.Get(0).(*v1.Container), args.Error(1) 50 | } 51 | 52 | func (m *MockValidation) ValidateDefault() { 53 | m.On("Validate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&v1.Container{}, nil) 54 | } 55 | 56 | func (m *MockValidation) AllDefaults() { 57 | m.ValidateDefault() 58 | } 59 | -------------------------------------------------------------------------------- /internal/pod/vpaconst.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 pod 18 | 19 | var knownVpaAnnotations = []string{"vpaObservedContainers", "vpaUpdates"} 20 | -------------------------------------------------------------------------------- /internal/retry/retry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 retry 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | ccontext "github.com/ExpediaGroup/container-startup-autoscaler/internal/context" 24 | "github.com/avast/retry-go/v4" 25 | ) 26 | 27 | var baseConfig = []retry.Option{ 28 | retry.DelayType(retry.FixedDelay), 29 | retry.RetryIf(retry.IsRecoverable), 30 | retry.LastErrorOnly(true), 31 | } 32 | 33 | // StandardRetryConfig returns the configuration necessary to perform a standard retry. 34 | func StandardRetryConfig(ctx context.Context) []retry.Option { 35 | newConfig := append(baseConfig, retry.Attempts(uint(ccontext.StandardRetryAttempts(ctx)))) 36 | newConfig = append(newConfig, retry.Delay(time.Duration(ccontext.StandardRetryDelaySecs(ctx))*time.Second)) 37 | return append(newConfig, retry.Context(ctx)) 38 | } 39 | 40 | // DoStandardRetry performs a standard retry for the supplied function. 41 | func DoStandardRetry(ctx context.Context, function retry.RetryableFunc) error { 42 | return retry.Do(function, StandardRetryConfig(ctx)...) 43 | } 44 | 45 | // DoStandardRetryWithMoreOpts performs a standard retry for the supplied function, with additional options. 46 | func DoStandardRetryWithMoreOpts(ctx context.Context, function retry.RetryableFunc, moreOpts []retry.Option) error { 47 | opts := append(StandardRetryConfig(ctx), moreOpts...) 48 | return retry.Do(function, opts...) 49 | } 50 | -------------------------------------------------------------------------------- /internal/retry/retry_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 retry 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "testing" 23 | "time" 24 | 25 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/context/contexttest" 26 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/logging" 27 | "github.com/avast/retry-go/v4" 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestStandardRetryConfig(t *testing.T) { 32 | got := StandardRetryConfig(contexttest.NewCtxBuilder(contexttest.NewNoRetryCtxConfig(nil)).Build()) 33 | assert.Len(t, got, 6) 34 | } 35 | 36 | func TestDoStandardRetry(t *testing.T) { 37 | ctx := contexttest.NewCtxBuilder(contexttest.NewCtxConfig()). 38 | LogBuffer(nil). 39 | StandardRetryAttempts(3). 40 | StandardRetryDelaySecs(1). 41 | Build() 42 | 43 | start := time.Now() 44 | err := DoStandardRetry(ctx, func() error { return errors.New("") }) 45 | assert.NotNil(t, err) 46 | assert.GreaterOrEqual(t, time.Since(start).Milliseconds(), int64(2000)) 47 | } 48 | 49 | func TestDoStandardRetryWithMoreOpts(t *testing.T) { 50 | buffer := &bytes.Buffer{} 51 | ctx := contexttest.NewCtxBuilder(contexttest.NewCtxConfig()). 52 | LogBuffer(buffer). 53 | StandardRetryAttempts(3). 54 | StandardRetryDelaySecs(1). 55 | Build() 56 | opt := retry.OnRetry(func(n uint, err error) { 57 | logging.Errorf(ctx, err, "test") 58 | }) 59 | start := time.Now() 60 | err := DoStandardRetryWithMoreOpts(ctx, func() error { return errors.New("") }, []retry.Option{opt}) 61 | assert.NotNil(t, err) 62 | assert.GreaterOrEqual(t, time.Since(start).Milliseconds(), int64(2000)) 63 | assert.Contains(t, buffer.String(), "test") 64 | } 65 | -------------------------------------------------------------------------------- /internal/scale/scalecommon/metadataconst.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 scalecommon 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/kube/kubecommon" 21 | ) 22 | 23 | const ( 24 | AnnotationTargetContainerName = kubecommon.Namespace + "/target-container-name" 25 | 26 | AnnotationCpuStartup = kubecommon.Namespace + "/cpu-startup" 27 | AnnotationCpuPostStartupRequests = kubecommon.Namespace + "/cpu-post-startup-requests" 28 | AnnotationCpuPostStartupLimits = kubecommon.Namespace + "/cpu-post-startup-limits" 29 | 30 | AnnotationMemoryStartup = kubecommon.Namespace + "/memory-startup" 31 | AnnotationMemoryPostStartupRequests = kubecommon.Namespace + "/memory-post-startup-requests" 32 | AnnotationMemoryPostStartupLimits = kubecommon.Namespace + "/memory-post-startup-limits" 33 | ) 34 | -------------------------------------------------------------------------------- /internal/scale/scalecommon/resources.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 scalecommon 18 | 19 | import "k8s.io/apimachinery/pkg/api/resource" 20 | 21 | // RawResources represents raw startup and post-started resources for a container. 22 | type RawResources struct { 23 | Startup string 24 | PostStartupRequests string 25 | PostStartupLimits string 26 | } 27 | 28 | func NewRawResources( 29 | startup string, 30 | postStartupRequests string, 31 | postStartupLimits string, 32 | ) RawResources { 33 | return RawResources{ 34 | Startup: startup, 35 | PostStartupRequests: postStartupRequests, 36 | PostStartupLimits: postStartupLimits, 37 | } 38 | } 39 | 40 | // Resources represents typed startup and post-started resources for a container. 41 | type Resources struct { 42 | Startup resource.Quantity 43 | PostStartupRequests resource.Quantity 44 | PostStartupLimits resource.Quantity 45 | } 46 | 47 | func NewResources( 48 | startup resource.Quantity, 49 | postStartupRequests resource.Quantity, 50 | postStartupLimits resource.Quantity, 51 | ) Resources { 52 | return Resources{ 53 | Startup: startup, 54 | PostStartupRequests: postStartupRequests, 55 | PostStartupLimits: postStartupLimits, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /internal/scale/scalecommon/resources_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 scalecommon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | "k8s.io/apimachinery/pkg/api/resource" 24 | ) 25 | 26 | func TestNewRawResources(t *testing.T) { 27 | resources := NewRawResources("3m", "1m", "2m") 28 | expected := RawResources{ 29 | Startup: "3m", 30 | PostStartupRequests: "1m", 31 | PostStartupLimits: "2m", 32 | } 33 | assert.Equal(t, expected, resources) 34 | } 35 | 36 | func TestNewResources(t *testing.T) { 37 | resources := NewResources(resource.MustParse("3m"), resource.MustParse("1m"), resource.MustParse("2m")) 38 | expected := Resources{ 39 | Startup: resource.MustParse("3m"), 40 | PostStartupRequests: resource.MustParse("1m"), 41 | PostStartupLimits: resource.MustParse("2m"), 42 | } 43 | assert.Equal(t, expected, resources) 44 | } 45 | -------------------------------------------------------------------------------- /internal/scale/scaletest/mockconfiguration.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 scaletest 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 21 | "github.com/stretchr/testify/mock" 22 | v1 "k8s.io/api/core/v1" 23 | ) 24 | 25 | type MockConfiguration struct { 26 | mock.Mock 27 | } 28 | 29 | func NewMockConfiguration(configFunc func(*MockConfiguration)) *MockConfiguration { 30 | m := &MockConfiguration{} 31 | if configFunc != nil { 32 | configFunc(m) 33 | } else { 34 | m.AllDefaults() 35 | } 36 | 37 | return m 38 | } 39 | 40 | func (m *MockConfiguration) ResourceName() v1.ResourceName { 41 | args := m.Called() 42 | return args.Get(0).(v1.ResourceName) 43 | } 44 | 45 | func (m *MockConfiguration) IsEnabled() bool { 46 | args := m.Called() 47 | return args.Bool(0) 48 | } 49 | 50 | func (m *MockConfiguration) Resources() scalecommon.Resources { 51 | args := m.Called() 52 | return args.Get(0).(scalecommon.Resources) 53 | } 54 | 55 | func (m *MockConfiguration) StoreFromAnnotations(pod *v1.Pod) error { 56 | args := m.Called(pod) 57 | return args.Error(0) 58 | } 59 | 60 | func (m *MockConfiguration) Validate(container *v1.Container) error { 61 | args := m.Called(container) 62 | return args.Error(0) 63 | } 64 | 65 | func (m *MockConfiguration) String() string { 66 | args := m.Called() 67 | return args.String(0) 68 | } 69 | 70 | func (m *MockConfiguration) ResourceNameDefault() { 71 | m.On("ResourceName").Return(v1.ResourceCPU) 72 | } 73 | 74 | func (m *MockConfiguration) IsEnabledDefault() { 75 | m.On("IsEnabled").Return(true) 76 | } 77 | 78 | func (m *MockConfiguration) ResourcesDefault() { 79 | m.On("Resources").Return(ResourcesCpuEnabled) 80 | } 81 | 82 | func (m *MockConfiguration) StoreFromAnnotationsDefault() { 83 | m.On("StoreFromAnnotations", mock.Anything).Return(nil) 84 | } 85 | 86 | func (m *MockConfiguration) ValidateDefault() { 87 | m.On("Validate", mock.Anything).Return(nil) 88 | } 89 | 90 | func (m *MockConfiguration) StringDefault() { 91 | m.On("String").Return("") 92 | } 93 | 94 | func (m *MockConfiguration) AllDefaults() { 95 | m.ResourceNameDefault() 96 | m.IsEnabledDefault() 97 | m.ResourcesDefault() 98 | m.StoreFromAnnotationsDefault() 99 | m.ValidateDefault() 100 | m.StringDefault() 101 | } 102 | -------------------------------------------------------------------------------- /internal/scale/updates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 scale 18 | 19 | import ( 20 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/scale/scalecommon" 21 | "k8s.io/api/core/v1" 22 | ) 23 | 24 | // updates is the default implementation of scalecommon.Updates. 25 | type updates struct { 26 | cpuUpdate scalecommon.Update 27 | memoryUpdate scalecommon.Update 28 | } 29 | 30 | func NewUpdates(configs scalecommon.Configurations) scalecommon.Updates { 31 | return &updates{ 32 | cpuUpdate: NewUpdate(v1.ResourceCPU, configs.ConfigurationFor(v1.ResourceCPU)), 33 | memoryUpdate: NewUpdate(v1.ResourceMemory, configs.ConfigurationFor(v1.ResourceMemory)), 34 | } 35 | } 36 | 37 | // StartupPodMutationFuncAll invokes StartupPodMutationFunc on each update within this collection and returns them. 38 | func (u *updates) StartupPodMutationFuncAll(container *v1.Container) []func(*v1.Pod) (bool, func(*v1.Pod) bool, error) { 39 | var funcs []func(*v1.Pod) (bool, func(*v1.Pod) bool, error) 40 | 41 | for _, update := range u.AllUpdates() { 42 | funcs = append(funcs, update.StartupPodMutationFunc(container)) 43 | } 44 | 45 | return funcs 46 | } 47 | 48 | // PostStartupPodMutationFuncAll invokes PostStartupPodMutationFunc on each update within this collection and returns 49 | // them. 50 | func (u *updates) PostStartupPodMutationFuncAll(container *v1.Container) []func(*v1.Pod) (bool, func(*v1.Pod) bool, error) { 51 | var funcs []func(*v1.Pod) (bool, func(*v1.Pod) bool, error) 52 | 53 | for _, update := range u.AllUpdates() { 54 | funcs = append(funcs, update.PostStartupPodMutationFunc(container)) 55 | } 56 | 57 | return funcs 58 | } 59 | 60 | // UpdateFor returns the update for the supplied resource name. 61 | func (u *updates) UpdateFor(resourceName v1.ResourceName) scalecommon.Update { 62 | switch resourceName { 63 | case v1.ResourceCPU: 64 | return u.cpuUpdate 65 | case v1.ResourceMemory: 66 | return u.memoryUpdate 67 | default: 68 | return nil 69 | } 70 | } 71 | 72 | // AllUpdates returns all updates within this collection. 73 | func (u *updates) AllUpdates() []scalecommon.Update { 74 | return []scalecommon.Update{u.cpuUpdate, u.memoryUpdate} 75 | } 76 | -------------------------------------------------------------------------------- /scripts/sandbox/config/kind.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | kind: Cluster 16 | apiVersion: kind.x-k8s.io/v1alpha4 17 | featureGates: 18 | InPlacePodVerticalScaling: true 19 | -------------------------------------------------------------------------------- /scripts/sandbox/config/metricsserver/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.4/components.yaml 3 | 4 | patches: 5 | - target: 6 | version: v1 7 | kind: Deployment 8 | name: metrics-server 9 | namespace: kube-system 10 | patch: |- 11 | - op: add 12 | path: /spec/template/spec/containers/0/args/- 13 | value: --kubelet-insecure-tls 14 | -------------------------------------------------------------------------------- /scripts/sandbox/config/vars.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Expedia Group, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # kind ----------------------------------------------------------------------------------------------------------------- 18 | 19 | kind_cluster_name="csa-sandbox-cluster" 20 | 21 | # shellcheck disable=SC2034 22 | kind_kube_version="v1.33.0" 23 | # shellcheck disable=SC2034 24 | kind_node_docker_tag="kindest/node:$kind_kube_version" 25 | 26 | # shellcheck disable=SC2034 27 | kind_kubeconfig="$HOME/.kube/config-$kind_cluster_name" 28 | # shellcheck disable=SC2034 29 | kind_container_name="$kind_cluster_name-control-plane" 30 | 31 | # echo-server ---------------------------------------------------------------------------------------------------------- 32 | 33 | # shellcheck disable=SC2034 34 | echo_server_docker_image_tag="ealen/echo-server:0.7.0" 35 | # shellcheck disable=SC2034 36 | echo_server_kube_namespace="echo-server" 37 | 38 | # metrics-server ------------------------------------------------------------------------------------------------------- 39 | 40 | # shellcheck disable=SC2034 41 | metrics_server_docker_image_tag="registry.k8s.io/metrics-server/metrics-server:v0.6.4" 42 | 43 | # CSA ------------------------------------------------------------------------------------------------------------------ 44 | 45 | # shellcheck disable=SC2034 46 | csa_name="container-startup-autoscaler" 47 | csa_docker_image="csa" 48 | csa_docker_tag="test" 49 | # shellcheck disable=SC2034 50 | csa_docker_image_tag="$csa_docker_image:$csa_docker_tag" 51 | # shellcheck disable=SC2034 52 | csa_helm_name="csa-sandbox" 53 | # shellcheck disable=SC2034 54 | csa_helm_timeout="60s" 55 | # shellcheck disable=SC2034 56 | csa_lease_name="csa-expediagroup-com" 57 | -------------------------------------------------------------------------------- /scripts/sandbox/csa-get-metrics.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Expedia Group, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | source config/vars.sh 18 | 19 | local_port="51234" 20 | 21 | # shellcheck disable=SC2154 22 | lease_holder=$(kubectl get lease "$csa_lease_name" \ 23 | -n "$csa_helm_name" \ 24 | -o=jsonpath='{.spec.holderIdentity}' \ 25 | --kubeconfig "$kind_kubeconfig" 26 | ) 27 | lease_holder_pod="${lease_holder%%_*}" 28 | 29 | # shellcheck disable=SC2154 30 | kubectl port-forward \ 31 | "pod/$lease_holder_pod" \ 32 | "$local_port:8080" \ 33 | -n "$csa_helm_name" \ 34 | --kubeconfig "$kind_kubeconfig" \ 35 | > /dev/null 2>&1 & 36 | pid=$! 37 | trap 'kill $pid' EXIT 38 | 39 | while ! nc -vz localhost $local_port > /dev/null 2>&1; do 40 | sleep 0.05 41 | done 42 | 43 | curl "http://localhost:$local_port/metrics" 44 | -------------------------------------------------------------------------------- /scripts/sandbox/csa-tail-logs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Expedia Group, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | source config/vars.sh 18 | 19 | # shellcheck disable=SC2154 20 | lease_holder=$(kubectl get lease "$csa_lease_name" \ 21 | -n "$csa_helm_name" \ 22 | -o=jsonpath='{.spec.holderIdentity}' \ 23 | --kubeconfig "$kind_kubeconfig" 24 | ) 25 | lease_holder_pod="${lease_holder%%_*}" 26 | 27 | # shellcheck disable=SC2154 28 | kubectl logs "$lease_holder_pod" \ 29 | -f \ 30 | --tail=-1 \ 31 | -c "$csa_name" \ 32 | -n "$csa_helm_name" \ 33 | --kubeconfig "$kind_kubeconfig" 34 | -------------------------------------------------------------------------------- /scripts/sandbox/csa-uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Expedia Group, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | source config/vars.sh 18 | 19 | # shellcheck disable=SC2154 20 | kind delete cluster --name="$kind_cluster_name" 21 | -------------------------------------------------------------------------------- /scripts/sandbox/echo-cause-container-restart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Expedia Group, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | source config/vars.sh 18 | 19 | # shellcheck disable=SC2154 20 | container_id=$(kubectl get pod \ 21 | -n "$echo_server_kube_namespace" \ 22 | -o=jsonpath='{.items[0].status.containerStatuses[0].containerID}' \ 23 | --kubeconfig "$kind_kubeconfig" 24 | ) 25 | 26 | fixed_container_id=${container_id/containerd:\/\//} 27 | 28 | # shellcheck disable=SC2154 29 | docker exec -it "$kind_container_name" bash -c "ctr -n k8s.io task kill -s SIGTERM $fixed_container_id" 30 | -------------------------------------------------------------------------------- /scripts/sandbox/echo-delete.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Expedia Group, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | source config/vars.sh 18 | 19 | # shellcheck disable=SC2154 20 | kubectl delete ns "$echo_server_kube_namespace" --kubeconfig "$kind_kubeconfig" 21 | -------------------------------------------------------------------------------- /scripts/sandbox/echo-reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Expedia Group, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | source config/vars.sh 18 | source echo-delete.sh 19 | 20 | # shellcheck disable=SC2154 21 | kubectl apply -f "$1" --kubeconfig "$kind_kubeconfig" 22 | # shellcheck disable=SC2154 23 | kubectl get pod -n "$echo_server_kube_namespace" --kubeconfig "$kind_kubeconfig" 24 | -------------------------------------------------------------------------------- /scripts/sandbox/echo-watch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Expedia Group, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | source config/vars.sh 18 | 19 | # shellcheck disable=SC2154 20 | watch -n0.5 -d "echo \"CSA Status\n----------\n\";" \ 21 | "kubectl get pod -n $echo_server_kube_namespace" \ 22 | "-o=jsonpath='{.items[0].metadata.annotations.csa\.expediagroup\.com\/status}'" \ 23 | "--kubeconfig $kind_kubeconfig | jq;" \ 24 | "echo \"\nContainer Enacted Resources\n---------------------------\n\";" \ 25 | "kubectl get pod -n $echo_server_kube_namespace" \ 26 | "-o=jsonpath='{.items[0].status.containerStatuses[0].resources}'" \ 27 | "--kubeconfig $kind_kubeconfig | jq" 28 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/failure-infeasible/cpu.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "10000" 38 | csa.expediagroup.com/cpu-post-startup-requests: "150m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "150m" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 150M 59 | requests: 60 | cpu: 150m 61 | memory: 150M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/failure-validation/cpu-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "100m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "150m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "150m" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 150M 59 | requests: 60 | cpu: 150m 61 | memory: 150M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/cpu-and-memory/both-probes.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | csa.expediagroup.com/memory-startup: "150M" 41 | csa.expediagroup.com/memory-post-startup-requests: "100M" 42 | csa.expediagroup.com/memory-post-startup-limits: "100M" 43 | spec: 44 | containers: 45 | - image: ealen/echo-server:0.7.0 46 | imagePullPolicy: IfNotPresent 47 | name: echo-server 48 | resizePolicy: 49 | - resourceName: cpu 50 | restartPolicy: NotRequired 51 | - resourceName: memory 52 | restartPolicy: NotRequired 53 | ports: 54 | - containerPort: 80 55 | env: 56 | - name: PORT 57 | value: "80" 58 | resources: 59 | limits: 60 | cpu: 100m 61 | memory: 100M 62 | requests: 63 | cpu: 100m 64 | memory: 100M 65 | startupProbe: 66 | httpGet: 67 | path: /?echo_code=200 68 | port: 80 69 | initialDelaySeconds: 15 70 | failureThreshold: 1 71 | periodSeconds: 5 72 | readinessProbe: 73 | httpGet: 74 | path: /?echo_code=200 75 | port: 80 76 | initialDelaySeconds: 30 77 | failureThreshold: 1 78 | periodSeconds: 5 79 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/cpu-and-memory/readiness-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | csa.expediagroup.com/memory-startup: "150M" 41 | csa.expediagroup.com/memory-post-startup-requests: "100M" 42 | csa.expediagroup.com/memory-post-startup-limits: "100M" 43 | spec: 44 | containers: 45 | - image: ealen/echo-server:0.7.0 46 | imagePullPolicy: IfNotPresent 47 | name: echo-server 48 | resizePolicy: 49 | - resourceName: cpu 50 | restartPolicy: NotRequired 51 | - resourceName: memory 52 | restartPolicy: NotRequired 53 | ports: 54 | - containerPort: 80 55 | env: 56 | - name: PORT 57 | value: "80" 58 | resources: 59 | limits: 60 | cpu: 100m 61 | memory: 100M 62 | requests: 63 | cpu: 100m 64 | memory: 100M 65 | readinessProbe: 66 | httpGet: 67 | path: /?echo_code=200 68 | port: 80 69 | initialDelaySeconds: 15 70 | failureThreshold: 1 71 | periodSeconds: 5 72 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/cpu-and-memory/startup-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | csa.expediagroup.com/memory-startup: "150M" 41 | csa.expediagroup.com/memory-post-startup-requests: "100M" 42 | csa.expediagroup.com/memory-post-startup-limits: "100M" 43 | spec: 44 | containers: 45 | - image: ealen/echo-server:0.7.0 46 | imagePullPolicy: IfNotPresent 47 | name: echo-server 48 | resizePolicy: 49 | - resourceName: cpu 50 | restartPolicy: NotRequired 51 | - resourceName: memory 52 | restartPolicy: NotRequired 53 | ports: 54 | - containerPort: 80 55 | env: 56 | - name: PORT 57 | value: "80" 58 | resources: 59 | limits: 60 | cpu: 100m 61 | memory: 100M 62 | requests: 63 | cpu: 100m 64 | memory: 100M 65 | startupProbe: 66 | httpGet: 67 | path: /?echo_code=200 68 | port: 80 69 | initialDelaySeconds: 15 70 | failureThreshold: 1 71 | periodSeconds: 5 72 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/cpu-only/both-probes.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 100m 58 | memory: 150M 59 | requests: 60 | cpu: 100m 61 | memory: 150M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | readinessProbe: 70 | httpGet: 71 | path: /?echo_code=200 72 | port: 80 73 | initialDelaySeconds: 30 74 | failureThreshold: 1 75 | periodSeconds: 5 76 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/cpu-only/readiness-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 100m 58 | memory: 150M 59 | requests: 60 | cpu: 100m 61 | memory: 150M 62 | readinessProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/cpu-only/startup-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 100m 58 | memory: 150M 59 | requests: 60 | cpu: 100m 61 | memory: 150M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/memory-only/both-probes.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/memory-startup: "150M" 38 | csa.expediagroup.com/memory-post-startup-requests: "100M" 39 | csa.expediagroup.com/memory-post-startup-limits: "100M" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 100M 59 | requests: 60 | cpu: 150m 61 | memory: 100M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | readinessProbe: 70 | httpGet: 71 | path: /?echo_code=200 72 | port: 80 73 | initialDelaySeconds: 30 74 | failureThreshold: 1 75 | periodSeconds: 5 76 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/memory-only/readiness-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/memory-startup: "150M" 38 | csa.expediagroup.com/memory-post-startup-requests: "100M" 39 | csa.expediagroup.com/memory-post-startup-limits: "100M" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 100M 59 | requests: 60 | cpu: 150m 61 | memory: 100M 62 | readinessProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/post-startup-resources/memory-only/startup-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/memory-startup: "150M" 38 | csa.expediagroup.com/memory-post-startup-requests: "100M" 39 | csa.expediagroup.com/memory-post-startup-limits: "100M" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 100M 59 | requests: 60 | cpu: 150m 61 | memory: 100M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/cpu-and-memory/both-probes.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | csa.expediagroup.com/memory-startup: "150M" 41 | csa.expediagroup.com/memory-post-startup-requests: "100M" 42 | csa.expediagroup.com/memory-post-startup-limits: "100M" 43 | spec: 44 | containers: 45 | - image: ealen/echo-server:0.7.0 46 | imagePullPolicy: IfNotPresent 47 | name: echo-server 48 | resizePolicy: 49 | - resourceName: cpu 50 | restartPolicy: NotRequired 51 | - resourceName: memory 52 | restartPolicy: NotRequired 53 | ports: 54 | - containerPort: 80 55 | env: 56 | - name: PORT 57 | value: "80" 58 | resources: 59 | limits: 60 | cpu: 150m 61 | memory: 150M 62 | requests: 63 | cpu: 150m 64 | memory: 150M 65 | startupProbe: 66 | httpGet: 67 | path: /?echo_code=200 68 | port: 80 69 | initialDelaySeconds: 15 70 | failureThreshold: 1 71 | periodSeconds: 5 72 | readinessProbe: 73 | httpGet: 74 | path: /?echo_code=200 75 | port: 80 76 | initialDelaySeconds: 30 77 | failureThreshold: 1 78 | periodSeconds: 5 79 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/cpu-and-memory/readiness-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | csa.expediagroup.com/memory-startup: "150M" 41 | csa.expediagroup.com/memory-post-startup-requests: "100M" 42 | csa.expediagroup.com/memory-post-startup-limits: "100M" 43 | spec: 44 | containers: 45 | - image: ealen/echo-server:0.7.0 46 | imagePullPolicy: IfNotPresent 47 | name: echo-server 48 | resizePolicy: 49 | - resourceName: cpu 50 | restartPolicy: NotRequired 51 | - resourceName: memory 52 | restartPolicy: NotRequired 53 | ports: 54 | - containerPort: 80 55 | env: 56 | - name: PORT 57 | value: "80" 58 | resources: 59 | limits: 60 | cpu: 150m 61 | memory: 150M 62 | requests: 63 | cpu: 150m 64 | memory: 150M 65 | readinessProbe: 66 | httpGet: 67 | path: /?echo_code=200 68 | port: 80 69 | initialDelaySeconds: 15 70 | failureThreshold: 1 71 | periodSeconds: 5 72 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/cpu-and-memory/startup-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | csa.expediagroup.com/memory-startup: "150M" 41 | csa.expediagroup.com/memory-post-startup-requests: "100M" 42 | csa.expediagroup.com/memory-post-startup-limits: "100M" 43 | spec: 44 | containers: 45 | - image: ealen/echo-server:0.7.0 46 | imagePullPolicy: IfNotPresent 47 | name: echo-server 48 | resizePolicy: 49 | - resourceName: cpu 50 | restartPolicy: NotRequired 51 | - resourceName: memory 52 | restartPolicy: NotRequired 53 | ports: 54 | - containerPort: 80 55 | env: 56 | - name: PORT 57 | value: "80" 58 | resources: 59 | limits: 60 | cpu: 150m 61 | memory: 150M 62 | requests: 63 | cpu: 150m 64 | memory: 150M 65 | startupProbe: 66 | httpGet: 67 | path: /?echo_code=200 68 | port: 80 69 | initialDelaySeconds: 15 70 | failureThreshold: 1 71 | periodSeconds: 5 72 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/cpu-only/both-probes.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 150M 59 | requests: 60 | cpu: 150m 61 | memory: 150M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | readinessProbe: 70 | httpGet: 71 | path: /?echo_code=200 72 | port: 80 73 | initialDelaySeconds: 30 74 | failureThreshold: 1 75 | periodSeconds: 5 76 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/cpu-only/readiness-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 150M 59 | requests: 60 | cpu: 150m 61 | memory: 150M 62 | readinessProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/cpu-only/startup-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/cpu-startup: "150m" 38 | csa.expediagroup.com/cpu-post-startup-requests: "100m" 39 | csa.expediagroup.com/cpu-post-startup-limits: "100m" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 150M 59 | requests: 60 | cpu: 150m 61 | memory: 150M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/memory-only/both-probes.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/memory-startup: "150M" 38 | csa.expediagroup.com/memory-post-startup-requests: "100M" 39 | csa.expediagroup.com/memory-post-startup-limits: "100M" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 150M 59 | requests: 60 | cpu: 150m 61 | memory: 150M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | readinessProbe: 70 | httpGet: 71 | path: /?echo_code=200 72 | port: 80 73 | initialDelaySeconds: 30 74 | failureThreshold: 1 75 | periodSeconds: 5 76 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/memory-only/readiness-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/memory-startup: "150M" 38 | csa.expediagroup.com/memory-post-startup-requests: "100M" 39 | csa.expediagroup.com/memory-post-startup-limits: "100M" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 150M 59 | requests: 60 | cpu: 150m 61 | memory: 150M 62 | readinessProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/echo/startup-resources/memory-only/startup-probe.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | name: echo-server 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: echo-server 24 | namespace: echo-server 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: echo-server 30 | template: 31 | metadata: 32 | labels: 33 | app: echo-server 34 | csa.expediagroup.com/enabled: "true" 35 | annotations: 36 | csa.expediagroup.com/target-container-name: "echo-server" 37 | csa.expediagroup.com/memory-startup: "150M" 38 | csa.expediagroup.com/memory-post-startup-requests: "100M" 39 | csa.expediagroup.com/memory-post-startup-limits: "100M" 40 | spec: 41 | containers: 42 | - image: ealen/echo-server:0.7.0 43 | imagePullPolicy: IfNotPresent 44 | name: echo-server 45 | resizePolicy: 46 | - resourceName: cpu 47 | restartPolicy: NotRequired 48 | - resourceName: memory 49 | restartPolicy: NotRequired 50 | ports: 51 | - containerPort: 80 52 | env: 53 | - name: PORT 54 | value: "80" 55 | resources: 56 | limits: 57 | cpu: 150m 58 | memory: 150M 59 | requests: 60 | cpu: 150m 61 | memory: 150M 62 | startupProbe: 63 | httpGet: 64 | path: /?echo_code=200 65 | port: 80 66 | initialDelaySeconds: 15 67 | failureThreshold: 1 68 | periodSeconds: 5 69 | -------------------------------------------------------------------------------- /scripts/sandbox/extracacert/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG BASE_IMAGE=kindest/base:v20250214-acbabc1a 16 | 17 | FROM ${BASE_IMAGE} 18 | ARG EXTRA_CA_CERT_FILENAME 19 | COPY ./${EXTRA_CA_CERT_FILENAME} /usr/local/share/ca-certificates/extra-ca-cert.crt 20 | RUN update-ca-certificates 21 | -------------------------------------------------------------------------------- /test/integration/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 integration 18 | 19 | import ( 20 | "os" 21 | "os/exec" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/ExpediaGroup/container-startup-autoscaler/internal/common" 26 | ) 27 | 28 | func cmdRun(t *testing.T, cmd *exec.Cmd, info string, coreErrMsg string, fatalOnErr bool, suppressInfo ...bool) (string, error) { 29 | suppress := false 30 | if len(suppressInfo) > 0 && suppressInfo[0] { 31 | suppress = true 32 | } 33 | 34 | if info != "" && !suppress { 35 | logMessage(t, info) 36 | } 37 | 38 | combinedOutput, err := cmd.CombinedOutput() 39 | if err != nil { 40 | trimmedOutput := strings.Trim(string(combinedOutput), "\n") 41 | wrappedErr := common.WrapErrorf(err, "%s (output: %s)", coreErrMsg, trimmedOutput) 42 | 43 | if fatalOnErr { 44 | logMessage(t, wrappedErr) 45 | os.Exit(1) 46 | } 47 | return trimmedOutput, wrappedErr 48 | } 49 | 50 | return strings.Trim(string(combinedOutput), "\n"), nil 51 | } 52 | -------------------------------------------------------------------------------- /test/integration/config/kind.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | kind: Cluster 16 | apiVersion: kind.x-k8s.io/v1alpha4 17 | featureGates: 18 | InPlacePodVerticalScaling: true 19 | -------------------------------------------------------------------------------- /test/integration/config/metricsserver/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | resources: 16 | - https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.4/components.yaml 17 | 18 | patches: 19 | - target: 20 | version: v1 21 | kind: Deployment 22 | name: metrics-server 23 | namespace: kube-system 24 | patch: |- 25 | - op: add 26 | path: /spec/template/spec/containers/0/args/- 27 | value: --kubelet-insecure-tls 28 | -------------------------------------------------------------------------------- /test/integration/extracacert/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Expedia Group, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG BASE_IMAGE=kindest/base:v20250214-acbabc1a 16 | 17 | FROM ${BASE_IMAGE} 18 | ARG EXTRA_CA_CERT_FILENAME 19 | COPY ./${EXTRA_CA_CERT_FILENAME} /usr/local/share/ca-certificates/extra-ca-cert.crt 20 | RUN update-ca-certificates 21 | -------------------------------------------------------------------------------- /test/integration/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 integration 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func logMessage(t *testing.T, log any) { 26 | testName := "" 27 | if t != nil { 28 | testName = t.Name() 29 | } 30 | 31 | prefix := fmt.Sprintf("[%s] [%s]:", time.Now().Format(time.RFC3339Nano), testName) 32 | 33 | if t != nil { 34 | t.Log(prefix, log) 35 | return 36 | } 37 | 38 | fmt.Println(prefix, log) 39 | } 40 | 41 | func maybeLogErrAndFailNow(t *testing.T, err error) { 42 | if err != nil { 43 | logMessage(t, err) 44 | t.FailNow() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/integration/path.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia Group, Inc. 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 integration 18 | 19 | import ( 20 | "path/filepath" 21 | "runtime" 22 | ) 23 | 24 | var rootAbsPath string 25 | 26 | func init() { 27 | _, b, _, _ := runtime.Caller(0) 28 | rootAbsPath = filepath.Join(filepath.Dir(b), ".."+pathSeparator+"..") 29 | } 30 | 31 | func pathAbsFromRel(relPath string) string { 32 | return rootAbsPath + pathSeparator + relPath 33 | } 34 | --------------------------------------------------------------------------------