├── .gitignore ├── .travis.yml ├── .zappr.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── SECURITY.md ├── cmd └── traffic │ └── main.go ├── controller ├── ingress.go ├── stack.go └── stackset.go ├── delivery.yaml ├── deploy └── apply │ ├── deployment.yaml │ ├── stack_crd.yaml │ └── stackset_crd.yaml ├── docs ├── deployment.yaml ├── rbac.yaml ├── stack_crd.yaml ├── stackset.yaml └── stackset_crd.yaml ├── hack ├── boilerplate.go.txt └── update-codegen.sh ├── main.go └── pkg ├── apis └── zalando │ ├── register.go │ └── v1 │ ├── register.go │ ├── types.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 │ │ └── v1 │ │ ├── doc.go │ │ ├── fake │ │ ├── doc.go │ │ ├── fake_stack.go │ │ ├── fake_stackset.go │ │ └── fake_zalando_client.go │ │ ├── generated_expansion.go │ │ ├── stack.go │ │ ├── stackset.go │ │ └── zalando_client.go ├── informers │ └── externalversions │ │ ├── factory.go │ │ ├── generic.go │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ └── zalando │ │ ├── interface.go │ │ └── v1 │ │ ├── interface.go │ │ ├── stack.go │ │ └── stackset.go └── listers │ └── zalando │ └── v1 │ ├── expansion_generated.go │ ├── stack.go │ └── stackset.go └── traffic └── traffic.go /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | 4 | go: 5 | - 1.10.x 6 | 7 | before_install: 8 | - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 9 | 10 | install: 11 | - dep ensure --vendor-only -v 12 | 13 | script: 14 | - go get golang.org/x/lint/golint 15 | - make check 16 | - make test 17 | - make build.docker 18 | -------------------------------------------------------------------------------- /.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 | FROM registry.opensource.zalan.do/stups/alpine:latest 2 | MAINTAINER Team Teapot @ Zalando SE 3 | 4 | # add binary 5 | ADD build/linux/stackset-controller / 6 | 7 | ENTRYPOINT ["/stackset-controller"] 8 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/alecthomas/kingpin" 6 | packages = ["."] 7 | revision = "947dcec5ba9c011838740e680966fd7087a71d0d" 8 | version = "v2.2.6" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/alecthomas/template" 13 | packages = [ 14 | ".", 15 | "parse" 16 | ] 17 | revision = "a0175ee3bccc567396460bf5acd36800cb10c49c" 18 | 19 | [[projects]] 20 | branch = "master" 21 | name = "github.com/alecthomas/units" 22 | packages = ["."] 23 | revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" 24 | 25 | [[projects]] 26 | branch = "master" 27 | name = "github.com/beorn7/perks" 28 | packages = ["quantile"] 29 | revision = "3a771d992973f24aa725d07868b467d1ddfceafb" 30 | 31 | [[projects]] 32 | name = "github.com/davecgh/go-spew" 33 | packages = ["spew"] 34 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 35 | version = "v1.1.0" 36 | 37 | [[projects]] 38 | name = "github.com/ghodss/yaml" 39 | packages = ["."] 40 | revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" 41 | version = "v1.0.0" 42 | 43 | [[projects]] 44 | name = "github.com/gogo/protobuf" 45 | packages = [ 46 | "proto", 47 | "sortkeys" 48 | ] 49 | revision = "636bf0302bc95575d69441b25a2603156ffdddf1" 50 | version = "v1.1.1" 51 | 52 | [[projects]] 53 | branch = "master" 54 | name = "github.com/golang/glog" 55 | packages = ["."] 56 | revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" 57 | 58 | [[projects]] 59 | name = "github.com/golang/protobuf" 60 | packages = [ 61 | "proto", 62 | "ptypes", 63 | "ptypes/any", 64 | "ptypes/duration", 65 | "ptypes/timestamp" 66 | ] 67 | revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" 68 | version = "v1.1.0" 69 | 70 | [[projects]] 71 | branch = "master" 72 | name = "github.com/google/btree" 73 | packages = ["."] 74 | revision = "e89373fe6b4a7413d7acd6da1725b83ef713e6e4" 75 | 76 | [[projects]] 77 | name = "github.com/google/go-cmp" 78 | packages = [ 79 | "cmp", 80 | "cmp/cmpopts", 81 | "cmp/internal/diff", 82 | "cmp/internal/function", 83 | "cmp/internal/value" 84 | ] 85 | revision = "3af367b6b30c263d47e8895973edcca9a49cf029" 86 | version = "v0.2.0" 87 | 88 | [[projects]] 89 | branch = "master" 90 | name = "github.com/google/gofuzz" 91 | packages = ["."] 92 | revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" 93 | 94 | [[projects]] 95 | name = "github.com/googleapis/gnostic" 96 | packages = [ 97 | "OpenAPIv2", 98 | "compiler", 99 | "extensions" 100 | ] 101 | revision = "7c663266750e7d82587642f65e60bc4083f1f84e" 102 | version = "v0.2.0" 103 | 104 | [[projects]] 105 | branch = "master" 106 | name = "github.com/gregjones/httpcache" 107 | packages = [ 108 | ".", 109 | "diskcache" 110 | ] 111 | revision = "9cad4c3443a7200dd6400aef47183728de563a38" 112 | 113 | [[projects]] 114 | branch = "master" 115 | name = "github.com/hashicorp/golang-lru" 116 | packages = [ 117 | ".", 118 | "simplelru" 119 | ] 120 | revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3" 121 | 122 | [[projects]] 123 | name = "github.com/imdario/mergo" 124 | packages = ["."] 125 | revision = "9316a62528ac99aaecb4e47eadd6dc8aa6533d58" 126 | version = "v0.3.5" 127 | 128 | [[projects]] 129 | name = "github.com/json-iterator/go" 130 | packages = ["."] 131 | revision = "f2b4162afba35581b6d4a50d3b8f34e33c144682" 132 | 133 | [[projects]] 134 | name = "github.com/matttproud/golang_protobuf_extensions" 135 | packages = ["pbutil"] 136 | revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" 137 | version = "v1.0.1" 138 | 139 | [[projects]] 140 | name = "github.com/modern-go/concurrent" 141 | packages = ["."] 142 | revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" 143 | version = "1.0.3" 144 | 145 | [[projects]] 146 | name = "github.com/modern-go/reflect2" 147 | packages = ["."] 148 | revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" 149 | version = "1.0.1" 150 | 151 | [[projects]] 152 | branch = "master" 153 | name = "github.com/petar/GoLLRB" 154 | packages = ["llrb"] 155 | revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" 156 | 157 | [[projects]] 158 | name = "github.com/peterbourgon/diskv" 159 | packages = ["."] 160 | revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" 161 | version = "v2.0.1" 162 | 163 | [[projects]] 164 | name = "github.com/prometheus/client_golang" 165 | packages = [ 166 | "prometheus", 167 | "prometheus/promhttp" 168 | ] 169 | revision = "c5b7fccd204277076155f10851dad72b76a49317" 170 | version = "v0.8.0" 171 | 172 | [[projects]] 173 | branch = "master" 174 | name = "github.com/prometheus/client_model" 175 | packages = ["go"] 176 | revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f" 177 | 178 | [[projects]] 179 | branch = "master" 180 | name = "github.com/prometheus/common" 181 | packages = [ 182 | "expfmt", 183 | "internal/bitbucket.org/ww/goautoneg", 184 | "model" 185 | ] 186 | revision = "7600349dcfe1abd18d72d3a1770870d9800a7801" 187 | 188 | [[projects]] 189 | branch = "master" 190 | name = "github.com/prometheus/procfs" 191 | packages = [ 192 | ".", 193 | "internal/util", 194 | "nfs", 195 | "xfs" 196 | ] 197 | revision = "ae68e2d4c00fed4943b5f6698d504a5fe083da8a" 198 | 199 | [[projects]] 200 | name = "github.com/sirupsen/logrus" 201 | packages = ["."] 202 | revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" 203 | version = "v1.0.5" 204 | 205 | [[projects]] 206 | name = "github.com/spf13/pflag" 207 | packages = ["."] 208 | revision = "583c0c0531f06d5278b7d917446061adc344b5cd" 209 | version = "v1.0.1" 210 | 211 | [[projects]] 212 | branch = "master" 213 | name = "golang.org/x/crypto" 214 | packages = ["ssh/terminal"] 215 | revision = "a2144134853fc9a27a7b1e3eb4f19f1a76df13c9" 216 | 217 | [[projects]] 218 | branch = "master" 219 | name = "golang.org/x/net" 220 | packages = [ 221 | "context", 222 | "http/httpguts", 223 | "http2", 224 | "http2/hpack", 225 | "idna" 226 | ] 227 | revision = "a680a1efc54dd51c040b3b5ce4939ea3cf2ea0d1" 228 | 229 | [[projects]] 230 | branch = "master" 231 | name = "golang.org/x/sys" 232 | packages = [ 233 | "unix", 234 | "windows" 235 | ] 236 | revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4" 237 | 238 | [[projects]] 239 | name = "golang.org/x/text" 240 | packages = [ 241 | "collate", 242 | "collate/build", 243 | "internal/colltab", 244 | "internal/gen", 245 | "internal/tag", 246 | "internal/triegen", 247 | "internal/ucd", 248 | "language", 249 | "secure/bidirule", 250 | "transform", 251 | "unicode/bidi", 252 | "unicode/cldr", 253 | "unicode/norm", 254 | "unicode/rangetable" 255 | ] 256 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 257 | version = "v0.3.0" 258 | 259 | [[projects]] 260 | branch = "master" 261 | name = "golang.org/x/time" 262 | packages = ["rate"] 263 | revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" 264 | 265 | [[projects]] 266 | name = "gopkg.in/alecthomas/kingpin.v2" 267 | packages = ["."] 268 | revision = "947dcec5ba9c011838740e680966fd7087a71d0d" 269 | version = "v2.2.6" 270 | 271 | [[projects]] 272 | name = "gopkg.in/inf.v0" 273 | packages = ["."] 274 | revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf" 275 | version = "v0.9.1" 276 | 277 | [[projects]] 278 | name = "gopkg.in/yaml.v2" 279 | packages = ["."] 280 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 281 | version = "v2.2.1" 282 | 283 | [[projects]] 284 | branch = "master" 285 | name = "k8s.io/api" 286 | packages = [ 287 | "admissionregistration/v1alpha1", 288 | "admissionregistration/v1beta1", 289 | "apps/v1", 290 | "apps/v1beta1", 291 | "apps/v1beta2", 292 | "authentication/v1", 293 | "authentication/v1beta1", 294 | "authorization/v1", 295 | "authorization/v1beta1", 296 | "autoscaling/v1", 297 | "autoscaling/v2beta1", 298 | "batch/v1", 299 | "batch/v1beta1", 300 | "batch/v2alpha1", 301 | "certificates/v1beta1", 302 | "core/v1", 303 | "events/v1beta1", 304 | "extensions/v1beta1", 305 | "networking/v1", 306 | "policy/v1beta1", 307 | "rbac/v1", 308 | "rbac/v1alpha1", 309 | "rbac/v1beta1", 310 | "scheduling/v1alpha1", 311 | "scheduling/v1beta1", 312 | "settings/v1alpha1", 313 | "storage/v1", 314 | "storage/v1alpha1", 315 | "storage/v1beta1" 316 | ] 317 | revision = "183f3326a9353bd6d41430fc80f96259331d029c" 318 | 319 | [[projects]] 320 | name = "k8s.io/apimachinery" 321 | packages = [ 322 | "pkg/api/errors", 323 | "pkg/api/meta", 324 | "pkg/api/resource", 325 | "pkg/apis/meta/internalversion", 326 | "pkg/apis/meta/v1", 327 | "pkg/apis/meta/v1/unstructured", 328 | "pkg/apis/meta/v1beta1", 329 | "pkg/conversion", 330 | "pkg/conversion/queryparams", 331 | "pkg/fields", 332 | "pkg/labels", 333 | "pkg/runtime", 334 | "pkg/runtime/schema", 335 | "pkg/runtime/serializer", 336 | "pkg/runtime/serializer/json", 337 | "pkg/runtime/serializer/protobuf", 338 | "pkg/runtime/serializer/recognizer", 339 | "pkg/runtime/serializer/streaming", 340 | "pkg/runtime/serializer/versioning", 341 | "pkg/selection", 342 | "pkg/types", 343 | "pkg/util/cache", 344 | "pkg/util/clock", 345 | "pkg/util/diff", 346 | "pkg/util/errors", 347 | "pkg/util/framer", 348 | "pkg/util/intstr", 349 | "pkg/util/json", 350 | "pkg/util/mergepatch", 351 | "pkg/util/net", 352 | "pkg/util/runtime", 353 | "pkg/util/sets", 354 | "pkg/util/strategicpatch", 355 | "pkg/util/validation", 356 | "pkg/util/validation/field", 357 | "pkg/util/wait", 358 | "pkg/util/yaml", 359 | "pkg/version", 360 | "pkg/watch", 361 | "third_party/forked/golang/json", 362 | "third_party/forked/golang/reflect" 363 | ] 364 | revision = "103fd098999dc9c0c88536f5c9ad2e5da39373ae" 365 | 366 | [[projects]] 367 | name = "k8s.io/client-go" 368 | packages = [ 369 | "discovery", 370 | "discovery/fake", 371 | "kubernetes", 372 | "kubernetes/scheme", 373 | "kubernetes/typed/admissionregistration/v1alpha1", 374 | "kubernetes/typed/admissionregistration/v1beta1", 375 | "kubernetes/typed/apps/v1", 376 | "kubernetes/typed/apps/v1beta1", 377 | "kubernetes/typed/apps/v1beta2", 378 | "kubernetes/typed/authentication/v1", 379 | "kubernetes/typed/authentication/v1beta1", 380 | "kubernetes/typed/authorization/v1", 381 | "kubernetes/typed/authorization/v1beta1", 382 | "kubernetes/typed/autoscaling/v1", 383 | "kubernetes/typed/autoscaling/v2beta1", 384 | "kubernetes/typed/batch/v1", 385 | "kubernetes/typed/batch/v1beta1", 386 | "kubernetes/typed/batch/v2alpha1", 387 | "kubernetes/typed/certificates/v1beta1", 388 | "kubernetes/typed/core/v1", 389 | "kubernetes/typed/events/v1beta1", 390 | "kubernetes/typed/extensions/v1beta1", 391 | "kubernetes/typed/networking/v1", 392 | "kubernetes/typed/policy/v1beta1", 393 | "kubernetes/typed/rbac/v1", 394 | "kubernetes/typed/rbac/v1alpha1", 395 | "kubernetes/typed/rbac/v1beta1", 396 | "kubernetes/typed/scheduling/v1alpha1", 397 | "kubernetes/typed/scheduling/v1beta1", 398 | "kubernetes/typed/settings/v1alpha1", 399 | "kubernetes/typed/storage/v1", 400 | "kubernetes/typed/storage/v1alpha1", 401 | "kubernetes/typed/storage/v1beta1", 402 | "pkg/apis/clientauthentication", 403 | "pkg/apis/clientauthentication/v1alpha1", 404 | "pkg/apis/clientauthentication/v1beta1", 405 | "pkg/version", 406 | "plugin/pkg/client/auth/exec", 407 | "rest", 408 | "rest/watch", 409 | "testing", 410 | "tools/auth", 411 | "tools/cache", 412 | "tools/clientcmd", 413 | "tools/clientcmd/api", 414 | "tools/clientcmd/api/latest", 415 | "tools/clientcmd/api/v1", 416 | "tools/metrics", 417 | "tools/pager", 418 | "tools/reference", 419 | "transport", 420 | "util/buffer", 421 | "util/cert", 422 | "util/connrotation", 423 | "util/flowcontrol", 424 | "util/homedir", 425 | "util/integer", 426 | "util/retry" 427 | ] 428 | revision = "7d04d0e2a0a1a4d4a1cd6baa432a2301492e4e65" 429 | version = "v8.0.0" 430 | 431 | [[projects]] 432 | branch = "master" 433 | name = "k8s.io/kube-openapi" 434 | packages = ["pkg/util/proto"] 435 | revision = "d8ea2fe547a448256204cfc68dfee7b26c720acb" 436 | 437 | [solve-meta] 438 | analyzer-name = "dep" 439 | analyzer-version = 1 440 | inputs-digest = "3d0eb779292e54a39dae0d5a7dc175947a06b7b9d6043dde0e3ac94cd8914d45" 441 | solver-name = "gps-cdcl" 442 | solver-version = 1 443 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [[constraint]] 2 | name = "github.com/prometheus/client_golang" 3 | version = "0.8.0" 4 | 5 | [[constraint]] 6 | name = "github.com/sirupsen/logrus" 7 | version = "1.0.5" 8 | 9 | [[constraint]] 10 | name = "gopkg.in/alecthomas/kingpin.v2" 11 | version = "2.2.6" 12 | 13 | [[constraint]] 14 | branch = "master" 15 | name = "k8s.io/api" 16 | 17 | [[constraint]] 18 | revision = "103fd098999dc9c0c88536f5c9ad2e5da39373ae" 19 | name = "k8s.io/apimachinery" 20 | 21 | [[override]] 22 | revision = "f2b4162afba35581b6d4a50d3b8f34e33c144682" 23 | name = "github.com/json-iterator/go" 24 | 25 | [[constraint]] 26 | name = "k8s.io/client-go" 27 | version = "8.0.0" 28 | 29 | [prune] 30 | go-tests = true 31 | unused-packages = true 32 | -------------------------------------------------------------------------------- /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 | VERSION ?= $(shell git describe --tags --always --dirty) 5 | IMAGE ?= registry-write.opensource.zalan.do/teapot/$(BINARY) 6 | TAG ?= $(VERSION) 7 | SOURCES = $(shell find . -name '*.go') 8 | GENERATED = pkg/client pkg/apis/zalando/v1/zz_generated.deepcopy.go 9 | DOCKERFILE ?= Dockerfile 10 | GOPKGS = $(shell go list ./...) 11 | BUILD_FLAGS ?= -v 12 | LDFLAGS ?= -X main.version=$(VERSION) -w -s 13 | 14 | default: build.local 15 | 16 | clean: 17 | rm -rf build 18 | rm -rf $(GENERATED) 19 | 20 | test: $(GENERATED) 21 | go test -v $(GOPKGS) 22 | 23 | check: 24 | golint $(GOPKGS) 25 | go vet -v $(GOPKGS) 26 | 27 | dep: 28 | dep ensure 29 | 30 | $(GENERATED): 31 | ./hack/update-codegen.sh 32 | 33 | build.local: build/$(BINARY) build/traffic 34 | build.linux: build/linux/$(BINARY) 35 | build.osx: build/osx/$(BINARY) 36 | 37 | build/traffic: $(GENERATED) $(SOURCES) 38 | CGO_ENABLED=0 go build -o build/traffic $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" ./cmd/traffic 39 | 40 | build/$(BINARY): $(GENERATED) $(SOURCES) 41 | CGO_ENABLED=0 go build -o build/$(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" . 42 | 43 | build/linux/$(BINARY): $(GENERATED) $(SOURCES) 44 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(BUILD_FLAGS) -o build/linux/$(BINARY) -ldflags "$(LDFLAGS)" . 45 | 46 | build/osx/$(BINARY): $(GENERATED) $(SOURCES) 47 | GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build $(BUILD_FLAGS) -o build/osx/$(BINARY) -ldflags "$(LDFLAGS)" . 48 | 49 | build.docker: build.linux 50 | docker build --rm -t "$(IMAGE):$(TAG)" -f $(DOCKERFILE) . 51 | 52 | build.push: build.docker 53 | docker push "$(IMAGE):$(TAG)" 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes StackSet Controller 2 | [![Build Status](https://travis-ci.org/zalando-incubator/stackset-controller.svg?branch=master)](https://travis-ci.org/zalando-incubator/stackset-controller) 3 | 4 | **Consider this alpha quality, things are subject to change, feedback very welcome!** 5 | 6 | The Kubernetes StackSet Controller is a proposal (along with an 7 | implementation) for easing and automating application life cycle for 8 | certain types of applications running on Kubernetes. 9 | 10 | It is not meant to be a generic solution for all types of applications but it's 11 | explicitly focusing on "Web Applications", that is, application which receive 12 | HTTP traffic and are continuously deployed with new versions which should 13 | receive traffic either instantly or gradually fading traffic from one version 14 | of the application to the next one. Think Blue/Green deployments as one 15 | example. 16 | 17 | By default Kubernetes offers the 18 | [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) 19 | resource type which, combined with a 20 | [Service](https://kubernetes.io/docs/concepts/services-networking/service/), 21 | can provide some level of application life cycle in the form of rolling updates. 22 | While rolling updates are a powerful concept, there are some limitations for 23 | certain use cases: 24 | 25 | * Switching traffic in a Blue/Green style is not possible with rolling 26 | updates. 27 | * Splitting traffic between versions of the application can only be done by 28 | scaling the number of Pods. E.g. if you want to give 1% of traffic to a new 29 | version, you need at least 100 Pods. 30 | * Impossible to run smoke tests against a new version of the application before 31 | it gets traffic. 32 | 33 | To work around these limitations I propose a different type of resource called 34 | an `StackSet` which has the concept of `Stacks`. 35 | 36 | The `StackSet` is a declarative way of describing the application stack as a 37 | whole, and the `Stacks` describe individual versions of the 38 | application. The `StackSet` also allows defining a "global" load balancer 39 | spanning all stacks of the stackset which makes it possible to switch 40 | traffic to different stacks at the load balancer (Ingress) level. 41 | 42 | 43 | ``` 44 | +-----------------------+ 45 | | | 46 | | Load Balancer | 47 | | (Ingress) | 48 | | | 49 | +--+--------+--------+--+ 50 | | 0% | 20% | 80% 51 | +-------------+ | +------------+ 52 | | | | 53 | +---------v---------+ +---------v---------+ +--------v----------+ 54 | | | | | | | 55 | | Stack | | Stack | | Stack | 56 | | Version 1 | | Version 2 | | Version 3 | 57 | | | | | | | 58 | +-------------------+ +-------------------+ +-------------------+ 59 | ``` 60 | 61 | The `StackSet` and `Stack` resources are implemented as 62 | [CRDs][crd]. An `StackSet` looks like this: 63 | 64 | ```yaml 65 | apiVersion: zalando.org/v1 66 | kind: StackSet 67 | metadata: 68 | name: my-app 69 | spec: 70 | # optional Ingress definition. 71 | ingress: 72 | hosts: [my-app.example.org, alt.name.org] 73 | stackLifecycle: 74 | scaledownTTLSeconds: 300 75 | limit: 5 76 | stackTemplate: 77 | spec: 78 | version: v1 # version of the Stack. 79 | replicas: 3 80 | # optional horizontalPodAutoscaler definition (will create an HPA for the stack). 81 | horizontalPodAutoscaler: 82 | minReplicas: 3 83 | maxReplicas: 10 84 | metrics: 85 | - type: Resource 86 | resource: 87 | name: cpu 88 | targetAverageUtilization: 50 89 | # full Pod template. 90 | podTemplate: 91 | spec: 92 | containers: 93 | - name: nginx 94 | image: nginx 95 | ports: 96 | - containerPort: 80 97 | name: ingress 98 | resources: 99 | limits: 100 | cpu: 10m 101 | memory: 50Mi 102 | requests: 103 | cpu: 10m 104 | memory: 50Mi 105 | ``` 106 | 107 | The above `StackSet` would generate a `Stack` that looks like this: 108 | 109 | ```yaml 110 | apiVersion: zalando.org/v1 111 | kind: Stack 112 | metadata: 113 | name: my-app-v1 114 | labels: 115 | stackset: my-app 116 | stackset-version: v1 117 | spec: 118 | replicas: 3 119 | horizontalPodAutoscaler: 120 | minReplicas: 3 121 | maxReplicas: 10 122 | metrics: 123 | - type: Resource 124 | resource: 125 | name: cpu 126 | targetAverageUtilization: 50 127 | podTemplate: 128 | spec: 129 | containers: 130 | - image: nginx 131 | name: nginx 132 | ports: 133 | - containerPort: 80 134 | name: ingress 135 | resources: 136 | limits: 137 | cpu: 10m 138 | memory: 50Mi 139 | requests: 140 | cpu: 10m 141 | memory: 50Mi 142 | ``` 143 | 144 | For each `Stack` a `Service` and `Deployment` resource will be created 145 | automatically with the right labels. The service will also be attached to the 146 | "global" Ingress if the stack is configured to get traffic. An optional 147 | `HorizontalPodAutoscaler` resource can also be created per stack for 148 | horizontally scaling the deployment. 149 | 150 | For the most part the `Stacks` will be dynamically managed by the 151 | system and the users don't have to touch them. You can think of this similar to 152 | the relationship between `Deployments` and `ReplicaSets`. 153 | 154 | If the `Stack` is deleted the related resources like `Service` and 155 | `Deployment` will be automatically cleaned up. 156 | 157 | ## Features 158 | 159 | * Automatically create new Stacks when the `StackSet` is updated with a new 160 | version in the `stackTemplate`. 161 | * Do traffic switching between Stacks at the Ingress layer. Ingress 162 | resources are automatically updated when new stacks are created. (This 163 | require that your ingress controller implements the annotation `zalando.org/backend-weights: {"my-app-1": 80, "my-app-2": 20}`, for example use [skipper](https://github.com/zalando/skipper) for Ingress). 164 | * Safely switch traffic to scaled down stacks. If a stack is scaled down, it 165 | will be scaled up automatically before traffic is directed to it. 166 | * Dynamically provision Ingresses per stack, with per stack host names. I.e. 167 | `my-app.example.org`, `my-app-v1.example.org`, `my-app-v2.example.org`. 168 | * Automatically scale down stacks when they don't get traffic for a specified 169 | period. 170 | * Automatically delete stacks that have been scaled down and are not getting 171 | any traffic for longer time. 172 | * Automatically clean up all dependent resources when a `StackSet` or 173 | `Stack` resource is deleted. This includes `Service`, 174 | `Deployment`, `Ingress` and optionally `HorizontalPodAutoscaler`. 175 | * Command line utility (`traffic`) for showing and switching traffic between 176 | stacks. 177 | 178 | ## How it works 179 | 180 | The controller watches for `StackSet` resources and creates `Stack` resources 181 | whenever the version is updated in the `StackSet` `stackTemplate`. For each 182 | `StackSet` it will create an optional "main" `Ingress` resource and keep it up 183 | to date when new `Stacks` are created for the `StackSet`. For each `Stack` it 184 | will create a `Deployment`, a `Service` and optionally an 185 | `HorizontalPodAutoscaler` for the `Deployment`. These resources are all owned 186 | by the `Stack` and will be cleaned up if the stack is deleted. 187 | 188 | ## Setup 189 | 190 | The `stackset-controller` can be run as a deployment in the cluster. 191 | See [deployment.yaml](/docs/deployment.yaml). 192 | 193 | The controller depends on the [StackSet](/docs/stackset_crd.yaml) and 194 | [Stack](/docs/stack_crd.yaml) 195 | [CRDs][crd]. 196 | You must install these into your cluster before running the controller: 197 | 198 | ```bash 199 | $ kubectl apply -f docs/stackset_crd.yaml -f docs/stackset_stack_crd.yaml 200 | ``` 201 | 202 | After the CRDs are install the controller can be deployed: 203 | 204 | ```bash 205 | $ kubectl apply -f docs/deployment.yaml 206 | ``` 207 | 208 | ### Custom configuration 209 | 210 | ## controller-id 211 | 212 | There are cases where it might be desirable to run multiple instances of the 213 | stackset-controller in the same cluster, e.g. for development. 214 | 215 | To prevent the controllers from fighting over the same `StackSet` resources 216 | they can be configured with the flag `--controller-id=` which 217 | indicates that the controller should only manage the `StackSets` which has an 218 | annotation `stackset-controller.zalando.org/controller=` defined. 219 | If the controller-id is not configured, the controller will manage all 220 | `StackSets` which does not have the annotation defined. 221 | 222 | ## Quick intro 223 | 224 | Once you have deployed the controller you can create your first `StackSet` 225 | resource: 226 | 227 | ```bash 228 | $ kubectl apply -f docs/stackset.yaml 229 | stackset.zalando.org/my-app created 230 | ``` 231 | 232 | This will create the stackset in the cluster: 233 | 234 | ```bash 235 | $ kubectl get stacksets 236 | NAME CREATED AT 237 | my-app 21s 238 | ``` 239 | 240 | And soon after you will see the first `Stack` of the `my-app` 241 | stackset: 242 | 243 | ```bash 244 | $ kubectl get stacksetstacks 245 | NAME CREATED AT 246 | my-app-v1 30s 247 | ``` 248 | 249 | It will also create `Ingress`, `Service`, `Deployment` and 250 | `HorizontalPodAutoscaler` resources: 251 | 252 | ```bash 253 | $ kubectl get ingress,service,deployment.apps,hpa -l stackset=my-app 254 | NAME HOSTS ADDRESS PORTS AGE 255 | ingress.extensions/my-app my-app.example.org kube-ing-lb-3es9a....elb.amazonaws.com 80 7m 256 | ingress.extensions/my-app-v1 my-app-v1.example.org kube-ing-lb-3es9a....elb.amazonaws.com 80 7m 257 | 258 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 259 | service/my-app-v1 ClusterIP 10.3.204.136 80/TCP 7m 260 | 261 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 262 | deployment.apps/my-app-v1 1 1 1 1 7m 263 | 264 | NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE 265 | horizontalpodautoscaler.autoscaling/my-app-v1 Deployment/my-app-v1 /50% 3 10 0 20s 266 | ``` 267 | 268 | Imagine you want to roll out a new version of your stackset. You can do this 269 | by changing the `StackSet` resource. E.g. by changing the version: 270 | 271 | ```bash 272 | $ kubectl patch apps my-app --type='json' -p='[{"op": "replace", "path": "/spec/stackTemplate/spec/version", "value": "v2"}]' 273 | stackset.zalando.org/my-app patched 274 | ``` 275 | 276 | Soon after, we will see a new stack: 277 | 278 | ```bash 279 | $ kubectl get stacks -l stackset=my-app 280 | NAME CREATED AT 281 | my-app-v1 14m 282 | my-app-v2 46s 283 | ``` 284 | 285 | And using the `traffic` tool we can see how the traffic is distributed (see 286 | below for how to build the tool): 287 | 288 | ```bash 289 | ./build/traffic my-app 290 | STACK TRAFFIC WEIGHT 291 | my-app-v1 100.0% 292 | my-app-v2 0.0% 293 | ``` 294 | 295 | If we want to switch 100% traffic to the new stack we can do it like this: 296 | 297 | ```bash 298 | # traffic 299 | ./build/traffic my-app my-app-v2 100 300 | STACK TRAFFIC WEIGHT 301 | my-app-v1 0.0% 302 | my-app-v2 100.0% 303 | ``` 304 | 305 | Since the `my-app-v1` stack is no longer getting traffic it will be scaled down 306 | after some time and eventually deleted. 307 | 308 | If you want to delete it manually, you can simply do: 309 | 310 | ```bash 311 | $ kubectl delete appstack my-app-v1 312 | stacksetstack.zalando.org "my-app-v1" deleted 313 | ``` 314 | 315 | And all the related resources will be gone shortly after: 316 | 317 | ```bash 318 | $ kubectl get ingress,service,deployment.apps,hpa -l stackset=my-app,stackset-version=v1 319 | No resources found. 320 | ``` 321 | 322 | ## Building 323 | 324 | In order to build you first need to get the dependencies which are managed by 325 | [dep](https://github.com/golang/dep). Follow the [installation 326 | instructions](https://github.com/golang/dep#installation) to install it and 327 | then run the following: 328 | 329 | ```sh 330 | $ dep ensure -vendor-only # install all dependencies 331 | ``` 332 | 333 | After dependencies are installed the controller can be built simply by running: 334 | 335 | ```sh 336 | $ make 337 | ``` 338 | 339 | Note that the Go client interface for talking to the custom `StackSet` and 340 | `Stack` CRD is generated code living in `pkg/client/` and 341 | `pkg/apis/zalando/v1/zz_generated_deepcopy.go`. If you make changes to 342 | `pkg/apis/*` then you must run `make clean && make` to regenerate the code. 343 | 344 | To understand how this works see the upstream 345 | [example](https://github.com/kubernetes/apiextensions-apiserver/tree/master/examples/client-go) 346 | for generating client interface code for CRDs. 347 | 348 | [crd]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ 349 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | We acknowledge that every line of code that we write may potentially contain security issues. 2 | 3 | We are trying to deal with it responsibly and provide patches as quickly as possible. If you have anything to report to us please use the following channels: 4 | 5 | Email: Tech-Security@zalando.de 6 | OR 7 | Submit your vulnerability report through our bug bounty program at: https://hackerone.com/zalando 8 | -------------------------------------------------------------------------------- /cmd/traffic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/tabwriter" 7 | 8 | clientset "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 9 | "github.com/zalando-incubator/stackset-controller/pkg/traffic" 10 | "github.com/alecthomas/kingpin" 11 | log "github.com/sirupsen/logrus" 12 | "k8s.io/client-go/kubernetes" 13 | "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 | Application string 24 | Stack string 25 | Traffic float64 26 | Namespace string 27 | } 28 | ) 29 | 30 | func main() { 31 | kingpin.Arg("application", "help").Required().StringVar(&config.Application) 32 | kingpin.Arg("stack", "help").StringVar(&config.Stack) 33 | kingpin.Arg("traffic", "help").Default("-1").Float64Var(&config.Traffic) 34 | kingpin.Flag("namespace", "Namespace of the application resource.").Default(defaultNamespace).StringVar(&config.Namespace) 35 | kingpin.Parse() 36 | 37 | kubeconfig, err := newKubeConfig() 38 | if err != nil { 39 | log.Fatalf("Failed to setup Kubernetes client: %v.", err) 40 | } 41 | 42 | client, err := kubernetes.NewForConfig(kubeconfig) 43 | if err != nil { 44 | log.Fatalf("Failed to setup Kubernetes client: %v", err) 45 | } 46 | 47 | appClient, err := clientset.NewForConfig(kubeconfig) 48 | if err != nil { 49 | log.Fatalf("Failed to setup Kubernetes CRD client: %v", err) 50 | } 51 | 52 | trafficSwitcher := traffic.NewSwitcher(client, appClient) 53 | 54 | if config.Stack != "" && config.Traffic != -1 { 55 | weight := config.Traffic 56 | if weight < 0 || weight > 100 { 57 | log.Fatalf("Traffic weight must be between 0 and 100.") 58 | } 59 | 60 | stacks, err := trafficSwitcher.Switch(config.Application, config.Stack, config.Namespace, weight) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | printTrafficTable(stacks) 65 | return 66 | } 67 | 68 | stacks, err := trafficSwitcher.TrafficWeights(config.Application, config.Namespace) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | printTrafficTable(stacks) 73 | } 74 | 75 | func printTrafficTable(stacks []traffic.StackTrafficWeight) { 76 | var w *tabwriter.Writer 77 | 78 | w = tabwriter.NewWriter(os.Stdout, 8, 8, 4, ' ', 0) 79 | fmt.Fprintf(w, "%s\t%s\t%s\n", "STACK", "DESIRED TRAFFIC", "ACTUAL TRAFFIC") 80 | 81 | for _, stack := range stacks { 82 | fmt.Fprintf(w, 83 | "%s\t%s\t%s\n", 84 | stack.Name, 85 | fmt.Sprintf("%.1f%%", stack.Weight), 86 | fmt.Sprintf("%.1f%%", stack.ActualWeight), 87 | ) 88 | } 89 | 90 | w.Flush() 91 | } 92 | 93 | func newKubeConfig() (*rest.Config, error) { 94 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 95 | configOverrides := &clientcmd.ConfigOverrides{} 96 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) 97 | return kubeConfig.ClientConfig() 98 | } 99 | -------------------------------------------------------------------------------- /controller/ingress.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "sort" 9 | "strings" 10 | "time" 11 | 12 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 13 | clientset "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 14 | "github.com/google/go-cmp/cmp" 15 | log "github.com/sirupsen/logrus" 16 | v1beta1 "k8s.io/api/extensions/v1beta1" 17 | "k8s.io/apimachinery/pkg/api/errors" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | "k8s.io/apimachinery/pkg/labels" 20 | "k8s.io/apimachinery/pkg/util/intstr" 21 | "k8s.io/client-go/kubernetes" 22 | ) 23 | 24 | const ( 25 | stackTrafficWeightsAnnotationKey = "zalando.org/stack-traffic-weights" 26 | backendWeightsAnnotationKey = "zalando.org/backend-weights" 27 | ingressPortName = "ingress" 28 | ) 29 | 30 | // IngressController is a controller that can manage ingresses for an 31 | // stackset. 32 | type IngressController struct { 33 | logger *log.Entry 34 | kube kubernetes.Interface 35 | appClient clientset.Interface 36 | stackset zv1.StackSet 37 | done chan<- struct{} 38 | interval time.Duration 39 | } 40 | 41 | // NewIngressController initializes a new IngressController. 42 | func NewIngressController(client kubernetes.Interface, appClient clientset.Interface, stackset zv1.StackSet, done chan<- struct{}, interval time.Duration) *IngressController { 43 | return &IngressController{ 44 | logger: log.WithFields( 45 | log.Fields{ 46 | "controller": "ingress", 47 | "stackset": stackset.Name, 48 | "namespace": stackset.Namespace, 49 | }, 50 | ), 51 | kube: client, 52 | appClient: appClient, 53 | stackset: stackset, 54 | done: done, 55 | interval: interval, 56 | } 57 | } 58 | 59 | // Run runs the main loop of the IngressController. 60 | func (c *IngressController) Run(ctx context.Context) { 61 | for { 62 | err := c.runOnce() 63 | if err != nil { 64 | c.logger.Error(err) 65 | } 66 | 67 | select { 68 | case <-time.After(c.interval): 69 | case <-ctx.Done(): 70 | c.logger.Info("Terminating Ingress Controller.") 71 | c.done <- struct{}{} 72 | return 73 | } 74 | } 75 | } 76 | 77 | func (c *IngressController) runOnce() error { 78 | ing, err := getIngress(c.kube, &c.stackset) 79 | if err != nil { 80 | if !errors.IsNotFound(err) { 81 | return err 82 | } 83 | } 84 | 85 | heritageLabels := map[string]string{ 86 | stacksetHeritageLabelKey: c.stackset.Name, 87 | } 88 | opts := metav1.ListOptions{ 89 | LabelSelector: labels.Set(heritageLabels).String(), 90 | } 91 | 92 | stacks, err := c.appClient.ZalandoV1().Stacks(c.stackset.Namespace).List(opts) 93 | if err != nil { 94 | return fmt.Errorf("failed to list StackSet stacks of StackSet %s/%s: %v", c.stackset.Namespace, c.stackset.Name, err) 95 | } 96 | 97 | // cleanup Ingress if ingress is disabled. 98 | if c.stackset.Spec.Ingress == nil { 99 | if ing != nil { 100 | c.logger.Infof( 101 | "Deleting obsolete Ingress %s/%s for StackSet %s/%s", 102 | ing.Namespace, 103 | ing.Name, 104 | c.stackset.Namespace, 105 | c.stackset.Name, 106 | ) 107 | err := c.kube.ExtensionsV1beta1().Ingresses(ing.Namespace).Delete(ing.Name, nil) 108 | if err != nil { 109 | return fmt.Errorf( 110 | "failed to delete Ingress %s/%s for StackSet %s/%s: %v", 111 | ing.Namespace, 112 | ing.Name, 113 | c.stackset.Namespace, 114 | c.stackset.Name, 115 | err, 116 | ) 117 | } 118 | 119 | // cleanup any per stack ingresses. 120 | for _, stack := range stacks.Items { 121 | err := c.gcStackIngress(stack) 122 | if err != nil { 123 | log.Error(err) 124 | continue 125 | } 126 | } 127 | } 128 | return nil 129 | } 130 | 131 | stackStatuses, err := c.getStackStatuses(stacks.Items) 132 | if err != nil { 133 | return fmt.Errorf("failed to get Stack statuses for StackSet %s/%s: %v", c.stackset.Namespace, c.stackset.Name, err) 134 | } 135 | 136 | ingress, err := ingressForStackSet(&c.stackset, ing, stackStatuses) 137 | if err != nil { 138 | return fmt.Errorf("failed to generate Ingress for StackSet %s/%s: %v", c.stackset.Namespace, c.stackset.Name, err) 139 | } 140 | 141 | if ing == nil { 142 | c.logger.Infof("Creating Ingress %s/%s with %d service backend(s).", ingress.Namespace, ingress.Name, len(stacks.Items)) 143 | _, err := c.kube.ExtensionsV1beta1().Ingresses(ingress.Namespace).Create(ingress) 144 | if err != nil { 145 | return err 146 | } 147 | } else { 148 | ing.Status = v1beta1.IngressStatus{} 149 | if !reflect.DeepEqual(ing, ingress) { 150 | c.logger.Debugf("Ingress %s/%s changed: %s", ingress.Namespace, ingress.Name, cmp.Diff(ing, ingress)) 151 | c.logger.Infof("Updating Ingress %s/%s with %d service backend(s).", ingress.Namespace, ingress.Name, len(stacks.Items)) 152 | _, err := c.kube.ExtensionsV1beta1().Ingresses(ingress.Namespace).Update(ingress) 153 | if err != nil { 154 | return err 155 | } 156 | } 157 | } 158 | 159 | // create per stack ingress resources in order to have per stack 160 | // hostnames. The ingress created will be owned by the stack and thus 161 | // will automatically get deleted when the corresponding stack is 162 | // deleted. 163 | // Because of how the traffic switching works in skipper we can't just 164 | // have a single ingress with all the hostnames, as the traffic would 165 | // apply to all host rules, even though we don't want traffic switching 166 | // for the per stack hostnames. For this reason we must create extra 167 | // ingresses per stack. 168 | for _, stack := range stacks.Items { 169 | // don't manage ingress for terminating stack. 170 | if stack.DeletionTimestamp != nil && stack.DeletionTimestamp.Time.Before(time.Now().UTC()) { 171 | continue 172 | } 173 | 174 | err := c.stackIngress(stack) 175 | if err != nil { 176 | log.Error(err) 177 | continue 178 | } 179 | } 180 | 181 | return nil 182 | } 183 | 184 | type stackStatus struct { 185 | Stack zv1.Stack 186 | Available bool 187 | } 188 | 189 | func (c *IngressController) getStackStatuses(stacks []zv1.Stack) ([]stackStatus, error) { 190 | statuses := make([]stackStatus, 0, len(stacks)) 191 | for _, stack := range stacks { 192 | status := stackStatus{ 193 | Stack: stack, 194 | } 195 | 196 | // check that service has at least one endpoint, otherwise it 197 | // should not get traffic. 198 | endpoints, err := c.kube.CoreV1().Endpoints(stack.Namespace).Get(stack.Name, metav1.GetOptions{}) 199 | if err != nil { 200 | if !errors.IsNotFound(err) { 201 | return nil, fmt.Errorf( 202 | "failed to get Endpoints for stack %s/%s: %v", 203 | stack.Namespace, 204 | stack.Name, 205 | err, 206 | ) 207 | } 208 | status.Available = false 209 | } else { 210 | readyEndpoints := 0 211 | for _, subset := range endpoints.Subsets { 212 | readyEndpoints += len(subset.Addresses) 213 | } 214 | 215 | status.Available = readyEndpoints > 0 216 | } 217 | 218 | statuses = append(statuses, status) 219 | } 220 | 221 | return statuses, nil 222 | } 223 | 224 | func (c *IngressController) stackIngress(stack zv1.Stack) error { 225 | ingress, err := ingressForStack(&c.stackset, &stack) 226 | if err != nil { 227 | return fmt.Errorf("failed generate Ingress for Stack %s/%s: %s", stack.Namespace, stack.Name, err) 228 | } 229 | 230 | ing, err := c.kube.ExtensionsV1beta1().Ingresses(ingress.Namespace).Get(ingress.Name, metav1.GetOptions{}) 231 | if err != nil { 232 | if !errors.IsNotFound(err) { 233 | return fmt.Errorf("failed to get Ingress %s/%s: %s", ingress.Namespace, ingress.Name, err) 234 | } 235 | ing = nil 236 | } 237 | 238 | if ing == nil { 239 | c.logger.Infof("Creating Ingress %s/%s", ingress.Namespace, ingress.Name) 240 | _, err := c.kube.ExtensionsV1beta1().Ingresses(ingress.Namespace).Create(ingress) 241 | if err != nil { 242 | return fmt.Errorf("failed to create Ingress %s/%s: %s", ingress.Namespace, ingress.Name, err) 243 | } 244 | } else { 245 | // check if ingress is already owned by a different resource. 246 | if !isOwnedReference(stack.TypeMeta, stack.ObjectMeta, ing.ObjectMeta) { 247 | return fmt.Errorf("Ingress %s/%s already has a different owner: %v", ing.Namespace, ing.Name, ing.ObjectMeta.OwnerReferences) 248 | } 249 | 250 | // add objectMeta from existing ingress 251 | ingress.SelfLink = ing.SelfLink 252 | ingress.UID = ing.UID 253 | ingress.Generation = ing.Generation 254 | ingress.CreationTimestamp = ing.CreationTimestamp 255 | ingress.ResourceVersion = ing.ResourceVersion 256 | ing.Status = v1beta1.IngressStatus{} 257 | 258 | if !reflect.DeepEqual(ing, ingress) { 259 | c.logger.Debugf("Ingress %s/%s changed: %s", ingress.Namespace, ingress.Name, cmp.Diff(ing, ingress)) 260 | c.logger.Infof("Updating Ingress %s/%s.", ingress.Namespace, ingress.Name) 261 | _, err := c.kube.ExtensionsV1beta1().Ingresses(ingress.Namespace).Update(ingress) 262 | if err != nil { 263 | return fmt.Errorf("failed to update Ingress %s/%s: %v", ingress.Namespace, ingress.Name, err) 264 | } 265 | } 266 | } 267 | 268 | return nil 269 | } 270 | 271 | func (c *IngressController) gcStackIngress(stack zv1.Stack) error { 272 | ing, err := c.kube.ExtensionsV1beta1().Ingresses(stack.Namespace).Get(stack.Name, metav1.GetOptions{}) 273 | if err != nil { 274 | if !errors.IsNotFound(err) { 275 | return fmt.Errorf("failed to get Ingress %s/%s: %s", stack.Namespace, stack.Name, err) 276 | } 277 | return nil 278 | } 279 | 280 | // check if ingress is already owned by a different resource. 281 | if !isOwnedReference(stack.TypeMeta, stack.ObjectMeta, ing.ObjectMeta) { 282 | return fmt.Errorf("Ingress %s/%s already has a different owner: %v", ing.Namespace, ing.Name, ing.ObjectMeta.OwnerReferences) 283 | } 284 | 285 | c.logger.Infof("Deleting obsolete Ingress %s/%s.", ing.Namespace, ing.Name) 286 | err = c.kube.ExtensionsV1beta1().Ingresses(ing.Namespace).Delete(ing.Name, nil) 287 | if err != nil { 288 | return fmt.Errorf("failed to delete Ingress %s/%s: %v", ing.Namespace, ing.Name, err) 289 | } 290 | 291 | return nil 292 | } 293 | 294 | // ingressForStack generates an ingress object based on a stack. 295 | func ingressForStack(stackset *zv1.StackSet, stack *zv1.Stack) (*v1beta1.Ingress, error) { 296 | ingress := &v1beta1.Ingress{ 297 | ObjectMeta: metav1.ObjectMeta{ 298 | Name: stack.Name, 299 | Namespace: stack.Namespace, 300 | Labels: stack.Labels, 301 | OwnerReferences: []metav1.OwnerReference{ 302 | { 303 | APIVersion: stack.APIVersion, 304 | Kind: stack.Kind, 305 | Name: stack.Name, 306 | UID: stack.UID, 307 | }, 308 | }, 309 | }, 310 | Spec: v1beta1.IngressSpec{ 311 | Rules: make([]v1beta1.IngressRule, 0), 312 | }, 313 | } 314 | 315 | if stackset.Spec.Ingress.Annotations != nil { 316 | ingress.Annotations = map[string]string{} 317 | } 318 | 319 | // insert annotations 320 | for k, v := range stackset.Spec.Ingress.Annotations { 321 | ingress.Annotations[k] = v 322 | } 323 | 324 | rule := v1beta1.IngressRule{ 325 | IngressRuleValue: v1beta1.IngressRuleValue{ 326 | HTTP: &v1beta1.HTTPIngressRuleValue{ 327 | Paths: make([]v1beta1.HTTPIngressPath, 0), 328 | }, 329 | }, 330 | } 331 | 332 | path := v1beta1.HTTPIngressPath{ 333 | Path: "", // TODO: support paths 334 | Backend: v1beta1.IngressBackend{ 335 | ServiceName: stack.Name, 336 | ServicePort: intstr.FromString(ingressPortName), // TODO: find a better way for service port mapping. 337 | }, 338 | } 339 | rule.IngressRuleValue.HTTP.Paths = append(rule.IngressRuleValue.HTTP.Paths, path) 340 | 341 | // create rule per hostname 342 | for _, host := range stackset.Spec.Ingress.Hosts { 343 | r := rule 344 | newHost, err := createSubdomain(host, stack.Name) 345 | if err != nil { 346 | return nil, fmt.Errorf("failed to create domain name: %s", err) 347 | } 348 | r.Host = newHost 349 | ingress.Spec.Rules = append(ingress.Spec.Rules, r) 350 | } 351 | 352 | return ingress, nil 353 | } 354 | 355 | // ingressForStackSet 356 | func ingressForStackSet(stackset *zv1.StackSet, origIngress *v1beta1.Ingress, stackStatuses []stackStatus) (*v1beta1.Ingress, error) { 357 | heritageLabels := map[string]string{ 358 | stacksetHeritageLabelKey: stackset.Name, 359 | } 360 | 361 | labels := mergeLabels( 362 | heritageLabels, 363 | stackset.Labels, 364 | ) 365 | 366 | ingress := &v1beta1.Ingress{ 367 | ObjectMeta: metav1.ObjectMeta{ 368 | Name: stackset.Name, 369 | Namespace: stackset.Namespace, 370 | Labels: labels, 371 | Annotations: map[string]string{}, 372 | OwnerReferences: []metav1.OwnerReference{ 373 | { 374 | APIVersion: stackset.APIVersion, 375 | Kind: stackset.Kind, 376 | Name: stackset.Name, 377 | UID: stackset.UID, 378 | }, 379 | }, 380 | }, 381 | Spec: v1beta1.IngressSpec{ 382 | Rules: make([]v1beta1.IngressRule, 0), 383 | }, 384 | } 385 | 386 | // insert annotations 387 | for k, v := range stackset.Spec.Ingress.Annotations { 388 | ingress.Annotations[k] = v 389 | } 390 | 391 | // set ObjectMeta from the exisiting ingress resource 392 | // this is done to ensure we only update the resource if something 393 | // changes. 394 | if origIngress != nil { 395 | ingress.SelfLink = origIngress.SelfLink 396 | ingress.UID = origIngress.UID 397 | ingress.Generation = origIngress.Generation 398 | ingress.CreationTimestamp = origIngress.CreationTimestamp 399 | ingress.ResourceVersion = origIngress.ResourceVersion 400 | } 401 | 402 | rule := v1beta1.IngressRule{ 403 | IngressRuleValue: v1beta1.IngressRuleValue{ 404 | HTTP: &v1beta1.HTTPIngressRuleValue{ 405 | Paths: make([]v1beta1.HTTPIngressPath, 0), 406 | }, 407 | }, 408 | } 409 | 410 | // get current stack traffic weights stored on ingress. 411 | currentWeights := make(map[string]float64, len(stackStatuses)) 412 | if origIngress != nil { 413 | if weights, ok := origIngress.Annotations[stackTrafficWeightsAnnotationKey]; ok { 414 | err := json.Unmarshal([]byte(weights), ¤tWeights) 415 | if err != nil { 416 | return nil, fmt.Errorf("failed to get current Stack traffic weights: %v", err) 417 | } 418 | } 419 | } 420 | 421 | availableWeights, allWeights := computeBackendWeights(stackStatuses, currentWeights) 422 | 423 | for backend, traffic := range availableWeights { 424 | if traffic > 0 { 425 | path := v1beta1.HTTPIngressPath{ 426 | Path: "", // TODO: support paths 427 | Backend: v1beta1.IngressBackend{ 428 | ServiceName: backend, 429 | ServicePort: intstr.FromString(ingressPortName), // TODO: find a better way for service port mapping. 430 | }, 431 | } 432 | rule.IngressRuleValue.HTTP.Paths = append(rule.IngressRuleValue.HTTP.Paths, path) 433 | } 434 | } 435 | 436 | // sort backends by name to have a consitent generated ingress 437 | // resource. 438 | sort.Slice(rule.IngressRuleValue.HTTP.Paths, func(i, j int) bool { 439 | return rule.IngressRuleValue.HTTP.Paths[i].Backend.ServiceName < rule.IngressRuleValue.HTTP.Paths[j].Backend.ServiceName 440 | }) 441 | 442 | // create rule per hostname 443 | for _, host := range stackset.Spec.Ingress.Hosts { 444 | r := rule 445 | r.Host = host 446 | ingress.Spec.Rules = append(ingress.Spec.Rules, r) 447 | } 448 | 449 | availableWeightsData, err := json.Marshal(&availableWeights) 450 | if err != nil { 451 | return nil, err 452 | } 453 | 454 | allWeightsData, err := json.Marshal(&allWeights) 455 | if err != nil { 456 | return nil, err 457 | } 458 | 459 | if ingress.Annotations == nil { 460 | ingress.Annotations = map[string]string{} 461 | } 462 | 463 | ingress.Annotations[backendWeightsAnnotationKey] = string(availableWeightsData) 464 | ingress.Annotations[stackTrafficWeightsAnnotationKey] = string(allWeightsData) 465 | 466 | return ingress, nil 467 | } 468 | 469 | // allZero returns true if all weights defined in the map are 0. 470 | func allZero(weights map[string]float64) bool { 471 | for _, weight := range weights { 472 | if weight > 0 { 473 | return false 474 | } 475 | } 476 | return true 477 | } 478 | 479 | // normalizeWeights normalizes a map of backend weights. 480 | // If all weights are zero the total weight of 100 is distributed equally 481 | // between all backends. 482 | // If not all weights are zero they are normalized to a sum of 100. 483 | // Note this modifies the passed map inplace instead of returning a modified 484 | // copy. 485 | func normalizeWeights(backendWeights map[string]float64) { 486 | // if all weights are zero distribute them equally to all backends 487 | if allZero(backendWeights) && len(backendWeights) > 0 { 488 | eqWeight := 100 / float64(len(backendWeights)) 489 | for backend := range backendWeights { 490 | backendWeights[backend] = eqWeight 491 | } 492 | return 493 | } 494 | 495 | // if not all weights are zero, normalize them to a sum of 100 496 | sum := float64(0) 497 | for _, weight := range backendWeights { 498 | sum += weight 499 | } 500 | 501 | for backend, weight := range backendWeights { 502 | backendWeights[backend] = weight / sum * 100 503 | } 504 | } 505 | 506 | func computeBackendWeights(stacks []stackStatus, traffic map[string]float64) (map[string]float64, map[string]float64) { 507 | backendWeights := make(map[string]float64, len(stacks)) 508 | availableBackends := make(map[string]float64, len(stacks)) 509 | for _, stack := range stacks { 510 | backendWeights[stack.Stack.Name] = traffic[stack.Stack.Name] 511 | 512 | if stack.Available { 513 | availableBackends[stack.Stack.Name] = traffic[stack.Stack.Name] 514 | } 515 | } 516 | 517 | // TODO: validate this logic 518 | if !allZero(backendWeights) { 519 | normalizeWeights(backendWeights) 520 | } 521 | 522 | if len(availableBackends) == 0 { 523 | availableBackends = backendWeights 524 | } 525 | 526 | // TODO: think of case were all are zero and the service/deployment is 527 | // deleted. 528 | normalizeWeights(availableBackends) 529 | 530 | return availableBackends, backendWeights 531 | } 532 | 533 | func getIngress(client kubernetes.Interface, stackset *zv1.StackSet) (*v1beta1.Ingress, error) { 534 | // check for existing ingress object 535 | ing, err := client.ExtensionsV1beta1().Ingresses(stackset.Namespace).Get(stackset.Name, metav1.GetOptions{}) 536 | if err != nil { 537 | return nil, err 538 | } 539 | 540 | // check if ingress is owned by the stackset resource 541 | if !isOwnedReference(stackset.TypeMeta, stackset.ObjectMeta, ing.ObjectMeta) { 542 | return nil, fmt.Errorf( 543 | "found Ingress '%s/%s' not managed by the StackSet %s/%s", 544 | ing.Namespace, 545 | ing.Name, 546 | stackset.Namespace, 547 | stackset.Name, 548 | ) 549 | } 550 | 551 | return ing, nil 552 | } 553 | 554 | // isOwnedReference returns true of the dependent object is owned by the owner 555 | // object. 556 | func isOwnedReference(ownerTypeMeta metav1.TypeMeta, ownerObjectMeta, dependent metav1.ObjectMeta) bool { 557 | for _, ref := range dependent.OwnerReferences { 558 | if ref.APIVersion == ownerTypeMeta.APIVersion && 559 | ref.Kind == ownerTypeMeta.Kind && 560 | ref.UID == ownerObjectMeta.UID && 561 | ref.Name == ownerObjectMeta.Name { 562 | return true 563 | } 564 | } 565 | return false 566 | } 567 | 568 | // createSubdomain creates a subdomain giving an existing domain by replacing 569 | // the first section of the domain. E.g. given the domain: my-app.example.org 570 | // and the subdomain part my-new-app the resulting domain will be 571 | // my-new-app.example.org. 572 | func createSubdomain(domain, subdomain string) (string, error) { 573 | names := strings.SplitN(domain, ".", 2) 574 | if len(names) != 2 { 575 | return "", fmt.Errorf("unexpected domain format: %s", domain) 576 | } 577 | 578 | names[0] = subdomain 579 | 580 | return strings.Join(names, "."), nil 581 | } 582 | -------------------------------------------------------------------------------- /controller/stack.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "math" 8 | "reflect" 9 | "time" 10 | 11 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 12 | clientset "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 13 | "github.com/google/go-cmp/cmp" 14 | "github.com/google/go-cmp/cmp/cmpopts" 15 | log "github.com/sirupsen/logrus" 16 | appsv1 "k8s.io/api/apps/v1" 17 | autoscaling "k8s.io/api/autoscaling/v2beta1" 18 | "k8s.io/api/core/v1" 19 | "k8s.io/apimachinery/pkg/api/errors" 20 | "k8s.io/apimachinery/pkg/api/resource" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/labels" 23 | "k8s.io/apimachinery/pkg/types" 24 | "k8s.io/apimachinery/pkg/util/intstr" 25 | "k8s.io/client-go/kubernetes" 26 | ) 27 | 28 | const ( 29 | // noTrafficScaledownTTLAnnotationKey = "stacksetstacks.zalando.org/no-traffic-scaledown-ttl" 30 | noTrafficSinceAnnotationKey = "stacksetstacks.zalando.org/no-traffic-since" 31 | ) 32 | 33 | // StackController is a controller for managing StackSet Stack 34 | // resources like Deployment and Service. 35 | type StackController struct { 36 | logger *log.Entry 37 | kube kubernetes.Interface 38 | appClient clientset.Interface 39 | stackset zv1.StackSet 40 | noTrafficScaledownTTL time.Duration 41 | noTrafficTerminationTTL time.Duration 42 | interval time.Duration 43 | done chan<- struct{} 44 | } 45 | 46 | // NewStackController initializes a new StackController. 47 | func NewStackController(client kubernetes.Interface, appClient clientset.Interface, stackset zv1.StackSet, done chan<- struct{}, noTrafficScaledownTTL, noTrafficTerminationTTL, interval time.Duration) *StackController { 48 | return &StackController{ 49 | logger: log.WithFields( 50 | log.Fields{ 51 | "controller": "stack", 52 | "stackset": stackset.Name, 53 | "namespace": stackset.Namespace, 54 | }, 55 | ), 56 | kube: client, 57 | appClient: appClient, 58 | stackset: stackset, 59 | noTrafficScaledownTTL: noTrafficScaledownTTL, 60 | noTrafficTerminationTTL: noTrafficTerminationTTL, 61 | done: done, 62 | interval: interval, 63 | } 64 | } 65 | 66 | // Run runs the Stack Controller control loop. 67 | func (c *StackController) Run(ctx context.Context) { 68 | for { 69 | err := c.runOnce() 70 | if err != nil { 71 | c.logger.Error(err) 72 | } 73 | 74 | select { 75 | case <-time.After(c.interval): 76 | case <-ctx.Done(): 77 | c.logger.Info("Terminating Stack Controller.") 78 | c.done <- struct{}{} 79 | return 80 | } 81 | } 82 | } 83 | 84 | // runOnce runs one loop of the Stack Controller. 85 | func (c *StackController) runOnce() error { 86 | heritageLabels := map[string]string{ 87 | stacksetHeritageLabelKey: c.stackset.Name, 88 | } 89 | opts := metav1.ListOptions{ 90 | LabelSelector: labels.Set(heritageLabels).String(), 91 | } 92 | 93 | stacks, err := c.appClient.ZalandoV1().Stacks(c.stackset.Namespace).List(opts) 94 | if err != nil { 95 | return fmt.Errorf("failed to list Stacks of StackSet %s/%s: %v", c.stackset.Namespace, c.stackset.Name, err) 96 | } 97 | 98 | var traffic map[string]TrafficStatus 99 | if c.stackset.Spec.Ingress != nil && len(stacks.Items) > 0 { 100 | traffic, err = getIngressTraffic(c.kube, &c.stackset) 101 | if err != nil { 102 | return fmt.Errorf("failed to get Ingress traffic for StackSet %s/%s: %v", c.stackset.Namespace, c.stackset.Name, err) 103 | } 104 | } 105 | 106 | for _, stack := range stacks.Items { 107 | err = c.manageStack(stack, traffic) 108 | if err != nil { 109 | log.Errorf("Failed to manage Stack %s/%s: %v", stack.Namespace, stack.Name, err) 110 | continue 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func getIngressTraffic(client kubernetes.Interface, stackset *zv1.StackSet) (map[string]TrafficStatus, error) { 118 | ingress, err := getIngress(client, stackset) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | desiredTraffic := make(map[string]float64, 0) 124 | if weights, ok := ingress.Annotations[stackTrafficWeightsAnnotationKey]; ok { 125 | err := json.Unmarshal([]byte(weights), &desiredTraffic) 126 | if err != nil { 127 | return nil, fmt.Errorf("failed to get current desired Stack traffic weights: %v", err) 128 | } 129 | } 130 | 131 | actualTraffic := make(map[string]float64, 0) 132 | if weights, ok := ingress.Annotations[backendWeightsAnnotationKey]; ok { 133 | err := json.Unmarshal([]byte(weights), &actualTraffic) 134 | if err != nil { 135 | return nil, fmt.Errorf("failed to get current actual Stack traffic weights: %v", err) 136 | } 137 | } 138 | 139 | traffic := make(map[string]TrafficStatus, len(desiredTraffic)) 140 | 141 | for stackName, weight := range desiredTraffic { 142 | traffic[stackName] = TrafficStatus{ 143 | ActualWeight: actualTraffic[stackName], 144 | DesiredWeight: weight, 145 | } 146 | } 147 | 148 | return traffic, nil 149 | } 150 | 151 | // manageStack manages the stack by managing the related Deployment and Service 152 | // resources. 153 | func (c *StackController) manageStack(stack zv1.Stack, traffic map[string]TrafficStatus) error { 154 | err := c.manageDeployment(stack, traffic) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | return nil 160 | } 161 | 162 | // manageDeployment manages the deployment owned by the stack. 163 | func (c *StackController) manageDeployment(stack zv1.Stack, traffic map[string]TrafficStatus) error { 164 | deployment, err := getDeployment(c.kube, stack) 165 | if err != nil { 166 | if !errors.IsNotFound(err) { 167 | return err 168 | } 169 | } 170 | 171 | var origDeployment *appsv1.Deployment 172 | if deployment != nil { 173 | origDeployment = deployment.DeepCopy() 174 | } 175 | 176 | template := templateInjectLabels(stack.Spec.PodTemplate, stack.Labels) 177 | 178 | // apply default pod template spec values into the stack template. 179 | // This is sort of a hack to make sure the bare template from the stack 180 | // resource represents the defaults so we can compare changes later on 181 | // when deciding if the deployment resource should be updated or not. 182 | // TODO: find less ugly solution. 183 | template = applyPodTemplateSpecDefaults(template) 184 | 185 | createDeployment := false 186 | 187 | if deployment == nil { 188 | createDeployment = true 189 | deployment = &appsv1.Deployment{ 190 | ObjectMeta: metav1.ObjectMeta{ 191 | Name: stack.Name, 192 | Namespace: stack.Namespace, 193 | Annotations: map[string]string{}, 194 | // add custom label owner reference since we can't use Kubernetes 195 | // OwnerReferences as this would trigger a deletion of the Deployment 196 | // as soon as the Stack is deleted. 197 | Labels: map[string]string{stacksetStackOwnerReferenceLabelKey: string(stack.UID)}, 198 | }, 199 | Spec: appsv1.DeploymentSpec{ 200 | Selector: &metav1.LabelSelector{ 201 | MatchLabels: stack.Labels, 202 | }, 203 | Replicas: stack.Spec.Replicas, 204 | }, 205 | } 206 | } 207 | 208 | for k, v := range stack.Labels { 209 | deployment.Labels[k] = v 210 | } 211 | 212 | deployment.Spec.Template = template 213 | 214 | // if autoscaling is disabled or if autoscaling is ena 215 | // check if we need to explicitly set replicas on the deplotment. There 216 | // are two cases: 217 | // 1. Autoscaling is disabled and we should rely on the replicas set on 218 | // the stack 219 | // 2. Autoscaling is enabled, but current replica count is 0. In this 220 | // case we have to set a value > 0 otherwise the autoscaler won't do 221 | // anything. 222 | if stack.Spec.HorizontalPodAutoscaler == nil || 223 | (deployment.Spec.Replicas != nil && *deployment.Spec.Replicas == 0) { 224 | deployment.Spec.Replicas = stack.Spec.Replicas 225 | } 226 | 227 | if traffic != nil && traffic[stack.Name].Weight() <= 0 { 228 | 229 | // if the stack is being terminated and the stack isn't getting 230 | // traffic. Then there's no need to create a deployment. 231 | if createDeployment && stack.DeletionTimestamp != nil { 232 | return nil 233 | } 234 | 235 | if ttl, ok := deployment.Annotations[noTrafficSinceAnnotationKey]; ok { 236 | noTrafficSince, err := time.Parse(time.RFC3339, ttl) 237 | if err != nil { 238 | return fmt.Errorf("failed to parse no-traffic-since timestamp '%s': %v", ttl, err) 239 | } 240 | 241 | // TODO: make ttl configurable per app/stack 242 | if !noTrafficSince.IsZero() && time.Since(noTrafficSince) > c.noTrafficScaledownTTL { 243 | replicas := int32(0) 244 | deployment.Spec.Replicas = &replicas 245 | } 246 | 247 | if stack.DeletionTimestamp != nil && stack.DeletionTimestamp.Time.Before(time.Now().UTC()) { 248 | if !noTrafficSince.IsZero() && time.Since(noTrafficSince) > c.noTrafficTerminationTTL { 249 | // delete deployment 250 | if !createDeployment { 251 | c.logger.Infof("Deleting Deployment %s/%s no longer needed", deployment.Namespace, deployment.Name) 252 | err = c.kube.AppsV1().Deployments(deployment.Namespace).Delete(deployment.Name, nil) 253 | if err != nil { 254 | return fmt.Errorf( 255 | "failed to delete Deployment %s/%s owned by Stack %s/%s", 256 | deployment.Namespace, 257 | deployment.Name, 258 | stack.Namespace, 259 | stack.Name, 260 | ) 261 | } 262 | } 263 | return nil 264 | } 265 | } 266 | } else { 267 | deployment.Annotations[noTrafficSinceAnnotationKey] = time.Now().UTC().Format(time.RFC3339) 268 | } 269 | } else { 270 | // ensure the scaledown annotation is removed if the stack has 271 | // traffic. 272 | if _, ok := deployment.Annotations[noTrafficSinceAnnotationKey]; ok { 273 | delete(deployment.Annotations, noTrafficSinceAnnotationKey) 274 | } 275 | } 276 | 277 | // create deployment if stack is not terminating. 278 | if createDeployment && stack.DeletionTimestamp == nil { 279 | c.logger.Infof( 280 | "Creating Deployment %s/%s for StackSet stack %s/%s", 281 | deployment.Namespace, 282 | deployment.Name, 283 | stack.Namespace, 284 | stack.Name, 285 | ) 286 | deployment, err = c.kube.AppsV1().Deployments(deployment.Namespace).Create(deployment) 287 | if err != nil { 288 | return err 289 | } 290 | } else { 291 | // only update the resource if there are changes 292 | // TODO: still if we add just the annotation it could mess with 293 | // the HPA. 294 | if !reflect.DeepEqual(origDeployment, deployment) { 295 | c.logger.Debugf("Deployment %s/%s changed: %s", 296 | deployment.Namespace, deployment.Name, 297 | cmp.Diff( 298 | origDeployment, 299 | deployment, 300 | cmpopts.IgnoreUnexported(resource.Quantity{}), 301 | ), 302 | ) 303 | c.logger.Infof( 304 | "Updating Deployment %s/%s for StackSet stack %s/%s", 305 | deployment.Namespace, 306 | deployment.Name, 307 | stack.Namespace, 308 | stack.Name, 309 | ) 310 | deployment, err = c.kube.AppsV1().Deployments(deployment.Namespace).Update(deployment) 311 | if err != nil { 312 | return err 313 | } 314 | } 315 | } 316 | 317 | // set TypeMeta manually because of this bug: 318 | // https://github.com/kubernetes/client-go/issues/308 319 | deployment.APIVersion = "apps/v1" 320 | deployment.Kind = "Deployment" 321 | 322 | hpa, err := c.manageAutoscaling(stack, deployment, traffic) 323 | if err != nil { 324 | return err 325 | } 326 | 327 | err = c.manageService(stack, deployment) 328 | if err != nil { 329 | return err 330 | } 331 | 332 | // update stack status 333 | stack.Status.Replicas = deployment.Status.Replicas 334 | stack.Status.ReadyReplicas = deployment.Status.ReadyReplicas 335 | stack.Status.UpdatedReplicas = deployment.Status.UpdatedReplicas 336 | 337 | if traffic != nil { 338 | stack.Status.ActualTrafficWeight = traffic[stack.Name].ActualWeight 339 | stack.Status.DesiredTrafficWeight = traffic[stack.Name].DesiredWeight 340 | } 341 | 342 | if hpa != nil { 343 | stack.Status.DesiredReplicas = hpa.Status.DesiredReplicas 344 | } 345 | 346 | // TODO: log the change in status 347 | // update status of stackset 348 | _, err = c.appClient.ZalandoV1().Stacks(stack.Namespace).UpdateStatus(&stack) 349 | if err != nil { 350 | return err 351 | } 352 | 353 | return nil 354 | } 355 | 356 | type TrafficStatus struct { 357 | ActualWeight float64 358 | DesiredWeight float64 359 | } 360 | 361 | func (t TrafficStatus) Weight() float64 { 362 | return math.Max(t.ActualWeight, t.DesiredWeight) 363 | } 364 | 365 | // manageAutoscaling manages the HPA defined for the stack. 366 | func (c *StackController) manageAutoscaling(stack zv1.Stack, deployment *appsv1.Deployment, traffic map[string]TrafficStatus) (*autoscaling.HorizontalPodAutoscaler, error) { 367 | hpa, err := c.getHPA(deployment) 368 | if err != nil { 369 | if !errors.IsNotFound(err) { 370 | return nil, err 371 | } 372 | } 373 | 374 | var origHPA *autoscaling.HorizontalPodAutoscaler 375 | if hpa != nil { 376 | hpa.Status = autoscaling.HorizontalPodAutoscalerStatus{} 377 | origHPA = hpa.DeepCopy() 378 | } 379 | 380 | // cleanup HPA if autoscaling is disabled or the stack has 0 traffic. 381 | if stack.Spec.HorizontalPodAutoscaler == nil || (traffic != nil && traffic[stack.Name].Weight() <= 0) { 382 | if hpa != nil { 383 | c.logger.Infof( 384 | "Deleting obsolete HPA %s/%s for Deployment %s/%s", 385 | hpa.Namespace, 386 | hpa.Name, 387 | deployment.Namespace, 388 | deployment.Name, 389 | ) 390 | return nil, c.kube.AutoscalingV2beta1().HorizontalPodAutoscalers(hpa.Namespace).Delete(hpa.Name, nil) 391 | } 392 | return nil, nil 393 | } 394 | 395 | createHPA := false 396 | 397 | if hpa == nil { 398 | createHPA = true 399 | hpa = &autoscaling.HorizontalPodAutoscaler{ 400 | ObjectMeta: metav1.ObjectMeta{ 401 | Name: deployment.Name, 402 | Namespace: deployment.Namespace, 403 | Annotations: stack.Spec.HorizontalPodAutoscaler.Annotations, 404 | OwnerReferences: []metav1.OwnerReference{ 405 | { 406 | APIVersion: deployment.APIVersion, 407 | Kind: deployment.Kind, 408 | Name: deployment.Name, 409 | UID: deployment.UID, 410 | }, 411 | }, 412 | }, 413 | Spec: autoscaling.HorizontalPodAutoscalerSpec{ 414 | ScaleTargetRef: autoscaling.CrossVersionObjectReference{ 415 | APIVersion: deployment.APIVersion, 416 | Kind: deployment.Kind, 417 | Name: deployment.Name, 418 | }, 419 | }, 420 | } 421 | } 422 | 423 | hpa.Labels = deployment.Labels 424 | hpa.Spec.MinReplicas = stack.Spec.HorizontalPodAutoscaler.MinReplicas 425 | hpa.Spec.MaxReplicas = stack.Spec.HorizontalPodAutoscaler.MaxReplicas 426 | hpa.Spec.Metrics = stack.Spec.HorizontalPodAutoscaler.Metrics 427 | 428 | if createHPA { 429 | c.logger.Infof( 430 | "Creating HPA %s/%s for Deployment %s/%s", 431 | hpa.Namespace, 432 | hpa.Name, 433 | deployment.Namespace, 434 | deployment.Name, 435 | ) 436 | _, err := c.kube.AutoscalingV2beta1().HorizontalPodAutoscalers(hpa.Namespace).Create(hpa) 437 | if err != nil { 438 | return nil, err 439 | } 440 | } else { 441 | if !reflect.DeepEqual(origHPA, hpa) { 442 | c.logger.Debugf("HPA %s/%s changed: %s", hpa.Namespace, hpa.Name, cmp.Diff(origHPA, hpa)) 443 | c.logger.Infof( 444 | "Updating HPA %s/%s for Deployment %s/%s", 445 | hpa.Namespace, 446 | hpa.Name, 447 | deployment.Namespace, 448 | deployment.Name, 449 | ) 450 | hpa, err = c.kube.AutoscalingV2beta1().HorizontalPodAutoscalers(hpa.Namespace).Update(hpa) 451 | if err != nil { 452 | return nil, err 453 | } 454 | } 455 | } 456 | 457 | return hpa, nil 458 | } 459 | 460 | // getHPA gets HPA owned by the Deployment. 461 | func (c *StackController) getHPA(deployment *appsv1.Deployment) (*autoscaling.HorizontalPodAutoscaler, error) { 462 | // check for existing object 463 | hpa, err := c.kube.AutoscalingV2beta1().HorizontalPodAutoscalers(deployment.Namespace).Get(deployment.Name, metav1.GetOptions{}) 464 | if err != nil { 465 | return nil, err 466 | } 467 | 468 | // check if object is owned by the deployment resource 469 | if !isOwnedReference(deployment.TypeMeta, deployment.ObjectMeta, hpa.ObjectMeta) { 470 | return nil, fmt.Errorf( 471 | "found HPA '%s/%s' not managed by the Deployment %s/%s", 472 | hpa.Namespace, 473 | hpa.Name, 474 | deployment.Namespace, 475 | deployment.Name, 476 | ) 477 | } 478 | 479 | return hpa, nil 480 | } 481 | 482 | // manageService manages the service for a given stack. 483 | func (c *StackController) manageService(stack zv1.Stack, deployment *appsv1.Deployment) error { 484 | service, err := getStackService(c.kube, *deployment) 485 | if err != nil { 486 | if !errors.IsNotFound(err) { 487 | return err 488 | } 489 | } 490 | 491 | var origService *v1.Service 492 | if service != nil { 493 | origService = service.DeepCopy() 494 | } 495 | 496 | createService := false 497 | 498 | if service == nil { 499 | createService = true 500 | service = &v1.Service{ 501 | ObjectMeta: metav1.ObjectMeta{ 502 | Name: stack.Name, 503 | Namespace: stack.Namespace, 504 | OwnerReferences: []metav1.OwnerReference{ 505 | { 506 | APIVersion: deployment.APIVersion, 507 | Kind: deployment.Kind, 508 | Name: deployment.Name, 509 | UID: deployment.UID, 510 | }, 511 | }, 512 | }, 513 | Spec: v1.ServiceSpec{ 514 | Type: v1.ServiceTypeClusterIP, 515 | }, 516 | } 517 | } 518 | 519 | service.Labels = stack.Labels 520 | service.Spec.Selector = stack.Labels 521 | // TODO: iterate on this 522 | service.Spec.Ports = servicePortsFromTemplate(stack.Spec.PodTemplate) 523 | 524 | if createService { 525 | c.logger.Infof( 526 | "Creating Service %s/%s for StackSet stack %s/%s", 527 | service.Namespace, 528 | service.Name, 529 | stack.Namespace, 530 | stack.Name, 531 | ) 532 | _, err := c.kube.CoreV1().Services(service.Namespace).Create(service) 533 | if err != nil { 534 | return err 535 | } 536 | } else { 537 | if !reflect.DeepEqual(origService, service) { 538 | c.logger.Debugf("Service %s/%s changed: %s", service.Namespace, service.Name, cmp.Diff(origService, service)) 539 | c.logger.Infof( 540 | "Updating Service %s/%s for StackSet stack %s/%s", 541 | service.Namespace, 542 | service.Name, 543 | stack.Namespace, 544 | stack.Name, 545 | ) 546 | _, err := c.kube.CoreV1().Services(service.Namespace).Update(service) 547 | if err != nil { 548 | return err 549 | } 550 | } 551 | } 552 | 553 | return nil 554 | } 555 | 556 | // getDeployment gets Deployment owned by StackSet stack. 557 | func getDeployment(kube kubernetes.Interface, stack zv1.Stack) (*appsv1.Deployment, error) { 558 | // check for existing object 559 | deployment, err := kube.AppsV1().Deployments(stack.Namespace).Get(stack.Name, metav1.GetOptions{}) 560 | if err != nil { 561 | return nil, err 562 | } 563 | 564 | // check if object is owned by the stack resource 565 | if uid, ok := deployment.Labels[stacksetStackOwnerReferenceLabelKey]; !ok || types.UID(uid) != stack.UID { 566 | return nil, fmt.Errorf( 567 | "found Deployment '%s/%s' not managed by the StackSet stack %s/%s", 568 | deployment.Namespace, 569 | deployment.Name, 570 | stack.Namespace, 571 | stack.Name, 572 | ) 573 | } 574 | 575 | return deployment, nil 576 | } 577 | 578 | // getStackService gets service owned by StackSet stack. 579 | func getStackService(kube kubernetes.Interface, deployment appsv1.Deployment) (*v1.Service, error) { 580 | // check for existing object 581 | service, err := kube.CoreV1().Services(deployment.Namespace).Get(deployment.Name, metav1.GetOptions{}) 582 | if err != nil { 583 | return nil, err 584 | } 585 | 586 | // check if object is owned by the deployment resource 587 | if !isOwnedReference(deployment.TypeMeta, deployment.ObjectMeta, service.ObjectMeta) { 588 | return nil, fmt.Errorf( 589 | "found Service '%s/%s' not managed by the Deployment %s/%s", 590 | service.Namespace, 591 | service.Name, 592 | deployment.Namespace, 593 | deployment.Name, 594 | ) 595 | } 596 | 597 | return service, nil 598 | } 599 | 600 | // servicePortsFromTemplate gets service port from pod template. 601 | func servicePortsFromTemplate(template v1.PodTemplateSpec) []v1.ServicePort { 602 | ports := make([]v1.ServicePort, 0) 603 | for _, container := range template.Spec.Containers { 604 | for i, port := range container.Ports { 605 | name := fmt.Sprintf("port-%d", i) 606 | if port.Name != "" { 607 | name = port.Name 608 | } 609 | servicePort := v1.ServicePort{ 610 | Name: name, 611 | Protocol: port.Protocol, 612 | Port: port.ContainerPort, 613 | TargetPort: intstr.FromInt(int(port.ContainerPort)), 614 | } 615 | // set default protocol if not specified 616 | if servicePort.Protocol == "" { 617 | servicePort.Protocol = v1.ProtocolTCP 618 | } 619 | ports = append(ports, servicePort) 620 | } 621 | } 622 | return ports 623 | } 624 | 625 | // templateInjectLabels injects labels into a pod template spec. 626 | func templateInjectLabels(template v1.PodTemplateSpec, labels map[string]string) v1.PodTemplateSpec { 627 | if template.ObjectMeta.Labels == nil { 628 | template.ObjectMeta.Labels = map[string]string{} 629 | } 630 | 631 | for key, value := range labels { 632 | if _, ok := template.ObjectMeta.Labels[key]; !ok { 633 | template.ObjectMeta.Labels[key] = value 634 | } 635 | } 636 | return template 637 | } 638 | 639 | // applyPodTemplateSpecDefaults inject default values into a pod template spec. 640 | func applyPodTemplateSpecDefaults(template v1.PodTemplateSpec) v1.PodTemplateSpec { 641 | newTemplate := template.DeepCopy() 642 | for i, container := range newTemplate.Spec.Containers { 643 | for j, port := range container.Ports { 644 | if port.Protocol == "" { 645 | newTemplate.Spec.Containers[i].Ports[j].Protocol = v1.ProtocolTCP 646 | } 647 | } 648 | if container.TerminationMessagePath == "" { 649 | newTemplate.Spec.Containers[i].TerminationMessagePath = v1.TerminationMessagePathDefault 650 | } 651 | if container.TerminationMessagePolicy == "" { 652 | newTemplate.Spec.Containers[i].TerminationMessagePolicy = v1.TerminationMessageReadFile 653 | } 654 | if container.ImagePullPolicy == "" { 655 | newTemplate.Spec.Containers[i].ImagePullPolicy = v1.PullIfNotPresent 656 | } 657 | } 658 | if newTemplate.Spec.RestartPolicy == "" { 659 | newTemplate.Spec.RestartPolicy = v1.RestartPolicyAlways 660 | } 661 | if newTemplate.Spec.TerminationGracePeriodSeconds == nil { 662 | gracePeriod := int64(v1.DefaultTerminationGracePeriodSeconds) 663 | newTemplate.Spec.TerminationGracePeriodSeconds = &gracePeriod 664 | } 665 | if newTemplate.Spec.DNSPolicy == "" { 666 | newTemplate.Spec.DNSPolicy = v1.DNSClusterFirst 667 | } 668 | if newTemplate.Spec.SecurityContext == nil { 669 | newTemplate.Spec.SecurityContext = &v1.PodSecurityContext{} 670 | } 671 | if newTemplate.Spec.SchedulerName == "" { 672 | newTemplate.Spec.SchedulerName = v1.DefaultSchedulerName 673 | } 674 | return *newTemplate 675 | } 676 | -------------------------------------------------------------------------------- /controller/stackset.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "reflect" 8 | "sort" 9 | "sync" 10 | "time" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | "github.com/google/go-cmp/cmp/cmpopts" 14 | log "github.com/sirupsen/logrus" 15 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 16 | clientset "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 17 | "k8s.io/api/core/v1" 18 | "k8s.io/apimachinery/pkg/api/errors" 19 | "k8s.io/apimachinery/pkg/api/resource" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/fields" 22 | "k8s.io/apimachinery/pkg/labels" 23 | "k8s.io/apimachinery/pkg/types" 24 | "k8s.io/client-go/kubernetes" 25 | "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | const ( 29 | stacksetHeritageLabelKey = "stackset" 30 | stackVersionLabelKey = "stack-version" 31 | defaultVersion = "default" 32 | defaultStackLifecycleLimit = 10 33 | stacksetStackFinalizer = "finalizer.stacks.zalando.org" 34 | stacksetFinalizer = "finalizer.stacksets.zalando.org" // TODO: implement this 35 | stacksetStackOwnerReferenceLabelKey = "stackset-stack-owner-reference" 36 | stacksetControllerControllerAnnotationKey = "stackset-controller.zalando.org/controller" 37 | ) 38 | 39 | // StackSetController is the main controller. It watches for changes to 40 | // stackset resources and starts and maintains other controllers per 41 | // stackset resource. 42 | type StackSetController struct { 43 | logger *log.Entry 44 | kube kubernetes.Interface 45 | appClient clientset.Interface 46 | controllerID string 47 | interval time.Duration 48 | stacksetStackMinGCAge time.Duration 49 | noTrafficScaledownTTL time.Duration 50 | noTrafficTerminationTTL time.Duration 51 | controllerTable map[types.UID]controllerEntry 52 | stacksetEvents chan stacksetEvent 53 | sync.Mutex 54 | } 55 | 56 | type stacksetEvent struct { 57 | Deleted bool 58 | StackSet *zv1.StackSet 59 | } 60 | 61 | type controllerEntry struct { 62 | Cancel context.CancelFunc 63 | Done []<-chan struct{} 64 | } 65 | 66 | // NewStackSetController initializes a new StackSetController. 67 | func NewStackSetController(client kubernetes.Interface, appClient clientset.Interface, controllerID string, stacksetStackMinGCAge, noTrafficScaledownTTL, noTrafficTerminationTTL, interval time.Duration) *StackSetController { 68 | return &StackSetController{ 69 | logger: log.WithFields(log.Fields{"controller": "stackset"}), 70 | kube: client, 71 | appClient: appClient, 72 | controllerID: controllerID, 73 | stacksetStackMinGCAge: stacksetStackMinGCAge, 74 | noTrafficScaledownTTL: noTrafficScaledownTTL, 75 | noTrafficTerminationTTL: noTrafficTerminationTTL, 76 | stacksetEvents: make(chan stacksetEvent, 1), 77 | controllerTable: map[types.UID]controllerEntry{}, 78 | interval: interval, 79 | } 80 | } 81 | 82 | // Run runs the main loop of the StackSetController. Before the loops it 83 | // sets up a watcher to watch StackSet resources. The watch will send 84 | // changes over a channel which is polled from the main loop. 85 | func (c *StackSetController) Run(ctx context.Context) { 86 | c.startWatch(ctx) 87 | 88 | for { 89 | select { 90 | case e := <-c.stacksetEvents: 91 | stackset := *e.StackSet 92 | // set TypeMeta manually because of this bug: 93 | // https://github.com/kubernetes/client-go/issues/308 94 | stackset.APIVersion = "zalando.org/v1" 95 | stackset.Kind = "StackSet" 96 | 97 | // clear existing entry 98 | if entry, ok := c.controllerTable[stackset.UID]; ok { 99 | c.logger.Infof("Stopping controllers for StackSet %s/%s", stackset.Namespace, stackset.Name) 100 | entry.Cancel() 101 | for _, d := range entry.Done { 102 | <-d 103 | } 104 | delete(c.controllerTable, stackset.UID) 105 | c.logger.Infof("Controllers stopped for StackSet %s/%s", stackset.Namespace, stackset.Name) 106 | } 107 | 108 | if e.Deleted { 109 | continue 110 | } 111 | 112 | // check if stackset should be managed by the controller 113 | if !c.hasOwnership(&stackset) { 114 | continue 115 | } 116 | 117 | err := c.manageStackSet(&stackset) 118 | if err != nil { 119 | c.logger.Error(err) 120 | continue 121 | } 122 | 123 | c.logger.Infof("Starting controllers for StackSet %s/%s", stackset.Namespace, stackset.Name) 124 | ctx, cancel := context.WithCancel(ctx) 125 | entry := controllerEntry{ 126 | Cancel: cancel, 127 | Done: []<-chan struct{}{}, 128 | } 129 | 130 | stackControllerDone := make(chan struct{}, 1) 131 | entry.Done = append(entry.Done, stackControllerDone) 132 | stackController := NewStackController(c.kube, c.appClient, stackset, stackControllerDone, c.noTrafficScaledownTTL, c.noTrafficTerminationTTL, c.interval) 133 | go stackController.Run(ctx) 134 | 135 | ingressControllerDone := make(chan struct{}, 1) 136 | entry.Done = append(entry.Done, ingressControllerDone) 137 | ingressController := NewIngressController(c.kube, c.appClient, stackset, ingressControllerDone, c.interval) 138 | go ingressController.Run(ctx) 139 | 140 | stackGCDone := make(chan struct{}, 1) 141 | entry.Done = append(entry.Done, stackGCDone) 142 | go c.runStackGC(ctx, stackset, stackGCDone) 143 | 144 | updateStatusDone := make(chan struct{}, 1) 145 | entry.Done = append(entry.Done, updateStatusDone) 146 | go c.runUpdateStatus(ctx, stackset, updateStatusDone) 147 | 148 | c.controllerTable[stackset.UID] = entry 149 | case <-ctx.Done(): 150 | c.logger.Info("Terminating main controller loop.") 151 | // wait for all controllers 152 | for uid, entry := range c.controllerTable { 153 | entry.Cancel() 154 | for _, d := range entry.Done { 155 | <-d 156 | } 157 | delete(c.controllerTable, uid) 158 | } 159 | return 160 | } 161 | } 162 | } 163 | 164 | // hasOwnership returns true if the controller is the "owner" of the stackset. 165 | // Whether it's owner is determined by the value of the 166 | // 'stackset-controller.zalando.org/controller' annotation. If the value 167 | // matches the controllerID then it owns it, or if the controllerID is 168 | // "" and there's no annotation set. 169 | func (c *StackSetController) hasOwnership(stackset *zv1.StackSet) bool { 170 | if stackset.Annotations != nil { 171 | if owner, ok := stackset.Annotations[stacksetControllerControllerAnnotationKey]; ok { 172 | return owner == c.controllerID 173 | } 174 | } 175 | return c.controllerID == "" 176 | } 177 | 178 | func (c *StackSetController) startWatch(ctx context.Context) { 179 | informer := cache.NewSharedIndexInformer( 180 | cache.NewListWatchFromClient(c.appClient.ZalandoV1().RESTClient(), "stacksets", v1.NamespaceAll, fields.Everything()), 181 | &zv1.StackSet{}, 182 | 0, // skip resync 183 | cache.Indexers{}, 184 | ) 185 | 186 | informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 187 | AddFunc: c.add, 188 | UpdateFunc: c.update, 189 | DeleteFunc: c.del, 190 | }) 191 | 192 | go informer.Run(ctx.Done()) 193 | 194 | if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) { 195 | c.logger.Errorf("Timed out waiting for caches to sync") 196 | return 197 | } 198 | 199 | c.logger.Info("Synced StackSet watcher") 200 | } 201 | 202 | func (c *StackSetController) add(obj interface{}) { 203 | stackset, ok := obj.(*zv1.StackSet) 204 | if !ok { 205 | c.logger.Error("Failed to get StackSet object") 206 | return 207 | } 208 | 209 | c.logger.Infof("New StackSet added %s/%s", stackset.Namespace, stackset.Name) 210 | c.stacksetEvents <- stacksetEvent{ 211 | StackSet: stackset.DeepCopy(), 212 | } 213 | } 214 | 215 | func (c *StackSetController) update(oldObj, newObj interface{}) { 216 | newStackset, ok := newObj.(*zv1.StackSet) 217 | if !ok { 218 | c.logger.Error("Failed to get StackSet object") 219 | return 220 | } 221 | 222 | oldStackset, ok := oldObj.(*zv1.StackSet) 223 | if !ok { 224 | c.logger.Error("Failed to get StackSet object") 225 | return 226 | } 227 | 228 | c.logger.Debugf("StackSet %s/%s changed: %s", 229 | newStackset.Namespace, 230 | newStackset.Name, 231 | cmp.Diff(oldStackset, newStackset, cmpopts.IgnoreUnexported(resource.Quantity{})), 232 | ) 233 | 234 | c.logger.Infof("StackSet updated %s/%s", newStackset.Namespace, newStackset.Name) 235 | c.stacksetEvents <- stacksetEvent{ 236 | StackSet: newStackset.DeepCopy(), 237 | } 238 | } 239 | 240 | func (c *StackSetController) del(obj interface{}) { 241 | stackset, ok := obj.(*zv1.StackSet) 242 | if !ok { 243 | c.logger.Error("Failed to get StackSet object") 244 | return 245 | } 246 | 247 | c.logger.Infof("StackSet deleted %s/%s", stackset.Namespace, stackset.Name) 248 | c.stacksetEvents <- stacksetEvent{ 249 | StackSet: stackset.DeepCopy(), 250 | Deleted: true, 251 | } 252 | } 253 | 254 | func (c *StackSetController) runUpdateStatus(ctx context.Context, stackset zv1.StackSet, done chan<- struct{}) { 255 | for { 256 | err := c.updateStatus(stackset) 257 | if err != nil { 258 | c.logger.Error(err) 259 | } 260 | 261 | select { 262 | case <-time.After(c.interval): 263 | case <-ctx.Done(): 264 | c.logger.Info("Terminating update status loop.") 265 | done <- struct{}{} 266 | return 267 | } 268 | } 269 | } 270 | 271 | func (c *StackSetController) updateStatus(stackset zv1.StackSet) error { 272 | heritageLabels := map[string]string{ 273 | stacksetHeritageLabelKey: stackset.Name, 274 | } 275 | opts := metav1.ListOptions{ 276 | LabelSelector: labels.Set(heritageLabels).String(), 277 | } 278 | 279 | stacks, err := c.appClient.ZalandoV1().Stacks(stackset.Namespace).List(opts) 280 | if err != nil { 281 | return fmt.Errorf("failed to list Stacks of StackSet %s/%s: %v", stackset.Namespace, stackset.Name, err) 282 | } 283 | 284 | var traffic map[string]TrafficStatus 285 | if stackset.Spec.Ingress != nil && len(stacks.Items) > 0 { 286 | traffic, err = getIngressTraffic(c.kube, &stackset) 287 | if err != nil { 288 | return fmt.Errorf("failed to get Ingress traffic for StackSet %s/%s: %v", stackset.Namespace, stackset.Name, err) 289 | } 290 | } 291 | 292 | stacksWithTraffic := int32(0) 293 | for _, stack := range stacks.Items { 294 | if traffic != nil && traffic[stack.Name].Weight() > 0 { 295 | stacksWithTraffic++ 296 | } 297 | } 298 | 299 | newStatus := zv1.StackSetStatus{ 300 | Stacks: int32(len(stacks.Items)), 301 | StacksWithTraffic: stacksWithTraffic, 302 | ReadyStacks: readyStacks(stacks.Items), 303 | } 304 | 305 | if !reflect.DeepEqual(newStatus, stackset.Status) { 306 | stackset.Status = newStatus 307 | // TODO: log the change in status 308 | // update status of stackset 309 | _, err = c.appClient.ZalandoV1().StackSets(stackset.Namespace).UpdateStatus(&stackset) 310 | if err != nil { 311 | return err 312 | } 313 | } 314 | 315 | return nil 316 | } 317 | 318 | func readyStacks(stacks []zv1.Stack) int32 { 319 | var readyStacks int32 320 | for _, stack := range stacks { 321 | replicas := stack.Status.Replicas 322 | if replicas == stack.Status.ReadyReplicas && replicas == stack.Status.UpdatedReplicas { 323 | readyStacks++ 324 | } 325 | } 326 | return readyStacks 327 | } 328 | 329 | func (c *StackSetController) runStackGC(ctx context.Context, stackset zv1.StackSet, done chan<- struct{}) { 330 | for { 331 | err := c.gcStacks(stackset) 332 | if err != nil { 333 | c.logger.Error(err) 334 | } 335 | 336 | select { 337 | // TODO: change GC interval 338 | case <-time.After(c.interval): 339 | case <-ctx.Done(): 340 | c.logger.Info("Terminating stack Garbage collector.") 341 | done <- struct{}{} 342 | return 343 | } 344 | } 345 | } 346 | 347 | func (c *StackSetController) gcStacks(stackset zv1.StackSet) error { 348 | heritageLabels := map[string]string{ 349 | stacksetHeritageLabelKey: stackset.Name, 350 | } 351 | opts := metav1.ListOptions{ 352 | LabelSelector: labels.Set(heritageLabels).String(), 353 | } 354 | 355 | stacks, err := c.appClient.ZalandoV1().Stacks(stackset.Namespace).List(opts) 356 | if err != nil { 357 | return fmt.Errorf("failed to list Stacks of StackSet %s/%s: %v", stackset.Namespace, stackset.Name, err) 358 | } 359 | 360 | var traffic map[string]TrafficStatus 361 | if stackset.Spec.Ingress != nil && len(stacks.Items) > 0 { 362 | traffic, err = getIngressTraffic(c.kube, &stackset) 363 | if err != nil { 364 | return fmt.Errorf("failed to get Ingress traffic for StackSet %s/%s: %v", stackset.Namespace, stackset.Name, err) 365 | } 366 | } 367 | 368 | historyLimit := defaultStackLifecycleLimit 369 | if stackset.Spec.StackLifecycle != nil && stackset.Spec.StackLifecycle.Limit != nil { 370 | historyLimit = int(*stackset.Spec.StackLifecycle.Limit) 371 | } 372 | 373 | gcCandidates := make([]zv1.Stack, 0, len(stacks.Items)) 374 | for _, stack := range stacks.Items { 375 | // handle Stacks terminating 376 | if stack.DeletionTimestamp != nil && stack.DeletionTimestamp.Time.Before(time.Now().UTC()) && strInSlice(stacksetStackFinalizer, stack.Finalizers) { 377 | c.logger.Infof( 378 | "Stack %s/%s has been marked for termination. Checking if it's safe to remove it", 379 | stack.Namespace, 380 | stack.Name, 381 | ) 382 | 383 | if traffic != nil && traffic[stack.Name].Weight() > 0 { 384 | c.logger.Warnf( 385 | "Unable to delete terminating Stack '%s/%s'. Still getting %.1f%% traffic", 386 | stack.Namespace, 387 | stack.Name, 388 | traffic[stack.Name], 389 | ) 390 | continue 391 | } 392 | 393 | deployment, err := getDeployment(c.kube, stack) 394 | if err != nil { 395 | if !errors.IsNotFound(err) { 396 | return err 397 | } 398 | } 399 | 400 | // if deployment doesn't exist or has no replicas then 401 | // it's safe to delete the stack. 402 | if deployment == nil || deployment.Spec.Replicas == nil || *deployment.Spec.Replicas == 0 { 403 | finalizers := []string{} 404 | for _, finalizer := range stack.Finalizers { 405 | if finalizer != stacksetStackFinalizer { 406 | finalizers = append(finalizers, finalizer) 407 | } 408 | } 409 | 410 | stack.Finalizers = finalizers 411 | 412 | c.logger.Infof("Removing Finalizer '%s' from Stack %s/%s", stacksetStackFinalizer, stack.Namespace, stack.Name) 413 | _, err = c.appClient.ZalandoV1().Stacks(stack.Namespace).Update(&stack) 414 | if err != nil { 415 | return fmt.Errorf( 416 | "failed to update Stack %s/%s: %v", 417 | stack.Namespace, 418 | stack.Name, 419 | err, 420 | ) 421 | } 422 | continue 423 | } 424 | 425 | c.logger.Warnf( 426 | "Unable to delete terminating Stack '%s/%s'. Deployment still has %d replica(s)", 427 | stack.Namespace, 428 | stack.Name, 429 | *deployment.Spec.Replicas, 430 | ) 431 | continue 432 | } 433 | 434 | // never garbage collect stacks with traffic 435 | if traffic != nil && traffic[stack.Name].Weight() > 0 { 436 | continue 437 | } 438 | 439 | if time.Since(stack.CreationTimestamp.Time) > c.stacksetStackMinGCAge { 440 | gcCandidates = append(gcCandidates, stack) 441 | } 442 | } 443 | 444 | // only garbage collect if history limit is reached 445 | if len(stacks.Items) <= historyLimit { 446 | c.logger.Debugf("No Stacks to clean up for StackSet %s/%s (limit: %d/%d)", stackset.Namespace, stackset.Name, len(stacks.Items), historyLimit) 447 | return nil 448 | } 449 | 450 | // sort candidates by oldest 451 | sort.Slice(gcCandidates, func(i, j int) bool { 452 | return gcCandidates[i].CreationTimestamp.Time.Before(gcCandidates[j].CreationTimestamp.Time) 453 | }) 454 | 455 | excessStacks := len(stacks.Items) - historyLimit 456 | c.logger.Infof( 457 | "Found %d Stack(s) exeeding the StackHistoryLimit (%d) for StackSet %s/%s. %d candidate(s) for GC", 458 | excessStacks, 459 | historyLimit, 460 | stackset.Namespace, 461 | stackset.Name, 462 | len(gcCandidates), 463 | ) 464 | 465 | gcLimit := int(math.Min(float64(excessStacks), float64(len(gcCandidates)))) 466 | 467 | for _, stack := range gcCandidates[:gcLimit] { 468 | c.logger.Infof( 469 | "Deleting excess stack %s/%s for StackSet %s/%s", 470 | stack.Namespace, 471 | stack.Name, 472 | stackset.Namespace, 473 | stackset.Name, 474 | ) 475 | err := c.appClient.ZalandoV1().Stacks(stack.Namespace).Delete(stack.Name, nil) 476 | if err != nil { 477 | return err 478 | } 479 | } 480 | 481 | return nil 482 | } 483 | 484 | func strInSlice(str string, slice []string) bool { 485 | for _, s := range slice { 486 | if s == str { 487 | return true 488 | } 489 | } 490 | return false 491 | } 492 | 493 | func (c *StackSetController) manageStackSet(stackset *zv1.StackSet) error { 494 | heritageLabels := map[string]string{ 495 | stacksetHeritageLabelKey: stackset.Name, 496 | } 497 | opts := metav1.ListOptions{ 498 | LabelSelector: labels.Set(heritageLabels).String(), 499 | } 500 | 501 | version := stackset.Spec.StackTemplate.Spec.Version 502 | if version == "" { 503 | version = defaultVersion 504 | } 505 | 506 | stackName := stackset.Name + "-" + version 507 | 508 | stacks, err := c.appClient.ZalandoV1().Stacks(stackset.Namespace).List(opts) 509 | if err != nil { 510 | return fmt.Errorf("failed to list stacks of StackSet %s/%s: %v", stackset.Namespace, stackset.Name, err) 511 | } 512 | 513 | var stack *zv1.Stack 514 | for _, s := range stacks.Items { 515 | if s.Name == stackName { 516 | stack = &s 517 | break 518 | } 519 | } 520 | 521 | stackLabels := mergeLabels( 522 | heritageLabels, 523 | stackset.Labels, 524 | map[string]string{stackVersionLabelKey: version}, 525 | ) 526 | 527 | createStack := false 528 | 529 | if stack == nil { 530 | createStack = true 531 | stack = &zv1.Stack{ 532 | ObjectMeta: metav1.ObjectMeta{ 533 | Name: stackName, 534 | Namespace: stackset.Namespace, 535 | OwnerReferences: []metav1.OwnerReference{ 536 | { 537 | APIVersion: stackset.APIVersion, 538 | Kind: stackset.Kind, 539 | Name: stackset.Name, 540 | UID: stackset.UID, 541 | }, 542 | }, 543 | Finalizers: []string{stacksetStackFinalizer}, 544 | }, 545 | } 546 | } 547 | 548 | stack.Labels = stackLabels 549 | stack.Spec.PodTemplate = *stackset.Spec.StackTemplate.Spec.PodTemplate.DeepCopy() 550 | stack.Spec.Replicas = stackset.Spec.StackTemplate.Spec.Replicas 551 | stack.Spec.HorizontalPodAutoscaler = stackset.Spec.StackTemplate.Spec.HorizontalPodAutoscaler 552 | 553 | if createStack { 554 | c.logger.Infof( 555 | "Creating StackSet stack %s/%s for StackSet %s/%s", 556 | stack.Namespace, stack.Name, 557 | stackset.Namespace, 558 | stackset.Name, 559 | ) 560 | _, err := c.appClient.ZalandoV1().Stacks(stack.Namespace).Create(stack) 561 | if err != nil { 562 | return err 563 | } 564 | } else { 565 | c.logger.Infof( 566 | "Updating StackSet stack %s/%s for StackSet %s/%s", 567 | stack.Namespace, 568 | stack.Name, 569 | stackset.Namespace, 570 | stackset.Name, 571 | ) 572 | _, err := c.appClient.ZalandoV1().Stacks(stack.Namespace).Update(stack) 573 | if err != nil { 574 | return err 575 | } 576 | } 577 | 578 | return nil 579 | } 580 | 581 | func mergeLabels(labelMaps ...map[string]string) map[string]string { 582 | labels := make(map[string]string) 583 | for _, labelMap := range labelMaps { 584 | for k, v := range labelMap { 585 | labels[k] = v 586 | } 587 | } 588 | return labels 589 | } 590 | -------------------------------------------------------------------------------- /delivery.yaml: -------------------------------------------------------------------------------- 1 | version: "2017-09-20" 2 | pipeline: 3 | - id: build 4 | overlay: ci/golang 5 | type: script 6 | working_dir: /go/src/github.com/zalando-incubator/stackset-controller 7 | commands: 8 | - desc: install deps 9 | cmd: | 10 | dep ensure -v -vendor-only 11 | - desc: test 12 | cmd: | 13 | make check 14 | make test 15 | - desc: build 16 | cmd: | 17 | make build.docker 18 | - desc: push 19 | cmd: | 20 | if [[ $CDP_TARGET_BRANCH == master && ! $CDP_PULL_REQUEST_NUMBER ]]; then 21 | IMAGE=registry-write.opensource.zalan.do/teapot/stackset-controller 22 | else 23 | IMAGE=registry-write.opensource.zalan.do/teapot/stackset-controller-test 24 | fi 25 | IMAGE=$IMAGE VERSION=$CDP_BUILD_VERSION make build.push 26 | -------------------------------------------------------------------------------- /deploy/apply/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: "{{ APPLICATION }}" 5 | labels: 6 | application: "{{ APPLICATION }}" 7 | version: "{{ CDP_BUILD_VERSION }}" 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | application: "{{ APPLICATION }}" 13 | template: 14 | metadata: 15 | labels: 16 | application: "{{ APPLICATION }}" 17 | version: "{{ CDP_BUILD_VERSION }}" 18 | spec: 19 | serviceAccountName: cdp 20 | containers: 21 | - name: "{{ APPLICATION }}" 22 | image: "{{ IMAGE }}:{{ CDP_BUILD_VERSION }}" 23 | resources: 24 | limits: 25 | cpu: 10m 26 | memory: 100Mi 27 | requests: 28 | cpu: 10m 29 | memory: 100Mi 30 | -------------------------------------------------------------------------------- /deploy/apply/stack_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: stacks.zalando.org 5 | spec: 6 | group: zalando.org 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | kind: Stack 11 | singular: stack 12 | plural: stacks 13 | subresources: 14 | # status enables the status subresource. 15 | status: {} 16 | -------------------------------------------------------------------------------- /deploy/apply/stackset_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: stacksets.zalando.org 5 | spec: 6 | group: zalando.org 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | kind: StackSet 11 | singular: stackset 12 | plural: stacksets 13 | subresources: 14 | # status enables the status subresource. 15 | status: {} 16 | -------------------------------------------------------------------------------- /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: zalando-incubator/stackset-controller:v0.0.1 24 | resources: 25 | limits: 26 | cpu: 10m 27 | memory: 100Mi 28 | requests: 29 | cpu: 10m 30 | memory: 100Mi 31 | -------------------------------------------------------------------------------- /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 | - applications 17 | - applicationstacks 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - "apps" 28 | resources: 29 | - deployments 30 | verbs: 31 | - get 32 | - list 33 | - create 34 | - update 35 | - patch 36 | - delete 37 | - apiGroups: 38 | - "extensions" 39 | resources: 40 | - ingresses 41 | verbs: 42 | - get 43 | - list 44 | - create 45 | - update 46 | - patch 47 | - delete 48 | - apiGroups: 49 | - "" 50 | resources: 51 | - services 52 | verbs: 53 | - get 54 | - list 55 | - create 56 | - update 57 | - patch 58 | - delete 59 | - apiGroups: 60 | - "autoscaling" 61 | resources: 62 | - horizontalpodautoscalers 63 | verbs: 64 | - get 65 | - list 66 | - create 67 | - update 68 | - patch 69 | - delete 70 | --- 71 | apiVersion: rbac.authorization.k8s.io/v1 72 | kind: ClusterRoleBinding 73 | metadata: 74 | name: stackset-controller 75 | roleRef: 76 | apiGroup: rbac.authorization.k8s.io 77 | kind: ClusterRole 78 | name: stackset-controller 79 | subjects: 80 | - kind: ServiceAccount 81 | name: stackset-controller 82 | namespace: kube-system 83 | -------------------------------------------------------------------------------- /docs/stack_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: stacks.zalando.org 5 | spec: 6 | group: zalando.org 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | kind: Stack 11 | singular: stack 12 | plural: stacks 13 | subresources: 14 | # status enables the status subresource. 15 | status: {} 16 | -------------------------------------------------------------------------------- /docs/stackset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: zalando.org/v1 2 | kind: StackSet 3 | metadata: 4 | name: my-app 5 | spec: 6 | ingress: 7 | hosts: [my-app.example.org, alt.name.org] 8 | stackLifecycle: 9 | scaledownTTLSeconds: 300 10 | limit: 5 11 | stackTemplate: 12 | spec: 13 | version: v2 14 | replicas: 3 15 | horizontalPodAutoscaler: 16 | minReplicas: 3 17 | maxReplicas: 10 18 | metrics: 19 | - type: Resource 20 | resource: 21 | name: cpu 22 | targetAverageUtilization: 50 23 | podTemplate: 24 | spec: 25 | containers: 26 | - name: nginx 27 | image: nginx 28 | ports: 29 | - containerPort: 80 30 | name: ingress 31 | resources: 32 | limits: 33 | cpu: 10m 34 | memory: 50Mi 35 | requests: 36 | cpu: 10m 37 | memory: 50Mi 38 | -------------------------------------------------------------------------------- /docs/stackset_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: stacksets.zalando.org 5 | spec: 6 | group: zalando.org 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | kind: StackSet 11 | singular: stackset 12 | plural: stacksets 13 | subresources: 14 | # status enables the status subresource. 15 | status: {} 16 | -------------------------------------------------------------------------------- /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/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 | 22 | GOPKG="github.com/zalando-incubator/stackset-controller" 23 | CUSTOM_RESOURCE_NAME="zalando" 24 | CUSTOM_RESOURCE_VERSION="v1" 25 | 26 | SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/.. 27 | 28 | # use vendor/ as a temporary stash for code-generator. 29 | rm -rf ${SCRIPT_ROOT}/vendor/k8s.io/code-generator 30 | rm -rf ${SCRIPT_ROOT}/vendor/k8s.io/gengo 31 | git clone https://github.com/kubernetes/code-generator.git ${SCRIPT_ROOT}/vendor/k8s.io/code-generator 32 | git clone https://github.com/kubernetes/gengo.git ${SCRIPT_ROOT}/vendor/k8s.io/gengo 33 | 34 | CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${SCRIPT_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} 35 | 36 | # generate the code with: 37 | # --output-base because this script should also be able to run inside the vendor dir of 38 | # k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir 39 | # instead of the $GOPATH directly. For normal projects this can be dropped. 40 | 41 | cd ${SCRIPT_ROOT} 42 | ${CODEGEN_PKG}/generate-groups.sh all \ 43 | ${GOPKG}/pkg/client ${GOPKG}/pkg/apis \ 44 | ${CUSTOM_RESOURCE_NAME}:${CUSTOM_RESOURCE_VERSION} \ 45 | --go-header-file hack/boilerplate.go.txt 46 | # \ 47 | # --output-base "$(dirname ${BASH_SOURCE})/../../../../.." 48 | 49 | # To use your own boilerplate text append: 50 | # --go-header-file ${SCRIPT_ROOT}/hack/custom-boilerplate.go.txt 51 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus/promhttp" 14 | log "github.com/sirupsen/logrus" 15 | "github.com/zalando-incubator/stackset-controller/controller" 16 | clientset "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 17 | "gopkg.in/alecthomas/kingpin.v2" 18 | "k8s.io/client-go/kubernetes" 19 | "k8s.io/client-go/rest" 20 | "k8s.io/client-go/transport" 21 | ) 22 | 23 | const ( 24 | defaultInterval = "10s" 25 | defaultMetricsAddress = ":7979" 26 | defaultStackMinGCAge = "24h" 27 | defaultNoTrafficScaledownTTL = 1 * time.Hour 28 | defaultNoTrafficTerminationTTL = 1 * time.Minute 29 | defaultClientGOTimeout = 30 * time.Second 30 | ) 31 | 32 | var ( 33 | config struct { 34 | Debug bool 35 | Interval time.Duration 36 | APIServer *url.URL 37 | MetricsAddress string 38 | StackMinGCAge time.Duration 39 | NoTrafficScaledownTTL time.Duration 40 | NoTrafficTerminationTTL time.Duration 41 | ControllerID string 42 | } 43 | ) 44 | 45 | func main() { 46 | kingpin.Flag("debug", "Enable debug logging.").BoolVar(&config.Debug) 47 | kingpin.Flag("interval", "Interval between syncing ingresses."). 48 | Default(defaultInterval).DurationVar(&config.Interval) 49 | kingpin.Flag("apiserver", "API server url.").URLVar(&config.APIServer) 50 | kingpin.Flag("stackset-stack-min-gc-age", "Minimum age for stackset stacks before they are considered for garbage collection.").Default(defaultStackMinGCAge).DurationVar(&config.StackMinGCAge) 51 | kingpin.Flag("no-traffic-scaledown-ttl", "Default TTL for scaling down deployments not getting any traffic.").Default(defaultNoTrafficScaledownTTL.String()).DurationVar(&config.NoTrafficScaledownTTL) 52 | kingpin.Flag("no-traffic-termination-ttl", "Default TTL for terminating deployments after they are not getting any traffic.").Default(defaultNoTrafficTerminationTTL.String()).DurationVar(&config.NoTrafficTerminationTTL) 53 | kingpin.Flag("metrics-address", "defines where to serve metrics").Default(defaultMetricsAddress).StringVar(&config.MetricsAddress) 54 | kingpin.Flag("controller-id", "ID of the controller used to determine ownership of StackSet resources").StringVar(&config.ControllerID) 55 | kingpin.Parse() 56 | 57 | if config.Debug { 58 | log.SetLevel(log.DebugLevel) 59 | } 60 | 61 | ctx, cancel := context.WithCancel(context.Background()) 62 | kubeConfig, err := configureKubeConfig(config.APIServer, defaultClientGOTimeout, ctx.Done()) 63 | if err != nil { 64 | log.Fatalf("Failed to setup Kubernetes config: %v", err) 65 | } 66 | 67 | client, err := kubernetes.NewForConfig(kubeConfig) 68 | if err != nil { 69 | log.Fatalf("Failed to setup Kubernetes client: %v", err) 70 | } 71 | 72 | stacksetClient, err := clientset.NewForConfig(kubeConfig) 73 | if err != nil { 74 | log.Fatalf("Failed to setup Kubernetes CRD client: %v", err) 75 | } 76 | 77 | controller := controller.NewStackSetController( 78 | client, 79 | stacksetClient, 80 | config.ControllerID, 81 | config.StackMinGCAge, 82 | config.NoTrafficScaledownTTL, 83 | config.NoTrafficTerminationTTL, 84 | config.Interval, 85 | ) 86 | 87 | go handleSigterm(cancel) 88 | go serveMetrics(config.MetricsAddress) 89 | controller.Run(ctx) 90 | } 91 | 92 | // handleSigterm handles SIGTERM signal sent to the process. 93 | func handleSigterm(cancelFunc func()) { 94 | signals := make(chan os.Signal, 1) 95 | signal.Notify(signals, syscall.SIGTERM) 96 | <-signals 97 | log.Info("Received Term signal. Terminating...") 98 | cancelFunc() 99 | } 100 | 101 | // configureKubeConfig configures a kubeconfig. 102 | func configureKubeConfig(apiServerURL *url.URL, timeout time.Duration, stopCh <-chan struct{}) (*rest.Config, error) { 103 | tr := &http.Transport{ 104 | DialContext: (&net.Dialer{ 105 | Timeout: timeout, 106 | KeepAlive: 30 * time.Second, 107 | DualStack: false, // K8s do not work well with IPv6 108 | }).DialContext, 109 | TLSHandshakeTimeout: timeout, 110 | ResponseHeaderTimeout: 10 * time.Second, 111 | MaxIdleConns: 10, 112 | MaxIdleConnsPerHost: 2, 113 | IdleConnTimeout: 20 * time.Second, 114 | } 115 | 116 | // We need this to reliably fade on DNS change, which is right 117 | // now not fixed with IdleConnTimeout in the http.Transport. 118 | // https://github.com/golang/go/issues/23427 119 | go func(d time.Duration) { 120 | for { 121 | select { 122 | case <-time.After(d): 123 | tr.CloseIdleConnections() 124 | case <-stopCh: 125 | return 126 | } 127 | } 128 | }(20 * time.Second) 129 | 130 | if apiServerURL != nil { 131 | return &rest.Config{ 132 | Host: apiServerURL.String(), 133 | Timeout: timeout, 134 | Transport: tr, 135 | }, nil 136 | } 137 | 138 | config, err := rest.InClusterConfig() 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | // patch TLS config 144 | restTransportConfig, err := config.TransportConfig() 145 | if err != nil { 146 | return nil, err 147 | } 148 | restTLSConfig, err := transport.TLSConfigFor(restTransportConfig) 149 | if err != nil { 150 | return nil, err 151 | } 152 | tr.TLSClientConfig = restTLSConfig 153 | 154 | config.Timeout = timeout 155 | config.Transport = tr 156 | // disable TLSClientConfig to make the custom Transport work 157 | config.TLSClientConfig = rest.TLSClientConfig{} 158 | return config, nil 159 | } 160 | 161 | // gather go metrics 162 | func serveMetrics(address string) { 163 | http.Handle("/metrics", promhttp.Handler()) 164 | log.Fatal(http.ListenAndServe(address, nil)) 165 | } 166 | -------------------------------------------------------------------------------- /pkg/apis/zalando/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/v1/register.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | 8 | "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando" 9 | ) 10 | 11 | var ( 12 | schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 13 | // AddToScheme applies all the stored functions to the scheme. A non-nil error 14 | // indicates that one function failed and the attempt was abandoned. 15 | AddToScheme = schemeBuilder.AddToScheme 16 | ) 17 | 18 | // SchemeGroupVersion is the group version used to register these objects. 19 | var SchemeGroupVersion = schema.GroupVersion{Group: zalando.GroupName, Version: "v1"} 20 | 21 | // Resource takes an unqualified resource and returns a Group-qualified GroupResource. 22 | func Resource(resource string) schema.GroupResource { 23 | return SchemeGroupVersion.WithResource(resource).GroupResource() 24 | } 25 | 26 | // addKnownTypes adds the set of types defined in this package to the supplied scheme. 27 | func addKnownTypes(scheme *runtime.Scheme) error { 28 | scheme.AddKnownTypes(SchemeGroupVersion, 29 | &StackSet{}, 30 | &StackSetList{}, 31 | &Stack{}, 32 | &StackList{}, 33 | ) 34 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/apis/zalando/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | autoscaling "k8s.io/api/autoscaling/v2beta1" 5 | "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/util/intstr" 8 | ) 9 | 10 | // +genclient 11 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 12 | 13 | // StackSet describes an application resource. 14 | // +k8s:deepcopy-gen=true 15 | type StackSet struct { 16 | metav1.TypeMeta `json:",inline"` 17 | metav1.ObjectMeta `json:"metadata,omitempty"` 18 | 19 | Spec StackSetSpec `json:"spec"` 20 | Status StackSetStatus `json:"status"` 21 | } 22 | 23 | // StackSetSpec is the spec part of the StackSet. 24 | // +k8s:deepcopy-gen=true 25 | type StackSetSpec struct { 26 | Ingress *StackSetIngressSpec `json:"ingress"` 27 | StackLifecycle *StackLifecycle `json:"stackLifecycle"` 28 | StackTemplate StackTemplate `json:"stackTemplate"` 29 | } 30 | 31 | // StackSetIngressSpec is the ingress defintion of an StackSet. This 32 | // includes ingress annotations and a list of hostnames. 33 | // +k8s:deepcopy-gen=true 34 | type StackSetIngressSpec struct { 35 | metav1.ObjectMeta `json:"metadata,omitempty"` 36 | Hosts []string `json:"hosts"` 37 | BackendPort intstr.IntOrString `json:"backendPort"` 38 | Path string `json:"path"` 39 | } 40 | 41 | // StackLifecycle defines lifecycle of the Stacks of a StackSet. 42 | // +k8s:deepcopy-gen=true 43 | type StackLifecycle struct { 44 | // ScaledownTTLSeconds is the ttl in seconds for when Stacks of a 45 | // StackSet should be scaled down to 0 replicas in case they are not 46 | // getting traffic. 47 | // Defaults to 300 seconds. 48 | // +optional 49 | ScaledownTTLSeconds *int64 `json:"scaledownTTLSeconds,omitempty" protobuf:"varint,4,opt,name=scaledownTTLSeconds"` 50 | // Limit defines the maximum number of Stacks to keep around. If the 51 | // number of Stacks exceeds the limit then the oldest stacks which are 52 | // not getting traffic are deleted. 53 | Limit *int32 `json:"limit,omitempty"` 54 | } 55 | 56 | // StackTemplate defines the template used for the Stack created from a 57 | // StackSet definition. 58 | // +k8s:deepcopy-gen=true 59 | type StackTemplate struct { 60 | metav1.ObjectMeta `json:"metadata,omitempty"` 61 | Spec StackSpecTemplate `json:"spec"` 62 | } 63 | 64 | // HorizontalPodAutoscaler is the Autoscaling configuration of a Stack. If 65 | // defined an HPA will be created for the Stack. 66 | // +k8s:deepcopy-gen=true 67 | type HorizontalPodAutoscaler struct { 68 | metav1.ObjectMeta `json:"metadata,omitempty"` 69 | // minReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. 70 | // It defaults to 1 pod. 71 | // +optional 72 | MinReplicas *int32 `json:"minReplicas,omitempty" protobuf:"varint,2,opt,name=minReplicas"` 73 | // maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. 74 | // It cannot be less that minReplicas. 75 | MaxReplicas int32 `json:"maxReplicas" protobuf:"varint,3,opt,name=maxReplicas"` 76 | // metrics contains the specifications for which to use to calculate the 77 | // desired replica count (the maximum replica count across all metrics will 78 | // be used). The desired replica count is calculated multiplying the 79 | // ratio between the target value and the current value by the current 80 | // number of pods. Ergo, metrics used must decrease as the pod count is 81 | // increased, and vice-versa. See the individual metric source types for 82 | // more information about how each type of metric must respond. 83 | // +optional 84 | Metrics []autoscaling.MetricSpec `json:"metrics,omitempty" protobuf:"bytes,4,rep,name=metrics"` 85 | } 86 | 87 | // StackSetStatus is the status section of the StackSet resource. 88 | // +k8s:deepcopy-gen=true 89 | type StackSetStatus struct { 90 | // Stacks is the number of stacks managed by the StackSet. 91 | // +optional 92 | Stacks int32 `json:"stacks,omitempty" protobuf:"varint,2,opt,name=stacks"` 93 | // ReadyStacks is the number of stacks managed by the StackSet which 94 | // are considered ready. a Stack is considered ready if: 95 | // replicas == readyReplicas == updatedReplicas. 96 | // +optional 97 | ReadyStacks int32 `json:"readyStacks,omitempty" protobuf:"varint,2,opt,name=readyStacks"` 98 | // StacksWithTraffic is the number of stacks managed by the StackSet 99 | // which are getting traffic. 100 | // +optional 101 | StacksWithTraffic int32 `json:"stacksWithTraffic,omitempty" protobuf:"varint,2,opt,name=stacksWithTraffic"` 102 | } 103 | 104 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 105 | 106 | // StackSetList is a list of StackSets. 107 | // +k8s:deepcopy-gen=true 108 | type StackSetList struct { 109 | metav1.TypeMeta `json:",inline"` 110 | metav1.ListMeta `json:"metadata,omitempty"` 111 | 112 | Items []StackSet `json:"items"` 113 | } 114 | 115 | // +genclient 116 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 117 | 118 | // Stack defines one version of an application. It is possible to 119 | // switch traffic between multiple versions of an application. 120 | // +k8s:deepcopy-gen=true 121 | type Stack struct { 122 | metav1.TypeMeta `json:",inline"` 123 | metav1.ObjectMeta `json:"metadata,omitempty"` 124 | 125 | Spec StackSpec `json:"spec"` 126 | Status StackStatus `json:"status"` 127 | } 128 | 129 | // StackSpec is the spec part of the Stack. 130 | // +k8s:deepcopy-gen=true 131 | type StackSpec struct { 132 | // Number of desired pods. This is a pointer to distinguish between explicit 133 | // zero and not specified. Defaults to 1. 134 | // +optional 135 | Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` 136 | HorizontalPodAutoscaler *HorizontalPodAutoscaler `json:"horizontalPodAutoscaler"` 137 | // TODO: Service 138 | // PodTemplate describes the pods that will be created. 139 | PodTemplate v1.PodTemplateSpec `json:"podTemplate" protobuf:"bytes,3,opt,name=template"` 140 | } 141 | 142 | // StackSpecTemplate is the spec part of the Stack. 143 | // +k8s:deepcopy-gen=true 144 | type StackSpecTemplate struct { 145 | StackSpec 146 | Version string `json:"version"` 147 | } 148 | 149 | // StackStatus is the status part of the Stack. 150 | // +k8s:deepcopy-gen=true 151 | type StackStatus struct { 152 | // ActualTrafficWeight is the actual amount of traffic currently 153 | // routed to the stack. 154 | // TODO: should we be using floats in the API? 155 | // +optional 156 | ActualTrafficWeight float64 `json:"actualTrafficWeight" protobuf:"varint,2,opt,name=actualTrafficWeight"` 157 | // DesiredTrafficWeight is desired amount of traffic to be routed to 158 | // the stack. 159 | // +optional 160 | DesiredTrafficWeight float64 `json:"desiredTrafficWeight" protobuf:"varint,2,opt,name=desiredTrafficWeight"` 161 | // Replicas is the number of replicas in the Deployment managed by the 162 | // stack. 163 | // +optional 164 | Replicas int32 `json:"replicas" protobuf:"varint,2,opt,name=replicas"` 165 | // ReadyReplicas is the number of ready replicas in the Deployment 166 | // managed by the stack. 167 | // +optional 168 | ReadyReplicas int32 `json:"readyReplicas" protobuf:"varint,2,opt,name=readyReplicas"` 169 | // UpdatedReplicas is the number of updated replicas in the Deployment 170 | // managed by the stack. 171 | // +optional 172 | UpdatedReplicas int32 `json:"updatedReplicas" protobuf:"varint,2,opt,name=updatedReplicas"` 173 | // DesiredReplicas is the number of desired replicas as defined by the 174 | // optional HortizontalPodAutoscaler defined for the stack. 175 | // +optional 176 | DesiredReplicas int32 `json:"desiredReplicas,omitempty" protobuf:"varint,2,opt,name=desiredReplicas"` 177 | } 178 | 179 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 180 | 181 | // StackList is a list of Stacks. 182 | // +k8s:deepcopy-gen=true 183 | type StackList struct { 184 | metav1.TypeMeta `json:",inline"` 185 | metav1.ListMeta `json:"metadata,omitempty"` 186 | 187 | Items []Stack `json:"items"` 188 | } 189 | -------------------------------------------------------------------------------- /pkg/apis/zalando/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2018 The Kubernetes Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by deepcopy-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | v2beta1 "k8s.io/api/autoscaling/v2beta1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *HorizontalPodAutoscaler) DeepCopyInto(out *HorizontalPodAutoscaler) { 30 | *out = *in 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | if in.MinReplicas != nil { 33 | in, out := &in.MinReplicas, &out.MinReplicas 34 | *out = new(int32) 35 | **out = **in 36 | } 37 | if in.Metrics != nil { 38 | in, out := &in.Metrics, &out.Metrics 39 | *out = make([]v2beta1.MetricSpec, len(*in)) 40 | for i := range *in { 41 | (*in)[i].DeepCopyInto(&(*out)[i]) 42 | } 43 | } 44 | return 45 | } 46 | 47 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalPodAutoscaler. 48 | func (in *HorizontalPodAutoscaler) DeepCopy() *HorizontalPodAutoscaler { 49 | if in == nil { 50 | return nil 51 | } 52 | out := new(HorizontalPodAutoscaler) 53 | in.DeepCopyInto(out) 54 | return out 55 | } 56 | 57 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 58 | func (in *Stack) DeepCopyInto(out *Stack) { 59 | *out = *in 60 | out.TypeMeta = in.TypeMeta 61 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 62 | in.Spec.DeepCopyInto(&out.Spec) 63 | out.Status = in.Status 64 | return 65 | } 66 | 67 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Stack. 68 | func (in *Stack) DeepCopy() *Stack { 69 | if in == nil { 70 | return nil 71 | } 72 | out := new(Stack) 73 | in.DeepCopyInto(out) 74 | return out 75 | } 76 | 77 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 78 | func (in *Stack) DeepCopyObject() runtime.Object { 79 | if c := in.DeepCopy(); c != nil { 80 | return c 81 | } 82 | return nil 83 | } 84 | 85 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 86 | func (in *StackLifecycle) DeepCopyInto(out *StackLifecycle) { 87 | *out = *in 88 | if in.ScaledownTTLSeconds != nil { 89 | in, out := &in.ScaledownTTLSeconds, &out.ScaledownTTLSeconds 90 | *out = new(int64) 91 | **out = **in 92 | } 93 | if in.Limit != nil { 94 | in, out := &in.Limit, &out.Limit 95 | *out = new(int32) 96 | **out = **in 97 | } 98 | return 99 | } 100 | 101 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackLifecycle. 102 | func (in *StackLifecycle) DeepCopy() *StackLifecycle { 103 | if in == nil { 104 | return nil 105 | } 106 | out := new(StackLifecycle) 107 | in.DeepCopyInto(out) 108 | return out 109 | } 110 | 111 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 112 | func (in *StackList) DeepCopyInto(out *StackList) { 113 | *out = *in 114 | out.TypeMeta = in.TypeMeta 115 | out.ListMeta = in.ListMeta 116 | if in.Items != nil { 117 | in, out := &in.Items, &out.Items 118 | *out = make([]Stack, len(*in)) 119 | for i := range *in { 120 | (*in)[i].DeepCopyInto(&(*out)[i]) 121 | } 122 | } 123 | return 124 | } 125 | 126 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackList. 127 | func (in *StackList) DeepCopy() *StackList { 128 | if in == nil { 129 | return nil 130 | } 131 | out := new(StackList) 132 | in.DeepCopyInto(out) 133 | return out 134 | } 135 | 136 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 137 | func (in *StackList) DeepCopyObject() runtime.Object { 138 | if c := in.DeepCopy(); c != nil { 139 | return c 140 | } 141 | return nil 142 | } 143 | 144 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 145 | func (in *StackSet) DeepCopyInto(out *StackSet) { 146 | *out = *in 147 | out.TypeMeta = in.TypeMeta 148 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 149 | in.Spec.DeepCopyInto(&out.Spec) 150 | out.Status = in.Status 151 | return 152 | } 153 | 154 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackSet. 155 | func (in *StackSet) DeepCopy() *StackSet { 156 | if in == nil { 157 | return nil 158 | } 159 | out := new(StackSet) 160 | in.DeepCopyInto(out) 161 | return out 162 | } 163 | 164 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 165 | func (in *StackSet) DeepCopyObject() runtime.Object { 166 | if c := in.DeepCopy(); c != nil { 167 | return c 168 | } 169 | return nil 170 | } 171 | 172 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 173 | func (in *StackSetIngressSpec) DeepCopyInto(out *StackSetIngressSpec) { 174 | *out = *in 175 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 176 | if in.Hosts != nil { 177 | in, out := &in.Hosts, &out.Hosts 178 | *out = make([]string, len(*in)) 179 | copy(*out, *in) 180 | } 181 | out.BackendPort = in.BackendPort 182 | return 183 | } 184 | 185 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackSetIngressSpec. 186 | func (in *StackSetIngressSpec) DeepCopy() *StackSetIngressSpec { 187 | if in == nil { 188 | return nil 189 | } 190 | out := new(StackSetIngressSpec) 191 | in.DeepCopyInto(out) 192 | return out 193 | } 194 | 195 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 196 | func (in *StackSetList) DeepCopyInto(out *StackSetList) { 197 | *out = *in 198 | out.TypeMeta = in.TypeMeta 199 | out.ListMeta = in.ListMeta 200 | if in.Items != nil { 201 | in, out := &in.Items, &out.Items 202 | *out = make([]StackSet, len(*in)) 203 | for i := range *in { 204 | (*in)[i].DeepCopyInto(&(*out)[i]) 205 | } 206 | } 207 | return 208 | } 209 | 210 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackSetList. 211 | func (in *StackSetList) DeepCopy() *StackSetList { 212 | if in == nil { 213 | return nil 214 | } 215 | out := new(StackSetList) 216 | in.DeepCopyInto(out) 217 | return out 218 | } 219 | 220 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 221 | func (in *StackSetList) DeepCopyObject() runtime.Object { 222 | if c := in.DeepCopy(); c != nil { 223 | return c 224 | } 225 | return nil 226 | } 227 | 228 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 229 | func (in *StackSetSpec) DeepCopyInto(out *StackSetSpec) { 230 | *out = *in 231 | if in.Ingress != nil { 232 | in, out := &in.Ingress, &out.Ingress 233 | *out = new(StackSetIngressSpec) 234 | (*in).DeepCopyInto(*out) 235 | } 236 | if in.StackLifecycle != nil { 237 | in, out := &in.StackLifecycle, &out.StackLifecycle 238 | *out = new(StackLifecycle) 239 | (*in).DeepCopyInto(*out) 240 | } 241 | in.StackTemplate.DeepCopyInto(&out.StackTemplate) 242 | return 243 | } 244 | 245 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackSetSpec. 246 | func (in *StackSetSpec) DeepCopy() *StackSetSpec { 247 | if in == nil { 248 | return nil 249 | } 250 | out := new(StackSetSpec) 251 | in.DeepCopyInto(out) 252 | return out 253 | } 254 | 255 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 256 | func (in *StackSetStatus) DeepCopyInto(out *StackSetStatus) { 257 | *out = *in 258 | return 259 | } 260 | 261 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackSetStatus. 262 | func (in *StackSetStatus) DeepCopy() *StackSetStatus { 263 | if in == nil { 264 | return nil 265 | } 266 | out := new(StackSetStatus) 267 | in.DeepCopyInto(out) 268 | return out 269 | } 270 | 271 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 272 | func (in *StackSpec) DeepCopyInto(out *StackSpec) { 273 | *out = *in 274 | if in.Replicas != nil { 275 | in, out := &in.Replicas, &out.Replicas 276 | *out = new(int32) 277 | **out = **in 278 | } 279 | if in.HorizontalPodAutoscaler != nil { 280 | in, out := &in.HorizontalPodAutoscaler, &out.HorizontalPodAutoscaler 281 | *out = new(HorizontalPodAutoscaler) 282 | (*in).DeepCopyInto(*out) 283 | } 284 | in.PodTemplate.DeepCopyInto(&out.PodTemplate) 285 | return 286 | } 287 | 288 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackSpec. 289 | func (in *StackSpec) DeepCopy() *StackSpec { 290 | if in == nil { 291 | return nil 292 | } 293 | out := new(StackSpec) 294 | in.DeepCopyInto(out) 295 | return out 296 | } 297 | 298 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 299 | func (in *StackSpecTemplate) DeepCopyInto(out *StackSpecTemplate) { 300 | *out = *in 301 | in.StackSpec.DeepCopyInto(&out.StackSpec) 302 | return 303 | } 304 | 305 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackSpecTemplate. 306 | func (in *StackSpecTemplate) DeepCopy() *StackSpecTemplate { 307 | if in == nil { 308 | return nil 309 | } 310 | out := new(StackSpecTemplate) 311 | in.DeepCopyInto(out) 312 | return out 313 | } 314 | 315 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 316 | func (in *StackStatus) DeepCopyInto(out *StackStatus) { 317 | *out = *in 318 | return 319 | } 320 | 321 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackStatus. 322 | func (in *StackStatus) DeepCopy() *StackStatus { 323 | if in == nil { 324 | return nil 325 | } 326 | out := new(StackStatus) 327 | in.DeepCopyInto(out) 328 | return out 329 | } 330 | 331 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 332 | func (in *StackTemplate) DeepCopyInto(out *StackTemplate) { 333 | *out = *in 334 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 335 | in.Spec.DeepCopyInto(&out.Spec) 336 | return 337 | } 338 | 339 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackTemplate. 340 | func (in *StackTemplate) DeepCopy() *StackTemplate { 341 | if in == nil { 342 | return nil 343 | } 344 | out := new(StackTemplate) 345 | in.DeepCopyInto(out) 346 | return out 347 | } 348 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | zalandov1 "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/typed/zalando/v1" 23 | discovery "k8s.io/client-go/discovery" 24 | rest "k8s.io/client-go/rest" 25 | flowcontrol "k8s.io/client-go/util/flowcontrol" 26 | ) 27 | 28 | type Interface interface { 29 | Discovery() discovery.DiscoveryInterface 30 | ZalandoV1() zalandov1.ZalandoV1Interface 31 | // Deprecated: please explicitly pick a version if possible. 32 | Zalando() zalandov1.ZalandoV1Interface 33 | } 34 | 35 | // Clientset contains the clients for groups. Each group has exactly one 36 | // version included in a Clientset. 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 | // Deprecated: Zalando retrieves the default version of ZalandoClient. 48 | // Please explicitly pick a version. 49 | func (c *Clientset) Zalando() zalandov1.ZalandoV1Interface { 50 | return c.zalandoV1 51 | } 52 | 53 | // Discovery retrieves the DiscoveryClient 54 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 55 | if c == nil { 56 | return nil 57 | } 58 | return c.DiscoveryClient 59 | } 60 | 61 | // NewForConfig creates a new Clientset for the given config. 62 | func NewForConfig(c *rest.Config) (*Clientset, error) { 63 | configShallowCopy := *c 64 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 65 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 66 | } 67 | var cs Clientset 68 | var err error 69 | cs.zalandoV1, err = zalandov1.NewForConfig(&configShallowCopy) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return &cs, nil 79 | } 80 | 81 | // NewForConfigOrDie creates a new Clientset for the given config and 82 | // panics if there is an error in the config. 83 | func NewForConfigOrDie(c *rest.Config) *Clientset { 84 | var cs Clientset 85 | cs.zalandoV1 = zalandov1.NewForConfigOrDie(c) 86 | 87 | cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) 88 | return &cs 89 | } 90 | 91 | // New creates a new Clientset for the given RESTClient. 92 | func New(c rest.Interface) *Clientset { 93 | var cs Clientset 94 | cs.zalandoV1 = zalandov1.New(c) 95 | 96 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 97 | return &cs 98 | } 99 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 2018 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/v1" 24 | fakezalandov1 "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/typed/zalando/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 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 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 37 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 38 | for _, obj := range objects { 39 | if err := o.Add(obj); err != nil { 40 | panic(err) 41 | } 42 | } 43 | 44 | cs := &Clientset{} 45 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 46 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 47 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 48 | gvr := action.GetResource() 49 | ns := action.GetNamespace() 50 | watch, err := o.Watch(gvr, ns) 51 | if err != nil { 52 | return false, nil, err 53 | } 54 | return true, watch, nil 55 | }) 56 | 57 | return cs 58 | } 59 | 60 | // Clientset implements clientset.Interface. Meant to be embedded into a 61 | // struct to get a default implementation. This makes faking out just the method 62 | // you want to test easier. 63 | type Clientset struct { 64 | testing.Fake 65 | discovery *fakediscovery.FakeDiscovery 66 | } 67 | 68 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 69 | return c.discovery 70 | } 71 | 72 | var _ clientset.Interface = &Clientset{} 73 | 74 | // ZalandoV1 retrieves the ZalandoV1Client 75 | func (c *Clientset) ZalandoV1() zalandov1.ZalandoV1Interface { 76 | return &fakezalandov1.FakeZalandoV1{Fake: &c.Fake} 77 | } 78 | 79 | // Zalando retrieves the ZalandoV1Client 80 | func (c *Clientset) Zalando() zalandov1.ZalandoV1Interface { 81 | return &fakezalandov1.FakeZalandoV1{Fake: &c.Fake} 82 | } 83 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 2018 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/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/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 2018 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/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/v1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/v1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/v1/fake/fake_stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/v1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | labels "k8s.io/apimachinery/pkg/labels" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | types "k8s.io/apimachinery/pkg/types" 27 | watch "k8s.io/apimachinery/pkg/watch" 28 | testing "k8s.io/client-go/testing" 29 | ) 30 | 31 | // FakeStacks implements StackInterface 32 | type FakeStacks struct { 33 | Fake *FakeZalandoV1 34 | ns string 35 | } 36 | 37 | var stacksResource = schema.GroupVersionResource{Group: "zalando", Version: "v1", Resource: "stacks"} 38 | 39 | var stacksKind = schema.GroupVersionKind{Group: "zalando", Version: "v1", Kind: "Stack"} 40 | 41 | // Get takes name of the stack, and returns the corresponding stack object, and an error if there is any. 42 | func (c *FakeStacks) Get(name string, options v1.GetOptions) (result *zalandov1.Stack, err error) { 43 | obj, err := c.Fake. 44 | Invokes(testing.NewGetAction(stacksResource, c.ns, name), &zalandov1.Stack{}) 45 | 46 | if obj == nil { 47 | return nil, err 48 | } 49 | return obj.(*zalandov1.Stack), err 50 | } 51 | 52 | // List takes label and field selectors, and returns the list of Stacks that match those selectors. 53 | func (c *FakeStacks) List(opts v1.ListOptions) (result *zalandov1.StackList, err error) { 54 | obj, err := c.Fake. 55 | Invokes(testing.NewListAction(stacksResource, stacksKind, c.ns, opts), &zalandov1.StackList{}) 56 | 57 | if obj == nil { 58 | return nil, err 59 | } 60 | 61 | label, _, _ := testing.ExtractFromListOptions(opts) 62 | if label == nil { 63 | label = labels.Everything() 64 | } 65 | list := &zalandov1.StackList{ListMeta: obj.(*zalandov1.StackList).ListMeta} 66 | for _, item := range obj.(*zalandov1.StackList).Items { 67 | if label.Matches(labels.Set(item.Labels)) { 68 | list.Items = append(list.Items, item) 69 | } 70 | } 71 | return list, err 72 | } 73 | 74 | // Watch returns a watch.Interface that watches the requested stacks. 75 | func (c *FakeStacks) Watch(opts v1.ListOptions) (watch.Interface, error) { 76 | return c.Fake. 77 | InvokesWatch(testing.NewWatchAction(stacksResource, c.ns, opts)) 78 | 79 | } 80 | 81 | // 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. 82 | func (c *FakeStacks) Create(stack *zalandov1.Stack) (result *zalandov1.Stack, err error) { 83 | obj, err := c.Fake. 84 | Invokes(testing.NewCreateAction(stacksResource, c.ns, stack), &zalandov1.Stack{}) 85 | 86 | if obj == nil { 87 | return nil, err 88 | } 89 | return obj.(*zalandov1.Stack), err 90 | } 91 | 92 | // 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. 93 | func (c *FakeStacks) Update(stack *zalandov1.Stack) (result *zalandov1.Stack, err error) { 94 | obj, err := c.Fake. 95 | Invokes(testing.NewUpdateAction(stacksResource, c.ns, stack), &zalandov1.Stack{}) 96 | 97 | if obj == nil { 98 | return nil, err 99 | } 100 | return obj.(*zalandov1.Stack), err 101 | } 102 | 103 | // UpdateStatus was generated because the type contains a Status member. 104 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 105 | func (c *FakeStacks) UpdateStatus(stack *zalandov1.Stack) (*zalandov1.Stack, error) { 106 | obj, err := c.Fake. 107 | Invokes(testing.NewUpdateSubresourceAction(stacksResource, "status", c.ns, stack), &zalandov1.Stack{}) 108 | 109 | if obj == nil { 110 | return nil, err 111 | } 112 | return obj.(*zalandov1.Stack), err 113 | } 114 | 115 | // Delete takes name of the stack and deletes it. Returns an error if one occurs. 116 | func (c *FakeStacks) Delete(name string, options *v1.DeleteOptions) error { 117 | _, err := c.Fake. 118 | Invokes(testing.NewDeleteAction(stacksResource, c.ns, name), &zalandov1.Stack{}) 119 | 120 | return err 121 | } 122 | 123 | // DeleteCollection deletes a collection of objects. 124 | func (c *FakeStacks) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { 125 | action := testing.NewDeleteCollectionAction(stacksResource, c.ns, listOptions) 126 | 127 | _, err := c.Fake.Invokes(action, &zalandov1.StackList{}) 128 | return err 129 | } 130 | 131 | // Patch applies the patch and returns the patched stack. 132 | func (c *FakeStacks) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *zalandov1.Stack, err error) { 133 | obj, err := c.Fake. 134 | Invokes(testing.NewPatchSubresourceAction(stacksResource, c.ns, name, data, subresources...), &zalandov1.Stack{}) 135 | 136 | if obj == nil { 137 | return nil, err 138 | } 139 | return obj.(*zalandov1.Stack), err 140 | } 141 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando/v1/fake/fake_stackset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/v1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | labels "k8s.io/apimachinery/pkg/labels" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | types "k8s.io/apimachinery/pkg/types" 27 | watch "k8s.io/apimachinery/pkg/watch" 28 | testing "k8s.io/client-go/testing" 29 | ) 30 | 31 | // FakeStackSets implements StackSetInterface 32 | type FakeStackSets struct { 33 | Fake *FakeZalandoV1 34 | ns string 35 | } 36 | 37 | var stacksetsResource = schema.GroupVersionResource{Group: "zalando", Version: "v1", Resource: "stacksets"} 38 | 39 | var stacksetsKind = schema.GroupVersionKind{Group: "zalando", Version: "v1", Kind: "StackSet"} 40 | 41 | // Get takes name of the stackSet, and returns the corresponding stackSet object, and an error if there is any. 42 | func (c *FakeStackSets) Get(name string, options v1.GetOptions) (result *zalandov1.StackSet, err error) { 43 | obj, err := c.Fake. 44 | Invokes(testing.NewGetAction(stacksetsResource, c.ns, name), &zalandov1.StackSet{}) 45 | 46 | if obj == nil { 47 | return nil, err 48 | } 49 | return obj.(*zalandov1.StackSet), err 50 | } 51 | 52 | // List takes label and field selectors, and returns the list of StackSets that match those selectors. 53 | func (c *FakeStackSets) List(opts v1.ListOptions) (result *zalandov1.StackSetList, err error) { 54 | obj, err := c.Fake. 55 | Invokes(testing.NewListAction(stacksetsResource, stacksetsKind, c.ns, opts), &zalandov1.StackSetList{}) 56 | 57 | if obj == nil { 58 | return nil, err 59 | } 60 | 61 | label, _, _ := testing.ExtractFromListOptions(opts) 62 | if label == nil { 63 | label = labels.Everything() 64 | } 65 | list := &zalandov1.StackSetList{ListMeta: obj.(*zalandov1.StackSetList).ListMeta} 66 | for _, item := range obj.(*zalandov1.StackSetList).Items { 67 | if label.Matches(labels.Set(item.Labels)) { 68 | list.Items = append(list.Items, item) 69 | } 70 | } 71 | return list, err 72 | } 73 | 74 | // Watch returns a watch.Interface that watches the requested stackSets. 75 | func (c *FakeStackSets) Watch(opts v1.ListOptions) (watch.Interface, error) { 76 | return c.Fake. 77 | InvokesWatch(testing.NewWatchAction(stacksetsResource, c.ns, opts)) 78 | 79 | } 80 | 81 | // 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. 82 | func (c *FakeStackSets) Create(stackSet *zalandov1.StackSet) (result *zalandov1.StackSet, err error) { 83 | obj, err := c.Fake. 84 | Invokes(testing.NewCreateAction(stacksetsResource, c.ns, stackSet), &zalandov1.StackSet{}) 85 | 86 | if obj == nil { 87 | return nil, err 88 | } 89 | return obj.(*zalandov1.StackSet), err 90 | } 91 | 92 | // 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. 93 | func (c *FakeStackSets) Update(stackSet *zalandov1.StackSet) (result *zalandov1.StackSet, err error) { 94 | obj, err := c.Fake. 95 | Invokes(testing.NewUpdateAction(stacksetsResource, c.ns, stackSet), &zalandov1.StackSet{}) 96 | 97 | if obj == nil { 98 | return nil, err 99 | } 100 | return obj.(*zalandov1.StackSet), err 101 | } 102 | 103 | // UpdateStatus was generated because the type contains a Status member. 104 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 105 | func (c *FakeStackSets) UpdateStatus(stackSet *zalandov1.StackSet) (*zalandov1.StackSet, error) { 106 | obj, err := c.Fake. 107 | Invokes(testing.NewUpdateSubresourceAction(stacksetsResource, "status", c.ns, stackSet), &zalandov1.StackSet{}) 108 | 109 | if obj == nil { 110 | return nil, err 111 | } 112 | return obj.(*zalandov1.StackSet), err 113 | } 114 | 115 | // Delete takes name of the stackSet and deletes it. Returns an error if one occurs. 116 | func (c *FakeStackSets) Delete(name string, options *v1.DeleteOptions) error { 117 | _, err := c.Fake. 118 | Invokes(testing.NewDeleteAction(stacksetsResource, c.ns, name), &zalandov1.StackSet{}) 119 | 120 | return err 121 | } 122 | 123 | // DeleteCollection deletes a collection of objects. 124 | func (c *FakeStackSets) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { 125 | action := testing.NewDeleteCollectionAction(stacksetsResource, c.ns, listOptions) 126 | 127 | _, err := c.Fake.Invokes(action, &zalandov1.StackSetList{}) 128 | return err 129 | } 130 | 131 | // Patch applies the patch and returns the patched stackSet. 132 | func (c *FakeStackSets) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *zalandov1.StackSet, err error) { 133 | obj, err := c.Fake. 134 | Invokes(testing.NewPatchSubresourceAction(stacksetsResource, c.ns, name, data, subresources...), &zalandov1.StackSet{}) 135 | 136 | if obj == nil { 137 | return nil, err 138 | } 139 | return obj.(*zalandov1.StackSet), err 140 | } 141 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando/v1/fake/fake_zalando_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/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) Stacks(namespace string) v1.StackInterface { 32 | return &FakeStacks{c, namespace} 33 | } 34 | 35 | func (c *FakeZalandoV1) StackSets(namespace string) v1.StackSetInterface { 36 | return &FakeStackSets{c, namespace} 37 | } 38 | 39 | // RESTClient returns a RESTClient that is used to communicate 40 | // with API server by this client implementation. 41 | func (c *FakeZalandoV1) RESTClient() rest.Interface { 42 | var ret *rest.RESTClient 43 | return ret 44 | } 45 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando/v1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 StackExpansion interface{} 22 | 23 | type StackSetExpansion interface{} 24 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando/v1/stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 23 | scheme "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/scheme" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | types "k8s.io/apimachinery/pkg/types" 26 | watch "k8s.io/apimachinery/pkg/watch" 27 | rest "k8s.io/client-go/rest" 28 | ) 29 | 30 | // StacksGetter has a method to return a StackInterface. 31 | // A group's client should implement this interface. 32 | type StacksGetter interface { 33 | Stacks(namespace string) StackInterface 34 | } 35 | 36 | // StackInterface has methods to work with Stack resources. 37 | type StackInterface interface { 38 | Create(*v1.Stack) (*v1.Stack, error) 39 | Update(*v1.Stack) (*v1.Stack, error) 40 | UpdateStatus(*v1.Stack) (*v1.Stack, error) 41 | Delete(name string, options *metav1.DeleteOptions) error 42 | DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error 43 | Get(name string, options metav1.GetOptions) (*v1.Stack, error) 44 | List(opts metav1.ListOptions) (*v1.StackList, error) 45 | Watch(opts metav1.ListOptions) (watch.Interface, error) 46 | Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Stack, err error) 47 | StackExpansion 48 | } 49 | 50 | // stacks implements StackInterface 51 | type stacks struct { 52 | client rest.Interface 53 | ns string 54 | } 55 | 56 | // newStacks returns a Stacks 57 | func newStacks(c *ZalandoV1Client, namespace string) *stacks { 58 | return &stacks{ 59 | client: c.RESTClient(), 60 | ns: namespace, 61 | } 62 | } 63 | 64 | // Get takes name of the stack, and returns the corresponding stack object, and an error if there is any. 65 | func (c *stacks) Get(name string, options metav1.GetOptions) (result *v1.Stack, err error) { 66 | result = &v1.Stack{} 67 | err = c.client.Get(). 68 | Namespace(c.ns). 69 | Resource("stacks"). 70 | Name(name). 71 | VersionedParams(&options, scheme.ParameterCodec). 72 | Do(). 73 | Into(result) 74 | return 75 | } 76 | 77 | // List takes label and field selectors, and returns the list of Stacks that match those selectors. 78 | func (c *stacks) List(opts metav1.ListOptions) (result *v1.StackList, err error) { 79 | result = &v1.StackList{} 80 | err = c.client.Get(). 81 | Namespace(c.ns). 82 | Resource("stacks"). 83 | VersionedParams(&opts, scheme.ParameterCodec). 84 | Do(). 85 | Into(result) 86 | return 87 | } 88 | 89 | // Watch returns a watch.Interface that watches the requested stacks. 90 | func (c *stacks) Watch(opts metav1.ListOptions) (watch.Interface, error) { 91 | opts.Watch = true 92 | return c.client.Get(). 93 | Namespace(c.ns). 94 | Resource("stacks"). 95 | VersionedParams(&opts, scheme.ParameterCodec). 96 | Watch() 97 | } 98 | 99 | // 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. 100 | func (c *stacks) Create(stack *v1.Stack) (result *v1.Stack, err error) { 101 | result = &v1.Stack{} 102 | err = c.client.Post(). 103 | Namespace(c.ns). 104 | Resource("stacks"). 105 | Body(stack). 106 | Do(). 107 | Into(result) 108 | return 109 | } 110 | 111 | // 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. 112 | func (c *stacks) Update(stack *v1.Stack) (result *v1.Stack, err error) { 113 | result = &v1.Stack{} 114 | err = c.client.Put(). 115 | Namespace(c.ns). 116 | Resource("stacks"). 117 | Name(stack.Name). 118 | Body(stack). 119 | Do(). 120 | Into(result) 121 | return 122 | } 123 | 124 | // UpdateStatus was generated because the type contains a Status member. 125 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 126 | 127 | func (c *stacks) UpdateStatus(stack *v1.Stack) (result *v1.Stack, err error) { 128 | result = &v1.Stack{} 129 | err = c.client.Put(). 130 | Namespace(c.ns). 131 | Resource("stacks"). 132 | Name(stack.Name). 133 | SubResource("status"). 134 | Body(stack). 135 | Do(). 136 | Into(result) 137 | return 138 | } 139 | 140 | // Delete takes name of the stack and deletes it. Returns an error if one occurs. 141 | func (c *stacks) Delete(name string, options *metav1.DeleteOptions) error { 142 | return c.client.Delete(). 143 | Namespace(c.ns). 144 | Resource("stacks"). 145 | Name(name). 146 | Body(options). 147 | Do(). 148 | Error() 149 | } 150 | 151 | // DeleteCollection deletes a collection of objects. 152 | func (c *stacks) DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error { 153 | return c.client.Delete(). 154 | Namespace(c.ns). 155 | Resource("stacks"). 156 | VersionedParams(&listOptions, scheme.ParameterCodec). 157 | Body(options). 158 | Do(). 159 | Error() 160 | } 161 | 162 | // Patch applies the patch and returns the patched stack. 163 | func (c *stacks) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Stack, err error) { 164 | result = &v1.Stack{} 165 | err = c.client.Patch(pt). 166 | Namespace(c.ns). 167 | Resource("stacks"). 168 | SubResource(subresources...). 169 | Name(name). 170 | Body(data). 171 | Do(). 172 | Into(result) 173 | return 174 | } 175 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando/v1/stackset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 23 | scheme "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/scheme" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | types "k8s.io/apimachinery/pkg/types" 26 | watch "k8s.io/apimachinery/pkg/watch" 27 | rest "k8s.io/client-go/rest" 28 | ) 29 | 30 | // StackSetsGetter has a method to return a StackSetInterface. 31 | // A group's client should implement this interface. 32 | type StackSetsGetter interface { 33 | StackSets(namespace string) StackSetInterface 34 | } 35 | 36 | // StackSetInterface has methods to work with StackSet resources. 37 | type StackSetInterface interface { 38 | Create(*v1.StackSet) (*v1.StackSet, error) 39 | Update(*v1.StackSet) (*v1.StackSet, error) 40 | UpdateStatus(*v1.StackSet) (*v1.StackSet, error) 41 | Delete(name string, options *metav1.DeleteOptions) error 42 | DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error 43 | Get(name string, options metav1.GetOptions) (*v1.StackSet, error) 44 | List(opts metav1.ListOptions) (*v1.StackSetList, error) 45 | Watch(opts metav1.ListOptions) (watch.Interface, error) 46 | Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.StackSet, err error) 47 | StackSetExpansion 48 | } 49 | 50 | // stackSets implements StackSetInterface 51 | type stackSets struct { 52 | client rest.Interface 53 | ns string 54 | } 55 | 56 | // newStackSets returns a StackSets 57 | func newStackSets(c *ZalandoV1Client, namespace string) *stackSets { 58 | return &stackSets{ 59 | client: c.RESTClient(), 60 | ns: namespace, 61 | } 62 | } 63 | 64 | // Get takes name of the stackSet, and returns the corresponding stackSet object, and an error if there is any. 65 | func (c *stackSets) Get(name string, options metav1.GetOptions) (result *v1.StackSet, err error) { 66 | result = &v1.StackSet{} 67 | err = c.client.Get(). 68 | Namespace(c.ns). 69 | Resource("stacksets"). 70 | Name(name). 71 | VersionedParams(&options, scheme.ParameterCodec). 72 | Do(). 73 | Into(result) 74 | return 75 | } 76 | 77 | // List takes label and field selectors, and returns the list of StackSets that match those selectors. 78 | func (c *stackSets) List(opts metav1.ListOptions) (result *v1.StackSetList, err error) { 79 | result = &v1.StackSetList{} 80 | err = c.client.Get(). 81 | Namespace(c.ns). 82 | Resource("stacksets"). 83 | VersionedParams(&opts, scheme.ParameterCodec). 84 | Do(). 85 | Into(result) 86 | return 87 | } 88 | 89 | // Watch returns a watch.Interface that watches the requested stackSets. 90 | func (c *stackSets) Watch(opts metav1.ListOptions) (watch.Interface, error) { 91 | opts.Watch = true 92 | return c.client.Get(). 93 | Namespace(c.ns). 94 | Resource("stacksets"). 95 | VersionedParams(&opts, scheme.ParameterCodec). 96 | Watch() 97 | } 98 | 99 | // 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. 100 | func (c *stackSets) Create(stackSet *v1.StackSet) (result *v1.StackSet, err error) { 101 | result = &v1.StackSet{} 102 | err = c.client.Post(). 103 | Namespace(c.ns). 104 | Resource("stacksets"). 105 | Body(stackSet). 106 | Do(). 107 | Into(result) 108 | return 109 | } 110 | 111 | // 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. 112 | func (c *stackSets) Update(stackSet *v1.StackSet) (result *v1.StackSet, err error) { 113 | result = &v1.StackSet{} 114 | err = c.client.Put(). 115 | Namespace(c.ns). 116 | Resource("stacksets"). 117 | Name(stackSet.Name). 118 | Body(stackSet). 119 | Do(). 120 | Into(result) 121 | return 122 | } 123 | 124 | // UpdateStatus was generated because the type contains a Status member. 125 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 126 | 127 | func (c *stackSets) UpdateStatus(stackSet *v1.StackSet) (result *v1.StackSet, err error) { 128 | result = &v1.StackSet{} 129 | err = c.client.Put(). 130 | Namespace(c.ns). 131 | Resource("stacksets"). 132 | Name(stackSet.Name). 133 | SubResource("status"). 134 | Body(stackSet). 135 | Do(). 136 | Into(result) 137 | return 138 | } 139 | 140 | // Delete takes name of the stackSet and deletes it. Returns an error if one occurs. 141 | func (c *stackSets) Delete(name string, options *metav1.DeleteOptions) error { 142 | return c.client.Delete(). 143 | Namespace(c.ns). 144 | Resource("stacksets"). 145 | Name(name). 146 | Body(options). 147 | Do(). 148 | Error() 149 | } 150 | 151 | // DeleteCollection deletes a collection of objects. 152 | func (c *stackSets) DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error { 153 | return c.client.Delete(). 154 | Namespace(c.ns). 155 | Resource("stacksets"). 156 | VersionedParams(&listOptions, scheme.ParameterCodec). 157 | Body(options). 158 | Do(). 159 | Error() 160 | } 161 | 162 | // Patch applies the patch and returns the patched stackSet. 163 | func (c *stackSets) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.StackSet, err error) { 164 | result = &v1.StackSet{} 165 | err = c.client.Patch(pt). 166 | Namespace(c.ns). 167 | Resource("stacksets"). 168 | SubResource(subresources...). 169 | Name(name). 170 | Body(data). 171 | Do(). 172 | Into(result) 173 | return 174 | } 175 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/zalando/v1/zalando_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | v1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 23 | "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned/scheme" 24 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 25 | rest "k8s.io/client-go/rest" 26 | ) 27 | 28 | type ZalandoV1Interface interface { 29 | RESTClient() rest.Interface 30 | StacksGetter 31 | StackSetsGetter 32 | } 33 | 34 | // ZalandoV1Client is used to interact with features provided by the zalando group. 35 | type ZalandoV1Client struct { 36 | restClient rest.Interface 37 | } 38 | 39 | func (c *ZalandoV1Client) Stacks(namespace string) StackInterface { 40 | return newStacks(c, namespace) 41 | } 42 | 43 | func (c *ZalandoV1Client) StackSets(namespace string) StackSetInterface { 44 | return newStackSets(c, namespace) 45 | } 46 | 47 | // NewForConfig creates a new ZalandoV1Client for the given config. 48 | func NewForConfig(c *rest.Config) (*ZalandoV1Client, error) { 49 | config := *c 50 | if err := setConfigDefaults(&config); err != nil { 51 | return nil, err 52 | } 53 | client, err := rest.RESTClientFor(&config) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return &ZalandoV1Client{client}, nil 58 | } 59 | 60 | // NewForConfigOrDie creates a new ZalandoV1Client for the given config and 61 | // panics if there is an error in the config. 62 | func NewForConfigOrDie(c *rest.Config) *ZalandoV1Client { 63 | client, err := NewForConfig(c) 64 | if err != nil { 65 | panic(err) 66 | } 67 | return client 68 | } 69 | 70 | // New creates a new ZalandoV1Client for the given RESTClient. 71 | func New(c rest.Interface) *ZalandoV1Client { 72 | return &ZalandoV1Client{c} 73 | } 74 | 75 | func setConfigDefaults(config *rest.Config) error { 76 | gv := v1.SchemeGroupVersion 77 | config.GroupVersion = &gv 78 | config.APIPath = "/apis" 79 | config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} 80 | 81 | if config.UserAgent == "" { 82 | config.UserAgent = rest.DefaultKubernetesUserAgent() 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // RESTClient returns a RESTClient that is used to communicate 89 | // with API server by this client implementation. 90 | func (c *ZalandoV1Client) RESTClient() rest.Interface { 91 | if c == nil { 92 | return nil 93 | } 94 | return c.restClient 95 | } 96 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | reflect "reflect" 23 | sync "sync" 24 | time "time" 25 | 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 | zalando "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/zalando" 29 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | schema "k8s.io/apimachinery/pkg/runtime/schema" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // SharedInformerOption defines the functional option type for SharedInformerFactory. 36 | type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory 37 | 38 | type sharedInformerFactory struct { 39 | client versioned.Interface 40 | namespace string 41 | tweakListOptions internalinterfaces.TweakListOptionsFunc 42 | lock sync.Mutex 43 | defaultResync time.Duration 44 | customResync map[reflect.Type]time.Duration 45 | 46 | informers map[reflect.Type]cache.SharedIndexInformer 47 | // startedInformers is used for tracking which informers have been started. 48 | // This allows Start() to be called multiple times safely. 49 | startedInformers map[reflect.Type]bool 50 | } 51 | 52 | // WithCustomResyncConfig sets a custom resync period for the specified informer types. 53 | func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { 54 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 55 | for k, v := range resyncConfig { 56 | factory.customResync[reflect.TypeOf(k)] = v 57 | } 58 | return factory 59 | } 60 | } 61 | 62 | // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. 63 | func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { 64 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 65 | factory.tweakListOptions = tweakListOptions 66 | return factory 67 | } 68 | } 69 | 70 | // WithNamespace limits the SharedInformerFactory to the specified namespace. 71 | func WithNamespace(namespace string) SharedInformerOption { 72 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 73 | factory.namespace = namespace 74 | return factory 75 | } 76 | } 77 | 78 | // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. 79 | func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { 80 | return NewSharedInformerFactoryWithOptions(client, defaultResync) 81 | } 82 | 83 | // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. 84 | // Listers obtained via this SharedInformerFactory will be subject to the same filters 85 | // as specified here. 86 | // Deprecated: Please use NewSharedInformerFactoryWithOptions instead 87 | func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { 88 | return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) 89 | } 90 | 91 | // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. 92 | func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { 93 | factory := &sharedInformerFactory{ 94 | client: client, 95 | namespace: v1.NamespaceAll, 96 | defaultResync: defaultResync, 97 | informers: make(map[reflect.Type]cache.SharedIndexInformer), 98 | startedInformers: make(map[reflect.Type]bool), 99 | customResync: make(map[reflect.Type]time.Duration), 100 | } 101 | 102 | // Apply all options 103 | for _, opt := range options { 104 | factory = opt(factory) 105 | } 106 | 107 | return factory 108 | } 109 | 110 | // Start initializes all requested informers. 111 | func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { 112 | f.lock.Lock() 113 | defer f.lock.Unlock() 114 | 115 | for informerType, informer := range f.informers { 116 | if !f.startedInformers[informerType] { 117 | go informer.Run(stopCh) 118 | f.startedInformers[informerType] = true 119 | } 120 | } 121 | } 122 | 123 | // WaitForCacheSync waits for all started informers' cache were synced. 124 | func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { 125 | informers := func() map[reflect.Type]cache.SharedIndexInformer { 126 | f.lock.Lock() 127 | defer f.lock.Unlock() 128 | 129 | informers := map[reflect.Type]cache.SharedIndexInformer{} 130 | for informerType, informer := range f.informers { 131 | if f.startedInformers[informerType] { 132 | informers[informerType] = informer 133 | } 134 | } 135 | return informers 136 | }() 137 | 138 | res := map[reflect.Type]bool{} 139 | for informType, informer := range informers { 140 | res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) 141 | } 142 | return res 143 | } 144 | 145 | // InternalInformerFor returns the SharedIndexInformer for obj using an internal 146 | // client. 147 | func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { 148 | f.lock.Lock() 149 | defer f.lock.Unlock() 150 | 151 | informerType := reflect.TypeOf(obj) 152 | informer, exists := f.informers[informerType] 153 | if exists { 154 | return informer 155 | } 156 | 157 | resyncPeriod, exists := f.customResync[informerType] 158 | if !exists { 159 | resyncPeriod = f.defaultResync 160 | } 161 | 162 | informer = newFunc(f.client, resyncPeriod) 163 | f.informers[informerType] = informer 164 | 165 | return informer 166 | } 167 | 168 | // SharedInformerFactory provides shared informers for resources in all known 169 | // API group versions. 170 | type SharedInformerFactory interface { 171 | internalinterfaces.SharedInformerFactory 172 | ForResource(resource schema.GroupVersionResource) (GenericInformer, error) 173 | WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool 174 | 175 | Zalando() zalando.Interface 176 | } 177 | 178 | func (f *sharedInformerFactory) Zalando() zalando.Interface { 179 | return zalando.New(f, f.namespace, f.tweakListOptions) 180 | } 181 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/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, Version=v1 56 | case v1.SchemeGroupVersion.WithResource("stacks"): 57 | return &genericInformer{resource: resource.GroupResource(), informer: f.Zalando().V1().Stacks().Informer()}, nil 58 | case v1.SchemeGroupVersion.WithResource("stacksets"): 59 | return &genericInformer{resource: resource.GroupResource(), informer: f.Zalando().V1().StackSets().Informer()}, nil 60 | 61 | } 62 | 63 | return nil, fmt.Errorf("no informer found for %v", resource) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 31 | 32 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 33 | type SharedInformerFactory interface { 34 | Start(stopCh <-chan struct{}) 35 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 36 | } 37 | 38 | type TweakListOptionsFunc func(*v1.ListOptions) 39 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/zalando/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/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/v1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | // Stacks returns a StackInformer. 28 | Stacks() StackInformer 29 | // StackSets returns a StackSetInformer. 30 | StackSets() StackSetInformer 31 | } 32 | 33 | type version struct { 34 | factory internalinterfaces.SharedInformerFactory 35 | namespace string 36 | tweakListOptions internalinterfaces.TweakListOptionsFunc 37 | } 38 | 39 | // New returns a new Interface. 40 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 41 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 42 | } 43 | 44 | // Stacks returns a StackInformer. 45 | func (v *version) Stacks() StackInformer { 46 | return &stackInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 47 | } 48 | 49 | // StackSets returns a StackSetInformer. 50 | func (v *version) StackSets() StackSetInformer { 51 | return &stackSetInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 52 | } 53 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/zalando/v1/stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | time "time" 23 | 24 | zalandov1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 25 | versioned "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 26 | internalinterfaces "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/internalinterfaces" 27 | v1 "github.com/zalando-incubator/stackset-controller/pkg/client/listers/zalando/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | runtime "k8s.io/apimachinery/pkg/runtime" 30 | watch "k8s.io/apimachinery/pkg/watch" 31 | cache "k8s.io/client-go/tools/cache" 32 | ) 33 | 34 | // StackInformer provides access to a shared informer and lister for 35 | // Stacks. 36 | type StackInformer interface { 37 | Informer() cache.SharedIndexInformer 38 | Lister() v1.StackLister 39 | } 40 | 41 | type stackInformer struct { 42 | factory internalinterfaces.SharedInformerFactory 43 | tweakListOptions internalinterfaces.TweakListOptionsFunc 44 | namespace string 45 | } 46 | 47 | // NewStackInformer constructs a new informer for Stack type. 48 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 49 | // one. This reduces memory footprint and number of connections to the server. 50 | func NewStackInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 51 | return NewFilteredStackInformer(client, namespace, resyncPeriod, indexers, nil) 52 | } 53 | 54 | // NewFilteredStackInformer constructs a new informer for Stack type. 55 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 56 | // one. This reduces memory footprint and number of connections to the server. 57 | func NewFilteredStackInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 58 | return cache.NewSharedIndexInformer( 59 | &cache.ListWatch{ 60 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 61 | if tweakListOptions != nil { 62 | tweakListOptions(&options) 63 | } 64 | return client.ZalandoV1().Stacks(namespace).List(options) 65 | }, 66 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 67 | if tweakListOptions != nil { 68 | tweakListOptions(&options) 69 | } 70 | return client.ZalandoV1().Stacks(namespace).Watch(options) 71 | }, 72 | }, 73 | &zalandov1.Stack{}, 74 | resyncPeriod, 75 | indexers, 76 | ) 77 | } 78 | 79 | func (f *stackInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 80 | return NewFilteredStackInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 81 | } 82 | 83 | func (f *stackInformer) Informer() cache.SharedIndexInformer { 84 | return f.factory.InformerFor(&zalandov1.Stack{}, f.defaultInformer) 85 | } 86 | 87 | func (f *stackInformer) Lister() v1.StackLister { 88 | return v1.NewStackLister(f.Informer().GetIndexer()) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/zalando/v1/stackset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | time "time" 23 | 24 | zalandov1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 25 | versioned "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 26 | internalinterfaces "github.com/zalando-incubator/stackset-controller/pkg/client/informers/externalversions/internalinterfaces" 27 | v1 "github.com/zalando-incubator/stackset-controller/pkg/client/listers/zalando/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | runtime "k8s.io/apimachinery/pkg/runtime" 30 | watch "k8s.io/apimachinery/pkg/watch" 31 | cache "k8s.io/client-go/tools/cache" 32 | ) 33 | 34 | // StackSetInformer provides access to a shared informer and lister for 35 | // StackSets. 36 | type StackSetInformer interface { 37 | Informer() cache.SharedIndexInformer 38 | Lister() v1.StackSetLister 39 | } 40 | 41 | type stackSetInformer struct { 42 | factory internalinterfaces.SharedInformerFactory 43 | tweakListOptions internalinterfaces.TweakListOptionsFunc 44 | namespace string 45 | } 46 | 47 | // NewStackSetInformer constructs a new informer for StackSet type. 48 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 49 | // one. This reduces memory footprint and number of connections to the server. 50 | func NewStackSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 51 | return NewFilteredStackSetInformer(client, namespace, resyncPeriod, indexers, nil) 52 | } 53 | 54 | // NewFilteredStackSetInformer constructs a new informer for StackSet type. 55 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 56 | // one. This reduces memory footprint and number of connections to the server. 57 | func NewFilteredStackSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 58 | return cache.NewSharedIndexInformer( 59 | &cache.ListWatch{ 60 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 61 | if tweakListOptions != nil { 62 | tweakListOptions(&options) 63 | } 64 | return client.ZalandoV1().StackSets(namespace).List(options) 65 | }, 66 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 67 | if tweakListOptions != nil { 68 | tweakListOptions(&options) 69 | } 70 | return client.ZalandoV1().StackSets(namespace).Watch(options) 71 | }, 72 | }, 73 | &zalandov1.StackSet{}, 74 | resyncPeriod, 75 | indexers, 76 | ) 77 | } 78 | 79 | func (f *stackSetInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 80 | return NewFilteredStackSetInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 81 | } 82 | 83 | func (f *stackSetInformer) Informer() cache.SharedIndexInformer { 84 | return f.factory.InformerFor(&zalandov1.StackSet{}, f.defaultInformer) 85 | } 86 | 87 | func (f *stackSetInformer) Lister() v1.StackSetLister { 88 | return v1.NewStackSetLister(f.Informer().GetIndexer()) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/client/listers/zalando/v1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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 | // StackListerExpansion allows custom methods to be added to 22 | // StackLister. 23 | type StackListerExpansion interface{} 24 | 25 | // StackNamespaceListerExpansion allows custom methods to be added to 26 | // StackNamespaceLister. 27 | type StackNamespaceListerExpansion interface{} 28 | 29 | // StackSetListerExpansion allows custom methods to be added to 30 | // StackSetLister. 31 | type StackSetListerExpansion interface{} 32 | 33 | // StackSetNamespaceListerExpansion allows custom methods to be added to 34 | // StackSetNamespaceLister. 35 | type StackSetNamespaceListerExpansion interface{} 36 | -------------------------------------------------------------------------------- /pkg/client/listers/zalando/v1/stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/v1" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | "k8s.io/apimachinery/pkg/labels" 25 | "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // StackLister helps list Stacks. 29 | type StackLister interface { 30 | // List lists all Stacks in the indexer. 31 | List(selector labels.Selector) (ret []*v1.Stack, err error) 32 | // Stacks returns an object that can list and get Stacks. 33 | Stacks(namespace string) StackNamespaceLister 34 | StackListerExpansion 35 | } 36 | 37 | // stackLister implements the StackLister interface. 38 | type stackLister struct { 39 | indexer cache.Indexer 40 | } 41 | 42 | // NewStackLister returns a new StackLister. 43 | func NewStackLister(indexer cache.Indexer) StackLister { 44 | return &stackLister{indexer: indexer} 45 | } 46 | 47 | // List lists all Stacks in the indexer. 48 | func (s *stackLister) List(selector labels.Selector) (ret []*v1.Stack, err error) { 49 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 50 | ret = append(ret, m.(*v1.Stack)) 51 | }) 52 | return ret, err 53 | } 54 | 55 | // Stacks returns an object that can list and get Stacks. 56 | func (s *stackLister) Stacks(namespace string) StackNamespaceLister { 57 | return stackNamespaceLister{indexer: s.indexer, namespace: namespace} 58 | } 59 | 60 | // StackNamespaceLister helps list and get Stacks. 61 | type StackNamespaceLister interface { 62 | // List lists all Stacks in the indexer for a given namespace. 63 | List(selector labels.Selector) (ret []*v1.Stack, err error) 64 | // Get retrieves the Stack from the indexer for a given namespace and name. 65 | Get(name string) (*v1.Stack, error) 66 | StackNamespaceListerExpansion 67 | } 68 | 69 | // stackNamespaceLister implements the StackNamespaceLister 70 | // interface. 71 | type stackNamespaceLister struct { 72 | indexer cache.Indexer 73 | namespace string 74 | } 75 | 76 | // List lists all Stacks in the indexer for a given namespace. 77 | func (s stackNamespaceLister) List(selector labels.Selector) (ret []*v1.Stack, err error) { 78 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 79 | ret = append(ret, m.(*v1.Stack)) 80 | }) 81 | return ret, err 82 | } 83 | 84 | // Get retrieves the Stack from the indexer for a given namespace and name. 85 | func (s stackNamespaceLister) Get(name string) (*v1.Stack, error) { 86 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 87 | if err != nil { 88 | return nil, err 89 | } 90 | if !exists { 91 | return nil, errors.NewNotFound(v1.Resource("stack"), name) 92 | } 93 | return obj.(*v1.Stack), nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/client/listers/zalando/v1/stackset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 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/v1" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | "k8s.io/apimachinery/pkg/labels" 25 | "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // StackSetLister helps list StackSets. 29 | type StackSetLister interface { 30 | // List lists all StackSets in the indexer. 31 | List(selector labels.Selector) (ret []*v1.StackSet, err error) 32 | // StackSets returns an object that can list and get StackSets. 33 | StackSets(namespace string) StackSetNamespaceLister 34 | StackSetListerExpansion 35 | } 36 | 37 | // stackSetLister implements the StackSetLister interface. 38 | type stackSetLister struct { 39 | indexer cache.Indexer 40 | } 41 | 42 | // NewStackSetLister returns a new StackSetLister. 43 | func NewStackSetLister(indexer cache.Indexer) StackSetLister { 44 | return &stackSetLister{indexer: indexer} 45 | } 46 | 47 | // List lists all StackSets in the indexer. 48 | func (s *stackSetLister) List(selector labels.Selector) (ret []*v1.StackSet, err error) { 49 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 50 | ret = append(ret, m.(*v1.StackSet)) 51 | }) 52 | return ret, err 53 | } 54 | 55 | // StackSets returns an object that can list and get StackSets. 56 | func (s *stackSetLister) StackSets(namespace string) StackSetNamespaceLister { 57 | return stackSetNamespaceLister{indexer: s.indexer, namespace: namespace} 58 | } 59 | 60 | // StackSetNamespaceLister helps list and get StackSets. 61 | type StackSetNamespaceLister interface { 62 | // List lists all StackSets in the indexer for a given namespace. 63 | List(selector labels.Selector) (ret []*v1.StackSet, err error) 64 | // Get retrieves the StackSet from the indexer for a given namespace and name. 65 | Get(name string) (*v1.StackSet, error) 66 | StackSetNamespaceListerExpansion 67 | } 68 | 69 | // stackSetNamespaceLister implements the StackSetNamespaceLister 70 | // interface. 71 | type stackSetNamespaceLister struct { 72 | indexer cache.Indexer 73 | namespace string 74 | } 75 | 76 | // List lists all StackSets in the indexer for a given namespace. 77 | func (s stackSetNamespaceLister) List(selector labels.Selector) (ret []*v1.StackSet, err error) { 78 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 79 | ret = append(ret, m.(*v1.StackSet)) 80 | }) 81 | return ret, err 82 | } 83 | 84 | // Get retrieves the StackSet from the indexer for a given namespace and name. 85 | func (s stackSetNamespaceLister) Get(name string) (*v1.StackSet, error) { 86 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 87 | if err != nil { 88 | return nil, err 89 | } 90 | if !exists { 91 | return nil, errors.NewNotFound(v1.Resource("stackset"), name) 92 | } 93 | return obj.(*v1.StackSet), nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/traffic/traffic.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando/v1" 8 | clientset "github.com/zalando-incubator/stackset-controller/pkg/client/clientset/versioned" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/labels" 11 | "k8s.io/apimachinery/pkg/types" 12 | "k8s.io/client-go/kubernetes" 13 | ) 14 | 15 | const ( 16 | stacksetHeritageLabelKey = "stackset" 17 | stackTrafficWeightsAnnotationKey = "zalando.org/stack-traffic-weights" 18 | backendWeightsAnnotationKey = "zalando.org/backend-weights" 19 | ) 20 | 21 | // Switcher is able to switch traffic between stacks. 22 | type Switcher struct { 23 | kube kubernetes.Interface 24 | appClient clientset.Interface 25 | } 26 | 27 | // NewSwitcher initializes a new traffic switcher. 28 | func NewSwitcher(kube kubernetes.Interface, client clientset.Interface) *Switcher { 29 | return &Switcher{ 30 | kube: kube, 31 | appClient: client, 32 | } 33 | } 34 | 35 | // Switch changes traffic weight for a stack. 36 | func (t *Switcher) Switch(stackset, stack, namespace string, weight float64) ([]StackTrafficWeight, error) { 37 | stacks, err := t.getStacks(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.kube.ExtensionsV1beta1().Ingresses(namespace).Patch(stackset, types.StrategicMergePatchType, annotationData) 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(stackset, namespace string) ([]StackTrafficWeight, error) { 93 | stacks, err := t.getStacks(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(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.appClient.ZalandoV1().Stacks(namespace).List(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(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(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.kube.ExtensionsV1beta1().Ingresses(namespace).Get(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[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 | --------------------------------------------------------------------------------