├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── .golangci.yml ├── .zappr.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.e2e ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── SECURITY.md ├── VERSION ├── cmd ├── e2e │ ├── README.md │ ├── annotations_sync_test.go │ ├── basic_test.go │ ├── broken_stack_test.go │ ├── configuration_resources_test.go │ ├── configure_hpa_test.go │ ├── generated_autoscaler_test.go │ ├── prescaling_test.go │ ├── test_environment.go │ ├── test_utils.go │ ├── traffic_switch_test.go │ └── ttl_test.go ├── stackset-controller │ └── main.go └── traffic │ └── main.go ├── controller ├── stack_resources.go ├── stack_resources_test.go ├── stackset.go ├── stackset_test.go └── test_helpers.go ├── delivery.yaml ├── docs ├── deployment.yaml ├── howtos.md ├── rbac.yaml ├── stack_crd.yaml ├── stackset.yaml └── stackset_crd.yaml ├── e2e ├── apply │ ├── deployment.yaml │ ├── rbac.yaml │ └── sample.yaml └── run_e2e.sh ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt ├── crd │ └── trim.go ├── tools.go └── update-codegen.sh └── pkg ├── apis └── zalando.org │ ├── register.go │ └── v1 │ ├── register.go │ ├── types.go │ ├── types_pcs.go │ └── zz_generated.deepcopy.go ├── client ├── clientset │ └── versioned │ │ ├── clientset.go │ │ ├── doc.go │ │ ├── fake │ │ ├── clientset_generated.go │ │ ├── doc.go │ │ └── register.go │ │ ├── scheme │ │ ├── doc.go │ │ └── register.go │ │ └── typed │ │ └── zalando.org │ │ └── v1 │ │ ├── doc.go │ │ ├── fake │ │ ├── doc.go │ │ ├── fake_platformcredentialsset.go │ │ ├── fake_stack.go │ │ ├── fake_stackset.go │ │ └── fake_zalando.org_client.go │ │ ├── generated_expansion.go │ │ ├── platformcredentialsset.go │ │ ├── stack.go │ │ ├── stackset.go │ │ └── zalando.org_client.go ├── informers │ └── externalversions │ │ ├── factory.go │ │ ├── generic.go │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ └── zalando.org │ │ ├── interface.go │ │ └── v1 │ │ ├── interface.go │ │ ├── platformcredentialsset.go │ │ ├── stack.go │ │ └── stackset.go └── listers │ └── zalando.org │ └── v1 │ ├── expansion_generated.go │ ├── platformcredentialsset.go │ ├── stack.go │ └── stackset.go ├── clientset └── unified.go ├── core ├── autoscaler.go ├── autoscaler_test.go ├── helpers.go ├── helpers_test.go ├── metrics.go ├── stack_resources.go ├── stack_resources_test.go ├── stackset.go ├── stackset_test.go ├── test_helpers.go ├── traffic.go ├── traffic_prescaling.go ├── traffic_simple.go ├── traffic_test.go └── types.go ├── recorder └── recorder.go └── traffic └── traffic.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: docker 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "07:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: gomod 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "07:00" 14 | open-pull-requests-limit: 10 15 | groups: 16 | all-go-mod-patch-and-minor: 17 | patterns: ["*"] 18 | update-types: ["patch", "minor"] 19 | ignore: 20 | # Ignore k8s and its transitives modules as they are upgraded manually 21 | - dependency-name: "k8s.io/*" 22 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-go@v2 9 | with: 10 | go-version: '^1.23' 11 | - run: go version 12 | - run: go install github.com/mattn/goveralls@latest 13 | - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 14 | - run: make check 15 | - run: make test 16 | - run: make build.docker 17 | - run: goveralls -coverprofile=profile.cov -service=github 18 | env: 19 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # ignore binaries 15 | build/ 16 | 17 | # ignore vendored dependencies 18 | vendor/ 19 | profile.cov 20 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 4 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - errcheck 8 | - gosimple 9 | - govet 10 | - ineffassign 11 | - staticcheck 12 | - typecheck 13 | - unused 14 | -------------------------------------------------------------------------------- /.zappr.yaml: -------------------------------------------------------------------------------- 1 | # for github.com 2 | approvals: 3 | groups: 4 | zalando: 5 | minimum: 2 6 | from: 7 | orgs: 8 | - "zalando" 9 | X-Zalando-Team: teapot 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team see [MAINTAINERS.md](MAINTAINERS.md). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to stackset-controller 2 | 3 | **Thank you for your interest in making stackset-controller even better and more awesome. Your contributions are highly welcome.** 4 | 5 | There are multiple ways of getting involved: 6 | 7 | - [Report a bug](#report-a-bug) 8 | - [Suggest a feature](#suggest-a-feature) 9 | - [Contribute code](#contribute-code) 10 | 11 | Below are a few guidelines we would like you to follow. 12 | If you need help, please reach out to us: [MAINTAINERS.md](MAINTAINERS.md) 13 | 14 | ## Report a bug 15 | Reporting bugs is one of the best ways to contribute. Before creating a bug report, please check that an [issue](https://github.com/zalando-incubator/stackset-controller/issues) reporting the same problem does not already exist. If there is an such an issue, you may add your information as a comment. 16 | 17 | To report a new bug you should open an issue that summarizes the bug and set the label to "bug". 18 | 19 | If you want to provide a fix along with your bug report: That is great! In this case please send us a pull request as described in section [Contribute Code](#contribute-code). 20 | 21 | ## Suggest a feature 22 | To request a new feature you should open an [issue](https://github.com/zalando-incubator/stackset-controller/issues/new) and summarize the desired functionality and its use case. Set the issue label to "feature". 23 | 24 | ## Contribute code 25 | This is a rough outline of what the workflow for code contributions looks like: 26 | - Check the list of open [issues](https://github.com/zalando-incubator/stackset-controller/issues). Either assign an existing issue to yourself, or create a new one that you would like work on and discuss your ideas and use cases. 27 | - Fork the repository on GitHub 28 | - Create a topic branch (feature/<your-feature> bug/<a-bug>) from where you want to base your work. This is usually master. 29 | - Make commits of logical units. 30 | - Write good commit messages (see below). 31 | - Push your changes to a topic branch in your fork of the repository. 32 | - Submit a pull request to [zalando-incubator/stackset-controller](https://github.com/zalando-incubator/stackset-controller) 33 | - Your pull request must receive a :thumbsup: from two [Maintainers](https://github.com/zalando-incubator/stackset-controller/blob/master/MAINTAINERS.md) 34 | 35 | Thanks for your contributions! 36 | 37 | ### Code style 38 | Stackset-Controller is formatted with [gofmt](https://golang.org/cmd/gofmt/). Please run it on your code before making a pull request. The coding style suggested by the Golang community is the preferred one for the cases that are not covered by gofmt, see the [style doc](https://github.com/golang/go/wiki/CodeReviewComments) for details. 39 | 40 | ### Commit messages 41 | Your commit messages ideally can answer two questions: what changed and why. The subject line should feature the “what” and the body of the commit should describe the “why”. 42 | 43 | When creating a pull request, its comment should reference the corresponding issue id. 44 | 45 | **Have fun and enjoy hacking!** 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=registry.opensource.zalan.do/library/static:latest 2 | FROM ${BASE_IMAGE} 3 | LABEL maintainer="Team Teapot @ Zalando SE " 4 | 5 | ARG TARGETARCH 6 | 7 | # add binary 8 | ADD build/linux/${TARGETARCH}/stackset-controller / 9 | 10 | ENTRYPOINT ["/stackset-controller"] 11 | -------------------------------------------------------------------------------- /Dockerfile.e2e: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=registry.opensource.zalan.do/library/static:latest 2 | FROM ${BASE_IMAGE} 3 | LABEL maintainer="Team Teapot @ Zalando SE " 4 | 5 | ARG TARGETARCH 6 | 7 | # add binary 8 | ADD build/linux/${TARGETARCH}/e2e / 9 | 10 | ENTRYPOINT ["/e2e", "-test.parallel", "30"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zalando SE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | - Mikkel Larsen 2 | - Martin Linkhorst 3 | - Maksym Aryefyev 4 | - Sandor Szücs 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean test check build.local build.linux build.osx build.docker build.push 2 | 3 | BINARY = stackset-controller 4 | BINARIES = $(BINARY) traffic 5 | LOCAL_BINARIES = $(addprefix build/,$(BINARIES)) 6 | LINUX_BINARIES = $(addprefix build/linux/,$(BINARIES)) 7 | VERSION ?= $(shell git describe --tags --always --dirty) 8 | IMAGE ?= registry-write.opensource.zalan.do/teapot/$(BINARY) 9 | E2E_IMAGE ?= $(IMAGE)-e2e 10 | TAG ?= $(VERSION) 11 | SOURCES = $(shell find . -name '*.go') 12 | CRD_SOURCES = $(shell find pkg/apis/zalando.org -name '*.go') 13 | CRD_TYPE_SOURCE = pkg/apis/zalando.org/v1/types.go 14 | GENERATED_CRDS = docs/stackset_crd.yaml docs/stack_crd.yaml 15 | GENERATED = pkg/apis/zalando.org/v1/zz_generated.deepcopy.go 16 | GOPKGS = $(shell go list ./... | grep -v /e2e) 17 | BUILD_FLAGS ?= -v 18 | LDFLAGS ?= -X main.version=$(VERSION) -w -s 19 | 20 | default: build.local 21 | 22 | clean: 23 | rm -rf build 24 | rm -rf $(GENERATED) 25 | rm -f $(GENERATED_CRDS) 26 | 27 | test: $(GENERATED) 28 | go test -v -coverprofile=profile.cov $(GOPKGS) 29 | 30 | check: $(GENERATED) 31 | go mod download 32 | golangci-lint run --timeout=2m ./... 33 | 34 | $(GENERATED): go.mod $(CRD_TYPE_SOURCE) 35 | ./hack/update-codegen.sh 36 | 37 | $(GENERATED_CRDS): $(GENERATED) $(CRD_SOURCES) 38 | go run sigs.k8s.io/controller-tools/cmd/controller-gen crd:crdVersions=v1,allowDangerousTypes=true paths=./pkg/apis/... output:crd:dir=docs 39 | go run hack/crd/trim.go < docs/zalando.org_stacksets.yaml > docs/stackset_crd.yaml 40 | go run hack/crd/trim.go < docs/zalando.org_stacks.yaml > docs/stack_crd.yaml 41 | rm docs/zalando.org_stacksets.yaml docs/zalando.org_stacks.yaml 42 | rm -f docs/zalando.org_platformcredentialssets.yaml 43 | 44 | build.local: $(LOCAL_BINARIES) $(GENERATED_CRDS) 45 | build.linux: $(LINUX_BINARIES) 46 | build.linux.amd64: build/linux/amd64/$(BINARY) 47 | build.linux.arm64: build/linux/arm64/$(BINARY) 48 | 49 | build/linux/e2e: $(GENERATED) $(SOURCES) 50 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go test -c -o build/linux/$(notdir $@) $(BUILD_FLAGS) ./cmd/$(notdir $@) 51 | 52 | build/linux/amd64/e2e: $(GENERATED) $(SOURCES) 53 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go test -c -o build/linux/amd64/$(notdir $@) $(BUILD_FLAGS) ./cmd/$(notdir $@) 54 | 55 | build/linux/arm64/e2e: $(GENERATED) $(SOURCES) 56 | GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go test -c -o build/linux/arm64/$(notdir $@) $(BUILD_FLAGS) ./cmd/$(notdir $@) 57 | 58 | build/linux/%: $(GENERATED) $(SOURCES) 59 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(BUILD_FLAGS) -o build/linux/$(notdir $@) -ldflags "$(LDFLAGS)" ./cmd/$(notdir $@) 60 | 61 | build/linux/amd64/%: go.mod $(SOURCES) 62 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(BUILD_FLAGS) -o build/linux/amd64/$(notdir $@) -ldflags "$(LDFLAGS)" ./cmd/$(notdir $@) 63 | 64 | build/linux/arm64/%: go.mod $(SOURCES) 65 | GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build $(BUILD_FLAGS) -o build/linux/arm64/$(notdir $@) -ldflags "$(LDFLAGS)" ./cmd/$(notdir $@) 66 | 67 | build/e2e: $(GENERATED) $(SOURCES) 68 | CGO_ENABLED=0 go test -c -o build/$(notdir $@) ./cmd/$(notdir $@) 69 | 70 | build/%: $(GENERATED) $(SOURCES) 71 | CGO_ENABLED=0 go build -o build/$(notdir $@) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" ./cmd/$(notdir $@) 72 | 73 | build.docker: build.linux build/linux/e2e 74 | docker build --rm -t "$(E2E_IMAGE):$(TAG)" -f Dockerfile.e2e --build-arg TARGETARCH= . 75 | docker build --rm -t "$(IMAGE):$(TAG)" -f Dockerfile --build-arg TARGETARCH= . 76 | 77 | build.push: build.docker 78 | docker push "$(E2E_IMAGE):$(TAG)" 79 | docker push "$(IMAGE):$(TAG)" 80 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | We acknowledge that every line of code that we write may potentially contain security issues. 2 | We are trying to deal with it responsibly and provide patches as quickly as possible. 3 | 4 | We host our bug bounty program on HackerOne, it is currently private, therefore if you would like to report a vulnerability and get rewarded for it, please ask to join our program by filling this form: 5 | 6 | https://corporate.zalando.com/en/services-and-contact#security-form 7 | 8 | You can also send your report via this form if you do not want to join our bug bounty program and just want to report a vulnerability or security issue. 9 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v1.4 2 | -------------------------------------------------------------------------------- /cmd/e2e/README.md: -------------------------------------------------------------------------------- 1 | ### Running the End-To-End Tests 2 | 3 | The following environment variables should be set: 4 | 5 | 1. `E2E_NAMESPACE` is the namespace where the tests should be run. 6 | 2. `CLUSTER_DOMAIN` is the DNS domain managed in which ingresses should be created 7 | 3. `CLUSTER_DOMAIN_INTERNAL` is the internal DNS domain managed in which ingresses should be created 8 | 4. `CONTROLLER_ID` is set so that all stacks are only managed by the controller being currently tested. 9 | 5. `KUBECONFIG` with the path to the kubeconfig file 10 | 11 | To run the tests run the command: 12 | 13 | ``` 14 | go test -parallel $NUM_PARALLEL github.com/zalando-incubator/stackset-controller/cmd/e2e 15 | ``` 16 | 17 | Over here `$NUM_PARALLEL` can be set to a sufficiently high value which indicates how many 18 | of the parallel type tests can be run concurrently. 19 | 20 | #### Example - run E2E test 21 | 22 | 1. Start apiserver proxy 23 | ``` 24 | kubectl proxy 25 | ``` 26 | 2. use kubectl watch to show what happens 27 | ``` 28 | watch -n 10 "kubectl get -n foo stackset,stack,ing,ep,deployment" 29 | ``` 30 | 3. recreate namespace `foo` and run local build stackset-controller 31 | ``` 32 | kubectl delete namespace foo; kubectl create namespace foo 33 | make 34 | ./build/stackset-controller --apiserver=http://127.0.0.1:8001 \ 35 | --enable-configmap-support --enable-secret-support --enable-routegroup-support \ 36 | --enable-pcs-support --enable-traffic-segments --controller-id=foo \ 37 | --sync-ingress-annotation=example.org/i-haz-synchronize \ 38 | --sync-ingress-annotation=teapot.org/the-best \ 39 | --cluster-domain=${CLUSTER_DOMAIN} --cluster-domain=${CLUSTER_DOMAIN_INTERNAL} 40 | ``` 41 | 4. rebuild e2e test and run e2e tests in `foo` namespace 42 | ``` 43 | rm -f build/e2e; make build/e2e 44 | CLUSTER_DOMAIN=example.org CLUSTER_DOMAIN_INTERNAL=ingress.cluster.local CLUSTER_NAME=example E2E_NAMESPACE=foo CONTROLLER_ID=foo KUBECONFIG=$HOME/.kube/config ./build/e2e -test.v #-test.run=TestTrafficSwitch 45 | ``` 46 | -------------------------------------------------------------------------------- /cmd/e2e/annotations_sync_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | "github.com/zalando-incubator/stackset-controller/pkg/core" 11 | ) 12 | 13 | func TestSyncAnnotationsPropagateToSegments(t *testing.T) { 14 | t.Parallel() 15 | 16 | for i, tc := range []struct { 17 | annotationsIng []map[string]string 18 | annotationsRg []map[string]string 19 | expectedIng []map[string]string 20 | expectedRg []map[string]string 21 | }{ 22 | { 23 | annotationsIng: []map[string]string{ 24 | {}, 25 | { 26 | "example.org/i-haz-synchronize": "keep-sync", 27 | "teapot.org/the-best": "for-real", 28 | "example.org/no-sync-plz": "ditto", 29 | }, 30 | }, 31 | annotationsRg: []map[string]string{ 32 | {}, 33 | { 34 | "example.org/i-haz-synchronize": "synchronize", 35 | "teapot.org/the-best": "of-all-time", 36 | "example.org/no-sync-plz": "nope", 37 | }, 38 | }, 39 | expectedIng: []map[string]string{ 40 | { 41 | "example.org/i-haz-synchronize": "keep-sync", 42 | "teapot.org/the-best": "for-real", 43 | }, 44 | { 45 | "example.org/i-haz-synchronize": "keep-sync", 46 | "teapot.org/the-best": "for-real", 47 | "example.org/no-sync-plz": "ditto", 48 | }, 49 | }, 50 | expectedRg: []map[string]string{ 51 | { 52 | "example.org/i-haz-synchronize": "synchronize", 53 | "teapot.org/the-best": "of-all-time", 54 | }, 55 | { 56 | "example.org/i-haz-synchronize": "synchronize", 57 | "teapot.org/the-best": "of-all-time", 58 | "example.org/no-sync-plz": "nope", 59 | }, 60 | }, 61 | }, 62 | { 63 | annotationsIng: []map[string]string{ 64 | { 65 | "example.org/i-haz-synchronize": "keep-sync", 66 | "teapot.org/the-best": "for-real", 67 | }, 68 | { 69 | "example.org/i-haz-synchronize": "keep-sync", 70 | "example.org/no-sync-plz": "ditto", 71 | }, 72 | }, 73 | annotationsRg: []map[string]string{ 74 | { 75 | "example.org/i-haz-synchronize": "synchronize", 76 | "teapot.org/the-best": "of-all-time", 77 | }, 78 | { 79 | "teapot.org/the-best": "of-all-time", 80 | "example.org/no-sync-plz": "nope", 81 | }, 82 | }, 83 | expectedIng: []map[string]string{ 84 | { 85 | "example.org/i-haz-synchronize": "keep-sync", 86 | }, 87 | { 88 | "example.org/i-haz-synchronize": "keep-sync", 89 | "example.org/no-sync-plz": "ditto", 90 | }, 91 | }, 92 | expectedRg: []map[string]string{ 93 | { 94 | "teapot.org/the-best": "of-all-time", 95 | }, 96 | { 97 | "teapot.org/the-best": "of-all-time", 98 | "example.org/no-sync-plz": "nope", 99 | }, 100 | }, 101 | }, 102 | } { 103 | stacksetName := fmt.Sprintf("stackset-annotations-sync-case-%d", i) 104 | specFactory := NewTestStacksetSpecFactory( 105 | stacksetName, 106 | ).Ingress().RouteGroup() 107 | spec := specFactory.Create(t, "v0") 108 | 109 | for i, a := range tc.annotationsIng { 110 | version := fmt.Sprintf("v%d", i) 111 | spec.StackTemplate.Spec.Version = version 112 | spec.Ingress.Annotations = a 113 | spec.RouteGroup.Annotations = tc.annotationsRg[i] 114 | 115 | var err error 116 | if i == 0 { 117 | err = createStackSet(stacksetName, 1, spec) 118 | } else { 119 | err = updateStackSet(stacksetName, spec) 120 | } 121 | require.NoError(t, err) 122 | 123 | _, err = waitForIngressSegment(t, stacksetName, version) 124 | require.NoError(t, err) 125 | 126 | _, err = waitForRouteGroupSegment(t, stacksetName, version) 127 | require.NoError(t, err) 128 | } 129 | 130 | // Wait some time for the annotations to be propagated 131 | time.Sleep(time.Second * 10) 132 | 133 | for i := 0; i < len(tc.annotationsIng); i++ { 134 | version := fmt.Sprintf("v%d", i) 135 | ingress, err := waitForIngressSegment(t, stacksetName, version) 136 | require.NoError(t, err) 137 | 138 | delete( 139 | ingress.Annotations, 140 | "stackset-controller.zalando.org/stack-generation", 141 | ) 142 | delete( 143 | ingress.Annotations, 144 | core.IngressPredicateKey, 145 | ) 146 | 147 | if !reflect.DeepEqual(tc.expectedIng[i], ingress.Annotations) { 148 | t.Errorf( 149 | "Expected ingress annotations in %q to be %v, got %v", 150 | version, 151 | tc.expectedIng[i], 152 | ingress.Annotations, 153 | ) 154 | } 155 | 156 | routeGroup, err := waitForRouteGroupSegment( 157 | t, 158 | stacksetName, 159 | version, 160 | ) 161 | require.NoError(t, err) 162 | 163 | delete( 164 | routeGroup.Annotations, 165 | "stackset-controller.zalando.org/stack-generation", 166 | ) 167 | 168 | if !reflect.DeepEqual(tc.expectedRg[i], routeGroup.Annotations) { 169 | t.Errorf( 170 | "Expected routeGroup annotations in %q to be %v, got %v", 171 | version, 172 | tc.expectedRg[i], 173 | routeGroup.Annotations, 174 | ) 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /cmd/e2e/configuration_resources_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | "github.com/zalando-incubator/stackset-controller/pkg/core" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestConfigurationResources(t *testing.T) { 12 | suite.Run(t, new(ConfigurationResourcesConfigMapsTestSuite)) 13 | suite.Run(t, new(ConfigurationResourcesSecretsTestSuite)) 14 | suite.Run(t, new(ConfigurationResourcesPlatformCredentialsSetTestSuite)) 15 | } 16 | 17 | type ConfigurationResourcesTestSuite struct { 18 | suite.Suite 19 | 20 | stacksetSpecFactory *TestStacksetSpecFactory 21 | 22 | stacksetName string 23 | stackVersion string 24 | } 25 | 26 | type ConfigurationResourcesConfigMapsTestSuite struct { 27 | ConfigurationResourcesTestSuite 28 | } 29 | 30 | func (suite *ConfigurationResourcesConfigMapsTestSuite) SetupTest() { 31 | suite.stacksetName = "stackset-cr-cm" 32 | suite.stackVersion = "v1" 33 | 34 | suite.stacksetSpecFactory = NewTestStacksetSpecFactory(suite.stacksetName) 35 | } 36 | 37 | func (suite *ConfigurationResourcesConfigMapsTestSuite) TearDownTest() { 38 | err := deleteStackset(suite.stacksetName) 39 | suite.Require().NoError(err) 40 | } 41 | 42 | // TestReferencedConfigMaps tests that ConfigMaps referenced in the StackSet spec are owned by the Stack. 43 | func (suite *ConfigurationResourcesConfigMapsTestSuite) TestReferencedConfigMaps() { 44 | // Create a ConfigMap in the cluster following the naming convention 45 | configMapName := "stackset-cr-cm-v1-my-configmap" 46 | createConfigMap(suite.T(), configMapName) 47 | 48 | // Add the ConfigMap reference to the StackSet spec 49 | suite.stacksetSpecFactory.AddReferencedConfigMap(configMapName) 50 | 51 | // Generate the StackSet spec 52 | stacksetSpec := suite.stacksetSpecFactory.Create(suite.T(), suite.stackVersion) 53 | 54 | // Create the StackSet in the cluster 55 | err := createStackSet(suite.stacksetName, 0, stacksetSpec) 56 | suite.Require().NoError(err) 57 | 58 | // Wait for the first Stack to be created 59 | stack, err := waitForStack(suite.T(), suite.stacksetName, suite.stackVersion) 60 | suite.Require().NoError(err) 61 | 62 | // Ensure that the ConfigMap exists in the cluster 63 | _, err = waitForConfigMap(suite.T(), configMapName) 64 | suite.Require().NoError(err) 65 | 66 | // Ensure that the ConfigMap is owned by the Stack 67 | ownerReferences := []metav1.OwnerReference{ 68 | { 69 | APIVersion: core.APIVersion, 70 | Kind: core.KindStack, 71 | Name: stack.Name, 72 | UID: stack.UID, 73 | }, 74 | } 75 | err = waitForConfigMapOwnerReferences(suite.T(), configMapName, ownerReferences).await() 76 | suite.Require().NoError(err) 77 | } 78 | 79 | func (suite *ConfigurationResourcesSecretsTestSuite) SetupTest() { 80 | suite.stacksetName = "stackset-cr-sec" 81 | suite.stackVersion = "v1" 82 | 83 | suite.stacksetSpecFactory = NewTestStacksetSpecFactory(suite.stacksetName) 84 | } 85 | 86 | func (suite *ConfigurationResourcesSecretsTestSuite) TearDownTest() { 87 | err := deleteStackset(suite.stacksetName) 88 | suite.Require().NoError(err) 89 | } 90 | 91 | type ConfigurationResourcesSecretsTestSuite struct { 92 | ConfigurationResourcesTestSuite 93 | } 94 | 95 | // TestReferencedSecrets tests that Secrets referenced in the StackSet spec are owned by the Stack. 96 | func (suite *ConfigurationResourcesSecretsTestSuite) TestReferencedSecrets() { 97 | // Create a Secret in the cluster following the naming convention 98 | secretName := "stackset-cr-sec-v1-my-secret" 99 | createSecret(suite.T(), secretName) 100 | 101 | // Add the Secret reference to the StackSet spec 102 | suite.stacksetSpecFactory.AddReferencedSecret(secretName) 103 | 104 | // Generate the StackSet spec 105 | stacksetSpec := suite.stacksetSpecFactory.Create(suite.T(), suite.stackVersion) 106 | 107 | // Create the StackSet in the cluster 108 | err := createStackSet(suite.stacksetName, 0, stacksetSpec) 109 | suite.Require().NoError(err) 110 | 111 | // Wait for the first Stack to be created 112 | stack, err := waitForStack(suite.T(), suite.stacksetName, suite.stackVersion) 113 | suite.Require().NoError(err) 114 | 115 | // Ensure that the Secret exists in the cluster 116 | _, err = waitForSecret(suite.T(), secretName) 117 | suite.Require().NoError(err) 118 | 119 | // Ensure that the Secret is owned by the Stack 120 | ownerReferences := []metav1.OwnerReference{ 121 | { 122 | APIVersion: core.APIVersion, 123 | Kind: core.KindStack, 124 | Name: stack.Name, 125 | UID: stack.UID, 126 | }, 127 | } 128 | err = waitForSecretOwnerReferences(suite.T(), secretName, ownerReferences).await() 129 | suite.Require().NoError(err) 130 | } 131 | 132 | type ConfigurationResourcesPlatformCredentialsSetTestSuite struct { 133 | ConfigurationResourcesTestSuite 134 | } 135 | 136 | func (suite *ConfigurationResourcesPlatformCredentialsSetTestSuite) SetupTest() { 137 | suite.stacksetName = "stackset-cr-pcs" 138 | suite.stackVersion = "v1" 139 | 140 | suite.stacksetSpecFactory = NewTestStacksetSpecFactory(suite.stacksetName) 141 | } 142 | 143 | func (suite *ConfigurationResourcesPlatformCredentialsSetTestSuite) TearDownTest() { 144 | err := deleteStackset(suite.stacksetName) 145 | suite.Require().NoError(err) 146 | } 147 | 148 | // TestGeneratedPCS tests that PlatformCredentialsSets defined in the StackSet are 149 | // correctly created and owned by the Stack. 150 | func (suite *ConfigurationResourcesPlatformCredentialsSetTestSuite) TestGeneratedPCS() { 151 | // Add the PlatformCredentialsSet reference to the StackSet spec 152 | pcsName := suite.stacksetName + "-" + suite.stackVersion + "-my-pcs" 153 | suite.stacksetSpecFactory.AddPlatformCredentialsSetDefinition(pcsName) 154 | 155 | // Generate the StackSet spec 156 | stacksetSpec := suite.stacksetSpecFactory.Create(suite.T(), suite.stackVersion) 157 | 158 | // Create the StackSet in the cluster 159 | err := createStackSet(suite.stacksetName, 0, stacksetSpec) 160 | suite.Require().NoError(err) 161 | 162 | // Wait for the first Stack to be created 163 | stack, err := waitForStack(suite.T(), suite.stacksetName, suite.stackVersion) 164 | suite.Require().NoError(err) 165 | 166 | // Fetch the latest version of the PlatformCredentialsSet 167 | pcs, err := waitForPlatformCredentialsSet(suite.T(), pcsName) 168 | suite.Require().NoError(err) 169 | 170 | // Ensure that the PlatformCredentialsSet is owned by the Stack 171 | suite.Equal([]metav1.OwnerReference{ 172 | { 173 | APIVersion: core.APIVersion, 174 | Kind: core.KindStack, 175 | Name: stack.Name, 176 | UID: stack.UID, 177 | }, 178 | }, pcs.OwnerReferences) 179 | 180 | suite.Equal(stack.Labels["application"], pcs.Spec.Application) 181 | suite.Equal("v2", pcs.Spec.TokenVersion) 182 | suite.Equal([]string{"read"}, pcs.Spec.Tokens["token-example"].Privileges) 183 | } 184 | -------------------------------------------------------------------------------- /cmd/e2e/configure_hpa_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 9 | ) 10 | 11 | // TestConfigureHPA tests Behavior is reflected when stackset is created 12 | func TestConfigureAutoscaling(t *testing.T) { 13 | t.Parallel() 14 | stacksetName := "configured-autoscaler" 15 | var stabilizationWindow int32 = 60 16 | metrics := []zv1.AutoscalerMetrics{ 17 | makeCPUAutoscalerMetrics(50), 18 | makeAmazonSQSAutoscalerMetrics("test", "eu-central-1", 10), 19 | makeIngressAutoscalerMetrics(20), 20 | } 21 | require.Len(t, metrics, 3) 22 | 23 | factory := NewTestStacksetSpecFactory(stacksetName). 24 | Ingress(). 25 | Autoscaler(1, 10, metrics). 26 | Behavior(stabilizationWindow) 27 | firstVersion := "v1" 28 | spec := factory.Create(t, firstVersion) 29 | err := createStackSet(stacksetName, 0, spec) 30 | require.NoError(t, err) 31 | 32 | fullFirstName := fmt.Sprintf("%s-%s", stacksetName, firstVersion) 33 | hpa, err := waitForHPA(t, fullFirstName) 34 | require.NoError(t, err) 35 | 36 | require.NotNil(t, hpa.Spec.Behavior.ScaleDown.StabilizationWindowSeconds, "HPA StabilizationWindowSeconds is nil") 37 | require.EqualValues(t, stabilizationWindow, *hpa.Spec.Behavior.ScaleDown.StabilizationWindowSeconds) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/e2e/generated_autoscaler_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 10 | "k8s.io/apimachinery/pkg/api/resource" 11 | ) 12 | 13 | func makeCPUAutoscalerMetrics(utilization int32) zv1.AutoscalerMetrics { 14 | return zv1.AutoscalerMetrics{ 15 | Type: "CPU", 16 | AverageUtilization: pint32(utilization), 17 | } 18 | } 19 | 20 | func makeAmazonSQSAutoscalerMetrics(queueName, region string, averageQueueLength int64) zv1.AutoscalerMetrics { 21 | return zv1.AutoscalerMetrics{ 22 | Type: "AmazonSQS", 23 | Average: resource.NewQuantity(averageQueueLength, resource.DecimalSI), 24 | Queue: &zv1.MetricsQueue{Name: queueName, Region: region}, 25 | } 26 | } 27 | 28 | func makeIngressAutoscalerMetrics(average int64) zv1.AutoscalerMetrics { 29 | return zv1.AutoscalerMetrics{ 30 | Type: "Ingress", 31 | Average: resource.NewQuantity(average, resource.DecimalSI), 32 | } 33 | } 34 | 35 | func TestGenerateAutoscaler(t *testing.T) { 36 | t.Parallel() 37 | stacksetName := "generated-autoscaler" 38 | metrics := []zv1.AutoscalerMetrics{ 39 | makeAmazonSQSAutoscalerMetrics("test", "eu-central-1", 10), 40 | makeCPUAutoscalerMetrics(50), 41 | makeIngressAutoscalerMetrics(20), 42 | } 43 | require.Len(t, metrics, 3) 44 | 45 | factory := NewTestStacksetSpecFactory(stacksetName).Ingress().Autoscaler(1, 10, metrics) 46 | firstVersion := "v1" 47 | spec := factory.Create(t, firstVersion) 48 | err := createStackSet(stacksetName, 0, spec) 49 | require.NoError(t, err) 50 | 51 | fullFirstName := fmt.Sprintf("%s-%s", stacksetName, firstVersion) 52 | hpa, err := waitForHPA(t, fullFirstName) 53 | require.NoError(t, err) 54 | require.EqualValues(t, 1, *hpa.Spec.MinReplicas) 55 | require.EqualValues(t, 10, hpa.Spec.MaxReplicas) 56 | require.Len(t, hpa.Spec.Metrics, 3) 57 | 58 | require.EqualValues(t, 10, hpa.Spec.Metrics[0].External.Target.AverageValue.Value()) 59 | require.EqualValues(t, 50, *hpa.Spec.Metrics[1].Resource.Target.AverageUtilization) 60 | require.EqualValues(t, 20, hpa.Spec.Metrics[2].Object.Target.AverageValue.Value()) 61 | } 62 | 63 | func TestAutoscalerWithoutTraffic(t *testing.T) { 64 | t.Parallel() 65 | 66 | // Create a stackset with two stacks and an autoscaler for each stack 67 | stacksetName := "autoscaler-without-traffic" 68 | metrics := []zv1.AutoscalerMetrics{ 69 | makeCPUAutoscalerMetrics(50), 70 | } 71 | factory := NewTestStacksetSpecFactory(stacksetName).Ingress().Autoscaler(1, 3, metrics).StackGC(1, 30) 72 | firstStack := "v1" 73 | fullFirstStack := fmt.Sprintf("%s-%s", stacksetName, firstStack) 74 | spec := factory.Create(t, firstStack) 75 | err := createStackSet(stacksetName, 0, spec) 76 | require.NoError(t, err) 77 | _, err = waitForStack(t, stacksetName, firstStack) 78 | require.NoError(t, err) 79 | _, err = waitForHPA(t, fullFirstStack) 80 | require.NoError(t, err) 81 | 82 | secondStack := "v2" 83 | fullSecondStack := fmt.Sprintf("%s-%s", stacksetName, secondStack) 84 | spec = factory.Create(t, secondStack) 85 | err = updateStackSet(stacksetName, spec) 86 | require.NoError(t, err) 87 | _, err = waitForStack(t, stacksetName, secondStack) 88 | require.NoError(t, err) 89 | _, err = waitForHPA(t, fullSecondStack) 90 | require.NoError(t, err) 91 | 92 | // Switch traffic 100% to the first stack 93 | desiredTraffic := map[string]float64{ 94 | fullFirstStack: 100, 95 | fullSecondStack: 0, 96 | } 97 | err = setDesiredTrafficWeightsStackset(stacksetName, desiredTraffic) 98 | require.NoError(t, err) 99 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, desiredTraffic, nil).withTimeout(time.Minute * 1).await() 100 | require.NoError(t, err) 101 | 102 | // ensure that the HPA for the first stack is still there and that the HPA for the second stack is deleted 103 | err = resourceDeleted(t, "hpa", fullSecondStack, hpaInterface()).withTimeout(time.Minute * 1).await() 104 | require.NoError(t, err) 105 | _, err = waitForHPA(t, fullFirstStack) 106 | require.NoError(t, err) 107 | 108 | // Switch traffic 50% to each stack 109 | desiredTraffic = map[string]float64{ 110 | fullFirstStack: 50, 111 | fullSecondStack: 50, 112 | } 113 | err = setDesiredTrafficWeightsStackset(stacksetName, desiredTraffic) 114 | require.NoError(t, err) 115 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, desiredTraffic, nil).withTimeout(time.Minute * 1).await() 116 | require.NoError(t, err) 117 | 118 | // ensure that the HPAs for both stacks are still there 119 | _, err = waitForHPA(t, fullFirstStack) 120 | require.NoError(t, err) 121 | _, err = waitForHPA(t, fullSecondStack) 122 | require.NoError(t, err) 123 | 124 | // Switch traffic 100% to the second stack 125 | desiredTraffic = map[string]float64{ 126 | fullFirstStack: 0, 127 | fullSecondStack: 100, 128 | } 129 | err = setDesiredTrafficWeightsStackset(stacksetName, desiredTraffic) 130 | require.NoError(t, err) 131 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, desiredTraffic, nil).withTimeout(time.Minute * 1).await() 132 | require.NoError(t, err) 133 | 134 | // ensure that the HPA for the first stack is deleted and that the HPA for the second stack is still there 135 | err = resourceDeleted(t, "hpa", fullFirstStack, hpaInterface()).withTimeout(time.Minute * 1).await() 136 | require.NoError(t, err) 137 | _, err = waitForHPA(t, fullSecondStack) 138 | require.NoError(t, err) 139 | } 140 | -------------------------------------------------------------------------------- /cmd/e2e/test_environment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | rg "github.com/szuecs/routegroup-client/client/clientset/versioned" 11 | rgv1client "github.com/szuecs/routegroup-client/client/clientset/versioned/typed/zalando.org/v1" 12 | zv1client "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/typed/zalando.org/v1" 13 | "github.com/zalando-incubator/stackset-controller/pkg/clientset" 14 | "k8s.io/client-go/kubernetes" 15 | appsv1 "k8s.io/client-go/kubernetes/typed/apps/v1" 16 | autoscalingv2 "k8s.io/client-go/kubernetes/typed/autoscaling/v2" 17 | corev1typed "k8s.io/client-go/kubernetes/typed/core/v1" 18 | networking "k8s.io/client-go/kubernetes/typed/networking/v1" 19 | "k8s.io/client-go/rest" 20 | "k8s.io/client-go/tools/clientcmd" 21 | ) 22 | 23 | var ( 24 | kubernetesClient, stacksetClient, routegroupClient = createClients() 25 | namespace = requiredEnvar("E2E_NAMESPACE") 26 | clusterDomain = requiredEnvar("CLUSTER_DOMAIN") 27 | clusterDomainInternal = requiredEnvar("CLUSTER_DOMAIN_INTERNAL") 28 | clusterDomains = []string{clusterDomain, clusterDomainInternal} 29 | controllerId = os.Getenv("CONTROLLER_ID") 30 | waitTimeout time.Duration 31 | trafficSwitchWaitTimeout time.Duration 32 | ) 33 | 34 | func init() { 35 | flag.DurationVar(&waitTimeout, "wait-timeout", 60*time.Second, "Waiting interval before getting the resource") 36 | flag.DurationVar(&trafficSwitchWaitTimeout, "traffic-switch-wait-timeout", 150*time.Second, "Waiting interval before getting the checking stackset new traffic") 37 | logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true}) 38 | } 39 | 40 | func createClients() (kubernetes.Interface, clientset.Interface, rg.Interface) { 41 | kubeconfig := os.Getenv("KUBECONFIG") 42 | 43 | var cfg *rest.Config 44 | var err error 45 | if kubeconfig != "" { 46 | cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 47 | } else { 48 | cfg, err = rest.InClusterConfig() 49 | } 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | cfg.QPS = 100 55 | cfg.Burst = 100 56 | 57 | kubeClient, err := kubernetes.NewForConfig(cfg) 58 | if err != nil { 59 | panic(err) 60 | } 61 | stacksetClient, err := clientset.NewForConfig(cfg) 62 | if err != nil { 63 | panic(err) 64 | } 65 | routegroupClient, err := rg.NewForConfig(cfg) 66 | if err != nil { 67 | panic(err) 68 | } 69 | return kubeClient, stacksetClient, routegroupClient 70 | } 71 | 72 | func stacksetInterface() zv1client.StackSetInterface { 73 | return stacksetClient.ZalandoV1().StackSets(namespace) 74 | } 75 | 76 | func stackInterface() zv1client.StackInterface { 77 | return stacksetClient.ZalandoV1().Stacks(namespace) 78 | } 79 | 80 | func deploymentInterface() appsv1.DeploymentInterface { 81 | return kubernetesClient.AppsV1().Deployments(namespace) 82 | } 83 | 84 | func serviceInterface() corev1typed.ServiceInterface { 85 | return kubernetesClient.CoreV1().Services(namespace) 86 | } 87 | 88 | func hpaInterface() autoscalingv2.HorizontalPodAutoscalerInterface { 89 | return kubernetesClient.AutoscalingV2().HorizontalPodAutoscalers(namespace) 90 | } 91 | 92 | func ingressInterface() networking.IngressInterface { 93 | return kubernetesClient.NetworkingV1().Ingresses(namespace) 94 | } 95 | 96 | func routegroupInterface() rgv1client.RouteGroupInterface { 97 | return routegroupClient.ZalandoV1().RouteGroups(namespace) 98 | } 99 | 100 | func configMapInterface() corev1typed.ConfigMapInterface { 101 | return kubernetesClient.CoreV1().ConfigMaps(namespace) 102 | } 103 | 104 | func secretInterface() corev1typed.SecretInterface { 105 | return kubernetesClient.CoreV1().Secrets(namespace) 106 | } 107 | 108 | func platformCredentialsSetInterface() zv1client.PlatformCredentialsSetInterface { 109 | return stacksetClient.ZalandoV1().PlatformCredentialsSets(namespace) 110 | } 111 | 112 | func requiredEnvar(envar string) string { 113 | namespace := os.Getenv(envar) 114 | if namespace == "" { 115 | panic(fmt.Sprintf("%s not set", envar)) 116 | } 117 | return namespace 118 | } 119 | 120 | func hostnames(stacksetName string) []string { 121 | names := make([]string, 0, len(clusterDomains)) 122 | for _, domain := range clusterDomains { 123 | names = append(names, fmt.Sprintf("%s-%s.%s", namespace, stacksetName, domain)) 124 | } 125 | return names 126 | } 127 | -------------------------------------------------------------------------------- /cmd/e2e/traffic_switch_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // expectActualTrafficWeights waits until that both stackset.status and the ingress have the expected actual traffic weight, 11 | // and all stacks have their weights populated correctly 12 | func expectActualTrafficWeights(t *testing.T, stacksetName string, weights map[string]float64) { 13 | err := trafficWeightsUpdatedIngress(t, stacksetName, weights, nil).await() 14 | require.NoError(t, err) 15 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, weights, nil).await() 16 | require.NoError(t, err) 17 | } 18 | 19 | // expectStackTrafficWeights waits until the stack has the correct traffic weight values 20 | func expectStackTrafficWeights(t *testing.T, stackName string, actualTrafficWeight, desiredTrafficWeight float64) { 21 | err := stackStatusMatches(t, stackName, expectedStackStatus{ 22 | actualTrafficWeight: pfloat64(actualTrafficWeight), 23 | desiredTrafficWeight: pfloat64(desiredTrafficWeight), 24 | }).await() 25 | require.NoError(t, err) 26 | } 27 | 28 | func TestTrafficSwitchStackset(t *testing.T) { 29 | t.Parallel() 30 | 31 | stacksetName := "switch-traffic-stackset" 32 | firstVersion := "v1" 33 | firstStack := fmt.Sprintf("%s-%s", stacksetName, firstVersion) 34 | updatedVersion := "v2" 35 | updatedStack := fmt.Sprintf("%s-%s", stacksetName, updatedVersion) 36 | factory := NewTestStacksetSpecFactory(stacksetName).Ingress() 37 | spec := factory.Create(t, firstVersion) 38 | err := createStackSet(stacksetName, 0, spec) 39 | require.NoError(t, err) 40 | _, err = waitForStack(t, stacksetName, firstVersion) 41 | require.NoError(t, err) 42 | _, err = waitForIngressSegment(t, stacksetName, firstVersion) 43 | require.NoError(t, err) 44 | 45 | spec = factory.Create(t, updatedVersion) 46 | err = updateStackSet(stacksetName, spec) 47 | require.NoError(t, err) 48 | _, err = waitForStack(t, stacksetName, updatedVersion) 49 | require.NoError(t, err) 50 | _, err = waitForIngressSegment(t, stacksetName, updatedVersion) 51 | require.NoError(t, err) 52 | 53 | initialWeights := map[string]float64{firstStack: 100} 54 | expectActualTrafficWeights(t, stacksetName, initialWeights) 55 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindDesired, initialWeights, nil).await() 56 | require.NoError(t, err) 57 | require.NoError(t, err) 58 | 59 | expectStackTrafficWeights(t, firstStack, 100, 100) 60 | expectStackTrafficWeights(t, updatedStack, 0, 0) 61 | 62 | // Switch traffic 50/50 63 | desiredWeights := map[string]float64{firstStack: 50, updatedStack: 50} 64 | err = setDesiredTrafficWeightsStackset(stacksetName, desiredWeights) 65 | require.NoError(t, err) 66 | expectActualTrafficWeights(t, stacksetName, desiredWeights) 67 | require.NoError(t, err) 68 | 69 | expectStackTrafficWeights(t, firstStack, 50, 50) 70 | expectStackTrafficWeights(t, updatedStack, 50, 50) 71 | 72 | // Switch traffic 0/100 73 | newDesiredWeights := map[string]float64{updatedStack: 100} 74 | err = setDesiredTrafficWeightsStackset(stacksetName, newDesiredWeights) 75 | require.NoError(t, err) 76 | expectActualTrafficWeights(t, stacksetName, newDesiredWeights) 77 | require.NoError(t, err) 78 | 79 | expectStackTrafficWeights(t, firstStack, 0, 0) 80 | expectStackTrafficWeights(t, updatedStack, 100, 100) 81 | } 82 | 83 | func TestTrafficSwitchStacksetExternalIngress(t *testing.T) { 84 | t.Parallel() 85 | 86 | stacksetName := "switch-traffic-stackset-external" 87 | firstVersion := "v1" 88 | firstStack := fmt.Sprintf("%s-%s", stacksetName, firstVersion) 89 | updatedVersion := "v2" 90 | updatedStack := fmt.Sprintf("%s-%s", stacksetName, updatedVersion) 91 | factory := NewTestStacksetSpecFactory(stacksetName).ExternalIngress() 92 | spec := factory.Create(t, firstVersion) 93 | err := createStackSet(stacksetName, 0, spec) 94 | require.NoError(t, err) 95 | _, err = waitForStack(t, stacksetName, firstVersion) 96 | require.NoError(t, err) 97 | spec = factory.Create(t, updatedVersion) 98 | err = updateStackSet(stacksetName, spec) 99 | require.NoError(t, err) 100 | _, err = waitForStack(t, stacksetName, updatedVersion) 101 | require.NoError(t, err) 102 | 103 | initialWeights := map[string]float64{firstStack: 100} 104 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, initialWeights, nil).await() 105 | require.NoError(t, err) 106 | 107 | expectStackTrafficWeights(t, firstStack, 100, 100) 108 | expectStackTrafficWeights(t, updatedStack, 0, 0) 109 | 110 | // Switch traffic 50/50 111 | desiredWeights := map[string]float64{firstStack: 50, updatedStack: 50} 112 | err = setDesiredTrafficWeightsStackset(stacksetName, desiredWeights) 113 | require.NoError(t, err) 114 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, desiredWeights, nil).await() 115 | require.NoError(t, err) 116 | 117 | expectStackTrafficWeights(t, firstStack, 50, 50) 118 | expectStackTrafficWeights(t, updatedStack, 50, 50) 119 | 120 | // Switch traffic 0/100 121 | newDesiredWeights := map[string]float64{updatedStack: 100} 122 | err = setDesiredTrafficWeightsStackset(stacksetName, newDesiredWeights) 123 | require.NoError(t, err) 124 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, newDesiredWeights, nil).await() 125 | require.NoError(t, err) 126 | 127 | expectStackTrafficWeights(t, firstStack, 0, 0) 128 | expectStackTrafficWeights(t, updatedStack, 100, 100) 129 | } 130 | -------------------------------------------------------------------------------- /cmd/e2e/ttl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestStackTTLWithoutIngress(t *testing.T) { 12 | t.Parallel() 13 | stacksetName := "stackset-ttl-noingress" 14 | specFactory := NewTestStacksetSpecFactory(stacksetName).StackGC(3, 15) 15 | 16 | // Create 5 stacks in total and wait for their deployments to come up 17 | for i := 0; i < 5; i++ { 18 | stackVersion := fmt.Sprintf("v%d", i) 19 | var err error 20 | spec := specFactory.Create(t, stackVersion) 21 | if !stacksetExists(stacksetName) { 22 | err = createStackSet(stacksetName, 1, spec) 23 | } else { 24 | err = updateStackSet(stacksetName, spec) 25 | } 26 | require.NoError(t, err) 27 | _, err = waitForStack(t, stacksetName, stackVersion) 28 | require.NoError(t, err) 29 | _, err = waitForDeployment(t, fmt.Sprintf("%s-%s", stacksetName, stackVersion)) 30 | require.NoError(t, err) 31 | } 32 | 33 | // verify that only 3 stacks are present and the last 2 have been deleted 34 | for i := 2; i < 5; i++ { 35 | require.True(t, stackExists(stacksetName, fmt.Sprintf("v%d", i))) 36 | } 37 | 38 | // verify that the first 2 stacks which were created have been deleted 39 | for i := 0; i < 2; i++ { 40 | deploymentName := fmt.Sprintf("%s-v%d", stacksetName, i) 41 | err := resourceDeleted(t, "stack", deploymentName, deploymentInterface()).withTimeout(time.Second * 60).await() 42 | require.NoError(t, err) 43 | require.False(t, stackExists(stacksetName, fmt.Sprintf("v%d", i))) 44 | } 45 | } 46 | 47 | func TestStackTTLWithIngress(t *testing.T) { 48 | t.Parallel() 49 | stacksetName := "stackset-ttl-ingress" 50 | specFactory := NewTestStacksetSpecFactory(stacksetName).StackGC(3, 15).Ingress() 51 | 52 | // Create 6 stacks each with an ingress 53 | for i := 0; i < 6; i++ { 54 | stackVersion := fmt.Sprintf("v%d", i) 55 | var err error 56 | spec := specFactory.Create(t, stackVersion) 57 | if !stacksetExists(stacksetName) { 58 | err = createStackSet(stacksetName, 1, spec) 59 | } else { 60 | err = updateStackSet(stacksetName, spec) 61 | } 62 | require.NoError(t, err) 63 | _, err = waitForStack(t, stacksetName, stackVersion) 64 | require.NoError(t, err) 65 | fullStackName := fmt.Sprintf("%s-%s", stacksetName, stackVersion) 66 | _, err = waitForIngress(t, fullStackName) 67 | require.NoError(t, err) 68 | 69 | // once the stack is created switch full traffic to it 70 | newWeight := map[string]float64{fullStackName: 100} 71 | err = setDesiredTrafficWeightsStackset(stacksetName, newWeight) 72 | require.NoError(t, err) 73 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, newWeight, nil).withTimeout(10 * time.Minute).await() 74 | require.NoError(t, err) 75 | } 76 | 77 | // verify that only the last 4 created stacks are present 78 | for i := 2; i < 6; i++ { 79 | deploymentName := fmt.Sprintf("%s-v%d", stacksetName, i) 80 | require.True(t, stackExists(stacksetName, fmt.Sprintf("v%d", i))) 81 | _, err := waitForDeployment(t, deploymentName) 82 | require.NoError(t, err) 83 | } 84 | 85 | // verify that the first 2 created stacks have been deleted 86 | for i := 0; i < 2; i++ { 87 | deploymentName := fmt.Sprintf("%s-v%d", stacksetName, i) 88 | err := resourceDeleted(t, "stack", deploymentName, deploymentInterface()).withTimeout(time.Second * 60).await() 89 | require.NoError(t, err) 90 | require.False(t, stackExists(stacksetName, fmt.Sprintf("v%d", i))) 91 | } 92 | } 93 | 94 | func TestStackTTLWithExternalIngress(t *testing.T) { 95 | t.Parallel() 96 | stacksetName := "stackset-ttl-external-ingress" 97 | specFactory := NewTestStacksetSpecFactory(stacksetName).StackGC(3, 15).ExternalIngress() 98 | 99 | // Create 6 stacks each with an ingress 100 | for i := 0; i < 6; i++ { 101 | stackVersion := fmt.Sprintf("v%d", i) 102 | var err error 103 | spec := specFactory.Create(t, stackVersion) 104 | if !stacksetExists(stacksetName) { 105 | err = createStackSet(stacksetName, 1, spec) 106 | } else { 107 | err = updateStackSet(stacksetName, spec) 108 | } 109 | require.NoError(t, err) 110 | _, err = waitForStack(t, stacksetName, stackVersion) 111 | require.NoError(t, err) 112 | fullStackName := fmt.Sprintf("%s-%s", stacksetName, stackVersion) 113 | 114 | // once the stack is created switch full traffic to it 115 | newWeight := map[string]float64{fullStackName: 100} 116 | err = setDesiredTrafficWeightsStackset(stacksetName, newWeight) 117 | require.NoError(t, err) 118 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, newWeight, nil).withTimeout(10 * time.Minute).await() 119 | require.NoError(t, err) 120 | } 121 | 122 | // verify that only the last 4 created stacks are present 123 | for i := 2; i < 6; i++ { 124 | deploymentName := fmt.Sprintf("%s-v%d", stacksetName, i) 125 | require.True(t, stackExists(stacksetName, fmt.Sprintf("v%d", i))) 126 | _, err := waitForDeployment(t, deploymentName) 127 | require.NoError(t, err) 128 | } 129 | 130 | // verify that the first 2 created stacks have been deleted 131 | for i := 0; i < 2; i++ { 132 | deploymentName := fmt.Sprintf("%s-v%d", stacksetName, i) 133 | err := resourceDeleted(t, "stack", deploymentName, deploymentInterface()).withTimeout(time.Second * 60).await() 134 | require.NoError(t, err) 135 | require.False(t, stackExists(stacksetName, fmt.Sprintf("v%d", i))) 136 | } 137 | } 138 | 139 | // TestStackTTLForLatestStack tests that the latest stack gets scaled down and isn't treated differently 140 | func TestStackTTLForLatestStack(t *testing.T) { 141 | t.Parallel() 142 | stacksetName := "stackset-ttl-last-stack" 143 | specFactory := NewTestStacksetSpecFactory(stacksetName).StackGC(1, 15).Ingress() 144 | 145 | // Create 2 stacks in total and wait for their deployments to come up 146 | for i := 0; i < 2; i++ { 147 | stackVersion := fmt.Sprintf("v%d", i) 148 | var err error 149 | spec := specFactory.Create(t, stackVersion) 150 | if !stacksetExists(stacksetName) { 151 | err = createStackSet(stacksetName, 1, spec) 152 | } else { 153 | err = updateStackSet(stacksetName, spec) 154 | } 155 | require.NoError(t, err) 156 | 157 | _, err = waitForStack(t, stacksetName, stackVersion) 158 | require.NoError(t, err) 159 | 160 | fullStackName := fmt.Sprintf("%s-%s", stacksetName, stackVersion) 161 | _, err = waitForIngress(t, fullStackName) 162 | require.NoError(t, err) 163 | 164 | if i == 0 { 165 | // Explicitly switch traffic to the first stack 166 | newWeight := map[string]float64{fullStackName: 100} 167 | err = setDesiredTrafficWeightsStackset(stacksetName, newWeight) 168 | require.NoError(t, err) 169 | 170 | err = trafficWeightsUpdatedStackset(t, stacksetName, weightKindActual, newWeight, nil).withTimeout(10 * time.Minute).await() 171 | require.NoError(t, err) 172 | } 173 | } 174 | 175 | // verify that the 1st stack exists and the latest stack was scaled down 176 | stackVersion := 0 177 | require.True(t, stackExists(stacksetName, fmt.Sprintf("v%d", stackVersion))) 178 | 179 | stackVersion = 1 180 | fullStackName := fmt.Sprintf("%s-v%d", stacksetName, stackVersion) 181 | 182 | err := stackStatusMatches(t, fullStackName, expectedStackStatus{ 183 | replicas: pint32(0), 184 | }).await() 185 | require.NoError(t, err) 186 | } 187 | -------------------------------------------------------------------------------- /cmd/stackset-controller/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/alecthomas/kingpin/v2" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | log "github.com/sirupsen/logrus" 17 | "github.com/zalando-incubator/stackset-controller/controller" 18 | "github.com/zalando-incubator/stackset-controller/pkg/clientset" 19 | "github.com/zalando-incubator/stackset-controller/pkg/traffic" 20 | corev1 "k8s.io/api/core/v1" 21 | "k8s.io/client-go/rest" 22 | "k8s.io/client-go/transport" 23 | ) 24 | 25 | const ( 26 | defaultInterval = "10s" 27 | defaultIngressSourceSwitchTTL = "5m" 28 | defaultMetricsAddress = ":7979" 29 | defaultClientGOTimeout = 30 * time.Second 30 | defaultReconcileWorkers = "10" 31 | ) 32 | 33 | var ( 34 | config struct { 35 | Debug bool 36 | Interval time.Duration 37 | APIServer *url.URL 38 | Namespace string 39 | MetricsAddress string 40 | ClusterDomains []string 41 | NoTrafficScaledownTTL time.Duration 42 | ControllerID string 43 | BackendWeightsAnnotationKey string 44 | RouteGroupSupportEnabled bool 45 | SyncIngressAnnotations []string 46 | ReconcileWorkers int 47 | ConfigMapSupportEnabled bool 48 | SecretSupportEnabled bool 49 | PCSSupportEnabled bool 50 | } 51 | ) 52 | 53 | func main() { 54 | kingpin.Flag("debug", "Enable debug logging.").BoolVar(&config.Debug) 55 | kingpin.Flag("interval", "Interval between syncing stacksets."). 56 | Default(defaultInterval).DurationVar(&config.Interval) 57 | kingpin.Flag("apiserver", "API server url.").URLVar(&config.APIServer) 58 | kingpin.Flag("namespace", "Limit scope to a particular namespace.").Default(corev1.NamespaceAll).StringVar(&config.Namespace) 59 | kingpin.Flag("metrics-address", "defines where to serve metrics").Default(defaultMetricsAddress).StringVar(&config.MetricsAddress) 60 | kingpin.Flag("controller-id", "ID of the controller used to determine ownership of StackSet resources").StringVar(&config.ControllerID) 61 | kingpin.Flag("reconcile-workers", "The amount of stacksets to reconcile in parallel at a time."). 62 | Default(defaultReconcileWorkers).IntVar(&config.ReconcileWorkers) 63 | kingpin.Flag("backend-weights-key", "Backend weights annotation key the controller will use to set current traffic values").Default(traffic.DefaultBackendWeightsAnnotationKey).StringVar(&config.BackendWeightsAnnotationKey) 64 | kingpin.Flag("cluster-domain", "Main domains of the cluster, used for generating Stack Ingress hostnames").Envar("CLUSTER_DOMAIN").Required().StringsVar(&config.ClusterDomains) 65 | kingpin.Flag("enable-routegroup-support", "Enable support for RouteGroups on StackSets.").Default("false").BoolVar(&config.RouteGroupSupportEnabled) 66 | kingpin.Flag( 67 | "sync-ingress-annotation", 68 | "Ingress/RouteGroup annotation to propagate to all traffic segments.", 69 | ).StringsVar(&config.SyncIngressAnnotations) 70 | kingpin.Flag("enable-configmap-support", "Enable support for ConfigMaps on StackSets.").Default("false").BoolVar(&config.ConfigMapSupportEnabled) 71 | kingpin.Flag("enable-secret-support", "Enable support for Secrets on StackSets.").Default("false").BoolVar(&config.SecretSupportEnabled) 72 | kingpin.Flag("enable-pcs-support", "Enable support for PlatformCredentialsSet on StackSets.").Default("false").BoolVar(&config.PCSSupportEnabled) 73 | kingpin.Parse() 74 | 75 | if config.Debug { 76 | log.SetLevel(log.DebugLevel) 77 | } 78 | 79 | ctx, cancel := context.WithCancel(context.Background()) 80 | kubeConfig, err := configureKubeConfig(config.APIServer, defaultClientGOTimeout, ctx.Done()) 81 | if err != nil { 82 | log.Fatalf("Failed to setup Kubernetes config: %v", err) 83 | } 84 | 85 | client, err := clientset.NewForConfig(kubeConfig) 86 | if err != nil { 87 | log.Fatalf("Failed to initialize Kubernetes client: %v", err) 88 | } 89 | 90 | controller, err := controller.NewStackSetController( 91 | client, 92 | config.Namespace, 93 | config.ControllerID, 94 | config.ReconcileWorkers, 95 | config.BackendWeightsAnnotationKey, 96 | config.ClusterDomains, 97 | prometheus.DefaultRegisterer, 98 | config.Interval, 99 | config.RouteGroupSupportEnabled, 100 | config.SyncIngressAnnotations, 101 | config.ConfigMapSupportEnabled, 102 | config.SecretSupportEnabled, 103 | config.PCSSupportEnabled, 104 | ) 105 | if err != nil { 106 | log.Fatalf("Failed to create Stackset controller: %v", err) 107 | } 108 | 109 | go handleSigterm(cancel) 110 | go serveMetrics(config.MetricsAddress) 111 | err = controller.Run(ctx) 112 | if err != nil { 113 | cancel() 114 | log.Fatalf("Failed to run controller: %v", err) 115 | } 116 | } 117 | 118 | // handleSigterm handles SIGTERM signal sent to the process. 119 | func handleSigterm(cancelFunc func()) { 120 | signals := make(chan os.Signal, 1) 121 | signal.Notify(signals, syscall.SIGTERM) 122 | <-signals 123 | log.Info("Received Term signal. Terminating...") 124 | cancelFunc() 125 | } 126 | 127 | // configureKubeConfig configures a kubeconfig. 128 | func configureKubeConfig(apiServerURL *url.URL, timeout time.Duration, stopCh <-chan struct{}) (*rest.Config, error) { 129 | tr := &http.Transport{ 130 | DialContext: (&net.Dialer{ 131 | Timeout: timeout, 132 | KeepAlive: 30 * time.Second, 133 | DualStack: false, // K8s do not work well with IPv6 134 | }).DialContext, 135 | TLSHandshakeTimeout: timeout, 136 | ResponseHeaderTimeout: 10 * time.Second, 137 | MaxIdleConns: 10, 138 | MaxIdleConnsPerHost: 2, 139 | IdleConnTimeout: 20 * time.Second, 140 | } 141 | 142 | // We need this to reliably fade on DNS change, which is right 143 | // now not fixed with IdleConnTimeout in the http.Transport. 144 | // https://github.com/golang/go/issues/23427 145 | go func(d time.Duration) { 146 | for { 147 | select { 148 | case <-time.After(d): 149 | tr.CloseIdleConnections() 150 | case <-stopCh: 151 | return 152 | } 153 | } 154 | }(20 * time.Second) 155 | 156 | if apiServerURL != nil { 157 | return &rest.Config{ 158 | Host: apiServerURL.String(), 159 | Timeout: timeout, 160 | Transport: tr, 161 | QPS: 100.0, 162 | Burst: 500, 163 | }, nil 164 | } 165 | 166 | config, err := rest.InClusterConfig() 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | // patch TLS config 172 | restTransportConfig, err := config.TransportConfig() 173 | if err != nil { 174 | return nil, err 175 | } 176 | restTLSConfig, err := transport.TLSConfigFor(restTransportConfig) 177 | if err != nil { 178 | return nil, err 179 | } 180 | tr.TLSClientConfig = restTLSConfig 181 | 182 | config.Timeout = timeout 183 | config.Transport = tr 184 | config.QPS = 100.0 185 | config.Burst = 500 186 | // disable TLSClientConfig to make the custom Transport work 187 | config.TLSClientConfig = rest.TLSClientConfig{} 188 | return config, nil 189 | } 190 | 191 | // gather go metrics 192 | func serveMetrics(address string) { 193 | http.Handle("/metrics", promhttp.Handler()) 194 | log.Fatal(http.ListenAndServe(address, nil)) 195 | } 196 | -------------------------------------------------------------------------------- /cmd/traffic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "text/tabwriter" 8 | 9 | "github.com/alecthomas/kingpin" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/zalando-incubator/stackset-controller/pkg/clientset" 12 | "github.com/zalando-incubator/stackset-controller/pkg/traffic" 13 | rest "k8s.io/client-go/rest" 14 | "k8s.io/client-go/tools/clientcmd" 15 | ) 16 | 17 | const ( 18 | defaultNamespace = "default" 19 | ) 20 | 21 | var ( 22 | config struct { 23 | Stackset string 24 | Stack string 25 | Traffic float64 26 | Namespace string 27 | BackendWeightsAnnotationKey string 28 | } 29 | ) 30 | 31 | func main() { 32 | kingpin.Arg("stackset", "help").Required().StringVar(&config.Stackset) 33 | kingpin.Arg("stack", "help").StringVar(&config.Stack) 34 | kingpin.Arg("traffic", "help").Default("-1").Float64Var(&config.Traffic) 35 | kingpin.Flag("namespace", "Namespace of the stackset resource.").Default(defaultNamespace).StringVar(&config.Namespace) 36 | kingpin.Flag("backend-weights-key", "Backend weights annotation key the controller will use to set current traffic values").Default(traffic.DefaultBackendWeightsAnnotationKey).StringVar(&config.BackendWeightsAnnotationKey) 37 | kingpin.Parse() 38 | 39 | kubeconfig, err := newKubeConfig() 40 | if err != nil { 41 | log.Fatalf("Failed to setup Kubernetes client: %v.", err) 42 | } 43 | 44 | client, err := clientset.NewForConfig(kubeconfig) 45 | if err != nil { 46 | log.Fatalf("Failed to initialize Kubernetes client: %v.", err) 47 | } 48 | 49 | trafficSwitcher := traffic.NewSwitcher(client, config.BackendWeightsAnnotationKey) 50 | 51 | ctx := context.Background() 52 | 53 | if config.Stack != "" && config.Traffic != -1 { 54 | weight := config.Traffic 55 | if weight < 0 || weight > 100 { 56 | log.Fatalf("Traffic weight must be between 0 and 100.") 57 | } 58 | 59 | stacks, err := trafficSwitcher.Switch(ctx, config.Stackset, config.Stack, config.Namespace, weight) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | printTrafficTable(stacks) 64 | return 65 | } 66 | 67 | stacks, err := trafficSwitcher.TrafficWeights(ctx, config.Stackset, config.Namespace) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | printTrafficTable(stacks) 72 | } 73 | 74 | func printTrafficTable(stacks []traffic.StackTrafficWeight) { 75 | w := tabwriter.NewWriter(os.Stdout, 8, 8, 4, ' ', 0) 76 | fmt.Fprintf(w, "%s\t%s\t%s\n", "STACK", "DESIRED TRAFFIC", "ACTUAL TRAFFIC") 77 | 78 | for _, stack := range stacks { 79 | fmt.Fprintf(w, 80 | "%s\t%s\t%s\n", 81 | stack.Name, 82 | fmt.Sprintf("%.1f%%", stack.Weight), 83 | fmt.Sprintf("%.1f%%", stack.ActualWeight), 84 | ) 85 | } 86 | 87 | w.Flush() 88 | } 89 | 90 | func newKubeConfig() (*rest.Config, error) { 91 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 92 | configOverrides := &clientcmd.ConfigOverrides{} 93 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) 94 | return kubeConfig.ClientConfig() 95 | } 96 | -------------------------------------------------------------------------------- /delivery.yaml: -------------------------------------------------------------------------------- 1 | version: "2017-09-20" 2 | pipeline: 3 | - id: build 4 | vm_config: 5 | type: linux 6 | image: "cdp-runtime/go" 7 | size: large 8 | type: script 9 | env: 10 | GOFLAGS: "-mod=readonly" 11 | cache: 12 | paths: 13 | - /go/pkg/mod # pkg cache for Go modules 14 | - ~/.cache/go-build # Go build cache 15 | commands: 16 | - desc: test 17 | cmd: | 18 | go mod tidy 19 | if ! git diff --quiet go.mod go.sum; then 20 | echo "Running go mod tidy modified go.mod and/or go.sum" 21 | exit 1 22 | fi 23 | make check 24 | make test 25 | - desc: build 26 | cmd: | 27 | make build.docker 28 | - desc: push 29 | cmd: | 30 | IMAGE=registry-write.opensource.zalan.do/teapot/stackset-controller-test 31 | IMAGE=$IMAGE VERSION=$CDP_BUILD_VERSION make build.push 32 | - desc: Build and push image to Zalando's registry 33 | cmd: | 34 | IMAGE=container-registry-test.zalando.net/teapot/stackset-controller 35 | make build.linux.amd64 build.linux.arm64 build/linux/amd64/e2e build/linux/arm64/e2e 36 | 37 | docker buildx create --config /etc/cdp-buildkitd.toml --driver-opt network=host --bootstrap --use 38 | docker buildx build --rm --build-arg BASE_IMAGE=container-registry.zalando.net/library/static:latest -t "${IMAGE}:${CDP_BUILD_VERSION}" --platform linux/amd64,linux/arm64 --push . 39 | docker buildx build --rm --build-arg BASE_IMAGE=container-registry.zalando.net/library/static:latest -t "${IMAGE}-e2e:${CDP_BUILD_VERSION}" -f Dockerfile.e2e --platform linux/amd64,linux/arm64 --push . 40 | 41 | - id: e2e 42 | type: process 43 | desc: "E2E Tests" 44 | target: stups-test 45 | process: microservice_standard_test 46 | config: 47 | apply_manifests: 48 | env: 49 | - name: APPLICATION 50 | value: stackset-controller-test 51 | - name: DEPLOYMENT_PATH 52 | value: e2e 53 | - name: IMAGE 54 | value: "container-registry-test.zalando.net/teapot/stackset-controller:#{CDP_BUILD_VERSION}" 55 | - name: CONTROLLER_ID 56 | value: "#{CDP_BUILD_VERSION}" 57 | - name: CLUSTER_DOMAIN 58 | value: stups-test.zalan.do 59 | - name: CLUSTER_DOMAIN_INTERNAL 60 | value: ingress.cluster.local 61 | end2end_tests: 62 | metadata: 63 | name: e2e 64 | spec: 65 | serviceAccountName: stackset-controller 66 | restartPolicy: Never 67 | containers: 68 | - name: e2e 69 | image: "container-registry-test.zalando.net/teapot/stackset-controller-e2e:#{CDP_BUILD_VERSION}" 70 | args: 71 | - "-test.parallel" 72 | - "64" 73 | env: 74 | - name: "CONTROLLER_ID" 75 | value: "#{CDP_BUILD_VERSION}" 76 | - name: CLUSTER_DOMAIN 77 | value: stups-test.zalan.do 78 | - name: CLUSTER_DOMAIN_INTERNAL 79 | value: ingress.cluster.local 80 | - name: "E2E_NAMESPACE" 81 | valueFrom: 82 | fieldRef: 83 | fieldPath: metadata.namespace 84 | - id: buildprod 85 | when: 86 | branch: master 87 | vm_config: 88 | type: linux 89 | image: "cdp-runtime/go" 90 | size: large 91 | type: script 92 | env: 93 | GOFLAGS: "-mod=readonly" 94 | cache: 95 | paths: 96 | - /go/pkg/mod # pkg cache for Go modules 97 | - ~/.cache/go-build # Go build cache 98 | commands: 99 | - desc: create tag and release 100 | cmd: | 101 | LATEST_VERSION=$(git describe --tags --always | awk -F \- '{print $1}') 102 | CUR_PART=$(echo $LATEST_VERSION | awk -F . '{print $1"."$2}') 103 | VERSION_PART=$(cat VERSION) 104 | OLD_PATCH=$(echo $LATEST_VERSION | awk -F . '{print $3}') 105 | NEW_PATCH=$((OLD_PATCH + 1)) 106 | if [ "$CUR_PART" != "$VERSION_PART" ]; then NEW_PATCH=0; fi 107 | VERSION=${VERSION_PART}.${NEW_PATCH} 108 | echo "Creating release for tag: ${VERSION}" 109 | git gh-tag $VERSION 110 | echo "create and push docker container" 111 | IMAGE=registry-write.opensource.zalan.do/teapot/stackset-controller 112 | IMAGE=$IMAGE VERSION=$VERSION make build.docker 113 | git diff --stat --exit-code 114 | IMAGE=$IMAGE VERSION=$VERSION make build.push 115 | 116 | # Build and push image to Zalando's registry 117 | IMAGE=container-registry-test.zalando.net/teapot/stackset-controller 118 | make build.linux.amd64 build.linux.arm64 119 | 120 | docker buildx create --config /etc/cdp-buildkitd.toml --driver-opt network=host --bootstrap --use 121 | docker buildx build --rm --build-arg BASE_IMAGE=container-registry.zalando.net/library/static:latest -t "${IMAGE}:${VERSION}" --platform linux/amd64,linux/arm64 --push . 122 | cdp-promote-image "${IMAGE}:${VERSION}" 123 | 124 | echo "create release page" 125 | tf=$(mktemp) 126 | echo -e "### Changes\n" >$tf 127 | git log -1 --pretty=%B | grep -v 'Signed-off-by:' | grep -v -E '^\s*$' | grep -vE '^\*' >>$tf 128 | echo -e "\n### Docker image\n" >>$tf 129 | echo -e "Docker image is available in Zalando's Open Source registry:\n" >>$tf 130 | echo -e '```' >>$tf 131 | echo -e "docker run -it registry.opensource.zalan.do/teapot/stackset-controller:${VERSION} --help" >>$tf 132 | echo -e '```' >>$tf 133 | echo "################################" 134 | cat $tf 135 | echo "################################" 136 | git gh-release --message-from-file "${tf}" $VERSION 137 | -------------------------------------------------------------------------------- /docs/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: stackset-controller 5 | namespace: kube-system 6 | labels: 7 | application: stackset-controller 8 | version: latest 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | application: stackset-controller 14 | template: 15 | metadata: 16 | labels: 17 | application: stackset-controller 18 | version: latest 19 | spec: 20 | serviceAccountName: stackset-controller 21 | containers: 22 | - name: stackset-controller 23 | image: registry.opensource.zalan.do/teapot/stackset-controller:v1.3.37 24 | # please adjust this for your environment 25 | # the cluster-domain must match the application domain suffix 26 | # Example application domain: my-app.example.org 27 | args: ["--cluster-domain=example.org"] 28 | resources: 29 | limits: 30 | cpu: 10m 31 | memory: 100Mi 32 | requests: 33 | cpu: 10m 34 | memory: 100Mi 35 | -------------------------------------------------------------------------------- /docs/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: stackset-controller 6 | namespace: kube-system 7 | --- 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | name: stackset-controller 12 | rules: 13 | - apiGroups: 14 | - "zalando.org" 15 | resources: 16 | - stacks 17 | - stacks/status 18 | - stacksets 19 | - stacksets/status 20 | verbs: 21 | - get 22 | - list 23 | - watch 24 | - create 25 | - update 26 | - patch 27 | - delete 28 | - apiGroups: 29 | - "apps" 30 | resources: 31 | - deployments 32 | verbs: 33 | - get 34 | - list 35 | - create 36 | - update 37 | - patch 38 | - delete 39 | - apiGroups: 40 | - "networking.k8s.io" 41 | resources: 42 | - ingresses 43 | verbs: 44 | - get 45 | - list 46 | - create 47 | - update 48 | - patch 49 | - delete 50 | - apiGroups: 51 | - zalando.org 52 | resources: 53 | - routegroups 54 | verbs: 55 | - get 56 | - list 57 | - create 58 | - update 59 | - patch 60 | - delete 61 | - apiGroups: 62 | - "" 63 | resources: 64 | - services 65 | verbs: 66 | - get 67 | - list 68 | - create 69 | - update 70 | - patch 71 | - delete 72 | - apiGroups: 73 | - "autoscaling" 74 | resources: 75 | - horizontalpodautoscalers 76 | verbs: 77 | - get 78 | - list 79 | - create 80 | - update 81 | - patch 82 | - delete 83 | - apiGroups: 84 | - "" 85 | resources: 86 | - events 87 | verbs: 88 | - get 89 | - list 90 | - watch 91 | - create 92 | - update 93 | - patch 94 | --- 95 | apiVersion: rbac.authorization.k8s.io/v1 96 | kind: ClusterRoleBinding 97 | metadata: 98 | name: stackset-controller 99 | roleRef: 100 | apiGroup: rbac.authorization.k8s.io 101 | kind: ClusterRole 102 | name: stackset-controller 103 | subjects: 104 | - kind: ServiceAccount 105 | name: stackset-controller 106 | namespace: kube-system 107 | -------------------------------------------------------------------------------- /docs/stackset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: zalando.org/v1 2 | kind: StackSet 3 | metadata: 4 | name: my-app 5 | spec: 6 | ingress: 7 | hosts: 8 | - my-app.example.org 9 | - alt.name.org 10 | backendPort: 80 11 | traffic: 12 | - stackName: my-app-v1 13 | weight: 40 14 | - stackName: my-app-v2 15 | weight: 60 16 | minReadyPercent: 90 17 | stackLifecycle: 18 | scaledownTTLSeconds: 300 19 | limit: 5 20 | stackTemplate: 21 | spec: 22 | version: v1 23 | replicas: 3 24 | horizontalPodAutoscaler: 25 | minReplicas: 3 26 | maxReplicas: 10 27 | metrics: 28 | - type: Resource 29 | resource: 30 | name: cpu 31 | targetAverageUtilization: 50 32 | podTemplate: 33 | spec: 34 | containers: 35 | - name: skipper 36 | image: ghcr.io/zalando/skipper:latest 37 | args: 38 | - skipper 39 | - -inline-routes 40 | - '* -> inlineContent("OK") -> ' 41 | - -address=:80 42 | ports: 43 | - containerPort: 80 44 | name: ingress 45 | resources: 46 | limits: 47 | cpu: 10m 48 | memory: 50Mi 49 | requests: 50 | cpu: 10m 51 | memory: 50Mi 52 | -------------------------------------------------------------------------------- /e2e/apply/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: "{{{APPLICATION}}}" 5 | annotations: 6 | downscaler/exclude: "true" 7 | labels: 8 | application: "{{{APPLICATION}}}" 9 | version: "{{{CDP_BUILD_VERSION}}}" 10 | spec: 11 | replicas: 1 12 | selector: 13 | matchLabels: 14 | application: "{{{APPLICATION}}}" 15 | template: 16 | metadata: 17 | labels: 18 | application: "{{{APPLICATION}}}" 19 | version: "{{{CDP_BUILD_VERSION}}}" 20 | spec: 21 | serviceAccountName: stackset-controller 22 | containers: 23 | - name: "{{{APPLICATION}}}" 24 | image: "{{{IMAGE}}}" 25 | args: 26 | - "--namespace={{{CDP_DEPLOYMENT_ID}}}" 27 | - "--controller-id={{{CONTROLLER_ID}}}" 28 | - "--cluster-domain={{{CLUSTER_DOMAIN}}}" 29 | - "--cluster-domain={{{CLUSTER_DOMAIN_INTERNAL}}}" 30 | - "--enable-configmap-support" 31 | - "--enable-secret-support" 32 | - "--enable-routegroup-support" 33 | - "--enable-pcs-support" 34 | - "--sync-ingress-annotation=example.org/i-haz-synchronize" 35 | - "--sync-ingress-annotation=teapot.org/the-best" 36 | resources: 37 | limits: 38 | cpu: 10m 39 | memory: 100Mi 40 | requests: 41 | cpu: 10m 42 | memory: 100Mi 43 | securityContext: 44 | runAsNonRoot: true 45 | runAsUser: 65534 46 | readOnlyRootFilesystem: true 47 | capabilities: 48 | drop: ["ALL"] 49 | -------------------------------------------------------------------------------- /e2e/apply/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: stackset-controller 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: Role 9 | metadata: 10 | name: stackset-controller 11 | rules: 12 | - apiGroups: 13 | - "zalando.org" 14 | resources: 15 | - stacks 16 | - stacks/status 17 | - stacksets 18 | - stacksets/status 19 | verbs: 20 | - get 21 | - list 22 | - watch 23 | - create 24 | - update 25 | - patch 26 | - delete 27 | - apiGroups: 28 | - "apps" 29 | resources: 30 | - deployments 31 | verbs: 32 | - get 33 | - list 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "networking.k8s.io" 40 | resources: 41 | - ingresses 42 | verbs: 43 | - get 44 | - list 45 | - create 46 | - update 47 | - patch 48 | - delete 49 | - apiGroups: 50 | - "zalando.org" 51 | resources: 52 | - routegroups 53 | verbs: 54 | - get 55 | - list 56 | - create 57 | - update 58 | - patch 59 | - delete 60 | - apiGroups: 61 | - "" 62 | resources: 63 | - services 64 | verbs: 65 | - get 66 | - list 67 | - create 68 | - update 69 | - patch 70 | - delete 71 | - apiGroups: 72 | - "autoscaling" 73 | resources: 74 | - horizontalpodautoscalers 75 | verbs: 76 | - get 77 | - list 78 | - create 79 | - update 80 | - patch 81 | - delete 82 | - apiGroups: 83 | - "" 84 | resources: 85 | - configmaps 86 | verbs: 87 | - get 88 | - list 89 | - create 90 | - update 91 | - patch 92 | - delete 93 | - apiGroups: 94 | - "" 95 | resources: 96 | - secrets 97 | verbs: 98 | - get 99 | - list 100 | - create 101 | - update 102 | - patch 103 | - delete 104 | - apiGroups: 105 | - "zalando.org" 106 | resources: 107 | - platformcredentialssets 108 | verbs: 109 | - get 110 | - list 111 | - create 112 | - update 113 | - patch 114 | - delete 115 | - apiGroups: 116 | - "" 117 | resources: 118 | - events 119 | verbs: 120 | - get 121 | - list 122 | - watch 123 | - create 124 | - update 125 | - patch 126 | --- 127 | apiVersion: rbac.authorization.k8s.io/v1 128 | kind: RoleBinding 129 | metadata: 130 | name: stackset-controller-e2e 131 | roleRef: 132 | apiGroup: rbac.authorization.k8s.io 133 | kind: Role 134 | name: stackset-controller 135 | subjects: 136 | - kind: ServiceAccount 137 | name: stackset-controller 138 | -------------------------------------------------------------------------------- /e2e/apply/sample.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: zalando.org/v1 2 | kind: StackSet 3 | metadata: 4 | name: e2e-deploy-sample 5 | labels: 6 | application: "kubernetes" 7 | component: "e2e-deploy-sample" 8 | annotations: 9 | "stackset-controller.zalando.org/controller": "{{{CONTROLLER_ID}}}" 10 | spec: 11 | ingress: 12 | hosts: 13 | - "e2e-deploy-sample.{{{CLUSTER_DOMAIN}}}" 14 | backendPort: 80 15 | stackLifecycle: 16 | scaledownTTLSeconds: 300 17 | limit: 3 18 | stackTemplate: 19 | spec: 20 | version: "{{{CDP_BUILD_VERSION}}}" 21 | replicas: 2 22 | configurationResources: 23 | - configMapRef: 24 | name: e2e-deploy-sample-{{{CDP_BUILD_VERSION}}}-my-reference-config 25 | - secretRef: 26 | name: e2e-deploy-sample-{{{CDP_BUILD_VERSION}}}-my-reference-secret 27 | - platformCredentialsSet: 28 | name: e2e-deploy-sample-{{{CDP_BUILD_VERSION}}}-my-pcs 29 | tokens: 30 | sample-token: 31 | privileges: [] 32 | autoscaler: 33 | minReplicas: 2 34 | maxReplicas: 2 35 | metrics: 36 | - type: CPU 37 | averageUtilization: 50 38 | - type: Ingress 39 | average: 20000m 40 | podTemplate: 41 | spec: 42 | containers: 43 | - name: skipper 44 | image: ghcr.io/zalando/skipper:v0.15.33 45 | args: 46 | - skipper 47 | - -inline-routes 48 | - '* -> inlineContent("OK") -> ' 49 | - -address=:80 50 | ports: 51 | - containerPort: 80 52 | resources: 53 | requests: 54 | cpu: 1m 55 | memory: 100Mi 56 | limits: 57 | cpu: 1m 58 | memory: 100Mi 59 | volumeMounts: 60 | - name: my-reference-config 61 | mountPath: /etc/my-reference-config 62 | readOnly: true 63 | - name: my-reference-secret 64 | mountPath: /etc/my-reference-secret 65 | readOnly: true 66 | - name: my-pcs 67 | mountPath: /etc/my-pcs 68 | readOnly: true 69 | volumes: 70 | - name: my-reference-config 71 | configMap: 72 | name: e2e-deploy-sample-{{{CDP_BUILD_VERSION}}}-my-reference-config 73 | - name: my-reference-secret 74 | secret: 75 | secretName: e2e-deploy-sample-{{{CDP_BUILD_VERSION}}}-my-reference-secret 76 | - name: my-pcs 77 | secret: 78 | secretName: e2e-deploy-sample-{{{CDP_BUILD_VERSION}}}-my-pcs 79 | 80 | --- 81 | 82 | apiVersion: v1 83 | kind: ConfigMap 84 | metadata: 85 | name: e2e-deploy-sample-{{{CDP_BUILD_VERSION}}}-my-reference-config 86 | labels: 87 | application: "e2e-deploy-sample" 88 | data: 89 | something_is: configured_by_reference 90 | 91 | --- 92 | 93 | apiVersion: v1 94 | kind: Secret 95 | metadata: 96 | name: e2e-deploy-sample-{{{CDP_BUILD_VERSION}}}-my-reference-secret 97 | labels: 98 | application: "e2e-deploy-sample" 99 | data: 100 | something_is: c2VjcmV0bHlfY29uZmlndXJlZF9ieV9yZWZlcmVuY2U= 101 | -------------------------------------------------------------------------------- /e2e/run_e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euf -o pipefail 4 | shopt -s nullglob 5 | 6 | CLUSTER_DOMAIN=${CLUSTER_DOMAIN:-""} 7 | CLUSTER_DOMAIN_INTERNAL=${CLUSTER_DOMAIN_INTERNAL:-""} 8 | CLUSTER_NAME=${CLUSTER_NAME:-""} 9 | # Set TEST_NAME to run a single test 10 | # eg.: TEST_NAME=TestIngressToRouteGroupSwitch ./e2e/run_e2e.sh 11 | TEST_NAME=${TEST_NAME:-""} 12 | TEST_ARGS=${@:-""} 13 | CONTROLLER_ID="ssc-e2e-$(dd if=/dev/urandom bs=8 count=1 2>/dev/null | hexdump -e '"%x"')" 14 | 15 | if [[ -z "${CLUSTER_DOMAIN}" ]]; then 16 | echo "Please specify the cluster domain via CLUSTER_DOMAIN." 17 | exit 0 18 | fi 19 | 20 | if [[ -z "${CLUSTER_DOMAIN_INTERNAL}" ]]; then 21 | echo "Please specify the cluster domain via CLUSTER_DOMAIN_INTERNAL." 22 | exit 0 23 | fi 24 | 25 | if [[ -z "${CLUSTER_NAME}" ]]; then 26 | echo "Please specify the name of the cluster via CLUSTER_NAME." 27 | exit 0 28 | fi 29 | 30 | # Build the controller and its end-to-end tests. 31 | make build.local build/e2e 32 | 33 | cleanup() { 34 | zkubectl delete namespace $CONTROLLER_ID --context $CLUSTER_NAME --wait=false 35 | kill -- -$$ 36 | } 37 | trap cleanup EXIT 38 | 39 | # Forward API server calls to the stups-test cluster. 40 | zkubectl login $CLUSTER_NAME 41 | zkubectl proxy& 42 | # Listens on 127.0.0.1:8001. 43 | 44 | # Generate a controller ID for this run. 45 | 46 | # We'll store the controller logs in a separate file to keep stdout clean. 47 | controllerLog="/tmp/ssc-log-$(date +%s).log" 48 | echo ">>> Writing controller logs in $controllerLog" 49 | 50 | # Find and run the controller locally. 51 | sscPath=$(find build/ -name "stackset-controller" | head -n 1) 52 | command $sscPath --apiserver=http://127.0.0.1:8001 \ 53 | --sync-ingress-annotation=example.org/i-haz-synchronize \ 54 | --sync-ingress-annotation=teapot.org/the-best \ 55 | --enable-configmap-support \ 56 | --enable-secret-support \ 57 | --enable-pcs-support \ 58 | --enable-routegroup-support \ 59 | --cluster-domain=${CLUSTER_DOMAIN} \ 60 | --cluster-domain=${CLUSTER_DOMAIN_INTERNAL} \ 61 | --controller-id=$CONTROLLER_ID 2>$controllerLog& 62 | 63 | # Create the Kubernetes namespace to be used for this test run. 64 | zkubectl create ns $CONTROLLER_ID 65 | 66 | test_args="-test.v" 67 | if [[ -z "${TEST_NAME}" ]]; then 68 | test_args="${test_args} ${TEST_ARGS} -test.parallel 64" 69 | else 70 | test_args="${test_args} ${TEST_ARGS} -test.parallel 1 -test.run=${TEST_NAME}" 71 | fi 72 | 73 | # Run the end-to-end tests against the controller we just deployed. 74 | # -count=1 disables go test caching. 75 | env E2E_NAMESPACE=$CONTROLLER_ID \ 76 | CONTROLLER_ID=$CONTROLLER_ID \ 77 | KUBECONFIG=$HOME/.kube/config \ 78 | build/e2e ${test_args} || true 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zalando-incubator/stackset-controller 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.5 6 | 7 | require ( 8 | github.com/alecthomas/kingpin v2.2.6+incompatible 9 | github.com/alecthomas/kingpin/v2 v2.4.0 10 | github.com/google/go-cmp v0.7.0 11 | github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb 12 | github.com/prometheus/client_golang v1.22.0 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/stretchr/testify v1.10.0 15 | github.com/szuecs/routegroup-client v0.28.2 16 | golang.org/x/sync v0.14.0 17 | k8s.io/api v0.32.4 18 | k8s.io/apimachinery v0.32.4 19 | k8s.io/client-go v0.32.4 20 | k8s.io/code-generator v0.32.4 21 | sigs.k8s.io/controller-tools v0.17.3 22 | sigs.k8s.io/yaml v1.4.0 23 | ) 24 | 25 | require ( 26 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 27 | github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 31 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 32 | github.com/fatih/color v1.18.0 // indirect 33 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 34 | github.com/go-logr/logr v1.4.2 // indirect 35 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 36 | github.com/go-openapi/jsonreference v0.21.0 // indirect 37 | github.com/go-openapi/swag v0.23.0 // indirect 38 | github.com/gobuffalo/flect v1.0.3 // indirect 39 | github.com/gogo/protobuf v1.3.2 // indirect 40 | github.com/golang/protobuf v1.5.4 // indirect 41 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 42 | github.com/google/gofuzz v1.2.0 // indirect 43 | github.com/google/uuid v1.6.0 // indirect 44 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/mailru/easyjson v0.7.7 // indirect 48 | github.com/mattn/go-colorable v0.1.13 // indirect 49 | github.com/mattn/go-isatty v0.0.20 // indirect 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 51 | github.com/modern-go/reflect2 v1.0.2 // indirect 52 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 53 | github.com/pkg/errors v0.9.1 // indirect 54 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 55 | github.com/prometheus/client_model v0.6.1 // indirect 56 | github.com/prometheus/common v0.62.0 // indirect 57 | github.com/prometheus/procfs v0.15.1 // indirect 58 | github.com/spf13/cobra v1.9.1 // indirect 59 | github.com/spf13/pflag v1.0.6 // indirect 60 | github.com/x448/float16 v0.8.4 // indirect 61 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 62 | golang.org/x/mod v0.23.0 // indirect 63 | golang.org/x/net v0.38.0 // indirect 64 | golang.org/x/oauth2 v0.24.0 // indirect 65 | golang.org/x/sys v0.31.0 // indirect 66 | golang.org/x/term v0.30.0 // indirect 67 | golang.org/x/text v0.23.0 // indirect 68 | golang.org/x/time v0.7.0 // indirect 69 | golang.org/x/tools v0.30.0 // indirect 70 | google.golang.org/protobuf v1.36.5 // indirect 71 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect 72 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 73 | gopkg.in/inf.v0 v0.9.1 // indirect 74 | gopkg.in/yaml.v2 v2.4.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | k8s.io/apiextensions-apiserver v0.32.2 // indirect 77 | k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect 78 | k8s.io/klog/v2 v2.130.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 80 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 81 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 82 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright YEAR The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /hack/crd/trim.go: -------------------------------------------------------------------------------- 1 | // This program removes descriptions from the input CRD yaml to reduce its size. 2 | // 3 | // # Why 4 | // 5 | // When CRD is applied via `kubectl apply -f docs/stackset_crd.yaml` (aka client-side apply) kubectl stores 6 | // CRD content into the kubectl.kubernetes.io/last-applied-configuration annotation which has 7 | // size limit of 262144 bytes. 8 | // If the size of the annotation exceeds the limit, kubectl will fail with the following error: 9 | // 10 | // The CustomResourceDefinition "stacksets.zalando.org" is invalid: metadata.annotations: Too long: must have at most 262144 bytes 11 | // 12 | // See https://github.com/kubernetes/kubectl/issues/712 13 | // 14 | // The CRD contains a lot of descriptions for k8s.io types and controller-gen 15 | // does not allow to skip descriptions per field or per package, 16 | // see https://github.com/kubernetes-sigs/controller-tools/issues/441 17 | // 18 | // # How 19 | // 20 | // It removes descriptions starting at the deepest level of the yaml tree 21 | // until the size of the yaml converted to json is less than the maximum allowed annotation size. 22 | package main 23 | 24 | import ( 25 | "encoding/json" 26 | "io" 27 | "log" 28 | "os" 29 | "sort" 30 | 31 | "sigs.k8s.io/yaml" 32 | ) 33 | 34 | const maxAnnotationSize = 262144 35 | 36 | func must(err error) { 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | } 41 | 42 | func mustGet[T any](v T, err error) T { 43 | must(err) 44 | return v 45 | } 46 | 47 | type description struct { 48 | depth int 49 | property map[string]any 50 | value string 51 | } 52 | 53 | func main() { 54 | yamlBytes := mustGet(io.ReadAll(os.Stdin)) 55 | 56 | o := make(map[string]any) 57 | must(yaml.Unmarshal(yamlBytes, &o)) 58 | 59 | jsonBytes := mustGet(json.Marshal(o)) 60 | size := len(jsonBytes) 61 | 62 | descriptions := collect(o, 0) 63 | 64 | sort.Slice(descriptions, func(i, j int) bool { 65 | if descriptions[i].depth == descriptions[j].depth { 66 | return len(descriptions[i].value) > len(descriptions[j].value) 67 | } 68 | return descriptions[i].depth > descriptions[j].depth 69 | }) 70 | 71 | for _, d := range descriptions { 72 | if size <= maxAnnotationSize { 73 | break 74 | } 75 | size -= len(d.value) 76 | delete(d.property, "description") 77 | } 78 | 79 | if size > maxAnnotationSize { 80 | log.Fatalf("YAML converted to JSON must be at most %d bytes long but it is %d bytes", maxAnnotationSize, size) 81 | } 82 | 83 | outYaml := mustGet(yaml.Marshal(o)) 84 | mustGet(os.Stdout.Write(outYaml)) 85 | } 86 | 87 | func collect(o any, depth int) (descriptions []description) { 88 | switch o := o.(type) { 89 | case map[string]any: 90 | for key, value := range o { 91 | switch value := value.(type) { 92 | case string: 93 | if key == "description" { 94 | descriptions = append(descriptions, description{depth, o, value}) 95 | } 96 | default: 97 | descriptions = append(descriptions, collect(value, depth+1)...) 98 | } 99 | } 100 | case []any: 101 | for _, item := range o { 102 | descriptions = append(descriptions, collect(item, depth+1)...) 103 | } 104 | } 105 | return descriptions 106 | } 107 | -------------------------------------------------------------------------------- /hack/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | /* 5 | Copyright 2019 The Kubernetes Authors. 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | http://www.apache.org/licenses/LICENSE-2.0 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 | // This package imports things required by build scripts, to force `go mod` to see them as dependencies 18 | package tools 19 | 20 | import ( 21 | _ "k8s.io/code-generator" 22 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" 23 | ) 24 | -------------------------------------------------------------------------------- /hack/update-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 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 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SRC="github.com" 22 | GOPKG="$SRC/zalando-incubator/stackset-controller" 23 | CUSTOM_RESOURCE_NAME="zalando.org" 24 | CUSTOM_RESOURCE_VERSION="v1" 25 | 26 | SCRIPT_ROOT="$(dirname "${BASH_SOURCE[0]}")/.." 27 | 28 | OUTPUT_DIR="pkg/client" 29 | OUTPUT_PKG="${GOPKG}/${OUTPUT_DIR}" 30 | APIS_PKG="${GOPKG}/pkg/apis" 31 | GROUPS_WITH_VERSIONS="${CUSTOM_RESOURCE_NAME}:${CUSTOM_RESOURCE_VERSION}" 32 | 33 | echo "Generating deepcopy funcs" 34 | go run k8s.io/code-generator/cmd/deepcopy-gen \ 35 | --output-file zz_generated.deepcopy.go \ 36 | --bounding-dirs "${APIS_PKG}" \ 37 | --go-header-file "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ 38 | "${APIS_PKG}/${CUSTOM_RESOURCE_NAME}/${CUSTOM_RESOURCE_VERSION}" 39 | 40 | echo "Generating clientset for ${GROUPS_WITH_VERSIONS} at ${OUTPUT_PKG}/${CLIENTSET_PKG_NAME:-clientset}" 41 | go run k8s.io/code-generator/cmd/client-gen \ 42 | --clientset-name versioned \ 43 | --input-base "" \ 44 | --input "${APIS_PKG}/${CUSTOM_RESOURCE_NAME}/${CUSTOM_RESOURCE_VERSION}" \ 45 | --output-pkg "${OUTPUT_PKG}/clientset" \ 46 | --go-header-file "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ 47 | --output-dir "${OUTPUT_DIR}/clientset" 48 | 49 | echo "Generating listers for ${GROUPS_WITH_VERSIONS} at ${OUTPUT_PKG}/listers" 50 | go run k8s.io/code-generator/cmd/lister-gen \ 51 | --output-pkg "${OUTPUT_PKG}/listers" \ 52 | --go-header-file "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ 53 | --output-dir "${OUTPUT_DIR}/listers" \ 54 | "${APIS_PKG}/${CUSTOM_RESOURCE_NAME}/${CUSTOM_RESOURCE_VERSION}" 55 | 56 | echo "Generating informers for ${GROUPS_WITH_VERSIONS} at ${OUTPUT_PKG}/informers" 57 | go run k8s.io/code-generator/cmd/informer-gen \ 58 | --versioned-clientset-package "${OUTPUT_PKG}/${CLIENTSET_PKG_NAME:-clientset}/${CLIENTSET_NAME_VERSIONED:-versioned}" \ 59 | --listers-package "${OUTPUT_PKG}/listers" \ 60 | --output-pkg "${OUTPUT_PKG}/informers" \ 61 | --go-header-file "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ 62 | --output-dir "${OUTPUT_DIR}/informers" \ 63 | "${APIS_PKG}/${CUSTOM_RESOURCE_NAME}/${CUSTOM_RESOURCE_VERSION}" 64 | -------------------------------------------------------------------------------- /pkg/apis/zalando.org/register.go: -------------------------------------------------------------------------------- 1 | package zalando 2 | 3 | const ( 4 | // GroupName is the group name used in this package. 5 | GroupName = "zalando.org" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/apis/zalando.org/v1/register.go: -------------------------------------------------------------------------------- 1 | // Package v1 contains API Schema definitions for the zalando v1 API group 2 | // +kubebuilder:object:generate=true 3 | // +groupName=zalando.org 4 | package v1 5 | 6 | import ( 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | 11 | "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org" 12 | ) 13 | 14 | var ( 15 | schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 16 | // AddToScheme applies all the stored functions to the scheme. A non-nil error 17 | // indicates that one function failed and the attempt was abandoned. 18 | AddToScheme = schemeBuilder.AddToScheme 19 | ) 20 | 21 | // SchemeGroupVersion is the group version used to register these objects. 22 | var SchemeGroupVersion = schema.GroupVersion{Group: zalando.GroupName, Version: "v1"} 23 | 24 | // Resource takes an unqualified resource and returns a Group-qualified GroupResource. 25 | func Resource(resource string) schema.GroupResource { 26 | return SchemeGroupVersion.WithResource(resource).GroupResource() 27 | } 28 | 29 | // addKnownTypes adds the set of types defined in this package to the supplied scheme. 30 | func addKnownTypes(scheme *runtime.Scheme) error { 31 | scheme.AddKnownTypes(SchemeGroupVersion, 32 | &StackSet{}, 33 | &StackSetList{}, 34 | &Stack{}, 35 | &StackList{}, 36 | &PlatformCredentialsSet{}, 37 | &PlatformCredentialsSetList{}, 38 | ) 39 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/apis/zalando.org/v1/types_pcs.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 9 | 10 | // PlatformCredentialsSet describes a platform credentials set 11 | // +k8s:deepcopy-gen=true 12 | type PlatformCredentialsSet struct { 13 | metav1.TypeMeta `json:""` 14 | metav1.ObjectMeta `json:"metadata,omitempty"` 15 | 16 | Spec PlatformCredentialsSpec `json:"spec"` 17 | Status PlatformCredentialsStatus `json:"status"` 18 | } 19 | 20 | // PlatformCredentialsSpec is the spec part of the StackSet. 21 | // +k8s:deepcopy-gen=true 22 | type PlatformCredentialsSpec struct { 23 | Application string `json:"application"` 24 | Clients map[string]Client `json:"clients"` 25 | Tokens map[string]Token `json:"tokens"` 26 | TokenVersion string `json:"token_version"` 27 | } 28 | 29 | // +k8s:deepcopy-gen=true 30 | type Client struct { 31 | Realm string `json:"realm"` 32 | Grant string `json:"grant"` 33 | RedirectURI string `json:"redirectUri"` 34 | } 35 | 36 | // +k8s:deepcopy-gen=true 37 | type Token struct { 38 | Privileges []string `json:"privileges"` 39 | } 40 | 41 | // PlatformCredentialsStatus is the status part of the Stack. 42 | // +k8s:deepcopy-gen=true 43 | type PlatformCredentialsStatus struct { 44 | ObservedGeneration int64 `json:"observedGeneration"` 45 | Errors []string `json:"errors"` 46 | Problems []string `json:"problems"` 47 | Tokens map[string]TokenStatus `json:"tokens"` 48 | Clients map[string]ClientStatus `json:"clients"` 49 | } 50 | 51 | // +k8s:deepcopy-gen=true 52 | type ClientStatus struct{} 53 | 54 | // +k8s:deepcopy-gen=true 55 | type TokenStatus struct { 56 | // +optional 57 | Expiration *metav1.Time `json:"expiration,omitempty"` 58 | } 59 | 60 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 61 | 62 | // PlatformCredentialsSetList is a list of StackSets. 63 | // +k8s:deepcopy-gen=true 64 | type PlatformCredentialsSetList struct { 65 | metav1.TypeMeta `json:""` 66 | metav1.ListMeta `json:"metadata,omitempty"` 67 | 68 | Items []PlatformCredentialsSet `json:"items"` 69 | } 70 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package versioned 20 | 21 | import ( 22 | "fmt" 23 | "net/http" 24 | 25 | zalandov1 "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/typed/zalando.org/v1" 26 | discovery "k8s.io/client-go/discovery" 27 | rest "k8s.io/client-go/rest" 28 | flowcontrol "k8s.io/client-go/util/flowcontrol" 29 | ) 30 | 31 | type Interface interface { 32 | Discovery() discovery.DiscoveryInterface 33 | ZalandoV1() zalandov1.ZalandoV1Interface 34 | } 35 | 36 | // Clientset contains the clients for groups. 37 | type Clientset struct { 38 | *discovery.DiscoveryClient 39 | zalandoV1 *zalandov1.ZalandoV1Client 40 | } 41 | 42 | // ZalandoV1 retrieves the ZalandoV1Client 43 | func (c *Clientset) ZalandoV1() zalandov1.ZalandoV1Interface { 44 | return c.zalandoV1 45 | } 46 | 47 | // Discovery retrieves the DiscoveryClient 48 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 49 | if c == nil { 50 | return nil 51 | } 52 | return c.DiscoveryClient 53 | } 54 | 55 | // NewForConfig creates a new Clientset for the given config. 56 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 57 | // NewForConfig will generate a rate-limiter in configShallowCopy. 58 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 59 | // where httpClient was generated with rest.HTTPClientFor(c). 60 | func NewForConfig(c *rest.Config) (*Clientset, error) { 61 | configShallowCopy := *c 62 | 63 | if configShallowCopy.UserAgent == "" { 64 | configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() 65 | } 66 | 67 | // share the transport between all clients 68 | httpClient, err := rest.HTTPClientFor(&configShallowCopy) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return NewForConfigAndClient(&configShallowCopy, httpClient) 74 | } 75 | 76 | // NewForConfigAndClient creates a new Clientset for the given config and http client. 77 | // Note the http client provided takes precedence over the configured transport values. 78 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 79 | // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. 80 | func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { 81 | configShallowCopy := *c 82 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 83 | if configShallowCopy.Burst <= 0 { 84 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 85 | } 86 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 87 | } 88 | 89 | var cs Clientset 90 | var err error 91 | cs.zalandoV1, err = zalandov1.NewForConfigAndClient(&configShallowCopy, httpClient) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return &cs, nil 101 | } 102 | 103 | // NewForConfigOrDie creates a new Clientset for the given config and 104 | // panics if there is an error in the config. 105 | func NewForConfigOrDie(c *rest.Config) *Clientset { 106 | cs, err := NewForConfig(c) 107 | if err != nil { 108 | panic(err) 109 | } 110 | return cs 111 | } 112 | 113 | // New creates a new Clientset for the given RESTClient. 114 | func New(c rest.Interface) *Clientset { 115 | var cs Clientset 116 | cs.zalandoV1 = zalandov1.New(c) 117 | 118 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 119 | return &cs 120 | } 121 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated clientset. 20 | package versioned 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | clientset "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 23 | zalandov1 "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/typed/zalando.org/v1" 24 | fakezalandov1 "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/typed/zalando.org/v1/fake" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/watch" 27 | "k8s.io/client-go/discovery" 28 | fakediscovery "k8s.io/client-go/discovery/fake" 29 | "k8s.io/client-go/testing" 30 | ) 31 | 32 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 33 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 34 | // without applying any field management, validations and/or defaults. It shouldn't be considered a replacement 35 | // for a real clientset and is mostly useful in simple unit tests. 36 | // 37 | // DEPRECATED: NewClientset replaces this with support for field management, which significantly improves 38 | // server side apply testing. NewClientset is only available when apply configurations are generated (e.g. 39 | // via --with-applyconfig). 40 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 41 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 42 | for _, obj := range objects { 43 | if err := o.Add(obj); err != nil { 44 | panic(err) 45 | } 46 | } 47 | 48 | cs := &Clientset{tracker: o} 49 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 50 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 51 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 52 | gvr := action.GetResource() 53 | ns := action.GetNamespace() 54 | watch, err := o.Watch(gvr, ns) 55 | if err != nil { 56 | return false, nil, err 57 | } 58 | return true, watch, nil 59 | }) 60 | 61 | return cs 62 | } 63 | 64 | // Clientset implements clientset.Interface. Meant to be embedded into a 65 | // struct to get a default implementation. This makes faking out just the method 66 | // you want to test easier. 67 | type Clientset struct { 68 | testing.Fake 69 | discovery *fakediscovery.FakeDiscovery 70 | tracker testing.ObjectTracker 71 | } 72 | 73 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 74 | return c.discovery 75 | } 76 | 77 | func (c *Clientset) Tracker() testing.ObjectTracker { 78 | return c.tracker 79 | } 80 | 81 | var ( 82 | _ clientset.Interface = &Clientset{} 83 | _ testing.FakeClient = &Clientset{} 84 | ) 85 | 86 | // ZalandoV1 retrieves the ZalandoV1Client 87 | func (c *Clientset) ZalandoV1() zalandov1.ZalandoV1Interface { 88 | return &fakezalandov1.FakeZalandoV1{Fake: &c.Fake} 89 | } 90 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated fake clientset. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | zalandov1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | ) 29 | 30 | var scheme = runtime.NewScheme() 31 | var codecs = serializer.NewCodecFactory(scheme) 32 | 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | zalandov1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package contains the scheme of the automatically generated clientset. 20 | package scheme 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package scheme 20 | 21 | import ( 22 | zalandov1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | ) 29 | 30 | var Scheme = runtime.NewScheme() 31 | var Codecs = serializer.NewCodecFactory(Scheme) 32 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | zalandov1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(Scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated typed clients. 20 | package v1 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // Package fake has the automatically generated clients. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/fake/fake_platformcredentialsset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | "context" 23 | 24 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | labels "k8s.io/apimachinery/pkg/labels" 27 | types "k8s.io/apimachinery/pkg/types" 28 | watch "k8s.io/apimachinery/pkg/watch" 29 | testing "k8s.io/client-go/testing" 30 | ) 31 | 32 | // FakePlatformCredentialsSets implements PlatformCredentialsSetInterface 33 | type FakePlatformCredentialsSets struct { 34 | Fake *FakeZalandoV1 35 | ns string 36 | } 37 | 38 | var platformcredentialssetsResource = v1.SchemeGroupVersion.WithResource("platformcredentialssets") 39 | 40 | var platformcredentialssetsKind = v1.SchemeGroupVersion.WithKind("PlatformCredentialsSet") 41 | 42 | // Get takes name of the platformCredentialsSet, and returns the corresponding platformCredentialsSet object, and an error if there is any. 43 | func (c *FakePlatformCredentialsSets) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.PlatformCredentialsSet, err error) { 44 | emptyResult := &v1.PlatformCredentialsSet{} 45 | obj, err := c.Fake. 46 | Invokes(testing.NewGetActionWithOptions(platformcredentialssetsResource, c.ns, name, options), emptyResult) 47 | 48 | if obj == nil { 49 | return emptyResult, err 50 | } 51 | return obj.(*v1.PlatformCredentialsSet), err 52 | } 53 | 54 | // List takes label and field selectors, and returns the list of PlatformCredentialsSets that match those selectors. 55 | func (c *FakePlatformCredentialsSets) List(ctx context.Context, opts metav1.ListOptions) (result *v1.PlatformCredentialsSetList, err error) { 56 | emptyResult := &v1.PlatformCredentialsSetList{} 57 | obj, err := c.Fake. 58 | Invokes(testing.NewListActionWithOptions(platformcredentialssetsResource, platformcredentialssetsKind, c.ns, opts), emptyResult) 59 | 60 | if obj == nil { 61 | return emptyResult, err 62 | } 63 | 64 | label, _, _ := testing.ExtractFromListOptions(opts) 65 | if label == nil { 66 | label = labels.Everything() 67 | } 68 | list := &v1.PlatformCredentialsSetList{ListMeta: obj.(*v1.PlatformCredentialsSetList).ListMeta} 69 | for _, item := range obj.(*v1.PlatformCredentialsSetList).Items { 70 | if label.Matches(labels.Set(item.Labels)) { 71 | list.Items = append(list.Items, item) 72 | } 73 | } 74 | return list, err 75 | } 76 | 77 | // Watch returns a watch.Interface that watches the requested platformCredentialsSets. 78 | func (c *FakePlatformCredentialsSets) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { 79 | return c.Fake. 80 | InvokesWatch(testing.NewWatchActionWithOptions(platformcredentialssetsResource, c.ns, opts)) 81 | 82 | } 83 | 84 | // Create takes the representation of a platformCredentialsSet and creates it. Returns the server's representation of the platformCredentialsSet, and an error, if there is any. 85 | func (c *FakePlatformCredentialsSets) Create(ctx context.Context, platformCredentialsSet *v1.PlatformCredentialsSet, opts metav1.CreateOptions) (result *v1.PlatformCredentialsSet, err error) { 86 | emptyResult := &v1.PlatformCredentialsSet{} 87 | obj, err := c.Fake. 88 | Invokes(testing.NewCreateActionWithOptions(platformcredentialssetsResource, c.ns, platformCredentialsSet, opts), emptyResult) 89 | 90 | if obj == nil { 91 | return emptyResult, err 92 | } 93 | return obj.(*v1.PlatformCredentialsSet), err 94 | } 95 | 96 | // Update takes the representation of a platformCredentialsSet and updates it. Returns the server's representation of the platformCredentialsSet, and an error, if there is any. 97 | func (c *FakePlatformCredentialsSets) Update(ctx context.Context, platformCredentialsSet *v1.PlatformCredentialsSet, opts metav1.UpdateOptions) (result *v1.PlatformCredentialsSet, err error) { 98 | emptyResult := &v1.PlatformCredentialsSet{} 99 | obj, err := c.Fake. 100 | Invokes(testing.NewUpdateActionWithOptions(platformcredentialssetsResource, c.ns, platformCredentialsSet, opts), emptyResult) 101 | 102 | if obj == nil { 103 | return emptyResult, err 104 | } 105 | return obj.(*v1.PlatformCredentialsSet), err 106 | } 107 | 108 | // UpdateStatus was generated because the type contains a Status member. 109 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 110 | func (c *FakePlatformCredentialsSets) UpdateStatus(ctx context.Context, platformCredentialsSet *v1.PlatformCredentialsSet, opts metav1.UpdateOptions) (result *v1.PlatformCredentialsSet, err error) { 111 | emptyResult := &v1.PlatformCredentialsSet{} 112 | obj, err := c.Fake. 113 | Invokes(testing.NewUpdateSubresourceActionWithOptions(platformcredentialssetsResource, "status", c.ns, platformCredentialsSet, opts), emptyResult) 114 | 115 | if obj == nil { 116 | return emptyResult, err 117 | } 118 | return obj.(*v1.PlatformCredentialsSet), err 119 | } 120 | 121 | // Delete takes name of the platformCredentialsSet and deletes it. Returns an error if one occurs. 122 | func (c *FakePlatformCredentialsSets) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { 123 | _, err := c.Fake. 124 | Invokes(testing.NewDeleteActionWithOptions(platformcredentialssetsResource, c.ns, name, opts), &v1.PlatformCredentialsSet{}) 125 | 126 | return err 127 | } 128 | 129 | // DeleteCollection deletes a collection of objects. 130 | func (c *FakePlatformCredentialsSets) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { 131 | action := testing.NewDeleteCollectionActionWithOptions(platformcredentialssetsResource, c.ns, opts, listOpts) 132 | 133 | _, err := c.Fake.Invokes(action, &v1.PlatformCredentialsSetList{}) 134 | return err 135 | } 136 | 137 | // Patch applies the patch and returns the patched platformCredentialsSet. 138 | func (c *FakePlatformCredentialsSets) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.PlatformCredentialsSet, err error) { 139 | emptyResult := &v1.PlatformCredentialsSet{} 140 | obj, err := c.Fake. 141 | Invokes(testing.NewPatchSubresourceActionWithOptions(platformcredentialssetsResource, c.ns, name, pt, data, opts, subresources...), emptyResult) 142 | 143 | if obj == nil { 144 | return emptyResult, err 145 | } 146 | return obj.(*v1.PlatformCredentialsSet), err 147 | } 148 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/fake/fake_stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | "context" 23 | 24 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | labels "k8s.io/apimachinery/pkg/labels" 27 | types "k8s.io/apimachinery/pkg/types" 28 | watch "k8s.io/apimachinery/pkg/watch" 29 | testing "k8s.io/client-go/testing" 30 | ) 31 | 32 | // FakeStacks implements StackInterface 33 | type FakeStacks struct { 34 | Fake *FakeZalandoV1 35 | ns string 36 | } 37 | 38 | var stacksResource = v1.SchemeGroupVersion.WithResource("stacks") 39 | 40 | var stacksKind = v1.SchemeGroupVersion.WithKind("Stack") 41 | 42 | // Get takes name of the stack, and returns the corresponding stack object, and an error if there is any. 43 | func (c *FakeStacks) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.Stack, err error) { 44 | emptyResult := &v1.Stack{} 45 | obj, err := c.Fake. 46 | Invokes(testing.NewGetActionWithOptions(stacksResource, c.ns, name, options), emptyResult) 47 | 48 | if obj == nil { 49 | return emptyResult, err 50 | } 51 | return obj.(*v1.Stack), err 52 | } 53 | 54 | // List takes label and field selectors, and returns the list of Stacks that match those selectors. 55 | func (c *FakeStacks) List(ctx context.Context, opts metav1.ListOptions) (result *v1.StackList, err error) { 56 | emptyResult := &v1.StackList{} 57 | obj, err := c.Fake. 58 | Invokes(testing.NewListActionWithOptions(stacksResource, stacksKind, c.ns, opts), emptyResult) 59 | 60 | if obj == nil { 61 | return emptyResult, err 62 | } 63 | 64 | label, _, _ := testing.ExtractFromListOptions(opts) 65 | if label == nil { 66 | label = labels.Everything() 67 | } 68 | list := &v1.StackList{ListMeta: obj.(*v1.StackList).ListMeta} 69 | for _, item := range obj.(*v1.StackList).Items { 70 | if label.Matches(labels.Set(item.Labels)) { 71 | list.Items = append(list.Items, item) 72 | } 73 | } 74 | return list, err 75 | } 76 | 77 | // Watch returns a watch.Interface that watches the requested stacks. 78 | func (c *FakeStacks) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { 79 | return c.Fake. 80 | InvokesWatch(testing.NewWatchActionWithOptions(stacksResource, c.ns, opts)) 81 | 82 | } 83 | 84 | // Create takes the representation of a stack and creates it. Returns the server's representation of the stack, and an error, if there is any. 85 | func (c *FakeStacks) Create(ctx context.Context, stack *v1.Stack, opts metav1.CreateOptions) (result *v1.Stack, err error) { 86 | emptyResult := &v1.Stack{} 87 | obj, err := c.Fake. 88 | Invokes(testing.NewCreateActionWithOptions(stacksResource, c.ns, stack, opts), emptyResult) 89 | 90 | if obj == nil { 91 | return emptyResult, err 92 | } 93 | return obj.(*v1.Stack), err 94 | } 95 | 96 | // Update takes the representation of a stack and updates it. Returns the server's representation of the stack, and an error, if there is any. 97 | func (c *FakeStacks) Update(ctx context.Context, stack *v1.Stack, opts metav1.UpdateOptions) (result *v1.Stack, err error) { 98 | emptyResult := &v1.Stack{} 99 | obj, err := c.Fake. 100 | Invokes(testing.NewUpdateActionWithOptions(stacksResource, c.ns, stack, opts), emptyResult) 101 | 102 | if obj == nil { 103 | return emptyResult, err 104 | } 105 | return obj.(*v1.Stack), err 106 | } 107 | 108 | // UpdateStatus was generated because the type contains a Status member. 109 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 110 | func (c *FakeStacks) UpdateStatus(ctx context.Context, stack *v1.Stack, opts metav1.UpdateOptions) (result *v1.Stack, err error) { 111 | emptyResult := &v1.Stack{} 112 | obj, err := c.Fake. 113 | Invokes(testing.NewUpdateSubresourceActionWithOptions(stacksResource, "status", c.ns, stack, opts), emptyResult) 114 | 115 | if obj == nil { 116 | return emptyResult, err 117 | } 118 | return obj.(*v1.Stack), err 119 | } 120 | 121 | // Delete takes name of the stack and deletes it. Returns an error if one occurs. 122 | func (c *FakeStacks) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { 123 | _, err := c.Fake. 124 | Invokes(testing.NewDeleteActionWithOptions(stacksResource, c.ns, name, opts), &v1.Stack{}) 125 | 126 | return err 127 | } 128 | 129 | // DeleteCollection deletes a collection of objects. 130 | func (c *FakeStacks) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { 131 | action := testing.NewDeleteCollectionActionWithOptions(stacksResource, c.ns, opts, listOpts) 132 | 133 | _, err := c.Fake.Invokes(action, &v1.StackList{}) 134 | return err 135 | } 136 | 137 | // Patch applies the patch and returns the patched stack. 138 | func (c *FakeStacks) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Stack, err error) { 139 | emptyResult := &v1.Stack{} 140 | obj, err := c.Fake. 141 | Invokes(testing.NewPatchSubresourceActionWithOptions(stacksResource, c.ns, name, pt, data, opts, subresources...), emptyResult) 142 | 143 | if obj == nil { 144 | return emptyResult, err 145 | } 146 | return obj.(*v1.Stack), err 147 | } 148 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/fake/fake_stackset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | "context" 23 | 24 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | labels "k8s.io/apimachinery/pkg/labels" 27 | types "k8s.io/apimachinery/pkg/types" 28 | watch "k8s.io/apimachinery/pkg/watch" 29 | testing "k8s.io/client-go/testing" 30 | ) 31 | 32 | // FakeStackSets implements StackSetInterface 33 | type FakeStackSets struct { 34 | Fake *FakeZalandoV1 35 | ns string 36 | } 37 | 38 | var stacksetsResource = v1.SchemeGroupVersion.WithResource("stacksets") 39 | 40 | var stacksetsKind = v1.SchemeGroupVersion.WithKind("StackSet") 41 | 42 | // Get takes name of the stackSet, and returns the corresponding stackSet object, and an error if there is any. 43 | func (c *FakeStackSets) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.StackSet, err error) { 44 | emptyResult := &v1.StackSet{} 45 | obj, err := c.Fake. 46 | Invokes(testing.NewGetActionWithOptions(stacksetsResource, c.ns, name, options), emptyResult) 47 | 48 | if obj == nil { 49 | return emptyResult, err 50 | } 51 | return obj.(*v1.StackSet), err 52 | } 53 | 54 | // List takes label and field selectors, and returns the list of StackSets that match those selectors. 55 | func (c *FakeStackSets) List(ctx context.Context, opts metav1.ListOptions) (result *v1.StackSetList, err error) { 56 | emptyResult := &v1.StackSetList{} 57 | obj, err := c.Fake. 58 | Invokes(testing.NewListActionWithOptions(stacksetsResource, stacksetsKind, c.ns, opts), emptyResult) 59 | 60 | if obj == nil { 61 | return emptyResult, err 62 | } 63 | 64 | label, _, _ := testing.ExtractFromListOptions(opts) 65 | if label == nil { 66 | label = labels.Everything() 67 | } 68 | list := &v1.StackSetList{ListMeta: obj.(*v1.StackSetList).ListMeta} 69 | for _, item := range obj.(*v1.StackSetList).Items { 70 | if label.Matches(labels.Set(item.Labels)) { 71 | list.Items = append(list.Items, item) 72 | } 73 | } 74 | return list, err 75 | } 76 | 77 | // Watch returns a watch.Interface that watches the requested stackSets. 78 | func (c *FakeStackSets) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { 79 | return c.Fake. 80 | InvokesWatch(testing.NewWatchActionWithOptions(stacksetsResource, c.ns, opts)) 81 | 82 | } 83 | 84 | // Create takes the representation of a stackSet and creates it. Returns the server's representation of the stackSet, and an error, if there is any. 85 | func (c *FakeStackSets) Create(ctx context.Context, stackSet *v1.StackSet, opts metav1.CreateOptions) (result *v1.StackSet, err error) { 86 | emptyResult := &v1.StackSet{} 87 | obj, err := c.Fake. 88 | Invokes(testing.NewCreateActionWithOptions(stacksetsResource, c.ns, stackSet, opts), emptyResult) 89 | 90 | if obj == nil { 91 | return emptyResult, err 92 | } 93 | return obj.(*v1.StackSet), err 94 | } 95 | 96 | // Update takes the representation of a stackSet and updates it. Returns the server's representation of the stackSet, and an error, if there is any. 97 | func (c *FakeStackSets) Update(ctx context.Context, stackSet *v1.StackSet, opts metav1.UpdateOptions) (result *v1.StackSet, err error) { 98 | emptyResult := &v1.StackSet{} 99 | obj, err := c.Fake. 100 | Invokes(testing.NewUpdateActionWithOptions(stacksetsResource, c.ns, stackSet, opts), emptyResult) 101 | 102 | if obj == nil { 103 | return emptyResult, err 104 | } 105 | return obj.(*v1.StackSet), err 106 | } 107 | 108 | // UpdateStatus was generated because the type contains a Status member. 109 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 110 | func (c *FakeStackSets) UpdateStatus(ctx context.Context, stackSet *v1.StackSet, opts metav1.UpdateOptions) (result *v1.StackSet, err error) { 111 | emptyResult := &v1.StackSet{} 112 | obj, err := c.Fake. 113 | Invokes(testing.NewUpdateSubresourceActionWithOptions(stacksetsResource, "status", c.ns, stackSet, opts), emptyResult) 114 | 115 | if obj == nil { 116 | return emptyResult, err 117 | } 118 | return obj.(*v1.StackSet), err 119 | } 120 | 121 | // Delete takes name of the stackSet and deletes it. Returns an error if one occurs. 122 | func (c *FakeStackSets) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { 123 | _, err := c.Fake. 124 | Invokes(testing.NewDeleteActionWithOptions(stacksetsResource, c.ns, name, opts), &v1.StackSet{}) 125 | 126 | return err 127 | } 128 | 129 | // DeleteCollection deletes a collection of objects. 130 | func (c *FakeStackSets) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { 131 | action := testing.NewDeleteCollectionActionWithOptions(stacksetsResource, c.ns, opts, listOpts) 132 | 133 | _, err := c.Fake.Invokes(action, &v1.StackSetList{}) 134 | return err 135 | } 136 | 137 | // Patch applies the patch and returns the patched stackSet. 138 | func (c *FakeStackSets) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.StackSet, err error) { 139 | emptyResult := &v1.StackSet{} 140 | obj, err := c.Fake. 141 | Invokes(testing.NewPatchSubresourceActionWithOptions(stacksetsResource, c.ns, name, pt, data, opts, subresources...), emptyResult) 142 | 143 | if obj == nil { 144 | return emptyResult, err 145 | } 146 | return obj.(*v1.StackSet), err 147 | } 148 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/fake/fake_zalando.org_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | v1 "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/typed/zalando.org/v1" 23 | rest "k8s.io/client-go/rest" 24 | testing "k8s.io/client-go/testing" 25 | ) 26 | 27 | type FakeZalandoV1 struct { 28 | *testing.Fake 29 | } 30 | 31 | func (c *FakeZalandoV1) PlatformCredentialsSets(namespace string) v1.PlatformCredentialsSetInterface { 32 | return &FakePlatformCredentialsSets{c, namespace} 33 | } 34 | 35 | func (c *FakeZalandoV1) Stacks(namespace string) v1.StackInterface { 36 | return &FakeStacks{c, namespace} 37 | } 38 | 39 | func (c *FakeZalandoV1) StackSets(namespace string) v1.StackSetInterface { 40 | return &FakeStackSets{c, namespace} 41 | } 42 | 43 | // RESTClient returns a RESTClient that is used to communicate 44 | // with API server by this client implementation. 45 | func (c *FakeZalandoV1) RESTClient() rest.Interface { 46 | var ret *rest.RESTClient 47 | return ret 48 | } 49 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | type PlatformCredentialsSetExpansion interface{} 22 | 23 | type StackExpansion interface{} 24 | 25 | type StackSetExpansion interface{} 26 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/platformcredentialsset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | "context" 23 | 24 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 25 | scheme "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/scheme" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | types "k8s.io/apimachinery/pkg/types" 28 | watch "k8s.io/apimachinery/pkg/watch" 29 | gentype "k8s.io/client-go/gentype" 30 | ) 31 | 32 | // PlatformCredentialsSetsGetter has a method to return a PlatformCredentialsSetInterface. 33 | // A group's client should implement this interface. 34 | type PlatformCredentialsSetsGetter interface { 35 | PlatformCredentialsSets(namespace string) PlatformCredentialsSetInterface 36 | } 37 | 38 | // PlatformCredentialsSetInterface has methods to work with PlatformCredentialsSet resources. 39 | type PlatformCredentialsSetInterface interface { 40 | Create(ctx context.Context, platformCredentialsSet *v1.PlatformCredentialsSet, opts metav1.CreateOptions) (*v1.PlatformCredentialsSet, error) 41 | Update(ctx context.Context, platformCredentialsSet *v1.PlatformCredentialsSet, opts metav1.UpdateOptions) (*v1.PlatformCredentialsSet, error) 42 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 43 | UpdateStatus(ctx context.Context, platformCredentialsSet *v1.PlatformCredentialsSet, opts metav1.UpdateOptions) (*v1.PlatformCredentialsSet, error) 44 | Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error 45 | DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error 46 | Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.PlatformCredentialsSet, error) 47 | List(ctx context.Context, opts metav1.ListOptions) (*v1.PlatformCredentialsSetList, error) 48 | Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) 49 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.PlatformCredentialsSet, err error) 50 | PlatformCredentialsSetExpansion 51 | } 52 | 53 | // platformCredentialsSets implements PlatformCredentialsSetInterface 54 | type platformCredentialsSets struct { 55 | *gentype.ClientWithList[*v1.PlatformCredentialsSet, *v1.PlatformCredentialsSetList] 56 | } 57 | 58 | // newPlatformCredentialsSets returns a PlatformCredentialsSets 59 | func newPlatformCredentialsSets(c *ZalandoV1Client, namespace string) *platformCredentialsSets { 60 | return &platformCredentialsSets{ 61 | gentype.NewClientWithList[*v1.PlatformCredentialsSet, *v1.PlatformCredentialsSetList]( 62 | "platformcredentialssets", 63 | c.RESTClient(), 64 | scheme.ParameterCodec, 65 | namespace, 66 | func() *v1.PlatformCredentialsSet { return &v1.PlatformCredentialsSet{} }, 67 | func() *v1.PlatformCredentialsSetList { return &v1.PlatformCredentialsSetList{} }), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | "context" 23 | 24 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 25 | scheme "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/scheme" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | types "k8s.io/apimachinery/pkg/types" 28 | watch "k8s.io/apimachinery/pkg/watch" 29 | gentype "k8s.io/client-go/gentype" 30 | ) 31 | 32 | // StacksGetter has a method to return a StackInterface. 33 | // A group's client should implement this interface. 34 | type StacksGetter interface { 35 | Stacks(namespace string) StackInterface 36 | } 37 | 38 | // StackInterface has methods to work with Stack resources. 39 | type StackInterface interface { 40 | Create(ctx context.Context, stack *v1.Stack, opts metav1.CreateOptions) (*v1.Stack, error) 41 | Update(ctx context.Context, stack *v1.Stack, opts metav1.UpdateOptions) (*v1.Stack, error) 42 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 43 | UpdateStatus(ctx context.Context, stack *v1.Stack, opts metav1.UpdateOptions) (*v1.Stack, error) 44 | Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error 45 | DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error 46 | Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Stack, error) 47 | List(ctx context.Context, opts metav1.ListOptions) (*v1.StackList, error) 48 | Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) 49 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Stack, err error) 50 | StackExpansion 51 | } 52 | 53 | // stacks implements StackInterface 54 | type stacks struct { 55 | *gentype.ClientWithList[*v1.Stack, *v1.StackList] 56 | } 57 | 58 | // newStacks returns a Stacks 59 | func newStacks(c *ZalandoV1Client, namespace string) *stacks { 60 | return &stacks{ 61 | gentype.NewClientWithList[*v1.Stack, *v1.StackList]( 62 | "stacks", 63 | c.RESTClient(), 64 | scheme.ParameterCodec, 65 | namespace, 66 | func() *v1.Stack { return &v1.Stack{} }, 67 | func() *v1.StackList { return &v1.StackList{} }), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/stackset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | "context" 23 | 24 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 25 | scheme "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/scheme" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | types "k8s.io/apimachinery/pkg/types" 28 | watch "k8s.io/apimachinery/pkg/watch" 29 | gentype "k8s.io/client-go/gentype" 30 | ) 31 | 32 | // StackSetsGetter has a method to return a StackSetInterface. 33 | // A group's client should implement this interface. 34 | type StackSetsGetter interface { 35 | StackSets(namespace string) StackSetInterface 36 | } 37 | 38 | // StackSetInterface has methods to work with StackSet resources. 39 | type StackSetInterface interface { 40 | Create(ctx context.Context, stackSet *v1.StackSet, opts metav1.CreateOptions) (*v1.StackSet, error) 41 | Update(ctx context.Context, stackSet *v1.StackSet, opts metav1.UpdateOptions) (*v1.StackSet, error) 42 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 43 | UpdateStatus(ctx context.Context, stackSet *v1.StackSet, opts metav1.UpdateOptions) (*v1.StackSet, error) 44 | Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error 45 | DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error 46 | Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.StackSet, error) 47 | List(ctx context.Context, opts metav1.ListOptions) (*v1.StackSetList, error) 48 | Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) 49 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.StackSet, err error) 50 | StackSetExpansion 51 | } 52 | 53 | // stackSets implements StackSetInterface 54 | type stackSets struct { 55 | *gentype.ClientWithList[*v1.StackSet, *v1.StackSetList] 56 | } 57 | 58 | // newStackSets returns a StackSets 59 | func newStackSets(c *ZalandoV1Client, namespace string) *stackSets { 60 | return &stackSets{ 61 | gentype.NewClientWithList[*v1.StackSet, *v1.StackSetList]( 62 | "stacksets", 63 | c.RESTClient(), 64 | scheme.ParameterCodec, 65 | namespace, 66 | func() *v1.StackSet { return &v1.StackSet{} }, 67 | func() *v1.StackSetList { return &v1.StackSetList{} }), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando.org/v1/zalando.org_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | "net/http" 23 | 24 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 25 | "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/scheme" 26 | rest "k8s.io/client-go/rest" 27 | ) 28 | 29 | type ZalandoV1Interface interface { 30 | RESTClient() rest.Interface 31 | PlatformCredentialsSetsGetter 32 | StacksGetter 33 | StackSetsGetter 34 | } 35 | 36 | // ZalandoV1Client is used to interact with features provided by the zalando.org group. 37 | type ZalandoV1Client struct { 38 | restClient rest.Interface 39 | } 40 | 41 | func (c *ZalandoV1Client) PlatformCredentialsSets(namespace string) PlatformCredentialsSetInterface { 42 | return newPlatformCredentialsSets(c, namespace) 43 | } 44 | 45 | func (c *ZalandoV1Client) Stacks(namespace string) StackInterface { 46 | return newStacks(c, namespace) 47 | } 48 | 49 | func (c *ZalandoV1Client) StackSets(namespace string) StackSetInterface { 50 | return newStackSets(c, namespace) 51 | } 52 | 53 | // NewForConfig creates a new ZalandoV1Client for the given config. 54 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 55 | // where httpClient was generated with rest.HTTPClientFor(c). 56 | func NewForConfig(c *rest.Config) (*ZalandoV1Client, error) { 57 | config := *c 58 | if err := setConfigDefaults(&config); err != nil { 59 | return nil, err 60 | } 61 | httpClient, err := rest.HTTPClientFor(&config) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return NewForConfigAndClient(&config, httpClient) 66 | } 67 | 68 | // NewForConfigAndClient creates a new ZalandoV1Client for the given config and http client. 69 | // Note the http client provided takes precedence over the configured transport values. 70 | func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ZalandoV1Client, error) { 71 | config := *c 72 | if err := setConfigDefaults(&config); err != nil { 73 | return nil, err 74 | } 75 | client, err := rest.RESTClientForConfigAndClient(&config, h) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return &ZalandoV1Client{client}, nil 80 | } 81 | 82 | // NewForConfigOrDie creates a new ZalandoV1Client for the given config and 83 | // panics if there is an error in the config. 84 | func NewForConfigOrDie(c *rest.Config) *ZalandoV1Client { 85 | client, err := NewForConfig(c) 86 | if err != nil { 87 | panic(err) 88 | } 89 | return client 90 | } 91 | 92 | // New creates a new ZalandoV1Client for the given RESTClient. 93 | func New(c rest.Interface) *ZalandoV1Client { 94 | return &ZalandoV1Client{c} 95 | } 96 | 97 | func setConfigDefaults(config *rest.Config) error { 98 | gv := v1.SchemeGroupVersion 99 | config.GroupVersion = &gv 100 | config.APIPath = "/apis" 101 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 102 | 103 | if config.UserAgent == "" { 104 | config.UserAgent = rest.DefaultKubernetesUserAgent() 105 | } 106 | 107 | return nil 108 | } 109 | 110 | // RESTClient returns a RESTClient that is used to communicate 111 | // with API server by this client implementation. 112 | func (c *ZalandoV1Client) RESTClient() rest.Interface { 113 | if c == nil { 114 | return nil 115 | } 116 | return c.restClient 117 | } 118 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package externalversions 20 | 21 | import ( 22 | "fmt" 23 | 24 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | cache "k8s.io/client-go/tools/cache" 27 | ) 28 | 29 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 30 | // sharedInformers based on type 31 | type GenericInformer interface { 32 | Informer() cache.SharedIndexInformer 33 | Lister() cache.GenericLister 34 | } 35 | 36 | type genericInformer struct { 37 | informer cache.SharedIndexInformer 38 | resource schema.GroupResource 39 | } 40 | 41 | // Informer returns the SharedIndexInformer. 42 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 43 | return f.informer 44 | } 45 | 46 | // Lister returns the GenericLister. 47 | func (f *genericInformer) Lister() cache.GenericLister { 48 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 49 | } 50 | 51 | // ForResource gives generic access to a shared informer of the matching type 52 | // TODO extend this to unknown resources with a client pool 53 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 54 | switch resource { 55 | // Group=zalando.org, Version=v1 56 | case v1.SchemeGroupVersion.WithResource("platformcredentialssets"): 57 | return &genericInformer{resource: resource.GroupResource(), informer: f.Zalando().V1().PlatformCredentialsSets().Informer()}, nil 58 | case v1.SchemeGroupVersion.WithResource("stacks"): 59 | return &genericInformer{resource: resource.GroupResource(), informer: f.Zalando().V1().Stacks().Informer()}, nil 60 | case v1.SchemeGroupVersion.WithResource("stacksets"): 61 | return &genericInformer{resource: resource.GroupResource(), informer: f.Zalando().V1().StackSets().Informer()}, nil 62 | 63 | } 64 | 65 | return nil, fmt.Errorf("no informer found for %v", resource) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package internalinterfaces 20 | 21 | import ( 22 | time "time" 23 | 24 | versioned "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | cache "k8s.io/client-go/tools/cache" 28 | ) 29 | 30 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 31 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 32 | 33 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 34 | type SharedInformerFactory interface { 35 | Start(stopCh <-chan struct{}) 36 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 37 | } 38 | 39 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 40 | type TweakListOptionsFunc func(*v1.ListOptions) 41 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/zalando.org/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package zalando 20 | 21 | import ( 22 | internalinterfaces "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/internalinterfaces" 23 | v1 "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/zalando.org/v1" 24 | ) 25 | 26 | // Interface provides access to each of this group's versions. 27 | type Interface interface { 28 | // V1 provides access to shared informers for resources in V1. 29 | V1() v1.Interface 30 | } 31 | 32 | type group struct { 33 | factory internalinterfaces.SharedInformerFactory 34 | namespace string 35 | tweakListOptions internalinterfaces.TweakListOptionsFunc 36 | } 37 | 38 | // New returns a new Interface. 39 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 40 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 41 | } 42 | 43 | // V1 returns a new v1.Interface. 44 | func (g *group) V1() v1.Interface { 45 | return v1.New(g.factory, g.namespace, g.tweakListOptions) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/zalando.org/v1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | internalinterfaces "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/internalinterfaces" 23 | ) 24 | 25 | // Interface provides access to all the informers in this group version. 26 | type Interface interface { 27 | // PlatformCredentialsSets returns a PlatformCredentialsSetInformer. 28 | PlatformCredentialsSets() PlatformCredentialsSetInformer 29 | // Stacks returns a StackInformer. 30 | Stacks() StackInformer 31 | // StackSets returns a StackSetInformer. 32 | StackSets() StackSetInformer 33 | } 34 | 35 | type version struct { 36 | factory internalinterfaces.SharedInformerFactory 37 | namespace string 38 | tweakListOptions internalinterfaces.TweakListOptionsFunc 39 | } 40 | 41 | // New returns a new Interface. 42 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 43 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 44 | } 45 | 46 | // PlatformCredentialsSets returns a PlatformCredentialsSetInformer. 47 | func (v *version) PlatformCredentialsSets() PlatformCredentialsSetInformer { 48 | return &platformCredentialsSetInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 49 | } 50 | 51 | // Stacks returns a StackInformer. 52 | func (v *version) Stacks() StackInformer { 53 | return &stackInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 54 | } 55 | 56 | // StackSets returns a StackSetInformer. 57 | func (v *version) StackSets() StackSetInformer { 58 | return &stackSetInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 59 | } 60 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/zalando.org/v1/platformcredentialsset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | "context" 23 | time "time" 24 | 25 | zalandoorgv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 26 | versioned "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 27 | internalinterfaces "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/internalinterfaces" 28 | v1 "github.com/zalando-incubator/stackset-controller/pkg/client/listers/zalando.org/v1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | watch "k8s.io/apimachinery/pkg/watch" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // PlatformCredentialsSetInformer provides access to a shared informer and lister for 36 | // PlatformCredentialsSets. 37 | type PlatformCredentialsSetInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() v1.PlatformCredentialsSetLister 40 | } 41 | 42 | type platformCredentialsSetInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewPlatformCredentialsSetInformer constructs a new informer for PlatformCredentialsSet type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewPlatformCredentialsSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredPlatformCredentialsSetInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredPlatformCredentialsSetInformer constructs a new informer for PlatformCredentialsSet type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredPlatformCredentialsSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.ZalandoV1().PlatformCredentialsSets(namespace).List(context.TODO(), options) 66 | }, 67 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.ZalandoV1().PlatformCredentialsSets(namespace).Watch(context.TODO(), options) 72 | }, 73 | }, 74 | &zalandoorgv1.PlatformCredentialsSet{}, 75 | resyncPeriod, 76 | indexers, 77 | ) 78 | } 79 | 80 | func (f *platformCredentialsSetInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 81 | return NewFilteredPlatformCredentialsSetInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 82 | } 83 | 84 | func (f *platformCredentialsSetInformer) Informer() cache.SharedIndexInformer { 85 | return f.factory.InformerFor(&zalandoorgv1.PlatformCredentialsSet{}, f.defaultInformer) 86 | } 87 | 88 | func (f *platformCredentialsSetInformer) Lister() v1.PlatformCredentialsSetLister { 89 | return v1.NewPlatformCredentialsSetLister(f.Informer().GetIndexer()) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/zalando.org/v1/stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | "context" 23 | time "time" 24 | 25 | zalandoorgv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 26 | versioned "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 27 | internalinterfaces "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/internalinterfaces" 28 | v1 "github.com/zalando-incubator/stackset-controller/pkg/client/listers/zalando.org/v1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | watch "k8s.io/apimachinery/pkg/watch" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // StackInformer provides access to a shared informer and lister for 36 | // Stacks. 37 | type StackInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() v1.StackLister 40 | } 41 | 42 | type stackInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewStackInformer constructs a new informer for Stack type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewStackInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredStackInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredStackInformer constructs a new informer for Stack type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredStackInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.ZalandoV1().Stacks(namespace).List(context.TODO(), options) 66 | }, 67 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.ZalandoV1().Stacks(namespace).Watch(context.TODO(), options) 72 | }, 73 | }, 74 | &zalandoorgv1.Stack{}, 75 | resyncPeriod, 76 | indexers, 77 | ) 78 | } 79 | 80 | func (f *stackInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 81 | return NewFilteredStackInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 82 | } 83 | 84 | func (f *stackInformer) Informer() cache.SharedIndexInformer { 85 | return f.factory.InformerFor(&zalandoorgv1.Stack{}, f.defaultInformer) 86 | } 87 | 88 | func (f *stackInformer) Lister() v1.StackLister { 89 | return v1.NewStackLister(f.Informer().GetIndexer()) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/zalando.org/v1/stackset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | "context" 23 | time "time" 24 | 25 | zalandoorgv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 26 | versioned "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 27 | internalinterfaces "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/internalinterfaces" 28 | v1 "github.com/zalando-incubator/stackset-controller/pkg/client/listers/zalando.org/v1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | watch "k8s.io/apimachinery/pkg/watch" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // StackSetInformer provides access to a shared informer and lister for 36 | // StackSets. 37 | type StackSetInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() v1.StackSetLister 40 | } 41 | 42 | type stackSetInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewStackSetInformer constructs a new informer for StackSet type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewStackSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredStackSetInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredStackSetInformer constructs a new informer for StackSet type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredStackSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.ZalandoV1().StackSets(namespace).List(context.TODO(), options) 66 | }, 67 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.ZalandoV1().StackSets(namespace).Watch(context.TODO(), options) 72 | }, 73 | }, 74 | &zalandoorgv1.StackSet{}, 75 | resyncPeriod, 76 | indexers, 77 | ) 78 | } 79 | 80 | func (f *stackSetInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 81 | return NewFilteredStackSetInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 82 | } 83 | 84 | func (f *stackSetInformer) Informer() cache.SharedIndexInformer { 85 | return f.factory.InformerFor(&zalandoorgv1.StackSet{}, f.defaultInformer) 86 | } 87 | 88 | func (f *stackSetInformer) Lister() v1.StackSetLister { 89 | return v1.NewStackSetLister(f.Informer().GetIndexer()) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/client/listers/zalando.org/v1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | // PlatformCredentialsSetListerExpansion allows custom methods to be added to 22 | // PlatformCredentialsSetLister. 23 | type PlatformCredentialsSetListerExpansion interface{} 24 | 25 | // PlatformCredentialsSetNamespaceListerExpansion allows custom methods to be added to 26 | // PlatformCredentialsSetNamespaceLister. 27 | type PlatformCredentialsSetNamespaceListerExpansion interface{} 28 | 29 | // StackListerExpansion allows custom methods to be added to 30 | // StackLister. 31 | type StackListerExpansion interface{} 32 | 33 | // StackNamespaceListerExpansion allows custom methods to be added to 34 | // StackNamespaceLister. 35 | type StackNamespaceListerExpansion interface{} 36 | 37 | // StackSetListerExpansion allows custom methods to be added to 38 | // StackSetLister. 39 | type StackSetListerExpansion interface{} 40 | 41 | // StackSetNamespaceListerExpansion allows custom methods to be added to 42 | // StackSetNamespaceLister. 43 | type StackSetNamespaceListerExpansion interface{} 44 | -------------------------------------------------------------------------------- /pkg/client/listers/zalando.org/v1/platformcredentialsset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 23 | "k8s.io/apimachinery/pkg/labels" 24 | "k8s.io/client-go/listers" 25 | "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // PlatformCredentialsSetLister helps list PlatformCredentialsSets. 29 | // All objects returned here must be treated as read-only. 30 | type PlatformCredentialsSetLister interface { 31 | // List lists all PlatformCredentialsSets in the indexer. 32 | // Objects returned here must be treated as read-only. 33 | List(selector labels.Selector) (ret []*v1.PlatformCredentialsSet, err error) 34 | // PlatformCredentialsSets returns an object that can list and get PlatformCredentialsSets. 35 | PlatformCredentialsSets(namespace string) PlatformCredentialsSetNamespaceLister 36 | PlatformCredentialsSetListerExpansion 37 | } 38 | 39 | // platformCredentialsSetLister implements the PlatformCredentialsSetLister interface. 40 | type platformCredentialsSetLister struct { 41 | listers.ResourceIndexer[*v1.PlatformCredentialsSet] 42 | } 43 | 44 | // NewPlatformCredentialsSetLister returns a new PlatformCredentialsSetLister. 45 | func NewPlatformCredentialsSetLister(indexer cache.Indexer) PlatformCredentialsSetLister { 46 | return &platformCredentialsSetLister{listers.New[*v1.PlatformCredentialsSet](indexer, v1.Resource("platformcredentialsset"))} 47 | } 48 | 49 | // PlatformCredentialsSets returns an object that can list and get PlatformCredentialsSets. 50 | func (s *platformCredentialsSetLister) PlatformCredentialsSets(namespace string) PlatformCredentialsSetNamespaceLister { 51 | return platformCredentialsSetNamespaceLister{listers.NewNamespaced[*v1.PlatformCredentialsSet](s.ResourceIndexer, namespace)} 52 | } 53 | 54 | // PlatformCredentialsSetNamespaceLister helps list and get PlatformCredentialsSets. 55 | // All objects returned here must be treated as read-only. 56 | type PlatformCredentialsSetNamespaceLister interface { 57 | // List lists all PlatformCredentialsSets in the indexer for a given namespace. 58 | // Objects returned here must be treated as read-only. 59 | List(selector labels.Selector) (ret []*v1.PlatformCredentialsSet, err error) 60 | // Get retrieves the PlatformCredentialsSet from the indexer for a given namespace and name. 61 | // Objects returned here must be treated as read-only. 62 | Get(name string) (*v1.PlatformCredentialsSet, error) 63 | PlatformCredentialsSetNamespaceListerExpansion 64 | } 65 | 66 | // platformCredentialsSetNamespaceLister implements the PlatformCredentialsSetNamespaceLister 67 | // interface. 68 | type platformCredentialsSetNamespaceLister struct { 69 | listers.ResourceIndexer[*v1.PlatformCredentialsSet] 70 | } 71 | -------------------------------------------------------------------------------- /pkg/client/listers/zalando.org/v1/stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 23 | "k8s.io/apimachinery/pkg/labels" 24 | "k8s.io/client-go/listers" 25 | "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // StackLister helps list Stacks. 29 | // All objects returned here must be treated as read-only. 30 | type StackLister interface { 31 | // List lists all Stacks in the indexer. 32 | // Objects returned here must be treated as read-only. 33 | List(selector labels.Selector) (ret []*v1.Stack, err error) 34 | // Stacks returns an object that can list and get Stacks. 35 | Stacks(namespace string) StackNamespaceLister 36 | StackListerExpansion 37 | } 38 | 39 | // stackLister implements the StackLister interface. 40 | type stackLister struct { 41 | listers.ResourceIndexer[*v1.Stack] 42 | } 43 | 44 | // NewStackLister returns a new StackLister. 45 | func NewStackLister(indexer cache.Indexer) StackLister { 46 | return &stackLister{listers.New[*v1.Stack](indexer, v1.Resource("stack"))} 47 | } 48 | 49 | // Stacks returns an object that can list and get Stacks. 50 | func (s *stackLister) Stacks(namespace string) StackNamespaceLister { 51 | return stackNamespaceLister{listers.NewNamespaced[*v1.Stack](s.ResourceIndexer, namespace)} 52 | } 53 | 54 | // StackNamespaceLister helps list and get Stacks. 55 | // All objects returned here must be treated as read-only. 56 | type StackNamespaceLister interface { 57 | // List lists all Stacks in the indexer for a given namespace. 58 | // Objects returned here must be treated as read-only. 59 | List(selector labels.Selector) (ret []*v1.Stack, err error) 60 | // Get retrieves the Stack from the indexer for a given namespace and name. 61 | // Objects returned here must be treated as read-only. 62 | Get(name string) (*v1.Stack, error) 63 | StackNamespaceListerExpansion 64 | } 65 | 66 | // stackNamespaceLister implements the StackNamespaceLister 67 | // interface. 68 | type stackNamespaceLister struct { 69 | listers.ResourceIndexer[*v1.Stack] 70 | } 71 | -------------------------------------------------------------------------------- /pkg/client/listers/zalando.org/v1/stackset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1 20 | 21 | import ( 22 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 23 | "k8s.io/apimachinery/pkg/labels" 24 | "k8s.io/client-go/listers" 25 | "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // StackSetLister helps list StackSets. 29 | // All objects returned here must be treated as read-only. 30 | type StackSetLister interface { 31 | // List lists all StackSets in the indexer. 32 | // Objects returned here must be treated as read-only. 33 | List(selector labels.Selector) (ret []*v1.StackSet, err error) 34 | // StackSets returns an object that can list and get StackSets. 35 | StackSets(namespace string) StackSetNamespaceLister 36 | StackSetListerExpansion 37 | } 38 | 39 | // stackSetLister implements the StackSetLister interface. 40 | type stackSetLister struct { 41 | listers.ResourceIndexer[*v1.StackSet] 42 | } 43 | 44 | // NewStackSetLister returns a new StackSetLister. 45 | func NewStackSetLister(indexer cache.Indexer) StackSetLister { 46 | return &stackSetLister{listers.New[*v1.StackSet](indexer, v1.Resource("stackset"))} 47 | } 48 | 49 | // StackSets returns an object that can list and get StackSets. 50 | func (s *stackSetLister) StackSets(namespace string) StackSetNamespaceLister { 51 | return stackSetNamespaceLister{listers.NewNamespaced[*v1.StackSet](s.ResourceIndexer, namespace)} 52 | } 53 | 54 | // StackSetNamespaceLister helps list and get StackSets. 55 | // All objects returned here must be treated as read-only. 56 | type StackSetNamespaceLister interface { 57 | // List lists all StackSets in the indexer for a given namespace. 58 | // Objects returned here must be treated as read-only. 59 | List(selector labels.Selector) (ret []*v1.StackSet, err error) 60 | // Get retrieves the StackSet from the indexer for a given namespace and name. 61 | // Objects returned here must be treated as read-only. 62 | Get(name string) (*v1.StackSet, error) 63 | StackSetNamespaceListerExpansion 64 | } 65 | 66 | // stackSetNamespaceLister implements the StackSetNamespaceLister 67 | // interface. 68 | type stackSetNamespaceLister struct { 69 | listers.ResourceIndexer[*v1.StackSet] 70 | } 71 | -------------------------------------------------------------------------------- /pkg/clientset/unified.go: -------------------------------------------------------------------------------- 1 | package clientset 2 | 3 | import ( 4 | rg "github.com/szuecs/routegroup-client/client/clientset/versioned" 5 | rgv1 "github.com/szuecs/routegroup-client/client/clientset/versioned/typed/zalando.org/v1" 6 | stackset "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 7 | zalandov1 "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/typed/zalando.org/v1" 8 | "k8s.io/client-go/kubernetes" 9 | rest "k8s.io/client-go/rest" 10 | ) 11 | 12 | type Interface interface { 13 | kubernetes.Interface 14 | ZalandoV1() zalandov1.ZalandoV1Interface 15 | RouteGroupV1() rgv1.ZalandoV1Interface 16 | } 17 | 18 | type Clientset struct { 19 | kubernetes.Interface 20 | stackset stackset.Interface 21 | routegroup rg.Interface 22 | } 23 | 24 | func NewClientset(kubernetes kubernetes.Interface, stackset stackset.Interface, routegroup rg.Interface) *Clientset { 25 | return &Clientset{ 26 | kubernetes, 27 | stackset, 28 | routegroup, 29 | } 30 | } 31 | 32 | func NewForConfig(kubeconfig *rest.Config) (*Clientset, error) { 33 | kubeClient, err := kubernetes.NewForConfig(kubeconfig) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | stacksetClient, err := stackset.NewForConfig(kubeconfig) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | rgClient, err := rg.NewForConfig(kubeconfig) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return NewClientset(kubeClient, stacksetClient, rgClient), nil 49 | } 50 | 51 | func (c *Clientset) ZalandoV1() zalandov1.ZalandoV1Interface { 52 | return c.stackset.ZalandoV1() 53 | } 54 | 55 | func (c *Clientset) RouteGroupV1() rgv1.ZalandoV1Interface { 56 | return c.routegroup.ZalandoV1() 57 | } 58 | -------------------------------------------------------------------------------- /pkg/core/helpers.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/util/intstr" 11 | ) 12 | 13 | const ( 14 | APIVersion = "zalando.org/v1" 15 | KindStackSet = "StackSet" 16 | KindStack = "Stack" 17 | 18 | stackGenerationAnnotationKey = "stackset-controller.zalando.org/stack-generation" 19 | ) 20 | 21 | func mergeLabels(labelMaps ...map[string]string) map[string]string { 22 | labels := make(map[string]string) 23 | for _, labelMap := range labelMaps { 24 | for k, v := range labelMap { 25 | labels[k] = v 26 | } 27 | } 28 | return labels 29 | } 30 | 31 | // syncAnnotations synchronizes the given "dest" map with the key/pair values 32 | // from "src". The function removes all keys from "dest" that are not present in 33 | // "src", but specified in "annotationsToSync". 34 | func syncAnnotations(dest, src map[string]string, annotationsToSync []string) map[string]string { 35 | res := mergeLabels(dest, src) 36 | 37 | for _, k := range annotationsToSync { 38 | if _, ok := src[k]; !ok { 39 | delete(res, k) 40 | } 41 | } 42 | 43 | return res 44 | } 45 | 46 | // getKeyValues returns a map with the given keys and values from the given 47 | // annotations. 48 | func getKeyValues( 49 | keys []string, 50 | annotations map[string]string, 51 | ) map[string]string { 52 | 53 | result := make(map[string]string) 54 | for _, key := range keys { 55 | if value, ok := annotations[key]; ok { 56 | result[key] = value 57 | } 58 | } 59 | 60 | return result 61 | } 62 | 63 | // IsResourceUpToDate checks whether the stack is assigned to the resource 64 | // by comparing the stack generation with the corresponding resource annotation. 65 | func IsResourceUpToDate(stack *zv1.Stack, resourceMeta metav1.ObjectMeta) bool { 66 | // We only update the resourceMeta if there are changes. 67 | // We determine changes by comparing the stackGeneration 68 | // (observed generation) stored on the resourceMeta with the 69 | // generation of the Stack. 70 | actualGeneration := getStackGeneration(resourceMeta) 71 | return actualGeneration == stack.Generation 72 | } 73 | 74 | // AreAnnotationsUpToDate checks whether the annotations of the existing and 75 | // updated resource are up to date. 76 | func AreAnnotationsUpToDate(updated, existing metav1.ObjectMeta) bool { 77 | if len(updated.Annotations) != len(existing.Annotations) { 78 | return false 79 | } 80 | 81 | for k, v := range updated.Annotations { 82 | if k == stackGenerationAnnotationKey { 83 | continue 84 | } 85 | 86 | existingValue, ok := existing.GetAnnotations()[k] 87 | if ok && existingValue == v { 88 | continue 89 | } 90 | 91 | return false 92 | } 93 | 94 | return true 95 | } 96 | 97 | // getStackGeneration returns the generation of the stack associated to this resource. 98 | // This value is stored in an annotation of the resource object. 99 | func getStackGeneration(resource metav1.ObjectMeta) int64 { 100 | encodedGeneration := resource.GetAnnotations()[stackGenerationAnnotationKey] 101 | decodedGeneration, err := strconv.ParseInt(encodedGeneration, 10, 64) 102 | if err != nil { 103 | return 0 104 | } 105 | return decodedGeneration 106 | } 107 | 108 | // findBackendPort - given an ingress, routegroup and externalIngress, determine 109 | // which backendPort to use. 110 | func findBackendPort( 111 | ingress *zv1.StackSetIngressSpec, 112 | routeGroup *zv1.RouteGroupSpec, 113 | externalIngress *zv1.StackSetExternalIngressSpec, 114 | ) (*intstr.IntOrString, error) { 115 | var port *intstr.IntOrString 116 | 117 | if ingress != nil { 118 | port = &ingress.BackendPort 119 | } 120 | 121 | if routeGroup != nil { 122 | if port != nil && port.IntValue() != routeGroup.BackendPort { 123 | return nil, fmt.Errorf( 124 | "backendPort for Ingress and RouteGroup does not match %s!=%d", 125 | port.String(), 126 | routeGroup.BackendPort, 127 | ) 128 | } 129 | 130 | rgPort := intstr.FromInt(routeGroup.BackendPort) 131 | port = &rgPort 132 | } 133 | 134 | if port == nil && externalIngress != nil { 135 | return &externalIngress.BackendPort, nil 136 | } 137 | 138 | return port, nil 139 | } 140 | 141 | func wrapTime(time time.Time) *metav1.Time { 142 | if time.IsZero() { 143 | return nil 144 | } 145 | return &metav1.Time{Time: time} 146 | } 147 | 148 | func unwrapTime(tm *metav1.Time) time.Time { 149 | if tm.IsZero() { 150 | return time.Time{} 151 | } 152 | return tm.Time 153 | } 154 | 155 | func effectiveReplicas(replicas *int32) int32 { 156 | if replicas == nil { 157 | return 1 158 | } 159 | return *replicas 160 | } 161 | 162 | func wrapReplicas(replicas int32) *int32 { 163 | return &replicas 164 | } 165 | -------------------------------------------------------------------------------- /pkg/core/helpers_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestMergeLabels(t *testing.T) { 12 | labels1 := map[string]string{ 13 | "foo": "bar", 14 | } 15 | 16 | labels2 := map[string]string{ 17 | "foo": "baz", 18 | } 19 | 20 | merged := mergeLabels(labels1, labels2) 21 | require.Equal(t, labels2, merged) 22 | 23 | labels3 := map[string]string{ 24 | "bar": "foo", 25 | } 26 | 27 | merged = mergeLabels(labels1, labels3) 28 | require.Equal(t, map[string]string{"foo": "bar", "bar": "foo"}, merged) 29 | } 30 | 31 | func TestSyncAnnotations(t *testing.T) { 32 | for _, tc := range []struct { 33 | dest map[string]string 34 | src map[string]string 35 | annotationsToSync []string 36 | expected map[string]string 37 | }{ 38 | { 39 | dest: map[string]string{"a": "1", "b": "2"}, 40 | src: map[string]string{"a": "1"}, 41 | annotationsToSync: []string{"a"}, 42 | expected: map[string]string{"a": "1", "b": "2"}, 43 | }, 44 | { 45 | dest: map[string]string{"a": "1", "b": "2"}, 46 | src: map[string]string{"a": "3"}, 47 | annotationsToSync: []string{"a"}, 48 | expected: map[string]string{"a": "3", "b": "2"}, 49 | }, 50 | { 51 | dest: map[string]string{"a": "1", "b": "2"}, 52 | src: map[string]string{}, 53 | annotationsToSync: []string{"a"}, 54 | expected: map[string]string{"b": "2"}, 55 | }, 56 | { 57 | dest: map[string]string{"a": "1", "b": "2"}, 58 | src: map[string]string{}, 59 | annotationsToSync: []string{}, 60 | expected: map[string]string{"a": "1", "b": "2"}, 61 | }, 62 | } { 63 | res := syncAnnotations(tc.dest, tc.src, tc.annotationsToSync) 64 | if !reflect.DeepEqual(tc.expected, res) { 65 | t.Errorf("expected %v, got %v", tc.expected, res) 66 | } 67 | } 68 | } 69 | 70 | func TestGetKeyValues(t *testing.T) { 71 | for _, tc := range []struct { 72 | keys []string 73 | annotations map[string]string 74 | expected map[string]string 75 | }{ 76 | { 77 | keys: []string{"a"}, 78 | annotations: map[string]string{"a": "1", "b": "2"}, 79 | expected: map[string]string{"a": "1"}, 80 | }, 81 | { 82 | keys: []string{"a", "b"}, 83 | annotations: map[string]string{"a": "1", "b": "2"}, 84 | expected: map[string]string{"a": "1", "b": "2"}, 85 | }, 86 | { 87 | keys: []string{}, 88 | annotations: map[string]string{"a": "1", "b": "2"}, 89 | expected: map[string]string{}, 90 | }, 91 | { 92 | keys: []string{"c"}, 93 | annotations: map[string]string{"a": "1", "b": "2"}, 94 | expected: map[string]string{}, 95 | }, 96 | { 97 | keys: []string{"a", "c"}, 98 | annotations: map[string]string{"a": "1", "b": "2"}, 99 | expected: map[string]string{"a": "1"}, 100 | }, 101 | } { 102 | res := getKeyValues(tc.keys, tc.annotations) 103 | if !reflect.DeepEqual(tc.expected, res) { 104 | t.Errorf("expected %v, got %v", tc.expected, res) 105 | } 106 | } 107 | } 108 | 109 | func TestGetStackGeneration(t *testing.T) { 110 | for _, tc := range []struct { 111 | name string 112 | annotations map[string]string 113 | expected int64 114 | }{ 115 | { 116 | name: "returns 0 without the annotation", 117 | annotations: nil, 118 | expected: 0, 119 | }, 120 | { 121 | name: "returns 0 with an invalid annotation", 122 | annotations: map[string]string{stackGenerationAnnotationKey: "foo"}, 123 | expected: 0, 124 | }, 125 | { 126 | name: "returns parsed annotation value", 127 | annotations: map[string]string{stackGenerationAnnotationKey: "192"}, 128 | expected: 192, 129 | }, 130 | } { 131 | t.Run(tc.name, func(t *testing.T) { 132 | meta := metav1.ObjectMeta{ 133 | Name: "foo", 134 | Annotations: tc.annotations, 135 | } 136 | require.Equal(t, tc.expected, getStackGeneration(meta)) 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pkg/core/metrics.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 10 | ) 11 | 12 | const ( 13 | metricsNamespace = "stackset" 14 | 15 | metricsSubsystemStackset = "stackset" 16 | metricsSubsystemStack = "stack" 17 | metricsSubsystemErrors = "errors" 18 | ) 19 | 20 | type MetricsReporter struct { 21 | stacksetMetricLabels map[resourceKey]prometheus.Labels 22 | stackMetricLabels map[resourceKey]prometheus.Labels 23 | 24 | stacksetCount *prometheus.GaugeVec 25 | 26 | stackDesiredTrafficWeight *prometheus.GaugeVec 27 | stackActualTrafficWeight *prometheus.GaugeVec 28 | stackReady *prometheus.GaugeVec 29 | stackPrescalingActive *prometheus.GaugeVec 30 | stackPrescalingReplicas *prometheus.GaugeVec 31 | errorsCount prometheus.Counter 32 | panicsCount prometheus.Counter 33 | } 34 | 35 | type resourceKey struct { 36 | namespace string 37 | name string 38 | } 39 | 40 | func NewMetricsReporter(registry prometheus.Registerer) (*MetricsReporter, error) { 41 | stacksetLabelNames := []string{"namespace", "stackset", "application"} 42 | stackLabelNames := []string{"namespace", "stack", "application"} 43 | 44 | result := &MetricsReporter{ 45 | stacksetMetricLabels: make(map[resourceKey]prometheus.Labels), 46 | stackMetricLabels: make(map[resourceKey]prometheus.Labels), 47 | stacksetCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 48 | Namespace: metricsNamespace, 49 | Subsystem: metricsSubsystemStackset, 50 | Name: "stacks", 51 | Help: "Number of stacks for this stackset", 52 | }, stacksetLabelNames), 53 | stackDesiredTrafficWeight: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 54 | Namespace: metricsNamespace, 55 | Subsystem: metricsSubsystemStack, 56 | Name: "desired_traffic_weight", 57 | Help: "Desired traffic weight of the stack", 58 | }, stackLabelNames), 59 | stackActualTrafficWeight: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 60 | Namespace: metricsNamespace, 61 | Subsystem: metricsSubsystemStack, 62 | Name: "actual_traffic_weight", 63 | Help: "Actual traffic weight of the stack", 64 | }, stackLabelNames), 65 | stackReady: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 66 | Namespace: metricsNamespace, 67 | Subsystem: metricsSubsystemStack, 68 | Name: "ready", 69 | Help: "Whether the stack is ready", 70 | }, stackLabelNames), 71 | stackPrescalingActive: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 72 | Namespace: metricsNamespace, 73 | Subsystem: metricsSubsystemStack, 74 | Name: "prescaling_active", 75 | Help: "Whether prescaling is active for the stack", 76 | }, stackLabelNames), 77 | stackPrescalingReplicas: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 78 | Namespace: metricsNamespace, 79 | Subsystem: metricsSubsystemStack, 80 | Name: "prescaling_replicas", 81 | Help: "Amount of replicas needed for prescaling", 82 | }, stackLabelNames), 83 | errorsCount: prometheus.NewCounter(prometheus.CounterOpts{ 84 | Namespace: metricsNamespace, 85 | Subsystem: metricsSubsystemErrors, 86 | Name: "count", 87 | Help: "Number of errors encountered", 88 | }), 89 | panicsCount: prometheus.NewCounter(prometheus.CounterOpts{ 90 | Namespace: metricsNamespace, 91 | Subsystem: metricsSubsystemErrors, 92 | Name: "panic_count", 93 | Help: "Number of panics encountered", 94 | }), 95 | } 96 | 97 | for _, metric := range []prometheus.Collector{ 98 | result.stacksetCount, 99 | result.stackDesiredTrafficWeight, 100 | result.stackActualTrafficWeight, 101 | result.stackReady, 102 | result.stackPrescalingActive, 103 | result.stackPrescalingReplicas, 104 | result.errorsCount, 105 | result.panicsCount, 106 | } { 107 | err := registry.Register(metric) 108 | if err != nil { 109 | return nil, err 110 | } 111 | } 112 | 113 | // expose Kubernetes errors as metric 114 | utilruntime.ErrorHandlers = append( 115 | utilruntime.ErrorHandlers, 116 | func(_ context.Context, _ error, _ string, _ ...interface{}) { 117 | result.ReportError() 118 | }, 119 | ) 120 | 121 | return result, nil 122 | } 123 | 124 | func (reporter *MetricsReporter) Report(stacksets map[types.UID]*StackSetContainer) error { 125 | existingStacksets := make(map[resourceKey]struct{}) 126 | existingStacks := make(map[resourceKey]struct{}) 127 | 128 | for _, stackset := range stacksets { 129 | stacksetResource := resourceKey{ 130 | namespace: stackset.StackSet.Namespace, 131 | name: stackset.StackSet.Name, 132 | } 133 | existingStacksets[stacksetResource] = struct{}{} 134 | 135 | labels, ok := reporter.stacksetMetricLabels[stacksetResource] 136 | if !ok { 137 | labels = extractLabels("stackset", stackset.StackSet) 138 | reporter.stacksetMetricLabels[stacksetResource] = labels 139 | } 140 | reporter.reportStacksetMetrics(labels, stackset) 141 | 142 | for _, stack := range stackset.StackContainers { 143 | stackResource := resourceKey{ 144 | namespace: stack.Namespace(), 145 | name: stack.Name(), 146 | } 147 | existingStacks[stackResource] = struct{}{} 148 | 149 | labels, ok := reporter.stackMetricLabels[stackResource] 150 | if !ok { 151 | labels = extractLabels("stack", stack.Stack) 152 | reporter.stackMetricLabels[stackResource] = labels 153 | } 154 | reporter.reportStackMetrics(labels, stack) 155 | } 156 | } 157 | 158 | for resource, labels := range reporter.stacksetMetricLabels { 159 | if _, ok := existingStacksets[resource]; !ok { 160 | reporter.removeStacksetMetrics(labels) 161 | delete(reporter.stacksetMetricLabels, resource) 162 | } 163 | } 164 | 165 | for resource, labels := range reporter.stackMetricLabels { 166 | if _, ok := existingStacks[resource]; !ok { 167 | reporter.removeStackMetrics(labels) 168 | delete(reporter.stackMetricLabels, resource) 169 | } 170 | } 171 | return nil 172 | } 173 | 174 | func (reporter *MetricsReporter) ReportError() { 175 | reporter.errorsCount.Inc() 176 | } 177 | 178 | func extractLabels(nameKey string, obj metav1.Object) prometheus.Labels { 179 | return prometheus.Labels{ 180 | "namespace": obj.GetNamespace(), 181 | nameKey: obj.GetName(), 182 | "application": obj.GetLabels()["application"], 183 | } 184 | } 185 | 186 | func (reporter *MetricsReporter) reportStacksetMetrics(labels prometheus.Labels, stackset *StackSetContainer) { 187 | reporter.stacksetCount.With(labels).Set(float64(len(stackset.StackContainers))) 188 | } 189 | 190 | func (reporter *MetricsReporter) removeStacksetMetrics(labels prometheus.Labels) { 191 | reporter.stacksetCount.Delete(labels) 192 | } 193 | 194 | func (reporter *MetricsReporter) reportStackMetrics(labels prometheus.Labels, stack *StackContainer) { 195 | reporter.stackDesiredTrafficWeight.With(labels).Set(stack.desiredTrafficWeight) 196 | reporter.stackActualTrafficWeight.With(labels).Set(stack.actualTrafficWeight) 197 | 198 | if stack.IsReady() { 199 | reporter.stackReady.With(labels).Set(1.0) 200 | } else { 201 | reporter.stackReady.With(labels).Set(0.0) 202 | } 203 | 204 | if stack.prescalingActive { 205 | reporter.stackPrescalingActive.With(labels).Set(1.0) 206 | reporter.stackPrescalingReplicas.With(labels).Set(float64(stack.prescalingReplicas)) 207 | } else { 208 | reporter.stackPrescalingActive.With(labels).Set(0.0) 209 | reporter.stackPrescalingReplicas.With(labels).Set(0.0) 210 | } 211 | } 212 | 213 | func (reporter *MetricsReporter) removeStackMetrics(labels prometheus.Labels) { 214 | reporter.stackDesiredTrafficWeight.Delete(labels) 215 | reporter.stackActualTrafficWeight.Delete(labels) 216 | reporter.stackReady.Delete(labels) 217 | reporter.stackPrescalingActive.Delete(labels) 218 | reporter.stackPrescalingReplicas.Delete(labels) 219 | } 220 | 221 | func (reporter *MetricsReporter) ReportPanic() { 222 | reporter.panicsCount.Inc() 223 | } 224 | -------------------------------------------------------------------------------- /pkg/core/test_helpers.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "time" 5 | 6 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 7 | v1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/util/intstr" 10 | ) 11 | 12 | var ( 13 | testDefaultCreationTime = time.Now().Add(-time.Hour) 14 | testPort int32 = 8080 15 | intStrTestPort = intstr.FromInt(int(testPort)) 16 | ) 17 | 18 | type testStackFactory struct { 19 | container *StackContainer 20 | } 21 | 22 | func testStack(name string) *testStackFactory { 23 | return &testStackFactory{ 24 | container: &StackContainer{ 25 | backendPort: &intStrTestPort, 26 | Stack: &zv1.Stack{ 27 | ObjectMeta: metav1.ObjectMeta{ 28 | Name: name, 29 | CreationTimestamp: metav1.Time{Time: testDefaultCreationTime}, 30 | }, 31 | }, 32 | Resources: StackResources{ 33 | Service: &v1.Service{ 34 | Spec: v1.ServiceSpec{ 35 | Ports: []v1.ServicePort{ 36 | { 37 | Port: int32(testPort), 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | } 45 | } 46 | 47 | func (f *testStackFactory) ready(replicas int32) *testStackFactory { 48 | return f.partiallyReady(replicas, replicas) 49 | } 50 | 51 | func (f *testStackFactory) partiallyReady(readyReplicas, replicas int32) *testStackFactory { 52 | f.container.resourcesUpdated = true 53 | f.container.deploymentReplicas = replicas 54 | f.container.updatedReplicas = readyReplicas 55 | f.container.readyReplicas = readyReplicas 56 | return f 57 | } 58 | 59 | func (f *testStackFactory) deployment(resourcesUpdated bool, deploymentReplicas, updatedReplicas, readyReplicas int32) *testStackFactory { 60 | f.container.resourcesUpdated = resourcesUpdated 61 | f.container.deploymentReplicas = deploymentReplicas 62 | f.container.updatedReplicas = updatedReplicas 63 | f.container.readyReplicas = readyReplicas 64 | return f 65 | } 66 | 67 | func (f *testStackFactory) traffic(desiredTrafficWeight, actualTrafficWeight float64) *testStackFactory { 68 | f.container.desiredTrafficWeight = desiredTrafficWeight 69 | f.container.actualTrafficWeight = actualTrafficWeight 70 | f.container.currentActualTrafficWeight = actualTrafficWeight 71 | return f 72 | } 73 | 74 | func (f *testStackFactory) currentActualTrafficWeight(weight float64) *testStackFactory { 75 | f.container.currentActualTrafficWeight = weight 76 | return f 77 | } 78 | 79 | func (f *testStackFactory) maxReplicas(replicas int32) *testStackFactory { 80 | f.container.Stack.Spec.StackSpec.Autoscaler = &zv1.Autoscaler{ 81 | MaxReplicas: replicas, 82 | } 83 | return f 84 | } 85 | 86 | func (f *testStackFactory) createdAt(creationTime time.Time) *testStackFactory { 87 | f.container.Stack.CreationTimestamp = metav1.Time{Time: creationTime} 88 | return f 89 | } 90 | 91 | func (f *testStackFactory) noTrafficSince(since time.Time) *testStackFactory { 92 | f.container.noTrafficSince = since 93 | return f 94 | } 95 | 96 | func (f *testStackFactory) pendingRemoval() *testStackFactory { 97 | f.container.PendingRemoval = true 98 | return f 99 | } 100 | 101 | func (f *testStackFactory) prescaling(replicas int32, desiredTrafficWeight float64, lastTrafficIncrease time.Time) *testStackFactory { 102 | f.container.prescalingActive = true 103 | f.container.prescalingReplicas = replicas 104 | f.container.prescalingDesiredTrafficWeight = desiredTrafficWeight 105 | f.container.prescalingLastTrafficIncrease = lastTrafficIncrease 106 | return f 107 | } 108 | 109 | func (f *testStackFactory) stack() *StackContainer { 110 | return f.container 111 | } 112 | -------------------------------------------------------------------------------- /pkg/core/traffic_prescaling.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sort" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // PrescalingTrafficReconciler is a traffic reconciler that forcibly scales up the deployment 12 | // before switching traffic 13 | type PrescalingTrafficReconciler struct { 14 | ResetHPAMinReplicasTimeout time.Duration 15 | } 16 | 17 | func (r PrescalingTrafficReconciler) Reconcile(stacks map[string]*StackContainer, currentTimestamp time.Time) error { 18 | // Calculate how many replicas we need per unit of traffic 19 | totalReplicas := 0.0 20 | totalTraffic := 0.0 21 | 22 | for _, stack := range stacks { 23 | if stack.prescalingActive { 24 | // Stack is prescaled, there are several possibilities 25 | if stack.deploymentReplicas <= stack.prescalingReplicas && stack.prescalingDesiredTrafficWeight > 0 { 26 | // We can't get information out of the HPA, so let's use the information captured previously 27 | totalReplicas += float64(stack.prescalingReplicas) 28 | totalTraffic += stack.prescalingDesiredTrafficWeight 29 | } else if stack.deploymentReplicas > stack.prescalingReplicas && stack.actualTrafficWeight > 0 { 30 | // Even though prescaling is active, stack is scaled up to more replicas and it has traffic, 31 | // let's assume that we can get more precise replicas/traffic information this way 32 | totalReplicas += float64(stack.deploymentReplicas) 33 | totalTraffic += stack.actualTrafficWeight 34 | } 35 | } else if stack.actualTrafficWeight > 0 { 36 | // Stack has traffic and is not prescaled 37 | totalReplicas += float64(stack.deploymentReplicas) 38 | totalTraffic += stack.actualTrafficWeight 39 | } 40 | } 41 | 42 | // Prescale stacks if needed 43 | for _, stack := range stacks { 44 | // If traffic needs to be increased 45 | if stack.desiredTrafficWeight > stack.actualTrafficWeight { 46 | // If prescaling is not active, or desired weight changed since the last prescaling attempt, update 47 | // the target replica count 48 | if !stack.prescalingActive || stack.prescalingDesiredTrafficWeight < stack.desiredTrafficWeight { 49 | stack.prescalingDesiredTrafficWeight = stack.desiredTrafficWeight 50 | 51 | if totalTraffic != 0 { 52 | stack.prescalingReplicas = int32(math.Ceil(stack.desiredTrafficWeight * totalReplicas / totalTraffic)) 53 | } 54 | 55 | // Unable to determine target scale, fallback to stack replicas 56 | if stack.prescalingReplicas == 0 { 57 | stack.prescalingReplicas = effectiveReplicas(stack.Stack.Spec.StackSpec.Replicas) 58 | } 59 | 60 | // Limit to MaxReplicas 61 | if stack.prescalingReplicas > stack.MaxReplicas() { 62 | stack.prescalingReplicas = stack.MaxReplicas() 63 | } 64 | 65 | } 66 | 67 | stack.prescalingActive = true 68 | stack.prescalingLastTrafficIncrease = currentTimestamp 69 | } 70 | 71 | // If prescaling is active and the prescaling timeout has expired then deactivate the prescaling 72 | if stack.prescalingActive && !stack.prescalingLastTrafficIncrease.IsZero() && time.Since(stack.prescalingLastTrafficIncrease) > r.ResetHPAMinReplicasTimeout { 73 | stack.prescalingActive = false 74 | stack.prescalingReplicas = 0 75 | stack.prescalingDesiredTrafficWeight = 0 76 | stack.prescalingLastTrafficIncrease = time.Time{} 77 | } 78 | } 79 | 80 | // Update the traffic weights: 81 | // * If prescaling is active on the stack then it only gets traffic if it has readyReplicas >= prescaleReplicas. 82 | // * If stack is getting traffic but ReadyReplicas < prescaleReplicas, don't remove traffic from it. 83 | // * If no stacks are currently being prescaled fall back to the current weights. 84 | // * If no stacks are getting traffic fall back to desired weight without checking health. 85 | var nonReadyStacks []string 86 | actualWeights := make(map[string]float64, len(stacks)) 87 | for stackName, stack := range stacks { 88 | // Check if we're increasing traffic but the stack is not ready 89 | if stack.desiredTrafficWeight > stack.actualTrafficWeight { 90 | var desiredReplicas = stack.deploymentReplicas 91 | if stack.prescalingActive { 92 | desiredReplicas = stack.prescalingReplicas 93 | } 94 | if !stack.IsReady() || stack.updatedReplicas < desiredReplicas || stack.readyReplicas < desiredReplicas { 95 | nonReadyStacks = append(nonReadyStacks, stackName) 96 | continue 97 | } 98 | } 99 | 100 | actualWeights[stackName] = stack.desiredTrafficWeight 101 | } 102 | 103 | if len(nonReadyStacks) > 0 { 104 | sort.Strings(nonReadyStacks) 105 | return fmt.Errorf("stacks not ready: %s", strings.Join(nonReadyStacks, ", ")) 106 | } 107 | 108 | // TODO: think of case were all are zero and the service/deployment is deleted. 109 | normalizeWeights(actualWeights) 110 | 111 | for stackName, stack := range stacks { 112 | stack.actualTrafficWeight = actualWeights[stackName] 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /pkg/core/traffic_simple.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // SimpleTrafficReconciler is the most simple traffic reconciler which 11 | // implements the default traffic switching supported in the 12 | // stackset-controller. 13 | type SimpleTrafficReconciler struct{} 14 | 15 | func (SimpleTrafficReconciler) Reconcile(stacks map[string]*StackContainer, currentTimestamp time.Time) error { 16 | actualWeights := make(map[string]float64, len(stacks)) 17 | 18 | var nonReadyStacks []string 19 | for stackName, stack := range stacks { 20 | if stack.desiredTrafficWeight > stack.actualTrafficWeight && !stack.IsReady() { 21 | nonReadyStacks = append(nonReadyStacks, stackName) 22 | } 23 | actualWeights[stackName] = stack.desiredTrafficWeight 24 | } 25 | 26 | if len(nonReadyStacks) > 0 { 27 | sort.Strings(nonReadyStacks) 28 | return fmt.Errorf("stacks not ready: %s", strings.Join(nonReadyStacks, ", ")) 29 | } 30 | 31 | // TODO: think of case were all are zero and the service/deployment is deleted. 32 | normalizeWeights(actualWeights) 33 | 34 | for stackName, stack := range stacks { 35 | stack.actualTrafficWeight = actualWeights[stackName] 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/recorder/recorder.go: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | import ( 4 | clientv1 "k8s.io/api/core/v1" 5 | clientset "k8s.io/client-go/kubernetes" 6 | "k8s.io/client-go/kubernetes/fake" 7 | "k8s.io/client-go/kubernetes/scheme" 8 | v1core "k8s.io/client-go/kubernetes/typed/core/v1" 9 | kube_record "k8s.io/client-go/tools/record" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // CreateEventRecorder creates an event recorder to send custom events to Kubernetes to be recorded for targeted Kubernetes objects 15 | func CreateEventRecorder(kubeClient clientset.Interface) kube_record.EventRecorder { 16 | eventBroadcaster := kube_record.NewBroadcasterWithCorrelatorOptions(kube_record.CorrelatorOptions{ 17 | QPS: 1. / 30., 18 | }) 19 | eventBroadcaster.StartLogging(logrus.Infof) 20 | if _, isfake := kubeClient.(*fake.Clientset); !isfake { 21 | eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: v1core.New(kubeClient.CoreV1().RESTClient()).Events("")}) 22 | } 23 | return eventBroadcaster.NewRecorder(scheme.Scheme, clientv1.EventSource{Component: "stackset-controller"}) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/traffic/traffic.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1" 9 | "github.com/zalando-incubator/stackset-controller/pkg/clientset" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/labels" 12 | "k8s.io/apimachinery/pkg/types" 13 | ) 14 | 15 | const ( 16 | stacksetHeritageLabelKey = "stackset" 17 | StackTrafficWeightsAnnotationKey = "zalando.org/stack-traffic-weights" 18 | DefaultBackendWeightsAnnotationKey = "zalando.org/backend-weights" 19 | ) 20 | 21 | // Switcher is able to switch traffic between stacks. 22 | type Switcher struct { 23 | client clientset.Interface 24 | backendWeightsAnnotationKey string 25 | } 26 | 27 | // NewSwitcher initializes a new traffic switcher. 28 | func NewSwitcher(client clientset.Interface, backendWeightsAnnotationKey string) *Switcher { 29 | return &Switcher{ 30 | client: client, 31 | backendWeightsAnnotationKey: backendWeightsAnnotationKey, 32 | } 33 | } 34 | 35 | // Switch changes traffic weight for a stack. 36 | func (t *Switcher) Switch(ctx context.Context, stackset, stack, namespace string, weight float64) ([]StackTrafficWeight, error) { 37 | stacks, err := t.getStacks(ctx, stackset, namespace) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | normalized := normalizeWeights(stacks) 43 | newWeights, err := setWeightForStacks(normalized, stack, weight) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | changeNeeded := false 49 | stackWeights := make(map[string]float64, len(newWeights)) 50 | for i, stack := range newWeights { 51 | if stack.Weight != stacks[i].Weight { 52 | changeNeeded = true 53 | } 54 | stackWeights[stack.Name] = stack.Weight 55 | } 56 | 57 | if changeNeeded { 58 | stackWeightsData, err := json.Marshal(&stackWeights) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | annotation := map[string]map[string]map[string]string{ 64 | "metadata": map[string]map[string]string{ 65 | "annotations": map[string]string{ 66 | StackTrafficWeightsAnnotationKey: string(stackWeightsData), 67 | }, 68 | }, 69 | } 70 | 71 | annotationData, err := json.Marshal(&annotation) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | _, err = t.client.NetworkingV1().Ingresses(namespace).Patch(ctx, stackset, types.StrategicMergePatchType, annotationData, metav1.PatchOptions{}) 77 | if err != nil { 78 | return nil, err 79 | } 80 | } 81 | 82 | return newWeights, nil 83 | } 84 | 85 | type StackTrafficWeight struct { 86 | Name string 87 | Weight float64 88 | ActualWeight float64 89 | } 90 | 91 | // TrafficWeights returns a list of stacks with their current traffic weight. 92 | func (t *Switcher) TrafficWeights(ctx context.Context, stackset, namespace string) ([]StackTrafficWeight, error) { 93 | stacks, err := t.getStacks(ctx, stackset, namespace) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return normalizeWeights(stacks), nil 98 | } 99 | 100 | // getStacks returns the stacks of the stackset. 101 | func (t *Switcher) getStacks(ctx context.Context, stackset, namespace string) ([]StackTrafficWeight, error) { 102 | heritageLabels := map[string]string{ 103 | stacksetHeritageLabelKey: stackset, 104 | } 105 | opts := metav1.ListOptions{ 106 | LabelSelector: labels.Set(heritageLabels).String(), 107 | } 108 | 109 | stacks, err := t.client.ZalandoV1().Stacks(namespace).List(ctx, opts) 110 | if err != nil { 111 | return nil, fmt.Errorf("failed to list stacks of stackset %s/%s: %v", namespace, stackset, err) 112 | } 113 | 114 | desired, actual, err := t.getIngressTraffic(ctx, stackset, namespace, stacks.Items) 115 | if err != nil { 116 | return nil, fmt.Errorf("failed to get Ingress traffic for StackSet %s/%s: %v", namespace, stackset, err) 117 | } 118 | 119 | stackWeights := make([]StackTrafficWeight, 0, len(stacks.Items)) 120 | for _, stack := range stacks.Items { 121 | stackWeight := StackTrafficWeight{ 122 | Name: stack.Name, 123 | Weight: desired[stack.Name], 124 | ActualWeight: actual[stack.Name], 125 | } 126 | 127 | stackWeights = append(stackWeights, stackWeight) 128 | } 129 | return stackWeights, nil 130 | } 131 | 132 | func (t *Switcher) getIngressTraffic(ctx context.Context, name, namespace string, stacks []zv1.Stack) (map[string]float64, map[string]float64, error) { 133 | if len(stacks) == 0 { 134 | return map[string]float64{}, map[string]float64{}, nil 135 | } 136 | 137 | ingress, err := t.client.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{}) 138 | if err != nil { 139 | return nil, nil, err 140 | } 141 | 142 | desiredTraffic := make(map[string]float64, len(stacks)) 143 | if weights, ok := ingress.Annotations[StackTrafficWeightsAnnotationKey]; ok { 144 | err := json.Unmarshal([]byte(weights), &desiredTraffic) 145 | if err != nil { 146 | return nil, nil, fmt.Errorf("failed to get current desired Stack traffic weights: %v", err) 147 | } 148 | } 149 | 150 | actualTraffic := make(map[string]float64, len(stacks)) 151 | if weights, ok := ingress.Annotations[t.backendWeightsAnnotationKey]; ok { 152 | err := json.Unmarshal([]byte(weights), &actualTraffic) 153 | if err != nil { 154 | return nil, nil, fmt.Errorf("failed to get current actual Stack traffic weights: %v", err) 155 | } 156 | } 157 | 158 | return desiredTraffic, actualTraffic, nil 159 | } 160 | 161 | // setWeightForStacks sets new traffic weight for the specified stack and adjusts 162 | // the other stack weights relatively. 163 | // It's assumed that the sum of weights over all stacks are 100. 164 | func setWeightForStacks(stacks []StackTrafficWeight, stackName string, weight float64) ([]StackTrafficWeight, error) { 165 | newWeights := make([]StackTrafficWeight, len(stacks)) 166 | currentWeight := float64(0) 167 | for i, stack := range stacks { 168 | if stack.Name == stackName { 169 | currentWeight = stack.Weight 170 | stack.Weight = weight 171 | newWeights[i] = stack 172 | break 173 | } 174 | } 175 | 176 | change := float64(0) 177 | 178 | if currentWeight < 100 { 179 | change = (100 - weight) / (100 - currentWeight) 180 | } else if weight < 100 { 181 | return nil, fmt.Errorf("'%s' is the only Stack getting traffic, Can't reduce it to %.1f%%", stackName, weight) 182 | } 183 | 184 | for i, stack := range stacks { 185 | if stack.Name != stackName { 186 | stack.Weight *= change 187 | newWeights[i] = stack 188 | } 189 | } 190 | 191 | return newWeights, nil 192 | } 193 | 194 | // allZero returns true if all weights defined in the map are 0. 195 | func allZero(stacks []StackTrafficWeight) bool { 196 | for _, stack := range stacks { 197 | if stack.Weight > 0 { 198 | return false 199 | } 200 | } 201 | return true 202 | } 203 | 204 | // normalizeWeights normalizes the traffic weights specified on the stacks. 205 | // If all weights are zero the total weight of 100 is distributed equally 206 | // between all stacks. 207 | // If not all weights are zero they are normalized to a sum of 100. 208 | func normalizeWeights(stacks []StackTrafficWeight) []StackTrafficWeight { 209 | newWeights := make([]StackTrafficWeight, len(stacks)) 210 | // if all weights are zero distribute them equally to all backends 211 | if allZero(stacks) && len(stacks) > 0 { 212 | eqWeight := 100 / float64(len(stacks)) 213 | for i, stack := range stacks { 214 | stack.Weight = eqWeight 215 | newWeights[i] = stack 216 | } 217 | return newWeights 218 | } 219 | 220 | // if not all weights are zero, normalize them to a sum of 100 221 | sum := float64(0) 222 | for _, stack := range stacks { 223 | sum += stack.Weight 224 | } 225 | 226 | for i, stack := range stacks { 227 | stack.Weight = stack.Weight / sum * 100 228 | newWeights[i] = stack 229 | } 230 | 231 | return newWeights 232 | } 233 | --------------------------------------------------------------------------------