├── .dockerignore ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.dev ├── Gemfile ├── Gemfile.lock ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── README.md ├── apis └── metacontroller │ └── v1alpha1 │ ├── doc.go │ ├── register.go │ ├── roundtrip_test.go │ ├── types.go │ └── zz_generated.deepcopy.go ├── client └── generated │ ├── clientset │ └── internalclientset │ │ ├── clientset.go │ │ ├── doc.go │ │ ├── scheme │ │ ├── doc.go │ │ └── register.go │ │ └── typed │ │ └── metacontroller │ │ └── v1alpha1 │ │ ├── compositecontroller.go │ │ ├── controllerrevision.go │ │ ├── controllerrevision_expansion.go │ │ ├── decoratorcontroller.go │ │ ├── doc.go │ │ ├── generated_expansion.go │ │ └── metacontroller_client.go │ ├── informer │ └── externalversions │ │ ├── factory.go │ │ ├── generic.go │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ └── metacontroller │ │ ├── interface.go │ │ └── v1alpha1 │ │ ├── compositecontroller.go │ │ ├── controllerrevision.go │ │ ├── decoratorcontroller.go │ │ └── interface.go │ └── lister │ └── metacontroller │ └── v1alpha1 │ ├── compositecontroller.go │ ├── controllerrevision.go │ ├── decoratorcontroller.go │ └── expansion_generated.go ├── code-of-conduct.md ├── controller ├── common │ ├── common.go │ ├── finalizer │ │ └── finalizer.go │ ├── manage_children.go │ └── manage_children_test.go ├── composite │ ├── controller.go │ ├── controller_revision.go │ ├── hooks.go │ ├── metacontroller.go │ └── rolling_update.go └── decorator │ ├── controller.go │ ├── hooks.go │ ├── metacontroller.go │ └── selector.go ├── docs ├── .gitignore ├── 404.html ├── _api │ ├── apply.md │ ├── compositecontroller.md │ ├── controllerrevision.md │ ├── decoratorcontroller.md │ └── hook.md ├── _config.yml ├── _contrib │ └── build.md ├── _data │ └── navigation.yml ├── _design │ ├── customize-hook.md │ └── map-controller.md ├── _guide │ ├── best-practices.md │ ├── create.md │ ├── install.md │ └── troubleshooting.md ├── _includes │ └── footer.html ├── _redirects ├── api.md ├── assets │ └── css │ │ └── main.scss ├── concepts.md ├── contrib.md ├── design.md ├── examples.md ├── faq.md ├── features.md ├── go-import.html ├── guide.md ├── intro.md └── pronunciation.md ├── dynamic ├── apply │ ├── apply.go │ └── apply_test.go ├── clientset │ └── clientset.go ├── controllerref │ ├── controller_ref.go │ ├── controller_revision.go │ └── unstructured.go ├── discovery │ └── discovery.go ├── informer │ ├── factory.go │ └── informer.go ├── lister │ └── lister.go └── object │ ├── metadata.go │ ├── metadata_test.go │ └── status.go ├── examples ├── bluegreen │ ├── README.md │ ├── bluegreen-controller.yaml │ ├── my-bluegreen.yaml │ ├── sync.js │ └── test.sh ├── catset │ ├── README.md │ ├── catset-controller.yaml │ ├── my-catset.yaml │ ├── sync.js │ └── test.sh ├── clusteredparent │ ├── README.md │ ├── cluster-parent.yaml │ ├── my-clusterrole.yaml │ ├── sync.py │ └── test.sh ├── crd-roles │ ├── README.md │ ├── crd-role-controller.yaml │ ├── my-crd.yaml │ ├── sync.py │ └── test.sh ├── daemonjob │ ├── README.md │ ├── daemonjob-controller.yaml │ ├── my-daemonjob.yaml │ ├── sync.py │ └── test.sh ├── go │ ├── .gitignore │ ├── Dockerfile │ ├── Gopkg.lock │ ├── Gopkg.toml │ ├── README.md │ ├── main.go │ ├── my-thing.yaml │ └── thing-controller.yaml ├── indexedjob │ ├── README.md │ ├── indexedjob-controller.yaml │ ├── my-indexedjob.yaml │ ├── sync.py │ └── test.sh ├── jsonnetd │ ├── .gitignore │ ├── Dockerfile │ ├── Gopkg.lock │ ├── Gopkg.toml │ ├── Makefile │ ├── README.md │ ├── extensions.go │ └── main.go ├── nodejs │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ └── server.js ├── service-per-pod │ ├── README.md │ ├── hooks │ │ ├── finalize-service-per-pod.jsonnet │ │ ├── sync-pod-name-label.jsonnet │ │ └── sync-service-per-pod.jsonnet │ ├── my-statefulset.yaml │ ├── service-per-pod.yaml │ └── test.sh ├── status │ ├── README.md │ ├── my-noop.yaml │ ├── noop-controller.yaml │ ├── sync.js │ └── test.sh ├── test.sh └── vitess │ └── README.md ├── hack └── get-kube-binaries.sh ├── hooks ├── hooks.go └── webhook.go ├── kustomization.yaml ├── main.go ├── manifests ├── dev │ ├── args.yaml │ ├── image.yaml │ └── kustomization.yaml ├── metacontroller-namespace.yaml ├── metacontroller-rbac.yaml └── metacontroller.yaml ├── netlify.toml ├── server └── server.go ├── skaffold.yaml ├── test └── integration │ ├── composite │ └── composite_test.go │ ├── decorator │ └── decorator_test.go │ └── framework │ ├── apiserver.go │ ├── crd.go │ ├── etcd.go │ ├── fixture.go │ ├── main.go │ ├── metacontroller.go │ └── webhook.go └── third_party └── kubernetes ├── LICENSE ├── controller.go ├── controller_ref_manager.go ├── pointer.go └── unstructured.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignoring things that aren't needed to build the Docker image 2 | # helps Skaffold know when not to rebuild the image. 3 | docs 4 | examples 5 | manifests 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | /metacontroller 4 | /hack/bin/ 5 | .*.swp 6 | .history 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.11.x" 4 | go_import_path: metacontroller.app 5 | cache: 6 | directories: 7 | - vendor 8 | addons: 9 | apt: 10 | packages: 11 | - wget 12 | install: 13 | - hack/get-kube-binaries.sh 14 | - go get -u github.com/golang/dep/cmd/dep 15 | - dep ensure 16 | script: 17 | # We intentionally don't generate files in CI, since we check them in. 18 | - go install 19 | - make unit-test 20 | - make integration-test 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Contributor Guide 26 | 27 | Aside from the above administrative requirements, you can find more 28 | technical details about project internals in the 29 | [contributor guide](https://metacontroller.app/contrib/). 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.10 AS build 2 | 3 | RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 4 | 5 | COPY . /go/src/metacontroller.app/ 6 | WORKDIR /go/src/metacontroller.app/ 7 | RUN dep ensure && go install 8 | 9 | FROM debian:stretch-slim 10 | RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates && rm -rf /var/lib/apt/lists/* 11 | COPY --from=build /go/bin/metacontroller.app /usr/bin/metacontroller 12 | CMD ["/usr/bin/metacontroller"] 13 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # This is the same as Dockerfile, but skips `dep ensure`. 2 | # It assumes you already ran that locally. 3 | FROM golang:1.10 AS build 4 | 5 | COPY . /go/src/metacontroller.app/ 6 | WORKDIR /go/src/metacontroller.app/ 7 | RUN go install 8 | 9 | FROM debian:stretch-slim 10 | RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates && rm -rf /var/lib/apt/lists/* 11 | COPY --from=build /go/bin/metacontroller.app /usr/bin/metacontroller 12 | CMD ["/usr/bin/metacontroller"] 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Hello! This is where you manage which Jekyll version is used to run. 4 | # When you want to use a different version, change it below, save the 5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 6 | # 7 | # bundle exec jekyll serve 8 | # 9 | # This will help ensure the proper Jekyll version is running. 10 | # Happy Jekylling! 11 | gem "jekyll", "~> 3.7.3" 12 | 13 | gem "minimal-mistakes-jekyll" 14 | 15 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 16 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 17 | #gem "github-pages", group: :jekyll_plugins 18 | 19 | # If you have any plugins, put them here! 20 | group :jekyll_plugins do 21 | gem "jekyll-feed", "~> 0.6" 22 | gem "jekyll-data" 23 | end 24 | 25 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 26 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.0" if Gem.win_platform? 30 | 31 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.10) 5 | i18n (~> 0.7) 6 | minitest (~> 5.1) 7 | thread_safe (~> 0.3, >= 0.3.4) 8 | tzinfo (~> 1.1) 9 | addressable (2.5.2) 10 | public_suffix (>= 2.0.2, < 4.0) 11 | colorator (1.1.0) 12 | concurrent-ruby (1.0.5) 13 | em-websocket (0.5.1) 14 | eventmachine (>= 0.12.9) 15 | http_parser.rb (~> 0.6.0) 16 | eventmachine (1.2.5) 17 | faraday (0.14.0) 18 | multipart-post (>= 1.2, < 3) 19 | ffi (1.9.23) 20 | forwardable-extended (2.6.0) 21 | gemoji (3.0.0) 22 | html-pipeline (2.7.1) 23 | activesupport (>= 2) 24 | nokogiri (>= 1.4) 25 | http_parser.rb (0.6.0) 26 | i18n (0.9.5) 27 | concurrent-ruby (~> 1.0) 28 | jekyll (3.7.3) 29 | addressable (~> 2.4) 30 | colorator (~> 1.0) 31 | em-websocket (~> 0.5) 32 | i18n (~> 0.7) 33 | jekyll-sass-converter (~> 1.0) 34 | jekyll-watch (~> 2.0) 35 | kramdown (~> 1.14) 36 | liquid (~> 4.0) 37 | mercenary (~> 0.3.3) 38 | pathutil (~> 0.9) 39 | rouge (>= 1.7, < 4) 40 | safe_yaml (~> 1.0) 41 | jekyll-data (1.0.0) 42 | jekyll (~> 3.3) 43 | jekyll-feed (0.9.3) 44 | jekyll (~> 3.3) 45 | jekyll-gist (1.5.0) 46 | octokit (~> 4.2) 47 | jekyll-paginate (1.1.0) 48 | jekyll-sass-converter (1.5.2) 49 | sass (~> 3.4) 50 | jekyll-sitemap (1.2.0) 51 | jekyll (~> 3.3) 52 | jekyll-watch (2.0.0) 53 | listen (~> 3.0) 54 | jemoji (0.9.0) 55 | activesupport (~> 4.0, >= 4.2.9) 56 | gemoji (~> 3.0) 57 | html-pipeline (~> 2.2) 58 | jekyll (~> 3.0) 59 | kramdown (1.16.2) 60 | liquid (4.0.0) 61 | listen (3.1.5) 62 | rb-fsevent (~> 0.9, >= 0.9.4) 63 | rb-inotify (~> 0.9, >= 0.9.7) 64 | ruby_dep (~> 1.2) 65 | mercenary (0.3.6) 66 | mini_portile2 (2.3.0) 67 | minimal-mistakes-jekyll (4.11.1) 68 | jekyll (~> 3.6) 69 | jekyll-data (~> 1.0) 70 | jekyll-feed (~> 0.9.2) 71 | jekyll-gist (~> 1.4) 72 | jekyll-paginate (~> 1.1) 73 | jekyll-sitemap (~> 1.1) 74 | jemoji (~> 0.8) 75 | minitest (5.11.3) 76 | multipart-post (2.0.0) 77 | nokogiri (1.8.2) 78 | mini_portile2 (~> 2.3.0) 79 | octokit (4.8.0) 80 | sawyer (~> 0.8.0, >= 0.5.3) 81 | pathutil (0.16.1) 82 | forwardable-extended (~> 2.6) 83 | public_suffix (3.0.2) 84 | rb-fsevent (0.10.3) 85 | rb-inotify (0.9.10) 86 | ffi (>= 0.5.0, < 2) 87 | rouge (3.1.1) 88 | ruby_dep (1.5.0) 89 | safe_yaml (1.0.4) 90 | sass (3.5.6) 91 | sass-listen (~> 4.0.0) 92 | sass-listen (4.0.0) 93 | rb-fsevent (~> 0.9, >= 0.9.4) 94 | rb-inotify (~> 0.9, >= 0.9.7) 95 | sawyer (0.8.1) 96 | addressable (>= 2.3.5, < 2.6) 97 | faraday (~> 0.8, < 1.0) 98 | thread_safe (0.3.6) 99 | tzinfo (1.2.5) 100 | thread_safe (~> 0.1) 101 | 102 | PLATFORMS 103 | ruby 104 | 105 | DEPENDENCIES 106 | jekyll (~> 3.7.3) 107 | jekyll-data 108 | jekyll-feed (~> 0.6) 109 | minimal-mistakes-jekyll 110 | tzinfo-data 111 | 112 | BUNDLED WITH 113 | 1.16.1 114 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | # Adding the generators here allows us to pin them to a particular version. 24 | required = [ 25 | "k8s.io/code-generator/cmd/deepcopy-gen", 26 | "k8s.io/code-generator/cmd/client-gen", 27 | "k8s.io/code-generator/cmd/lister-gen", 28 | "k8s.io/code-generator/cmd/informer-gen" 29 | ] 30 | 31 | # The code in examples/ is not part of the actual build. 32 | ignored = [ 33 | "metacontroller.app/examples/*" 34 | ] 35 | 36 | [[constraint]] 37 | name = "k8s.io/client-go" 38 | version = "8.0.0" 39 | 40 | [[constraint]] 41 | name = "k8s.io/apimachinery" 42 | version = "kubernetes-1.11.0" 43 | 44 | [[constraint]] 45 | name = "k8s.io/apiextensions-apiserver" 46 | version = "kubernetes-1.11.0" 47 | 48 | [[constraint]] 49 | name = "k8s.io/code-generator" 50 | version = "kubernetes-1.11.0" 51 | 52 | [[override]] 53 | name = "github.com/json-iterator/go" 54 | version = "1.1.5" # same minor version track as used by apimachinery@kubernetes-1.11.0 55 | 56 | [[override]] 57 | name = "github.com/prometheus/client_model" 58 | revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" 59 | # last version compatable with golang/protobuf@v1.0.0 (the version used by apimachinery@kubernetes-1.9.9) 60 | 61 | [prune] 62 | go-tests = true 63 | unused-packages = true 64 | 65 | [[prune.project]] 66 | name = "k8s.io/code-generator" 67 | unused-packages = false 68 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TAG = dev 2 | 3 | PKG := metacontroller.app 4 | API_GROUPS := metacontroller/v1alpha1 5 | 6 | all: install 7 | 8 | install: generated_files 9 | go install 10 | 11 | unit-test: 12 | pkgs="$$(go list ./... | grep -v '/test/integration/\|/examples/')" ; \ 13 | go test -i $${pkgs} && \ 14 | go test $${pkgs} 15 | 16 | integration-test: 17 | go test -i ./test/integration/... 18 | PATH="$(PWD)/hack/bin:$(PATH)" go test ./test/integration/... -v -timeout 5m -args -v=6 19 | 20 | image: generated_files 21 | docker build -t metacontroller/metacontroller:$(TAG) . 22 | 23 | push: image 24 | docker push metacontroller/metacontroller:$(TAG) 25 | 26 | # Code generators 27 | # https://github.com/kubernetes/community/blob/master/contributors/devel/api_changes.md#generate-code 28 | 29 | generated_files: deepcopy clientset lister informer 30 | 31 | # also builds vendored version of deepcopy-gen tool 32 | deepcopy: 33 | @go install ./vendor/k8s.io/code-generator/cmd/deepcopy-gen 34 | @echo "+ Generating deepcopy funcs for $(API_GROUPS)" 35 | @deepcopy-gen \ 36 | --input-dirs $(PKG)/apis/$(API_GROUPS) \ 37 | --output-file-base zz_generated.deepcopy 38 | 39 | # also builds vendored version of client-gen tool 40 | clientset: 41 | @go install ./vendor/k8s.io/code-generator/cmd/client-gen 42 | @echo "+ Generating clientsets for $(API_GROUPS)" 43 | @client-gen \ 44 | --fake-clientset=false \ 45 | --input $(API_GROUPS) \ 46 | --input-base $(PKG)/apis \ 47 | --clientset-path $(PKG)/client/generated/clientset 48 | 49 | # also builds vendored version of lister-gen tool 50 | lister: 51 | @go install ./vendor/k8s.io/code-generator/cmd/lister-gen 52 | @echo "+ Generating lister for $(API_GROUPS)" 53 | @lister-gen \ 54 | --input-dirs $(PKG)/apis/$(API_GROUPS) \ 55 | --output-package $(PKG)/client/generated/lister 56 | 57 | # also builds vendored version of informer-gen tool 58 | informer: 59 | @go install ./vendor/k8s.io/code-generator/cmd/informer-gen 60 | @echo "+ Generating informer for $(API_GROUPS)" 61 | @informer-gen \ 62 | --input-dirs $(PKG)/apis/$(API_GROUPS) \ 63 | --output-package $(PKG)/client/generated/informer \ 64 | --versioned-clientset-package $(PKG)/client/generated/clientset/internalclientset \ 65 | --listers-package $(PKG)/client/generated/lister 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## :zap: Metacontroller Has Moved 2 | Active development of metacontroller continues at [metacontroller/metacontroller](https://github.com/metacontroller/metacontroller). This repository is no longer maintained. 3 | -------------------------------------------------------------------------------- /apis/metacontroller/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://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 | // +k8s:deepcopy-gen=package,register 18 | 19 | package v1alpha1 // import "metacontroller.app/apis/metacontroller/v1alpha1" 20 | -------------------------------------------------------------------------------- /apis/metacontroller/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | ) 24 | 25 | var ( 26 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 27 | AddToScheme = SchemeBuilder.AddToScheme 28 | ) 29 | 30 | // GroupName is the group name used in this package. 31 | const GroupName = "metacontroller.k8s.io" 32 | 33 | // SchemeGroupVersion is the group version used to register these objects. 34 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} 35 | 36 | // Resource takes an unqualified resource and returns a Group-qualified GroupResource. 37 | func Resource(resource string) schema.GroupResource { 38 | return SchemeGroupVersion.WithResource(resource).GroupResource() 39 | } 40 | 41 | // addKnownTypes adds the set of types defined in this package to the supplied scheme. 42 | func addKnownTypes(scheme *runtime.Scheme) error { 43 | scheme.AddKnownTypes(SchemeGroupVersion, 44 | &CompositeController{}, 45 | &CompositeControllerList{}, 46 | &DecoratorController{}, 47 | &DecoratorControllerList{}, 48 | &ControllerRevision{}, 49 | &ControllerRevisionList{}, 50 | ) 51 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /apis/metacontroller/v1alpha1/roundtrip_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "math/rand" 21 | "testing" 22 | 23 | "k8s.io/apimachinery/pkg/api/testing/fuzzer" 24 | roundtrip "k8s.io/apimachinery/pkg/api/testing/roundtrip" 25 | metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/runtime/serializer" 28 | ) 29 | 30 | // TestRoundTrip tests that the third-party kinds can be marshaled and unmarshaled correctly to/from JSON 31 | // without the loss of information. Moreover, deep copy is tested. 32 | func TestRoundTrip(t *testing.T) { 33 | scheme := runtime.NewScheme() 34 | codecs := serializer.NewCodecFactory(scheme) 35 | 36 | AddToScheme(scheme) 37 | 38 | fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(1), codecs) 39 | 40 | roundtrip.RoundTripSpecificKindWithoutProtobuf(t, SchemeGroupVersion.WithKind("CompositeController"), scheme, codecs, fuzzer, nil) 41 | roundtrip.RoundTripSpecificKindWithoutProtobuf(t, SchemeGroupVersion.WithKind("CompositeControllerList"), scheme, codecs, fuzzer, nil) 42 | roundtrip.RoundTripSpecificKindWithoutProtobuf(t, SchemeGroupVersion.WithKind("DecoratorController"), scheme, codecs, fuzzer, nil) 43 | roundtrip.RoundTripSpecificKindWithoutProtobuf(t, SchemeGroupVersion.WithKind("DecoratorControllerList"), scheme, codecs, fuzzer, nil) 44 | roundtrip.RoundTripSpecificKindWithoutProtobuf(t, SchemeGroupVersion.WithKind("ControllerRevision"), scheme, codecs, fuzzer, nil) 45 | roundtrip.RoundTripSpecificKindWithoutProtobuf(t, SchemeGroupVersion.WithKind("ControllerRevisionList"), scheme, codecs, fuzzer, nil) 46 | } 47 | -------------------------------------------------------------------------------- /client/generated/clientset/internalclientset/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 internalclientset 20 | 21 | import ( 22 | discovery "k8s.io/client-go/discovery" 23 | rest "k8s.io/client-go/rest" 24 | flowcontrol "k8s.io/client-go/util/flowcontrol" 25 | metacontrollerv1alpha1 "metacontroller.app/client/generated/clientset/internalclientset/typed/metacontroller/v1alpha1" 26 | ) 27 | 28 | type Interface interface { 29 | Discovery() discovery.DiscoveryInterface 30 | MetacontrollerV1alpha1() metacontrollerv1alpha1.MetacontrollerV1alpha1Interface 31 | // Deprecated: please explicitly pick a version if possible. 32 | Metacontroller() metacontrollerv1alpha1.MetacontrollerV1alpha1Interface 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 | metacontrollerV1alpha1 *metacontrollerv1alpha1.MetacontrollerV1alpha1Client 40 | } 41 | 42 | // MetacontrollerV1alpha1 retrieves the MetacontrollerV1alpha1Client 43 | func (c *Clientset) MetacontrollerV1alpha1() metacontrollerv1alpha1.MetacontrollerV1alpha1Interface { 44 | return c.metacontrollerV1alpha1 45 | } 46 | 47 | // Deprecated: Metacontroller retrieves the default version of MetacontrollerClient. 48 | // Please explicitly pick a version. 49 | func (c *Clientset) Metacontroller() metacontrollerv1alpha1.MetacontrollerV1alpha1Interface { 50 | return c.metacontrollerV1alpha1 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.metacontrollerV1alpha1, err = metacontrollerv1alpha1.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.metacontrollerV1alpha1 = metacontrollerv1alpha1.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.metacontrollerV1alpha1 = metacontrollerv1alpha1.New(c) 95 | 96 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 97 | return &cs 98 | } 99 | -------------------------------------------------------------------------------- /client/generated/clientset/internalclientset/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 internalclientset 21 | -------------------------------------------------------------------------------- /client/generated/clientset/internalclientset/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | -------------------------------------------------------------------------------- /client/generated/clientset/internalclientset/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | runtime "k8s.io/apimachinery/pkg/runtime" 24 | schema "k8s.io/apimachinery/pkg/runtime/schema" 25 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 26 | metacontrollerv1alpha1 "metacontroller.app/apis/metacontroller/v1alpha1" 27 | ) 28 | 29 | var Scheme = runtime.NewScheme() 30 | var Codecs = serializer.NewCodecFactory(Scheme) 31 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 32 | 33 | func init() { 34 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 35 | AddToScheme(Scheme) 36 | } 37 | 38 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 39 | // of clientsets, like in: 40 | // 41 | // import ( 42 | // "k8s.io/client-go/kubernetes" 43 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 44 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 45 | // ) 46 | // 47 | // kclientset, _ := kubernetes.NewForConfig(c) 48 | // aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 49 | // 50 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 51 | // correctly. 52 | func AddToScheme(scheme *runtime.Scheme) { 53 | metacontrollerv1alpha1.AddToScheme(scheme) 54 | } 55 | -------------------------------------------------------------------------------- /client/generated/clientset/internalclientset/typed/metacontroller/v1alpha1/controllerrevision_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "fmt" 21 | 22 | apierrors "k8s.io/apimachinery/pkg/api/errors" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/client-go/util/retry" 25 | 26 | v1alpha1 "metacontroller.app/apis/metacontroller/v1alpha1" 27 | ) 28 | 29 | type ControllerRevisionExpansion interface { 30 | UpdateWithRetries(orig *v1alpha1.ControllerRevision, updateFn func(*v1alpha1.ControllerRevision) bool) (result *v1alpha1.ControllerRevision, err error) 31 | } 32 | 33 | func (c *controllerRevisions) UpdateWithRetries(orig *v1alpha1.ControllerRevision, updateFn func(*v1alpha1.ControllerRevision) bool) (result *v1alpha1.ControllerRevision, err error) { 34 | name := orig.GetName() 35 | err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { 36 | current, err := c.Get(name, metav1.GetOptions{}) 37 | if err != nil { 38 | return err 39 | } 40 | if current.GetUID() != orig.GetUID() { 41 | return apierrors.NewGone(fmt.Sprintf("can't update ControllerRevision %v/%v: original object is gone: got uid %v, want %v", orig.GetNamespace(), orig.GetName(), current.GetUID(), orig.GetUID())) 42 | } 43 | if changed := updateFn(current); !changed { 44 | // There's nothing to do. 45 | return nil 46 | } 47 | result, err = c.Update(current) 48 | return err 49 | }) 50 | return result, err 51 | } 52 | -------------------------------------------------------------------------------- /client/generated/clientset/internalclientset/typed/metacontroller/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 21 | -------------------------------------------------------------------------------- /client/generated/clientset/internalclientset/typed/metacontroller/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 20 | 21 | type CompositeControllerExpansion interface{} 22 | 23 | type DecoratorControllerExpansion interface{} 24 | -------------------------------------------------------------------------------- /client/generated/clientset/internalclientset/typed/metacontroller/v1alpha1/metacontroller_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 20 | 21 | import ( 22 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 23 | rest "k8s.io/client-go/rest" 24 | v1alpha1 "metacontroller.app/apis/metacontroller/v1alpha1" 25 | "metacontroller.app/client/generated/clientset/internalclientset/scheme" 26 | ) 27 | 28 | type MetacontrollerV1alpha1Interface interface { 29 | RESTClient() rest.Interface 30 | CompositeControllersGetter 31 | ControllerRevisionsGetter 32 | DecoratorControllersGetter 33 | } 34 | 35 | // MetacontrollerV1alpha1Client is used to interact with features provided by the metacontroller group. 36 | type MetacontrollerV1alpha1Client struct { 37 | restClient rest.Interface 38 | } 39 | 40 | func (c *MetacontrollerV1alpha1Client) CompositeControllers() CompositeControllerInterface { 41 | return newCompositeControllers(c) 42 | } 43 | 44 | func (c *MetacontrollerV1alpha1Client) ControllerRevisions(namespace string) ControllerRevisionInterface { 45 | return newControllerRevisions(c, namespace) 46 | } 47 | 48 | func (c *MetacontrollerV1alpha1Client) DecoratorControllers() DecoratorControllerInterface { 49 | return newDecoratorControllers(c) 50 | } 51 | 52 | // NewForConfig creates a new MetacontrollerV1alpha1Client for the given config. 53 | func NewForConfig(c *rest.Config) (*MetacontrollerV1alpha1Client, error) { 54 | config := *c 55 | if err := setConfigDefaults(&config); err != nil { 56 | return nil, err 57 | } 58 | client, err := rest.RESTClientFor(&config) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &MetacontrollerV1alpha1Client{client}, nil 63 | } 64 | 65 | // NewForConfigOrDie creates a new MetacontrollerV1alpha1Client for the given config and 66 | // panics if there is an error in the config. 67 | func NewForConfigOrDie(c *rest.Config) *MetacontrollerV1alpha1Client { 68 | client, err := NewForConfig(c) 69 | if err != nil { 70 | panic(err) 71 | } 72 | return client 73 | } 74 | 75 | // New creates a new MetacontrollerV1alpha1Client for the given RESTClient. 76 | func New(c rest.Interface) *MetacontrollerV1alpha1Client { 77 | return &MetacontrollerV1alpha1Client{c} 78 | } 79 | 80 | func setConfigDefaults(config *rest.Config) error { 81 | gv := v1alpha1.SchemeGroupVersion 82 | config.GroupVersion = &gv 83 | config.APIPath = "/apis" 84 | config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} 85 | 86 | if config.UserAgent == "" { 87 | config.UserAgent = rest.DefaultKubernetesUserAgent() 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // RESTClient returns a RESTClient that is used to communicate 94 | // with API server by this client implementation. 95 | func (c *MetacontrollerV1alpha1Client) RESTClient() rest.Interface { 96 | if c == nil { 97 | return nil 98 | } 99 | return c.restClient 100 | } 101 | -------------------------------------------------------------------------------- /client/generated/informer/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | schema "k8s.io/apimachinery/pkg/runtime/schema" 25 | cache "k8s.io/client-go/tools/cache" 26 | v1alpha1 "metacontroller.app/apis/metacontroller/v1alpha1" 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=metacontroller, Version=v1alpha1 56 | case v1alpha1.SchemeGroupVersion.WithResource("compositecontrollers"): 57 | return &genericInformer{resource: resource.GroupResource(), informer: f.Metacontroller().V1alpha1().CompositeControllers().Informer()}, nil 58 | case v1alpha1.SchemeGroupVersion.WithResource("controllerrevisions"): 59 | return &genericInformer{resource: resource.GroupResource(), informer: f.Metacontroller().V1alpha1().ControllerRevisions().Informer()}, nil 60 | case v1alpha1.SchemeGroupVersion.WithResource("decoratorcontrollers"): 61 | return &genericInformer{resource: resource.GroupResource(), informer: f.Metacontroller().V1alpha1().DecoratorControllers().Informer()}, nil 62 | 63 | } 64 | 65 | return nil, fmt.Errorf("no informer found for %v", resource) 66 | } 67 | -------------------------------------------------------------------------------- /client/generated/informer/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | cache "k8s.io/client-go/tools/cache" 27 | internalclientset "metacontroller.app/client/generated/clientset/internalclientset" 28 | ) 29 | 30 | type NewInformerFunc func(internalclientset.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 | -------------------------------------------------------------------------------- /client/generated/informer/externalversions/metacontroller/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 metacontroller 20 | 21 | import ( 22 | internalinterfaces "metacontroller.app/client/generated/informer/externalversions/internalinterfaces" 23 | v1alpha1 "metacontroller.app/client/generated/informer/externalversions/metacontroller/v1alpha1" 24 | ) 25 | 26 | // Interface provides access to each of this group's versions. 27 | type Interface interface { 28 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 29 | V1alpha1() v1alpha1.Interface 30 | } 31 | 32 | type group struct { 33 | factory internalinterfaces.SharedInformerFactory 34 | namespace string 35 | tweakListOptions internalinterfaces.TweakListOptionsFunc 36 | } 37 | 38 | // New returns a new Interface. 39 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 40 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 41 | } 42 | 43 | // V1alpha1 returns a new v1alpha1.Interface. 44 | func (g *group) V1alpha1() v1alpha1.Interface { 45 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 46 | } 47 | -------------------------------------------------------------------------------- /client/generated/informer/externalversions/metacontroller/v1alpha1/compositecontroller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 20 | 21 | import ( 22 | time "time" 23 | 24 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | watch "k8s.io/apimachinery/pkg/watch" 27 | cache "k8s.io/client-go/tools/cache" 28 | metacontrollerv1alpha1 "metacontroller.app/apis/metacontroller/v1alpha1" 29 | internalclientset "metacontroller.app/client/generated/clientset/internalclientset" 30 | internalinterfaces "metacontroller.app/client/generated/informer/externalversions/internalinterfaces" 31 | v1alpha1 "metacontroller.app/client/generated/lister/metacontroller/v1alpha1" 32 | ) 33 | 34 | // CompositeControllerInformer provides access to a shared informer and lister for 35 | // CompositeControllers. 36 | type CompositeControllerInformer interface { 37 | Informer() cache.SharedIndexInformer 38 | Lister() v1alpha1.CompositeControllerLister 39 | } 40 | 41 | type compositeControllerInformer struct { 42 | factory internalinterfaces.SharedInformerFactory 43 | tweakListOptions internalinterfaces.TweakListOptionsFunc 44 | } 45 | 46 | // NewCompositeControllerInformer constructs a new informer for CompositeController type. 47 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 48 | // one. This reduces memory footprint and number of connections to the server. 49 | func NewCompositeControllerInformer(client internalclientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 50 | return NewFilteredCompositeControllerInformer(client, resyncPeriod, indexers, nil) 51 | } 52 | 53 | // NewFilteredCompositeControllerInformer constructs a new informer for CompositeController type. 54 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 55 | // one. This reduces memory footprint and number of connections to the server. 56 | func NewFilteredCompositeControllerInformer(client internalclientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 57 | return cache.NewSharedIndexInformer( 58 | &cache.ListWatch{ 59 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 60 | if tweakListOptions != nil { 61 | tweakListOptions(&options) 62 | } 63 | return client.MetacontrollerV1alpha1().CompositeControllers().List(options) 64 | }, 65 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 66 | if tweakListOptions != nil { 67 | tweakListOptions(&options) 68 | } 69 | return client.MetacontrollerV1alpha1().CompositeControllers().Watch(options) 70 | }, 71 | }, 72 | &metacontrollerv1alpha1.CompositeController{}, 73 | resyncPeriod, 74 | indexers, 75 | ) 76 | } 77 | 78 | func (f *compositeControllerInformer) defaultInformer(client internalclientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 79 | return NewFilteredCompositeControllerInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 80 | } 81 | 82 | func (f *compositeControllerInformer) Informer() cache.SharedIndexInformer { 83 | return f.factory.InformerFor(&metacontrollerv1alpha1.CompositeController{}, f.defaultInformer) 84 | } 85 | 86 | func (f *compositeControllerInformer) Lister() v1alpha1.CompositeControllerLister { 87 | return v1alpha1.NewCompositeControllerLister(f.Informer().GetIndexer()) 88 | } 89 | -------------------------------------------------------------------------------- /client/generated/informer/externalversions/metacontroller/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 20 | 21 | import ( 22 | internalinterfaces "metacontroller.app/client/generated/informer/externalversions/internalinterfaces" 23 | ) 24 | 25 | // Interface provides access to all the informers in this group version. 26 | type Interface interface { 27 | // CompositeControllers returns a CompositeControllerInformer. 28 | CompositeControllers() CompositeControllerInformer 29 | // ControllerRevisions returns a ControllerRevisionInformer. 30 | ControllerRevisions() ControllerRevisionInformer 31 | // DecoratorControllers returns a DecoratorControllerInformer. 32 | DecoratorControllers() DecoratorControllerInformer 33 | } 34 | 35 | type version struct { 36 | factory internalinterfaces.SharedInformerFactory 37 | namespace string 38 | tweakListOptions internalinterfaces.TweakListOptionsFunc 39 | } 40 | 41 | // New returns a new Interface. 42 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 43 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 44 | } 45 | 46 | // CompositeControllers returns a CompositeControllerInformer. 47 | func (v *version) CompositeControllers() CompositeControllerInformer { 48 | return &compositeControllerInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} 49 | } 50 | 51 | // ControllerRevisions returns a ControllerRevisionInformer. 52 | func (v *version) ControllerRevisions() ControllerRevisionInformer { 53 | return &controllerRevisionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 54 | } 55 | 56 | // DecoratorControllers returns a DecoratorControllerInformer. 57 | func (v *version) DecoratorControllers() DecoratorControllerInformer { 58 | return &decoratorControllerInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} 59 | } 60 | -------------------------------------------------------------------------------- /client/generated/lister/metacontroller/v1alpha1/compositecontroller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 20 | 21 | import ( 22 | "k8s.io/apimachinery/pkg/api/errors" 23 | "k8s.io/apimachinery/pkg/labels" 24 | "k8s.io/client-go/tools/cache" 25 | v1alpha1 "metacontroller.app/apis/metacontroller/v1alpha1" 26 | ) 27 | 28 | // CompositeControllerLister helps list CompositeControllers. 29 | type CompositeControllerLister interface { 30 | // List lists all CompositeControllers in the indexer. 31 | List(selector labels.Selector) (ret []*v1alpha1.CompositeController, err error) 32 | // Get retrieves the CompositeController from the index for a given name. 33 | Get(name string) (*v1alpha1.CompositeController, error) 34 | CompositeControllerListerExpansion 35 | } 36 | 37 | // compositeControllerLister implements the CompositeControllerLister interface. 38 | type compositeControllerLister struct { 39 | indexer cache.Indexer 40 | } 41 | 42 | // NewCompositeControllerLister returns a new CompositeControllerLister. 43 | func NewCompositeControllerLister(indexer cache.Indexer) CompositeControllerLister { 44 | return &compositeControllerLister{indexer: indexer} 45 | } 46 | 47 | // List lists all CompositeControllers in the indexer. 48 | func (s *compositeControllerLister) List(selector labels.Selector) (ret []*v1alpha1.CompositeController, err error) { 49 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 50 | ret = append(ret, m.(*v1alpha1.CompositeController)) 51 | }) 52 | return ret, err 53 | } 54 | 55 | // Get retrieves the CompositeController from the index for a given name. 56 | func (s *compositeControllerLister) Get(name string) (*v1alpha1.CompositeController, error) { 57 | obj, exists, err := s.indexer.GetByKey(name) 58 | if err != nil { 59 | return nil, err 60 | } 61 | if !exists { 62 | return nil, errors.NewNotFound(v1alpha1.Resource("compositecontroller"), name) 63 | } 64 | return obj.(*v1alpha1.CompositeController), nil 65 | } 66 | -------------------------------------------------------------------------------- /client/generated/lister/metacontroller/v1alpha1/controllerrevision.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 20 | 21 | import ( 22 | "k8s.io/apimachinery/pkg/api/errors" 23 | "k8s.io/apimachinery/pkg/labels" 24 | "k8s.io/client-go/tools/cache" 25 | v1alpha1 "metacontroller.app/apis/metacontroller/v1alpha1" 26 | ) 27 | 28 | // ControllerRevisionLister helps list ControllerRevisions. 29 | type ControllerRevisionLister interface { 30 | // List lists all ControllerRevisions in the indexer. 31 | List(selector labels.Selector) (ret []*v1alpha1.ControllerRevision, err error) 32 | // ControllerRevisions returns an object that can list and get ControllerRevisions. 33 | ControllerRevisions(namespace string) ControllerRevisionNamespaceLister 34 | ControllerRevisionListerExpansion 35 | } 36 | 37 | // controllerRevisionLister implements the ControllerRevisionLister interface. 38 | type controllerRevisionLister struct { 39 | indexer cache.Indexer 40 | } 41 | 42 | // NewControllerRevisionLister returns a new ControllerRevisionLister. 43 | func NewControllerRevisionLister(indexer cache.Indexer) ControllerRevisionLister { 44 | return &controllerRevisionLister{indexer: indexer} 45 | } 46 | 47 | // List lists all ControllerRevisions in the indexer. 48 | func (s *controllerRevisionLister) List(selector labels.Selector) (ret []*v1alpha1.ControllerRevision, err error) { 49 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 50 | ret = append(ret, m.(*v1alpha1.ControllerRevision)) 51 | }) 52 | return ret, err 53 | } 54 | 55 | // ControllerRevisions returns an object that can list and get ControllerRevisions. 56 | func (s *controllerRevisionLister) ControllerRevisions(namespace string) ControllerRevisionNamespaceLister { 57 | return controllerRevisionNamespaceLister{indexer: s.indexer, namespace: namespace} 58 | } 59 | 60 | // ControllerRevisionNamespaceLister helps list and get ControllerRevisions. 61 | type ControllerRevisionNamespaceLister interface { 62 | // List lists all ControllerRevisions in the indexer for a given namespace. 63 | List(selector labels.Selector) (ret []*v1alpha1.ControllerRevision, err error) 64 | // Get retrieves the ControllerRevision from the indexer for a given namespace and name. 65 | Get(name string) (*v1alpha1.ControllerRevision, error) 66 | ControllerRevisionNamespaceListerExpansion 67 | } 68 | 69 | // controllerRevisionNamespaceLister implements the ControllerRevisionNamespaceLister 70 | // interface. 71 | type controllerRevisionNamespaceLister struct { 72 | indexer cache.Indexer 73 | namespace string 74 | } 75 | 76 | // List lists all ControllerRevisions in the indexer for a given namespace. 77 | func (s controllerRevisionNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ControllerRevision, err error) { 78 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 79 | ret = append(ret, m.(*v1alpha1.ControllerRevision)) 80 | }) 81 | return ret, err 82 | } 83 | 84 | // Get retrieves the ControllerRevision from the indexer for a given namespace and name. 85 | func (s controllerRevisionNamespaceLister) Get(name string) (*v1alpha1.ControllerRevision, 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(v1alpha1.Resource("controllerrevision"), name) 92 | } 93 | return obj.(*v1alpha1.ControllerRevision), nil 94 | } 95 | -------------------------------------------------------------------------------- /client/generated/lister/metacontroller/v1alpha1/decoratorcontroller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 20 | 21 | import ( 22 | "k8s.io/apimachinery/pkg/api/errors" 23 | "k8s.io/apimachinery/pkg/labels" 24 | "k8s.io/client-go/tools/cache" 25 | v1alpha1 "metacontroller.app/apis/metacontroller/v1alpha1" 26 | ) 27 | 28 | // DecoratorControllerLister helps list DecoratorControllers. 29 | type DecoratorControllerLister interface { 30 | // List lists all DecoratorControllers in the indexer. 31 | List(selector labels.Selector) (ret []*v1alpha1.DecoratorController, err error) 32 | // Get retrieves the DecoratorController from the index for a given name. 33 | Get(name string) (*v1alpha1.DecoratorController, error) 34 | DecoratorControllerListerExpansion 35 | } 36 | 37 | // decoratorControllerLister implements the DecoratorControllerLister interface. 38 | type decoratorControllerLister struct { 39 | indexer cache.Indexer 40 | } 41 | 42 | // NewDecoratorControllerLister returns a new DecoratorControllerLister. 43 | func NewDecoratorControllerLister(indexer cache.Indexer) DecoratorControllerLister { 44 | return &decoratorControllerLister{indexer: indexer} 45 | } 46 | 47 | // List lists all DecoratorControllers in the indexer. 48 | func (s *decoratorControllerLister) List(selector labels.Selector) (ret []*v1alpha1.DecoratorController, err error) { 49 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 50 | ret = append(ret, m.(*v1alpha1.DecoratorController)) 51 | }) 52 | return ret, err 53 | } 54 | 55 | // Get retrieves the DecoratorController from the index for a given name. 56 | func (s *decoratorControllerLister) Get(name string) (*v1alpha1.DecoratorController, error) { 57 | obj, exists, err := s.indexer.GetByKey(name) 58 | if err != nil { 59 | return nil, err 60 | } 61 | if !exists { 62 | return nil, errors.NewNotFound(v1alpha1.Resource("decoratorcontroller"), name) 63 | } 64 | return obj.(*v1alpha1.DecoratorController), nil 65 | } 66 | -------------------------------------------------------------------------------- /client/generated/lister/metacontroller/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 v1alpha1 20 | 21 | // CompositeControllerListerExpansion allows custom methods to be added to 22 | // CompositeControllerLister. 23 | type CompositeControllerListerExpansion interface{} 24 | 25 | // ControllerRevisionListerExpansion allows custom methods to be added to 26 | // ControllerRevisionLister. 27 | type ControllerRevisionListerExpansion interface{} 28 | 29 | // ControllerRevisionNamespaceListerExpansion allows custom methods to be added to 30 | // ControllerRevisionNamespaceLister. 31 | type ControllerRevisionNamespaceListerExpansion interface{} 32 | 33 | // DecoratorControllerListerExpansion allows custom methods to be added to 34 | // DecoratorControllerLister. 35 | type DecoratorControllerListerExpansion interface{} 36 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /controller/common/finalizer/finalizer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package finalizer 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 | 23 | dynamicclientset "metacontroller.app/dynamic/clientset" 24 | dynamicobject "metacontroller.app/dynamic/object" 25 | ) 26 | 27 | // Manager encapsulates controller logic for dealing with finalizers. 28 | type Manager struct { 29 | Name string 30 | Enabled bool 31 | } 32 | 33 | // SyncObject adds or removes the finalizer on the given object as necessary. 34 | func (m *Manager) SyncObject(client *dynamicclientset.ResourceClient, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { 35 | // If the cached object passed in is already in the right state, 36 | // we'll assume we don't need to check the live object. 37 | if dynamicobject.HasFinalizer(obj, m.Name) == m.Enabled { 38 | return obj, nil 39 | } 40 | // Otherwise, we may need to update the object. 41 | if m.Enabled { 42 | // If the object is already pending deletion, we don't add the finalizer. 43 | // We might have already removed it. 44 | if obj.GetDeletionTimestamp() != nil { 45 | return obj, nil 46 | } 47 | return client.Namespace(obj.GetNamespace()).AddFinalizer(obj, m.Name) 48 | } else { 49 | return client.Namespace(obj.GetNamespace()).RemoveFinalizer(obj, m.Name) 50 | } 51 | } 52 | 53 | // ShouldFinalize returns true if the controller should take action to manage 54 | // children even though the parent is pending deletion (i.e. finalize). 55 | func (m *Manager) ShouldFinalize(parent metav1.Object) bool { 56 | // There's no point managing children if the parent has a GC finalizer, 57 | // because we'd be fighting the GC. 58 | if hasGCFinalizer(parent) { 59 | return false 60 | } 61 | // If we already removed the finalizer, don't try to manage children anymore. 62 | if !dynamicobject.HasFinalizer(parent, m.Name) { 63 | return false 64 | } 65 | return m.Enabled 66 | } 67 | 68 | // hasGCFinalizer returns true if obj has any GC finalizer. 69 | // In other words, true means the GC will start messing with its children, 70 | // either deleting or orphaning them. 71 | func hasGCFinalizer(obj metav1.Object) bool { 72 | for _, item := range obj.GetFinalizers() { 73 | switch item { 74 | case metav1.FinalizerDeleteDependents, metav1.FinalizerOrphanDependents: 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | -------------------------------------------------------------------------------- /controller/common/manage_children_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/util/diff" 9 | "k8s.io/apimachinery/pkg/util/json" 10 | ) 11 | 12 | func TestRevertObjectMetaSystemFields(t *testing.T) { 13 | origJSON := `{ 14 | "metadata": { 15 | "origMeta": "should stay gone", 16 | "otherMeta": "should change value", 17 | "creationTimestamp": "should restore orig value", 18 | "deletionTimestamp": "should restore orig value", 19 | "uid": "should bring back removed value" 20 | }, 21 | "other": "should change value" 22 | }` 23 | newObjJSON := `{ 24 | "metadata": { 25 | "creationTimestamp": null, 26 | "deletionTimestamp": "new value", 27 | "newMeta": "new value", 28 | "otherMeta": "new value", 29 | "selfLink": "should be removed" 30 | }, 31 | "other": "new value" 32 | }` 33 | wantJSON := `{ 34 | "metadata": { 35 | "otherMeta": "new value", 36 | "newMeta": "new value", 37 | "creationTimestamp": "should restore orig value", 38 | "deletionTimestamp": "should restore orig value", 39 | "uid": "should bring back removed value" 40 | }, 41 | "other": "new value" 42 | }` 43 | 44 | orig := make(map[string]interface{}) 45 | if err := json.Unmarshal([]byte(origJSON), &orig); err != nil { 46 | t.Fatalf("can't unmarshal orig: %v", err) 47 | } 48 | newObj := make(map[string]interface{}) 49 | if err := json.Unmarshal([]byte(newObjJSON), &newObj); err != nil { 50 | t.Fatalf("can't unmarshal newObj: %v", err) 51 | } 52 | want := make(map[string]interface{}) 53 | if err := json.Unmarshal([]byte(wantJSON), &want); err != nil { 54 | t.Fatalf("can't unmarshal want: %v", err) 55 | } 56 | 57 | err := revertObjectMetaSystemFields(&unstructured.Unstructured{Object: newObj}, &unstructured.Unstructured{Object: orig}) 58 | if err != nil { 59 | t.Fatalf("revertObjectMetaSystemFields error: %v", err) 60 | } 61 | 62 | if got := newObj; !reflect.DeepEqual(got, want) { 63 | t.Logf("reflect diff: a=got, b=want:\n%s", diff.ObjectReflectDiff(got, want)) 64 | t.Fatalf("revertObjectMetaSystemFields() = %#v, want %#v", got, want) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /controller/composite/hooks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package composite 18 | 19 | import ( 20 | "fmt" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | 24 | "metacontroller.app/apis/metacontroller/v1alpha1" 25 | "metacontroller.app/controller/common" 26 | "metacontroller.app/hooks" 27 | ) 28 | 29 | // SyncHookRequest is the object sent as JSON to the sync hook. 30 | type SyncHookRequest struct { 31 | Controller *v1alpha1.CompositeController `json:"controller"` 32 | Parent *unstructured.Unstructured `json:"parent"` 33 | Children common.ChildMap `json:"children"` 34 | Finalizing bool `json:"finalizing"` 35 | } 36 | 37 | // SyncHookResponse is the expected format of the JSON response from the sync hook. 38 | type SyncHookResponse struct { 39 | Status map[string]interface{} `json:"status"` 40 | Children []*unstructured.Unstructured `json:"children"` 41 | 42 | ResyncAfterSeconds float64 `json:"resyncAfterSeconds"` 43 | 44 | // Finalized is only used by the finalize hook. 45 | Finalized bool `json:"finalized"` 46 | } 47 | 48 | func callSyncHook(cc *v1alpha1.CompositeController, request *SyncHookRequest) (*SyncHookResponse, error) { 49 | if cc.Spec.Hooks == nil { 50 | return nil, fmt.Errorf("no hooks defined") 51 | } 52 | 53 | var response SyncHookResponse 54 | 55 | // First check if we should instead call the finalize hook, 56 | // which has the same API as the sync hook except that it's 57 | // called while the object is pending deletion. 58 | if request.Parent.GetDeletionTimestamp() != nil && cc.Spec.Hooks.Finalize != nil { 59 | // Finalize 60 | request.Finalizing = true 61 | if err := hooks.Call(cc.Spec.Hooks.Finalize, request, &response); err != nil { 62 | return nil, fmt.Errorf("finalize hook failed: %v", err) 63 | } 64 | } else { 65 | // Sync 66 | request.Finalizing = false 67 | if cc.Spec.Hooks.Sync == nil { 68 | return nil, fmt.Errorf("sync hook not defined") 69 | } 70 | 71 | if err := hooks.Call(cc.Spec.Hooks.Sync, request, &response); err != nil { 72 | return nil, fmt.Errorf("sync hook failed: %v", err) 73 | } 74 | } 75 | 76 | return &response, nil 77 | } 78 | -------------------------------------------------------------------------------- /controller/decorator/hooks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package decorator 18 | 19 | import ( 20 | "fmt" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | 24 | "metacontroller.app/apis/metacontroller/v1alpha1" 25 | "metacontroller.app/controller/common" 26 | "metacontroller.app/hooks" 27 | ) 28 | 29 | // SyncHookRequest is the object sent as JSON to the sync hook. 30 | type SyncHookRequest struct { 31 | Controller *v1alpha1.DecoratorController `json:"controller"` 32 | Object *unstructured.Unstructured `json:"object"` 33 | Attachments common.ChildMap `json:"attachments"` 34 | Finalizing bool `json:"finalizing"` 35 | } 36 | 37 | // SyncHookResponse is the expected format of the JSON response from the sync hook. 38 | type SyncHookResponse struct { 39 | Labels map[string]*string `json:"labels"` 40 | Annotations map[string]*string `json:"annotations"` 41 | Status map[string]interface{} `json:"status"` 42 | Attachments []*unstructured.Unstructured `json:"attachments"` 43 | 44 | ResyncAfterSeconds float64 `json:"resyncAfterSeconds"` 45 | 46 | // Finalized is only used by the finalize hook. 47 | Finalized bool `json:"finalized"` 48 | } 49 | 50 | func (c *decoratorController) callSyncHook(request *SyncHookRequest) (*SyncHookResponse, error) { 51 | if c.dc.Spec.Hooks == nil { 52 | return nil, fmt.Errorf("no hooks defined") 53 | } 54 | 55 | var response SyncHookResponse 56 | 57 | // First check if we should instead call the finalize hook, 58 | // which has the same API as the sync hook except that it's 59 | // called while the object is pending deletion. 60 | // 61 | // In addition to finalizing when the object is deleted, we also finalize 62 | // when the object no longer matches our decorator selector. 63 | // This allows the decorator to clean up after itself if the object has been 64 | // updated to disable the functionality added by the decorator. 65 | if c.dc.Spec.Hooks.Finalize != nil && 66 | (request.Object.GetDeletionTimestamp() != nil || !c.parentSelector.Matches(request.Object)) { 67 | // Finalize 68 | request.Finalizing = true 69 | if err := hooks.Call(c.dc.Spec.Hooks.Finalize, request, &response); err != nil { 70 | return nil, fmt.Errorf("finalize hook failed: %v", err) 71 | } 72 | } else { 73 | // Sync 74 | request.Finalizing = false 75 | if c.dc.Spec.Hooks.Sync == nil { 76 | return nil, fmt.Errorf("sync hook not defined") 77 | } 78 | 79 | if err := hooks.Call(c.dc.Spec.Hooks.Sync, request, &response); err != nil { 80 | return nil, fmt.Errorf("sync hook failed: %v", err) 81 | } 82 | } 83 | 84 | return &response, nil 85 | } 86 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | 18 | 19 |
20 |

404

21 | 22 |

Page not found :(

23 |

The requested page could not be found.

24 |
25 | -------------------------------------------------------------------------------- /docs/_api/controllerrevision.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ControllerRevision 3 | classes: wide 4 | --- 5 | ControllerRevision is an internal API used by Metacontroller to implement 6 | declarative rolling updates. 7 | 8 | Users of Metacontroller normally shouldn't need to know about this API, 9 | but it is documented here for Metacontroller [contributors](/contrib/), 10 | as well as for [troubleshooting](/guide/troubleshooting/). 11 | 12 | Note that this is different from the ControllerRevision in `apps/v1`, 13 | although it serves a similar purpose. 14 | You will likely need to use a fully-qualified resource name to inspect 15 | Metacontroller's ControllerRevisions: 16 | 17 | ```sh 18 | kubectl get controllerrevisions.metacontroller.k8s.io 19 | ``` 20 | 21 | Each ControllerRevision's name is a combination of the name and API group 22 | (excluding the version suffix) of the resource that it's a revision of, 23 | as well as a hash that is deterministic yet unique (used only for idempotent 24 | creation, not for lookup). 25 | 26 | By default, ControllerRevisions belonging to a particular parent instance 27 | will get garbage-collected if the parent is deleted. 28 | However, it is possible to orphan ControllerRevisions during parent 29 | deletion, and then create a replacement parent to adopt them. 30 | ControllerRevisions are adopted based on the parent's label selector, 31 | the same way controllers like ReplicaSet adopt Pods. 32 | 33 | ## Example 34 | 35 | ```yaml 36 | apiVersion: metacontroller.k8s.io/v1alpha1 37 | kind: ControllerRevision 38 | metadata: 39 | name: catsets.ctl.enisoc.com-5463ba99b804a121d35d14a5ab74546d1e8ba953 40 | labels: 41 | app: nginx 42 | component: backend 43 | metacontroller.k8s.io/apiGroup: ctl.enisoc.com 44 | metacontroller.k8s.io/resource: catsets 45 | parentPatch: 46 | spec: 47 | template: 48 | [...] 49 | children: 50 | - apiGroup: "" 51 | kind: Pod 52 | names: 53 | - nginx-backend-0 54 | - nginx-backend-1 55 | - nginx-backend-2 56 | ``` 57 | 58 | ## Parent Patch 59 | 60 | The `parentPatch` field stores a partial representation of the parent object 61 | at a given revision, containing only those fields listed by the lambda controller 62 | author as participating in rolling updates. 63 | 64 | For example, if a CompositeController's [revision history][] specifies 65 | a `fieldPaths` list of `["spec.template"]`, the parent patch will contain 66 | only `spec.template` and any subfields nested within it. 67 | 68 | This mirrors the selective behavior of rolling updates in built-in APIs 69 | like Deployment and StatefulSet. 70 | Any fields that aren't part of the parent patch take effect immediately, 71 | rather than rolling out gradually. 72 | 73 | [revision history]: /api/compositecontroller/#revision-history 74 | 75 | ## Children 76 | 77 | The `children` field stores a list of child objects that "belong" to this 78 | particular revision of the parent. 79 | 80 | This is how Metacontroller keeps track of the current desired revision of 81 | a given child. 82 | For example, if a Pod that hasn't been updated yet gets deleted by a Node 83 | drain, it should be replaced at the revision it was on before it got deleted, 84 | not at the latest revision. 85 | 86 | When Metacontroller decides it's time to update a given child to another 87 | revision, it first records this intention by updating the relevant 88 | ControllerRevision objects. 89 | After committing these records, it then begins updating that child according 90 | to the configured [child update strategy](/api/compositecontroller/#child-update-strategy). 91 | This ensures that the intermediate progress of the rollout is persisted 92 | in the API server so it survives process restarts. 93 | 94 | Children are grouped by API Group (excluding the version suffix) and Kind. 95 | For each Group-Kind, we store a list of object names. 96 | Note that parent and children must be in the same namespace, 97 | and ControllerRevisions for a given parent also live in that 98 | parent's namespace. -------------------------------------------------------------------------------- /docs/_api/hook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hook 3 | classes: wide 4 | --- 5 | This page describes how hook targets are defined in various APIs. 6 | 7 | Each hook that you define as part of using one of the hook-based APIs 8 | has the following fields: 9 | 10 | | Field | Description | 11 | | ----- | ----------- | 12 | | [webhook](#webhook) | Specify how to invoke this hook over HTTP(S). | 13 | 14 | ## Example 15 | 16 | ```yaml 17 | webhook: 18 | url: http://my-controller-svc/sync 19 | ``` 20 | 21 | ## Webhook 22 | 23 | Each Webhook has the following fields: 24 | 25 | | Field | Description | 26 | | ----- | ----------- | 27 | | url | A full URL for the webhook (e.g. `http://my-controller-svc/hook`). If present, this overrides any values provided for `path` and `service`. | 28 | | timeout | A duration (in the format of Go's time.Duration) indicating the time that Metacontroller should wait for a response. If the webhook takes longer than this time, the webhook call is aborted and retried later. Defaults to 10s. | 29 | | path | A path to be appended to the accompanying `service` to reach this hook (e.g. `/hook`). Ignored if full `url` is specified. | 30 | | [service](#service-reference) | A reference to a Kubernetes Service through which this hook can be reached. | 31 | 32 | ### Service Reference 33 | 34 | Within a `webhook`, the `service` field has the following subfields: 35 | 36 | | Field | Description | 37 | | ----- | ----------- | 38 | | name | The `metadata.name` of the target Service. | 39 | | namespace | The `metadata.namespace` of the target Service. | 40 | | port | The port number to connect to on the target Service. Defaults to `80`. | 41 | | protocol | The protocol to use for the target Service. Defaults to `http`. | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | 11 | # Site settings 12 | # These are used to personalize your new site. If you look in the HTML files, 13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 14 | # You can create any custom variable you would like, and they will be accessible 15 | # in the templates via {{ site.myvariable }}. 16 | title: Metacontroller 17 | title_separator: "-" 18 | locale: en-US 19 | encoding: utf-8 20 | description: Lightweight Kubernetes controllers as a service 21 | baseurl: "" # the subpath of your site, e.g. /blog 22 | url: "" # the base hostname & protocol for your site, e.g. http://example.com 23 | repository: GoogleCloudPlatform/metacontroller 24 | repo_url: https://github.com/GoogleCloudPlatform/metacontroller 25 | repo_dir: https://github.com/GoogleCloudPlatform/metacontroller/tree/master 26 | repo_file: https://github.com/GoogleCloudPlatform/metacontroller/blob/master 27 | repo_raw: https://raw.githubusercontent.com/GoogleCloudPlatform/metacontroller/master 28 | 29 | breadcrumbs: true 30 | analytics: 31 | provider: google 32 | google: 33 | tracking_id: UA-136881871-1 34 | 35 | # Build settings 36 | markdown: kramdown 37 | highlighter: rouge 38 | theme: minimal-mistakes-jekyll 39 | plugins: 40 | - jekyll-paginate 41 | - jekyll-sitemap 42 | - jekyll-gist 43 | - jekyll-feed 44 | sass: 45 | sass_dir: _sass 46 | style: compressed 47 | timezone: America/Los_Angeles 48 | 49 | collections: 50 | api: 51 | output: true 52 | permalink: /:collection/:path/ 53 | guide: 54 | output: true 55 | permalink: /:collection/:path/ 56 | contrib: 57 | output: true 58 | permalink: /:collection/:path/ 59 | design: 60 | output: true 61 | permalink: /:collection/:path/ 62 | 63 | defaults: 64 | - scope: 65 | path: "" 66 | values: 67 | layout: single 68 | toc: true 69 | sidebar: 70 | nav: docs 71 | 72 | include: 73 | - "_redirects" 74 | -------------------------------------------------------------------------------- /docs/_data/navigation.yml: -------------------------------------------------------------------------------- 1 | main: 2 | - title: GitHub 3 | url: https://github.com/GoogleCloudPlatform/metacontroller 4 | - title: Slack 5 | url: https://kubernetes.slack.com/messages/metacontroller/ 6 | - title: Forum 7 | url: https://groups.google.com/forum/#!forum/metacontroller 8 | - title: Announcements 9 | url: https://groups.google.com/forum/#!forum/metacontroller-announce 10 | 11 | docs: 12 | - title: Getting Started 13 | url: / 14 | children: 15 | - title: Introduction 16 | url: / 17 | - title: Examples 18 | url: /examples/ 19 | - title: Concepts 20 | url: /concepts/ 21 | - title: Features 22 | url: /features/ 23 | - title: FAQ 24 | url: /faq/ 25 | - title: User Guide 26 | url: /guide/ 27 | children: 28 | - title: Install Metacontroller 29 | url: /guide/install/ 30 | - title: Create a Controller 31 | url: /guide/create/ 32 | - title: Best Practices 33 | url: /guide/best-practices/ 34 | - title: Troubleshooting 35 | url: /guide/troubleshooting/ 36 | - title: API Reference 37 | url: /api/ 38 | children: 39 | - title: CompositeController 40 | url: /api/compositecontroller/ 41 | - title: DecoratorController 42 | url: /api/decoratorcontroller/ 43 | - title: Contributing 44 | url: /contrib/ 45 | -------------------------------------------------------------------------------- /docs/_design/customize-hook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Customize Hook 3 | --- 4 | ### Controller changes 5 | 6 | The composite and decorator controller `sync` and `finalize` hooks will contain a new field, `related`. 7 | 8 | This field is in the same format as `children` / `ChildMap`. 9 | 10 | ### Customize Hook 11 | 12 | If the `customize` hook is defined, Metacontroller will ask for which related 13 | objects, or classes of objects that your `sync` and `finalize` hooks need to 14 | know about. 15 | 16 | This is useful for mapping across many objects. One example would be a 17 | controller that lets you specify ConfigMaps to be placed in every Namespace. 18 | 19 | Another use-case is being able to reference other objects, e.g. the `env` 20 | section from a core `Pod` object. 21 | 22 | If you don't define a `customize` hook, then the related section of the hooks will 23 | be empty. 24 | 25 | The `customize` hook will not provide any information about the current state of 26 | the cluster. Thus, the set of related objects may only depend on the state of 27 | the parent object. 28 | 29 | This hook may also accept other fields in future, for other customizations. 30 | 31 | #### Customize Hook Request 32 | 33 | A separate request will be sent for each parent object, 34 | so your hook only needs to think about one parent at a time. 35 | 36 | The body of the request (a POST in the case of a [webhook][]) 37 | will be a JSON object with the following fields: 38 | 39 | | Field | Description | 40 | | ----- | ----------- | 41 | | `controller` | The whole CompositeController object, like what you might get from `kubectl get compositecontroller -o json`. | 42 | | `parent` | The parent object, like what you might get from `kubectl get -o json`. | 43 | 44 | #### Customize Hook Response 45 | 46 | The body of your response should be a JSON object with the following fields: 47 | 48 | | Field | Description | 49 | | ----- | ----------- | 50 | | `relatedResources` | A list of JSON objects representing all the desired related resource label selectors. | 51 | 52 | The `relatedResources` field should contain a flat list of objects, 53 | not an associative array. 54 | 55 | Each resource rule object should be a JSON object with the following fields: 56 | | Field | Description | 57 | | ----- | ----------- | 58 | | `apiVersion` | The API `/` of the parent resource, or just `` for core APIs. (e.g. `v1`, `apps/v1`, `batch/v1`) | 59 | | `resource` | The canonical, lowercase, plural name of the parent resource. (e.g. `deployments`, `replicasets`, `statefulsets`) | 60 | | `labelSelector` | A `v1.LabelSelector` object. | 61 | | `namespace` | Optional. The Namespace to select in | 62 | | `names` | Optional. A list of strings, representing individual objects to return | 63 | 64 | If the parent resource is cluster scoped and the related resource is namespaced, 65 | the namespace may be used to restrict which objects to look at. If the parent 66 | resource is namespaced, the related resources must come from the same namespace. 67 | Specifying the namespace is optional, but if specified must match. 68 | 69 | Note that your webhook handler must return a response with a status code of `200` 70 | to be considered successful. Metacontroller will wait for a response for up to the 71 | amount defined in the [Webhook spec](/api/hook/#webhook). 72 | -------------------------------------------------------------------------------- /docs/_guide/best-practices.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Best Practices 3 | --- 4 | This is a collection of recommendations for writing controllers with Metacontroller. 5 | 6 | If you have something to add to the collection, please send a pull request against 7 | [this document]({{ site.repo_file }}/docs/_guide/best-practices.md). 8 | 9 | ## Lambda Hooks 10 | 11 | ### Apply Semantics 12 | 13 | Because Metacontroller uses [apply semantics](/api/apply/), you don't have to 14 | think about whether a given object needs to be created (because it doesn't exist) 15 | or patched (because it exists and some fields don't match your desired state). 16 | In either case, you should generate a fresh object from scratch with only the 17 | fields you care about filled in. 18 | 19 | For example, suppose you create an object like this: 20 | 21 | ```yaml 22 | apiVersion: example.com/v1 23 | kind: Foo 24 | metadata: 25 | name: my-foo 26 | spec: 27 | importantField: 1 28 | ``` 29 | 30 | Then later you decide to change the value of `importantField` to 2. 31 | 32 | Since Kubernetes API objects can be edited by the API server, users, and other 33 | controllers to collaboratively produce emergent behavior, the object you observe 34 | might now look like this: 35 | 36 | ```yaml 37 | apiVersion: example.com/v1 38 | kind: Foo 39 | metadata: 40 | name: my-foo 41 | stuffFilledByAPIServer: blah 42 | spec: 43 | importantField: 1 44 | otherField: 5 45 | ``` 46 | 47 | To avoid overwriting the parts of the object you don't care about, you would 48 | ordinarily need to either build a patch or use a retry loop to send 49 | concurrency-safe updates. 50 | With apply semantics, you instead just call your "generate object" function 51 | again with the new values you want, and return this (as JSON): 52 | 53 | ```yaml 54 | apiVersion: example.com/v1 55 | kind: Foo 56 | metadata: 57 | name: my-foo 58 | spec: 59 | importantField: 2 60 | ``` 61 | 62 | Metacontroller will take care of merging your change to `importantField` while 63 | preserving the fields you don't care about that were set by others. 64 | 65 | ### Side Effects 66 | 67 | Your hook code should generally be free of side effects whenever possible. 68 | Ideally, you should interpret a call to your hook as asking, 69 | "Hypothetically, if the observed state of the world were like this, what would 70 | your desired state be?" 71 | 72 | In particular, Metacontroller may ask you about such hypothetical scenarios 73 | during rolling updates, when your object is undergoing a slow transition between 74 | two desired states. 75 | If your hook has to produce side effects to work, you should avoid enabling 76 | rolling updates on that controller. 77 | 78 | ### Status 79 | 80 | If your object uses the Spec/Status convention, keep in mind that the Status 81 | returned from your hook should ideally reflect a judgement on only the observed 82 | objects that were sent to you. 83 | The Status you compute should not yet account for your desired state, because 84 | the actual state of the world may not match what you want yet. 85 | 86 | For example, if you observe 2 Pods, but you return a desired list of 3 Pods, 87 | you should return a Status that reflects only the observed Pods 88 | (e.g. `replicas: 2`). 89 | This is important so that Status reflects present reality, not future desires. 90 | -------------------------------------------------------------------------------- /docs/_guide/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | classes: wide 4 | toc: false 5 | --- 6 | This page describes how to install Metacontroller, either to develop your own 7 | controllers or just to run third-party controllers that depend on it. 8 | 9 | ## Prerequisites 10 | 11 | * Kubernetes v1.9+ 12 | * You should have `kubectl` available and configured to talk to the desired cluster. 13 | 14 | ### Grant yourself cluster-admin (GKE only) 15 | 16 | Due to a [known issue](https://cloud.google.com/container-engine/docs/role-based-access-control#defining_permissions_in_a_role) 17 | in GKE, you'll need to first grant yourself `cluster-admin` privileges before 18 | you can install the necessary RBAC manifests. 19 | 20 | ```sh 21 | kubectl create clusterrolebinding -cluster-admin-binding --clusterrole=cluster-admin --user=@ 22 | ``` 23 | 24 | Replace `` and `` above based on the account you use to authenticate to GKE. 25 | 26 | ## Install Metacontroller 27 | 28 | ```sh 29 | # Create metacontroller namespace. 30 | kubectl create namespace metacontroller 31 | # Create metacontroller service account and role/binding. 32 | kubectl apply -f {{ site.repo_raw }}/manifests/metacontroller-rbac.yaml 33 | # Create CRDs for Metacontroller APIs, and the Metacontroller StatefulSet. 34 | kubectl apply -f {{ site.repo_raw }}/manifests/metacontroller.yaml 35 | ``` 36 | 37 | If you prefer to build and host your own images, please see the 38 | [build instructions](/contrib/build/) in the contributor guide. 39 | 40 | ## Configuration 41 | 42 | The Metacontroller server has a few settings that can be configured 43 | with command-line flags (by editing the Metacontroller StatefulSet 44 | in `manifests/metacontroller.yaml`): 45 | 46 | | Flag | Description | 47 | | ---- | ----------- | 48 | | `-v` | Set the logging verbosity level (e.g. `-v=4`). Level 4 logs Metacontroller's interaction with the API server. Levels 5 and up additionally log details of Metacontroller's invocation of lambda hooks. See the [troubleshooting guide](/guide/troubleshooting/) for more. | 49 | | `--discovery-interval` | How often to refresh discovery cache to pick up newly-installed resources (e.g. `--discovery-interval=10s`). | 50 | | `--cache-flush-interval` | How often to flush local caches and relist objects from the API server (e.g. `--cache-flush-interval=30m`). | 51 | -------------------------------------------------------------------------------- /docs/_guide/troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Troubleshooting 3 | --- 4 | This is a collection of tips for debugging controllers written with Metacontroller. 5 | 6 | If you have something to add to the collection, please send a pull request against 7 | [this document]({{ site.repo_file }}/docs/guide/troubleshooting.md). 8 | 9 | ## Metacontroller Logs 10 | 11 | Until Metacontroller [emits events]({{ site.repo_url }}//issues/7), 12 | the first place to look when troubleshooting controller behavior is the logs for 13 | the Metacontroller server itself. 14 | 15 | For example, you can fetch the last 25 lines with a command like this: 16 | 17 | ```sh 18 | kubectl -n metacontroller logs --tail=25 -l app=metacontroller 19 | ``` 20 | 21 | ### Log Levels 22 | 23 | You can customize the verbosity of the Metacontroller server's logs with the 24 | `-v=N` flag, where `N` is the log level. 25 | 26 | At all log levels, Metacontroller will log the progress of server startup and 27 | shutdown, as well as major changes like starting and stopping hosted controllers. 28 | 29 | At level 4 and above, Metacontroller will log actions (like create/update/delete) 30 | on individual objects (like Pods) that it takes on behalf of hosted controllers. 31 | It will also log when it decides to sync a given controller as well as events 32 | that may trigger a sync. 33 | 34 | At level 5 and above, Metacontroller will log the diffs between existing objects 35 | and the desired state of those objects returned by controller hooks. 36 | 37 | At level 6 and above, Metacontroller will log every hook invocation as well as 38 | the JSON request and response bodies. 39 | 40 | ### Common Log Messages 41 | 42 | Since API discovery info is refreshed periodically, you may see log messages 43 | like this when you start a controller that depends on a recently-installed CRD: 44 | 45 | ``` 46 | failed to sync CompositeController "my-controller": discovery: can't find resource in apiVersion / 47 | ``` 48 | 49 | Usually, this should fix itself within about 30s when the new CRD is discovered. 50 | If this message continues indefinitely, check that the resource name and API 51 | group/version are correct. 52 | 53 | You may also notice periodic log messages like this: 54 | 55 | ``` 56 | Watch close - *unstructured.Unstructured total items received 57 | ``` 58 | 59 | This comes from the underlying client-go library, and just indicates when the 60 | shared caches are periodically flushed to place an upper bound on cache 61 | inconsistency due to potential silent failures in long-running watches. 62 | 63 | ## Webhook Logs 64 | 65 | If you return an HTTP error code (e.g. 500) from your webhook, 66 | the Metacontroller server will log the text of the response body. 67 | 68 | If you need more detail on what's happening inside your hook code, as opposed to 69 | what Metacontroller does for you, you'll need to add log statements to your own 70 | code and inspect the logs on your webhook server. 71 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_redirects: -------------------------------------------------------------------------------- 1 | https://metacontroller.netlify.com/* https://metacontroller.app/:splat 301! 2 | /* go-get=1 /go-import.html 200! 3 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Reference 3 | layout: collection 4 | collection: api 5 | sort_by: title 6 | permalink: /api/ 7 | classes: wide 8 | --- 9 | This section contains detailed reference information for the APIs offered by Metacontroller. 10 | 11 | See the [user guide](/guide/) for introductions and step-by-step walkthroughs. -------------------------------------------------------------------------------- /docs/assets/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # Only the main Sass file needs front matter (the dashes are enough) 3 | --- 4 | 5 | @charset "utf-8"; 6 | 7 | @import "minimal-mistakes/skins/{{ site.minimal_mistakes_skin | default: 'default' }}"; // skin 8 | @import "minimal-mistakes"; // main partials 9 | 10 | .page__content p, 11 | .page__content li, 12 | .archive p { 13 | font-size: .8em; 14 | } 15 | 16 | .masthead__menu-item { 17 | font-size: 20px; 18 | } 19 | 20 | .nav-tag { 21 | font-size: 80%; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | } 27 | 28 | .page__content code { 29 | white-space: pre; 30 | } -------------------------------------------------------------------------------- /docs/contrib.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributor Guide 3 | layout: collection 4 | collection: contrib 5 | sort_by: title 6 | permalink: /contrib/ 7 | classes: wide 8 | --- 9 | This section contains information for people who want to hack on or 10 | contribute to Metacontroller. 11 | 12 | See the [User Guide](/guide/) if you just want to use Metacontroller. 13 | 14 | ## GitHub 15 | 16 | * [Issues]({{ site.repo_url }}/issues) 17 | * [Project Boards]({{ site.repo_url }}/projects) 18 | * [Roadmap]({{ site.repo_url }}/issues/9) 19 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Design Docs 3 | layout: collection 4 | collection: design 5 | sort_by: title 6 | permalink: /design/ 7 | --- 8 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | permalink: /examples/ 4 | --- 5 | This page lists some examples of what you can make with Metacontroller. 6 | 7 | If you'd like to add a link to another example that demonstrates a new 8 | language or technique, please send a pull request against 9 | [this document]({{ site.repo_file }}/docs/examples.md). 10 | 11 | ## CompositeController 12 | 13 | [CompositeController](/api/compositecontroller/) 14 | is an API provided by Metacontroller, designed to facilitate 15 | custom controllers whose primary purpose is to manage a set of child objects 16 | based on the desired state specified in a parent object. 17 | Workload controllers like Deployment and StatefulSet are examples of existing 18 | controllers that fit this pattern. 19 | 20 | ### CatSet (JavaScript) 21 | 22 | [CatSet]({{ site.repo_dir }}/examples/catset) is a rewrite of 23 | [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/), 24 | including rolling updates, as a CompositeController. 25 | It shows that existing workload controllers already use a pattern that could 26 | fit within a CompositeController, namely managing child objects based on a 27 | parent spec. 28 | 29 | ### BlueGreenDeployment (JavaScript) 30 | 31 | [BlueGreenDeployment]({{ site.repo_dir }}/examples/bluegreen) 32 | is an alternative to [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) 33 | that implements a [Blue-Green](https://martinfowler.com/bliki/BlueGreenDeployment.html) 34 | rollout strategy. 35 | It shows how CompositeController can be used to add various automation on top 36 | of built-in APIs like ReplicaSet. 37 | 38 | ### IndexedJob (Python) 39 | 40 | [IndexedJob]({{ site.repo_dir }}/examples/indexedjob) 41 | is an alternative to [Job](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/) 42 | that gives each Pod a unique index, like StatefulSet. 43 | It shows how to write a CompositeController in Python, and also demonstrates 44 | [selector generation](/api/compositecontroller/#selector-generation). 45 | 46 | ### Vitess Operator (Jsonnet) 47 | 48 | The [Vitess Operator]({{ site.repo_dir }}/examples/vitess) 49 | is an example of using Metacontroller to write an Operator for a complex 50 | stateful application, in this case [Vitess](https://vitess.io). 51 | It shows how CompositeController can be layered to handle complex systems 52 | by breaking them down. 53 | 54 | ## DecoratorController 55 | 56 | [DecoratorController](/api/decoratorcontroller/) 57 | is an API provided by Metacontroller, designed to facilitate 58 | adding new behavior to existing resources. You can define rules for which 59 | resources to watch, as well as filters on labels and annotations. 60 | 61 | For each object you watch, you can add, edit, or remove labels and annotations, 62 | as well as create new objects and attach them. Unlike CompositeController, 63 | these new objects don't have to match the main object's label selector. 64 | Since they're attached to the main object, they'll be cleaned up automatically 65 | when the main object is deleted. 66 | 67 | ### Service Per Pod (Jsonnet) 68 | 69 | [Service Per Pod]({{ site.repo_dir }}/examples/service-per-pod) 70 | is an example DecoratorController that creates an individual Service for 71 | every Pod in a StatefulSet (e.g. to give them static IPs), effectively adding 72 | new behavior to StatefulSet without having to reimplement it. -------------------------------------------------------------------------------- /docs/go-import.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: User Guide 3 | layout: collection 4 | collection: guide 5 | sort_by: title 6 | permalink: /guide/ 7 | classes: wide 8 | --- 9 | This section contains general tips and step-by-step tutorials for using Metacontroller. 10 | 11 | See the [API Reference](/api/) for details about all the available options. 12 | -------------------------------------------------------------------------------- /docs/pronunciation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to pronounce Metacontroller 3 | permalink: /pronunciation/ 4 | --- 5 | *Metacontroller* is pronounced as *me-ta-con-trol-ler*. -------------------------------------------------------------------------------- /dynamic/controllerref/controller_ref.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllerref 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/types" 22 | ) 23 | 24 | func addOwnerReference(in []metav1.OwnerReference, add metav1.OwnerReference) []metav1.OwnerReference { 25 | out := make([]metav1.OwnerReference, 0, len(in)+1) 26 | found := false 27 | for _, ref := range in { 28 | if ref.UID == add.UID { 29 | // We already own this. Update other fields as needed. 30 | out = append(out, add) 31 | found = true 32 | continue 33 | } 34 | out = append(out, ref) 35 | } 36 | if !found { 37 | // Add ourselves to the list. 38 | // Note that server-side validation is responsible for ensuring only one ControllerRef. 39 | out = append(out, add) 40 | } 41 | return out 42 | } 43 | 44 | func removeOwnerReference(in []metav1.OwnerReference, uid types.UID) []metav1.OwnerReference { 45 | out := make([]metav1.OwnerReference, 0, len(in)) 46 | for _, ref := range in { 47 | if ref.UID != uid { 48 | out = append(out, ref) 49 | } 50 | } 51 | return out 52 | } 53 | -------------------------------------------------------------------------------- /dynamic/lister/lister.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lister 18 | 19 | import ( 20 | "fmt" 21 | 22 | "k8s.io/apimachinery/pkg/api/errors" 23 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | "k8s.io/apimachinery/pkg/labels" 25 | "k8s.io/apimachinery/pkg/runtime/schema" 26 | "k8s.io/client-go/tools/cache" 27 | ) 28 | 29 | type Lister struct { 30 | indexer cache.Indexer 31 | groupResource schema.GroupResource 32 | } 33 | 34 | func New(groupResource schema.GroupResource, indexer cache.Indexer) *Lister { 35 | return &Lister{ 36 | groupResource: groupResource, 37 | indexer: indexer, 38 | } 39 | } 40 | 41 | func (l *Lister) List(selector labels.Selector) (ret []*unstructured.Unstructured, err error) { 42 | err = cache.ListAll(l.indexer, selector, func(obj interface{}) { 43 | ret = append(ret, obj.(*unstructured.Unstructured)) 44 | }) 45 | return ret, err 46 | } 47 | 48 | func (l *Lister) ListNamespace(namespace string, selector labels.Selector) (ret []*unstructured.Unstructured, err error) { 49 | err = cache.ListAllByNamespace(l.indexer, namespace, selector, func(obj interface{}) { 50 | ret = append(ret, obj.(*unstructured.Unstructured)) 51 | }) 52 | return ret, err 53 | } 54 | 55 | func (l *Lister) Get(namespace, name string) (*unstructured.Unstructured, error) { 56 | key := name 57 | if namespace != "" { 58 | key = fmt.Sprintf("%s/%s", namespace, name) 59 | } 60 | obj, exists, err := l.indexer.GetByKey(key) 61 | if err != nil { 62 | return nil, err 63 | } 64 | if !exists { 65 | return nil, errors.NewNotFound(l.groupResource, name) 66 | } 67 | return obj.(*unstructured.Unstructured), nil 68 | } 69 | -------------------------------------------------------------------------------- /dynamic/object/metadata.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package object 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // HasFinalizer returns true if obj has the named finalizer. 24 | func HasFinalizer(obj metav1.Object, name string) bool { 25 | for _, item := range obj.GetFinalizers() { 26 | if item == name { 27 | return true 28 | } 29 | } 30 | return false 31 | } 32 | 33 | // AddFinalizer adds the named finalizer to obj, if it isn't already present. 34 | func AddFinalizer(obj metav1.Object, name string) { 35 | if HasFinalizer(obj, name) { 36 | // It's already present, so there's nothing to do. 37 | return 38 | } 39 | obj.SetFinalizers(append(obj.GetFinalizers(), name)) 40 | } 41 | 42 | // RemoveFinalizer removes the named finalizer from obj, if it's present. 43 | func RemoveFinalizer(obj metav1.Object, name string) { 44 | finalizers := obj.GetFinalizers() 45 | for i, item := range finalizers { 46 | if item == name { 47 | obj.SetFinalizers(append(finalizers[:i], finalizers[i+1:]...)) 48 | return 49 | } 50 | } 51 | // We never found it, so it's already gone and there's nothing to do. 52 | } 53 | -------------------------------------------------------------------------------- /dynamic/object/status.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package object 18 | 19 | import ( 20 | k8s "metacontroller.app/third_party/kubernetes" 21 | ) 22 | 23 | type StatusCondition struct { 24 | Type string `json:"type"` 25 | Status string `json:"status"` 26 | Reason string `json:"reason,omitempty"` 27 | Message string `json:"message,omitempty"` 28 | } 29 | 30 | func (c *StatusCondition) Object() map[string]interface{} { 31 | obj := map[string]interface{}{ 32 | "type": c.Type, 33 | "status": c.Status, 34 | } 35 | if c.Reason != "" { 36 | obj["reason"] = c.Reason 37 | } 38 | if c.Message != "" { 39 | obj["message"] = c.Message 40 | } 41 | return obj 42 | } 43 | 44 | func NewStatusCondition(obj map[string]interface{}) *StatusCondition { 45 | cond := &StatusCondition{} 46 | if ctype, ok := obj["type"].(string); ok { 47 | cond.Type = ctype 48 | } 49 | if cstatus, ok := obj["status"].(string); ok { 50 | cond.Status = cstatus 51 | } 52 | if creason, ok := obj["reason"].(string); ok { 53 | cond.Reason = creason 54 | } 55 | if cmessage, ok := obj["message"].(string); ok { 56 | cond.Message = cmessage 57 | } 58 | return cond 59 | } 60 | 61 | func GetStatusCondition(obj map[string]interface{}, conditionType string) *StatusCondition { 62 | conditions := k8s.GetNestedArray(obj, "status", "conditions") 63 | for _, item := range conditions { 64 | if obj, ok := item.(map[string]interface{}); ok { 65 | if ctype, ok := obj["type"].(string); ok && ctype == conditionType { 66 | return NewStatusCondition(obj) 67 | } 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | func SetCondition(status map[string]interface{}, condition *StatusCondition) { 74 | conditions := k8s.GetNestedArray(status, "conditions") 75 | // If the condition is already there, update it. 76 | for i, item := range conditions { 77 | if cobj, ok := item.(map[string]interface{}); ok { 78 | if ctype, ok := cobj["type"].(string); ok && ctype == condition.Type { 79 | conditions[i] = condition.Object() 80 | return 81 | } 82 | } 83 | } 84 | // The condition wasn't found. Append it. 85 | conditions = append(conditions, condition.Object()) 86 | k8s.SetNestedField(status, conditions, "conditions") 87 | } 88 | 89 | func SetStatusCondition(obj map[string]interface{}, condition *StatusCondition) { 90 | status := k8s.GetNestedObject(obj, "status") 91 | if status == nil { 92 | status = make(map[string]interface{}) 93 | } 94 | SetCondition(status, condition) 95 | k8s.SetNestedField(obj, status, "status") 96 | } 97 | 98 | func GetObservedGeneration(obj map[string]interface{}) int64 { 99 | return k8s.GetNestedInt64(obj, "status", "observedGeneration") 100 | } 101 | -------------------------------------------------------------------------------- /examples/bluegreen/README.md: -------------------------------------------------------------------------------- 1 | ## BlueGreenDeployment 2 | 3 | This is an example CompositeController that implements a custom rollout strategy 4 | based on a technique called Blue-Green Deployment. 5 | 6 | The controller ramps up a completely separate ReplicaSet in the background for any change to the 7 | Pod template. It then waits for the new ReplicaSet to be fully Ready and Available 8 | (all Pods satisfy minReadySeconds), and then switches a Service to point to the new ReplicaSet. 9 | Finally, it scales down the old ReplicaSet. 10 | 11 | ### Prerequisites 12 | 13 | * Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller) 14 | 15 | ### Deploy the controller 16 | 17 | ```sh 18 | kubectl create configmap bluegreen-controller -n metacontroller --from-file=sync.js 19 | kubectl apply -f bluegreen-controller.yaml 20 | ``` 21 | 22 | ### Create a BlueGreenDeployment 23 | 24 | ```sh 25 | kubectl apply -f my-bluegreen.yaml 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/bluegreen/bluegreen-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: bluegreendeployments.ctl.enisoc.com 5 | spec: 6 | group: ctl.enisoc.com 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | plural: bluegreendeployments 11 | singular: bluegreendeployment 12 | kind: BlueGreenDeployment 13 | shortNames: 14 | - bgd 15 | subresources: 16 | status: {} 17 | --- 18 | apiVersion: metacontroller.k8s.io/v1alpha1 19 | kind: CompositeController 20 | metadata: 21 | name: bluegreen-controller 22 | spec: 23 | parentResource: 24 | apiVersion: ctl.enisoc.com/v1 25 | resource: bluegreendeployments 26 | childResources: 27 | - apiVersion: v1 28 | resource: services 29 | updateStrategy: 30 | method: InPlace 31 | - apiVersion: extensions/v1beta1 32 | resource: replicasets 33 | updateStrategy: 34 | method: InPlace 35 | hooks: 36 | sync: 37 | webhook: 38 | url: http://bluegreen-controller.metacontroller/sync 39 | --- 40 | apiVersion: apps/v1beta1 41 | kind: Deployment 42 | metadata: 43 | name: bluegreen-controller 44 | namespace: metacontroller 45 | spec: 46 | replicas: 1 47 | selector: 48 | matchLabels: 49 | app: bluegreen-controller 50 | template: 51 | metadata: 52 | labels: 53 | app: bluegreen-controller 54 | spec: 55 | containers: 56 | - name: controller 57 | image: metacontroller/nodejs-server:0.1 58 | imagePullPolicy: Always 59 | volumeMounts: 60 | - name: hooks 61 | mountPath: /node/hooks 62 | volumes: 63 | - name: hooks 64 | configMap: 65 | name: bluegreen-controller 66 | --- 67 | apiVersion: v1 68 | kind: Service 69 | metadata: 70 | name: bluegreen-controller 71 | namespace: metacontroller 72 | spec: 73 | selector: 74 | app: bluegreen-controller 75 | ports: 76 | - port: 80 77 | -------------------------------------------------------------------------------- /examples/bluegreen/my-bluegreen.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ctl.enisoc.com/v1 2 | kind: BlueGreenDeployment 3 | metadata: 4 | name: nginx 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 3 9 | minReadySeconds: 5 10 | selector: 11 | matchLabels: 12 | app: nginx 13 | component: frontend 14 | template: 15 | metadata: 16 | labels: 17 | app: nginx 18 | component: frontend 19 | spec: 20 | containers: 21 | - name: nginx 22 | image: nginx:1.7.9 23 | ports: 24 | - containerPort: 80 25 | service: 26 | metadata: 27 | name: nginx-frontend 28 | labels: 29 | app: nginx 30 | component: frontend 31 | spec: 32 | selector: 33 | app: nginx 34 | component: frontend 35 | ports: 36 | - port: 80 37 | -------------------------------------------------------------------------------- /examples/bluegreen/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | echo "Clean up..." 6 | kubectl delete -f my-bluegreen.yaml 7 | kubectl delete rs,svc -l app=nginx,component=frontend 8 | kubectl delete -f bluegreen-controller.yaml 9 | kubectl delete configmap bluegreen-controller -n metacontroller 10 | } 11 | trap cleanup EXIT 12 | 13 | set -ex 14 | 15 | bgd="bluegreendeployments" 16 | 17 | echo "Install controller..." 18 | kubectl create configmap bluegreen-controller -n metacontroller --from-file=sync.js 19 | kubectl apply -f bluegreen-controller.yaml 20 | 21 | echo "Wait until CRD is available..." 22 | until kubectl get $bgd; do sleep 1; done 23 | 24 | echo "Create an object..." 25 | kubectl apply -f my-bluegreen.yaml 26 | 27 | # TODO(juntee): change observedGeneration steps to compare against generation number when k8s 1.10 and earlier retire. 28 | 29 | echo "Wait for nginx-blue RS to be active..." 30 | until [[ "$(kubectl get rs nginx-blue -o 'jsonpath={.status.readyReplicas}')" -eq 3 ]]; do sleep 1; done 31 | until [[ "$(kubectl get rs nginx-green -o 'jsonpath={.status.replicas}')" -eq 0 ]]; do sleep 1; done 32 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.activeColor}')" -eq "blue" ]]; do sleep 1; done 33 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.active.availableReplicas}')" -eq 3 ]]; do sleep 1; done 34 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.observedGeneration}')" -eq "$(kubectl get $bgd nginx -o 'jsonpath={.metadata.generation}')" ]]; do sleep 1; done 35 | 36 | 37 | echo "Trigger a rollout..." 38 | kubectl patch $bgd nginx --type=merge -p '{"spec":{"template":{"metadata":{"labels":{"new":"label"}}}}}' 39 | 40 | echo "Wait for nginx-green RS to be active..." 41 | until [[ "$(kubectl get rs nginx-green -o 'jsonpath={.status.readyReplicas}')" -eq 3 ]]; do sleep 1; done 42 | until [[ "$(kubectl get rs nginx-blue -o 'jsonpath={.status.replicas}')" -eq 0 ]]; do sleep 1; done 43 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.activeColor}')" -eq "green" ]]; do sleep 1; done 44 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.active.availableReplicas}')" -eq 3 ]]; do sleep 1; done 45 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.observedGeneration}')" -eq "$(kubectl get $bgd nginx -o 'jsonpath={.metadata.generation}')" ]]; do sleep 1; done 46 | 47 | echo "Trigger another rollout..." 48 | kubectl patch $bgd nginx --type=merge -p '{"spec":{"template":{"metadata":{"labels":{"new2":"label2"}}}}}' 49 | 50 | echo "Wait for nginx-blue RS to be active..." 51 | until [[ "$(kubectl get rs nginx-blue -o 'jsonpath={.status.readyReplicas}')" -eq 3 ]]; do sleep 1; done 52 | until [[ "$(kubectl get rs nginx-green -o 'jsonpath={.status.replicas}')" -eq 0 ]]; do sleep 1; done 53 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.activeColor}')" -eq "blue" ]]; do sleep 1; done 54 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.active.availableReplicas}')" -eq 3 ]]; do sleep 1; done 55 | until [[ "$(kubectl get $bgd nginx -o 'jsonpath={.status.observedGeneration}')" -eq "$(kubectl get $bgd nginx -o 'jsonpath={.metadata.generation}')" ]]; do sleep 1; done -------------------------------------------------------------------------------- /examples/catset/README.md: -------------------------------------------------------------------------------- 1 | ## CatSet 2 | 3 | This is a reimplementation of StatefulSet (now including rolling updates) as a CompositeController. 4 | 5 | CatSet also demonstrates using a finalizer with a lambda hook to support 6 | graceful, ordered teardown when the parent object is deleted. 7 | Unlike StatefulSet, which previously exhibited this behavior only because of a 8 | client-side kubectl feature, CatSet ordered teardown happens on the server side, 9 | so it works when the CatSet is deleted through any means (not just kubectl). 10 | 11 | For this example, you need a cluster with a default storage class and a dynamic provisioner. 12 | 13 | ### Prerequisites 14 | 15 | * Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller) 16 | 17 | ### Deploy the controller 18 | 19 | ```sh 20 | kubectl create configmap catset-controller -n metacontroller --from-file=sync.js 21 | kubectl apply -f catset-controller.yaml 22 | ``` 23 | 24 | ### Create a CatSet 25 | 26 | ```sh 27 | kubectl apply -f my-catset.yaml 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/catset/catset-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: catsets.ctl.enisoc.com 5 | spec: 6 | group: ctl.enisoc.com 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | plural: catsets 11 | singular: catset 12 | kind: CatSet 13 | shortNames: 14 | - cs 15 | subresources: 16 | status: {} 17 | --- 18 | apiVersion: metacontroller.k8s.io/v1alpha1 19 | kind: CompositeController 20 | metadata: 21 | name: catset-controller 22 | spec: 23 | parentResource: 24 | apiVersion: ctl.enisoc.com/v1 25 | resource: catsets 26 | revisionHistory: 27 | fieldPaths: 28 | - spec.template 29 | childResources: 30 | - apiVersion: v1 31 | resource: pods 32 | updateStrategy: 33 | method: RollingRecreate 34 | statusChecks: 35 | conditions: 36 | - type: Ready 37 | status: "True" 38 | - apiVersion: v1 39 | resource: persistentvolumeclaims 40 | hooks: 41 | sync: 42 | webhook: 43 | url: http://catset-controller.metacontroller/sync 44 | finalize: 45 | webhook: 46 | url: http://catset-controller.metacontroller/sync 47 | --- 48 | apiVersion: apps/v1beta1 49 | kind: Deployment 50 | metadata: 51 | name: catset-controller 52 | namespace: metacontroller 53 | spec: 54 | replicas: 1 55 | selector: 56 | matchLabels: 57 | app: catset-controller 58 | template: 59 | metadata: 60 | labels: 61 | app: catset-controller 62 | spec: 63 | containers: 64 | - name: controller 65 | image: metacontroller/nodejs-server:0.1 66 | imagePullPolicy: Always 67 | volumeMounts: 68 | - name: hooks 69 | mountPath: /node/hooks 70 | volumes: 71 | - name: hooks 72 | configMap: 73 | name: catset-controller 74 | --- 75 | apiVersion: v1 76 | kind: Service 77 | metadata: 78 | name: catset-controller 79 | namespace: metacontroller 80 | spec: 81 | selector: 82 | app: catset-controller 83 | ports: 84 | - port: 80 85 | -------------------------------------------------------------------------------- /examples/catset/my-catset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nginx-backend 5 | spec: 6 | ports: 7 | - port: 80 8 | name: web 9 | clusterIP: None 10 | selector: 11 | app: nginx 12 | --- 13 | apiVersion: ctl.enisoc.com/v1 14 | kind: CatSet 15 | metadata: 16 | name: nginx-backend 17 | spec: 18 | serviceName: nginx-backend 19 | replicas: 3 20 | selector: 21 | matchLabels: 22 | app: nginx 23 | template: 24 | metadata: 25 | labels: 26 | app: nginx 27 | component: backend 28 | spec: 29 | terminationGracePeriodSeconds: 1 30 | containers: 31 | - name: nginx 32 | image: gcr.io/google_containers/nginx-slim:0.8 33 | ports: 34 | - containerPort: 80 35 | name: web 36 | volumeMounts: 37 | - name: www 38 | mountPath: /usr/share/nginx/html 39 | volumeClaimTemplates: 40 | - metadata: 41 | name: www 42 | labels: 43 | app: nginx 44 | component: backend 45 | spec: 46 | accessModes: [ "ReadWriteOnce" ] 47 | resources: 48 | requests: 49 | storage: 1Gi 50 | -------------------------------------------------------------------------------- /examples/catset/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | echo "Clean up..." 6 | kubectl patch $cs nginx-backend --type=merge -p '{"metadata":{"finalizers":[]}}' 7 | kubectl delete -f my-catset.yaml 8 | kubectl delete po,pvc -l app=nginx,component=backend 9 | kubectl delete -f catset-controller.yaml 10 | kubectl delete configmap catset-controller -n metacontroller 11 | } 12 | trap cleanup EXIT 13 | 14 | set -ex 15 | 16 | cs="catsets" 17 | finalizer="metacontroller.app/catset-test" 18 | 19 | echo "Install controller..." 20 | kubectl create configmap catset-controller -n metacontroller --from-file=sync.js 21 | kubectl apply -f catset-controller.yaml 22 | 23 | echo "Wait until CRD is available..." 24 | until kubectl get $cs; do sleep 1; done 25 | 26 | echo "Create an object..." 27 | kubectl apply -f my-catset.yaml 28 | 29 | echo "Wait for 3 Pods to be Ready..." 30 | until [[ "$(kubectl get $cs nginx-backend -o 'jsonpath={.status.readyReplicas}')" -eq 3 ]]; do sleep 1; done 31 | 32 | echo "Scale up to 4 replicas..." 33 | kubectl patch $cs nginx-backend --type=merge -p '{"spec":{"replicas":4}}' 34 | 35 | echo "Wait for 4 Pods to be Ready..." 36 | until [[ "$(kubectl get $cs nginx-backend -o 'jsonpath={.status.readyReplicas}')" -eq 4 ]]; do sleep 1; done 37 | 38 | echo "Scale down to 2 replicas..." 39 | kubectl patch $cs nginx-backend --type=merge -p '{"spec":{"replicas":2}}' 40 | 41 | echo "Wait for 2 Pods to be Ready..." 42 | until [[ "$(kubectl get $cs nginx-backend -o 'jsonpath={.status.readyReplicas}')" -eq 2 ]]; do sleep 1; done 43 | 44 | echo "Append our own finalizer so we can read the final state..." 45 | kubectl patch $cs nginx-backend --type=json -p '[{"op":"add","path":"/metadata/finalizers/-","value":"'${finalizer}'"}]' 46 | 47 | echo "Delete CatSet..." 48 | kubectl delete $cs nginx-backend --wait=false 49 | 50 | echo "Expect CatSet's finalizer to scale the CatSet to 0 replicas..." 51 | until [[ "$(kubectl get $cs nginx-backend -o 'jsonpath={.status.replicas}')" -eq 0 ]]; do sleep 1; done 52 | 53 | echo "Wait for our finalizer to be the only one left, then remove it..." 54 | until [[ "$(kubectl get $cs nginx-backend -o 'jsonpath={.metadata.finalizers}')" == "[${finalizer}]" ]]; do sleep 1; done 55 | kubectl patch $cs nginx-backend --type=merge -p '{"metadata":{"finalizers":[]}}' 56 | -------------------------------------------------------------------------------- /examples/clusteredparent/README.md: -------------------------------------------------------------------------------- 1 | ## ClusterRole service account binding 2 | 3 | This is an example DecoratorController that creates a namespaced resources from a 4 | cluster scoped parent resource. 5 | 6 | This controller will bind any ClusterRole with the "default-service-account-binding" 7 | annotation to the default service account in the default namespace. 8 | 9 | ### Prerequisites 10 | 11 | * Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller) 12 | 13 | ### Deploy the controller 14 | 15 | ```sh 16 | kubectl create configmap cluster-parent-controller -n metacontroller --from-file=sync.py 17 | kubectl apply -f cluster-parent.yaml 18 | ``` 19 | 20 | ### Create a ClusterRole 21 | 22 | ```sh 23 | kubectl apply -f my-clusterole.yaml 24 | ``` 25 | 26 | A RoleBinding should be created for the ClusterRole: 27 | 28 | ```console 29 | $ kubectl get rolebinding -n default my-clusterrole -o wide 30 | NAME AGE ROLE USERS GROUPS SERVICEACCOUNTS 31 | my-clusterrole 40s ClusterRole/my-clusterrole default/default 32 | ``` 33 | -------------------------------------------------------------------------------- /examples/clusteredparent/cluster-parent.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: metacontroller.k8s.io/v1alpha1 2 | kind: DecoratorController 3 | metadata: 4 | name: cluster-parent 5 | spec: 6 | resources: 7 | - apiVersion: rbac.authorization.k8s.io/v1 8 | resource: clusterroles 9 | annotationSelector: 10 | matchExpressions: 11 | - {key: default-service-account-binding, operator: Exists} 12 | attachments: 13 | - apiVersion: rbac.authorization.k8s.io/v1 14 | resource: rolebindings 15 | hooks: 16 | sync: 17 | webhook: 18 | url: http://cluster-parent-controller.metacontroller/sync 19 | --- 20 | apiVersion: apps/v1beta1 21 | kind: Deployment 22 | metadata: 23 | name: clusterparent-controller 24 | namespace: metacontroller 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: cluster-parent-controller 30 | template: 31 | metadata: 32 | labels: 33 | app: cluster-parent-controller 34 | spec: 35 | containers: 36 | - name: controller 37 | image: python:2.7 38 | command: ["python", "/hooks/sync.py"] 39 | volumeMounts: 40 | - name: hooks 41 | mountPath: /hooks 42 | volumes: 43 | - name: hooks 44 | configMap: 45 | name: cluster-parent-controller 46 | --- 47 | apiVersion: v1 48 | kind: Service 49 | metadata: 50 | name: cluster-parent-controller 51 | namespace: metacontroller 52 | spec: 53 | selector: 54 | app: cluster-parent-controller 55 | ports: 56 | - port: 80 57 | -------------------------------------------------------------------------------- /examples/clusteredparent/my-clusterrole.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRole 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: my-clusterrole 5 | annotations: 6 | default-service-account-binding: "default" 7 | rules: 8 | - apiGroups: [""] 9 | resources: ["nodes"] 10 | verbs: ["get", "watch", "list"] 11 | --- 12 | -------------------------------------------------------------------------------- /examples/clusteredparent/sync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://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 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 18 | import json 19 | 20 | def new_rolebinding(name): 21 | rolebinding = {} 22 | rolebinding['apiVersion'] = 'rbac.authorization.k8s.io/v1' 23 | rolebinding['kind'] = 'RoleBinding' 24 | rolebinding['metadata'] = {} 25 | rolebinding['metadata']['name'] = name 26 | rolebinding['metadata']['namespace'] = "default" 27 | rolebinding['subjects'] = [{'kind': 'ServiceAccount', 'name': 'default', 'namespace': 'default'}] 28 | rolebinding['roleRef'] = {'kind': 'ClusterRole', 'name': name, 'apiGroup': 'rbac.authorization.k8s.io'} 29 | return rolebinding 30 | 31 | class Controller(BaseHTTPRequestHandler): 32 | def sync(self, clusterrole, children): 33 | return {'attachments': [new_rolebinding(clusterrole['metadata']['name'])] } 34 | 35 | 36 | def do_POST(self): 37 | observed = json.loads(self.rfile.read(int(self.headers.getheader('content-length')))) 38 | desired = self.sync(observed['object'], observed['attachments']) 39 | 40 | self.send_response(200) 41 | self.send_header('Content-type', 'application/json') 42 | self.end_headers() 43 | self.wfile.write(json.dumps(desired)) 44 | 45 | HTTPServer(('', 80), Controller).serve_forever() 46 | -------------------------------------------------------------------------------- /examples/clusteredparent/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | echo "Clean up..." 6 | kubectl delete -f my-clusterrole.yaml 7 | kubectl delete -f cluster-parent.yaml 8 | kubectl delete configmap cluster-parent-controller -n metacontroller 9 | } 10 | trap cleanup EXIT 11 | 12 | set -ex 13 | 14 | echo "Install controller..." 15 | kubectl create configmap cluster-parent-controller -n metacontroller --from-file=sync.py 16 | kubectl apply -f cluster-parent.yaml 17 | 18 | echo "Create a ClusterRole..." 19 | kubectl apply -f my-clusterrole.yaml 20 | 21 | echo "Wait for Namespaced child..." 22 | until [[ "$(kubectl get rolebinding -n default my-clusterrole -o 'jsonpath={.metadata.name}')" == "my-clusterrole" ]]; do sleep 1; done 23 | 24 | echo "Delete Namespaced child..." 25 | kubectl delete rolebinding -n default my-clusterrole --wait=true 26 | 27 | # Test that the controller with cluster-scoped parent notices the namespaced child got deleted. 28 | echo "Wait for Namespaced child to be recreated..." 29 | until [[ "$(kubectl get rolebinding -n default my-clusterrole -o 'jsonpath={.metadata.name}')" == "my-clusterrole" ]]; do sleep 1; done 30 | 31 | # Test to make sure cascading deletion of cross namespaces resources works. 32 | echo "Deleting ClusterRole..." 33 | kubectl delete -f my-clusterrole.yaml 34 | 35 | echo "Wait for Namespaced child cleanup..." 36 | until [[ "$(kubectl get clusterrole.rbac.authorization.k8s.io -n default my-clusterrole 2>&1 )" == *NotFound* ]]; do sleep 1; done 37 | -------------------------------------------------------------------------------- /examples/crd-roles/README.md: -------------------------------------------------------------------------------- 1 | ## CRD Roles Controller 2 | 3 | This is an example DecoratorController that manages Cluster scoped resources. 4 | Both the parent and child resouces are Cluster scoped. 5 | 6 | ### Prerequisites 7 | 8 | * Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller) 9 | 10 | ### Deploy the controller 11 | 12 | ```sh 13 | kubectl create configmap crd-role-contoller -n metacontroller --from-file=sync.py 14 | kubectl apply -f crd-role-controller.yaml 15 | ``` 16 | 17 | ### Create a CRD 18 | 19 | ```sh 20 | kubectl apply -f my-crd.yaml 21 | ``` 22 | 23 | A ClusterRole should be created configured with read access to the CRD. 24 | 25 | ```console 26 | $ kubectl get clusterrole my-crd-reader 27 | NAME AGE 28 | my-crd-reader 3m 29 | ``` 30 | -------------------------------------------------------------------------------- /examples/crd-roles/crd-role-controller.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: metacontroller.k8s.io/v1alpha1 3 | kind: DecoratorController 4 | metadata: 5 | name: crd-role-controller 6 | spec: 7 | resources: 8 | - apiVersion: apiextensions.k8s.io/v1beta1 9 | resource: customresourcedefinitions 10 | annotationSelector: 11 | matchExpressions: 12 | - {key: enable-default-roles, operator: Exists} 13 | attachments: 14 | - apiVersion: rbac.authorization.k8s.io/v1 15 | resource: clusterroles 16 | hooks: 17 | sync: 18 | webhook: 19 | url: http://crd-role-controller.metacontroller/sync-crd-role 20 | timeout: 10s 21 | --- 22 | apiVersion: apps/v1beta1 23 | kind: Deployment 24 | metadata: 25 | name: crd-role-controller 26 | namespace: metacontroller 27 | spec: 28 | replicas: 1 29 | selector: 30 | matchLabels: 31 | app: crd-role-controller 32 | template: 33 | metadata: 34 | labels: 35 | app: crd-role-controller 36 | spec: 37 | containers: 38 | - name: controller 39 | image: python:2.7 40 | command: ["python", "/hooks/sync.py"] 41 | volumeMounts: 42 | - name: hooks 43 | mountPath: /hooks 44 | volumes: 45 | - name: hooks 46 | configMap: 47 | name: crd-role-controller 48 | --- 49 | apiVersion: v1 50 | kind: Service 51 | metadata: 52 | name: crd-role-controller 53 | namespace: metacontroller 54 | spec: 55 | selector: 56 | app: crd-role-controller 57 | ports: 58 | - port: 80 59 | -------------------------------------------------------------------------------- /examples/crd-roles/my-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: my-tests.ctl.rlg.io 5 | annotations: 6 | enable-default-roles: "yes" 7 | spec: 8 | group: ctl.rlg.io 9 | version: v1 10 | scope: Cluster 11 | names: 12 | plural: my-tests 13 | singular: my-test 14 | kind: MyTest 15 | -------------------------------------------------------------------------------- /examples/crd-roles/sync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://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 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 18 | import json 19 | 20 | def new_cluster_role(crd): 21 | cr = {} 22 | cr['apiVersion'] = 'rbac.authorization.k8s.io/v1' 23 | cr['kind'] = "ClusterRole" 24 | cr['metadata'] = {} 25 | cr['metadata']['name'] = crd['metadata']['name'] + "-reader" 26 | apiGroup = crd['spec']['group'] 27 | resource = crd['spec']['names']['plural'] 28 | cr['rules'] = [] 29 | # cr['rules'] = [{'apiGroups': [apiGroup], 'resouces':[resource], 'verbs': ["*"]}] 30 | return cr 31 | 32 | class Controller(BaseHTTPRequestHandler): 33 | 34 | def do_POST(self): 35 | observed = json.loads(self.rfile.read(int(self.headers.getheader('content-length')))) 36 | desired = {'attachments': [new_cluster_role(observed['object'])]} 37 | 38 | self.send_response(200) 39 | self.send_header('Content-type', 'application/json') 40 | self.end_headers() 41 | self.wfile.write(json.dumps(desired)) 42 | 43 | HTTPServer(('', 80), Controller).serve_forever() 44 | -------------------------------------------------------------------------------- /examples/crd-roles/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | echo "Clean up..." 6 | kubectl delete -f my-crd.yaml 7 | kubectl delete -f crd-role-controller.yaml 8 | kubectl delete configmap crd-role-controller -n metacontroller 9 | } 10 | trap cleanup EXIT 11 | 12 | set -ex 13 | 14 | echo "Install controller..." 15 | kubectl create configmap crd-role-controller -n metacontroller --from-file=sync.py 16 | kubectl apply -f crd-role-controller.yaml 17 | 18 | echo "Create a CRD..." 19 | kubectl apply -f my-crd.yaml 20 | 21 | echo "Wait for ClusterRole..." 22 | until [[ "$(kubectl get clusterrole my-tests.ctl.rlg.io-reader -o 'jsonpath={.metadata.name}')" == "my-tests.ctl.rlg.io-reader" ]]; do sleep 1; done 23 | -------------------------------------------------------------------------------- /examples/daemonjob/README.md: -------------------------------------------------------------------------------- 1 | ## DaemonJob 2 | 3 | This is an example CompositeController that's similar to Job, 4 | except that a pod will be scheduled to each node, similar to DaemonSet. 5 | 6 | The implementation was inspired by this [blog post](http://blog.itaysk.com/2017/12/26/the-single-use-daemonset-pattern-and-prepulling-images-in-kubernetes). 7 | This particular pattern has some caveats, like how multiple containers run in 8 | series instead of concurrently, but it suffices as an example of using 9 | CompositeController to wrap up any such pattern you could think of. 10 | 11 | ### Prerequisites 12 | 13 | * Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller) 14 | 15 | ### Deploy the controller 16 | 17 | ```sh 18 | kubectl create configmap daemonjob-controller -n metacontroller --from-file=sync.py 19 | kubectl apply -f daemonjob-controller.yaml 20 | ``` 21 | 22 | ### Create a DaemonJob 23 | 24 | In a separate terminal watch for DaemonJobs, DaemonSets and Pods 25 | 26 | ```sh 27 | watch -n1 kubectl get ds,dj,po 28 | ``` 29 | 30 | Create the DaemonJob 31 | 32 | ```sh 33 | kubectl apply -f my-daemonjob.yaml 34 | ``` 35 | 36 | In the terminal where you have the `watch` command running you will see a 37 | DaemonSet being created as soon as the DaemonJob is deployed and then a pod will 38 | start on each node of your cluster. 39 | These pods will stay in the init stage for about 30s (sleep command in the 40 | container) and will be terminated as soon as all of them have reached the 41 | `Running` state. 42 | The DeamonSet generated by the DaemonJob will also be cleaned up after all the 43 | Pods are done. 44 | 45 | ### Clean up 46 | 47 | ```sh 48 | kubectl delete -f daemonjob-controller.yaml 49 | kubectl delete configmap -n metacontroller daemonjob-controller 50 | ``` 51 | -------------------------------------------------------------------------------- /examples/daemonjob/daemonjob-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: daemonjobs.ctl.example.com 5 | spec: 6 | group: ctl.example.com 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | plural: daemonjobs 11 | singular: daemonjob 12 | kind: DaemonJob 13 | shortNames: ["dj"] 14 | subresources: 15 | status: {} 16 | --- 17 | apiVersion: metacontroller.k8s.io/v1alpha1 18 | kind: CompositeController 19 | metadata: 20 | name: daemonjob-controller 21 | spec: 22 | generateSelector: true 23 | parentResource: 24 | apiVersion: ctl.example.com/v1 25 | resource: daemonjobs 26 | childResources: 27 | - apiVersion: apps/v1 28 | resource: daemonsets 29 | hooks: 30 | sync: 31 | webhook: 32 | url: http://daemonjob-controller.metacontroller/sync 33 | --- 34 | apiVersion: apps/v1 35 | kind: Deployment 36 | metadata: 37 | name: daemonjob-controller 38 | namespace: metacontroller 39 | spec: 40 | replicas: 1 41 | selector: 42 | matchLabels: 43 | app: daemonjob-controller 44 | template: 45 | metadata: 46 | labels: 47 | app: daemonjob-controller 48 | spec: 49 | containers: 50 | - name: controller 51 | image: python:2.7 52 | command: ["python", "/hooks/sync.py"] 53 | volumeMounts: 54 | - name: hooks 55 | mountPath: /hooks 56 | volumes: 57 | - name: hooks 58 | configMap: 59 | name: daemonjob-controller 60 | --- 61 | apiVersion: v1 62 | kind: Service 63 | metadata: 64 | name: daemonjob-controller 65 | namespace: metacontroller 66 | spec: 67 | selector: 68 | app: daemonjob-controller 69 | ports: 70 | - port: 80 71 | -------------------------------------------------------------------------------- /examples/daemonjob/my-daemonjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ctl.example.com/v1 2 | kind: DaemonJob 3 | metadata: 4 | name: hello-world 5 | spec: 6 | template: 7 | metadata: 8 | labels: 9 | app: hello-world 10 | spec: 11 | containers: 12 | - name: hello-world 13 | image: busybox 14 | command: ["sh", "-c", "echo 'Hello world' && sleep 30"] 15 | resources: 16 | requests: 17 | cpu: 10m 18 | terminationGracePeriodSeconds: 10 19 | -------------------------------------------------------------------------------- /examples/daemonjob/sync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2019 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://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 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 18 | import json 19 | import copy 20 | import re 21 | 22 | def is_job_finished(job): 23 | if 'status' in job: 24 | desiredNumberScheduled = job['status'].get('desiredNumberScheduled',1) 25 | numberReady = job['status'].get('numberReady',0) 26 | if desiredNumberScheduled == numberReady and desiredNumberScheduled > 0: 27 | return True 28 | return False 29 | 30 | def new_daemon(job): 31 | daemon = copy.deepcopy(job) 32 | daemon['apiVersion'] = 'apps/v1' 33 | daemon['kind'] = 'DaemonSet' 34 | daemon['metadata'] = {} 35 | daemon['metadata']['name'] = '%s-dj' % (job['metadata']['name']) 36 | daemon['metadata']['labels'] = copy.deepcopy(job['spec']['template']['metadata']['labels']) 37 | daemon['spec'] = {} 38 | daemon['spec']['template'] = copy.deepcopy(job['spec']['template']) 39 | daemon['spec']['template']['spec']['initContainers'] = copy.deepcopy(job['spec']['template']['spec']['containers']) 40 | daemon['spec']['template']['spec']['containers'] = [{ 41 | 'name': "pause", 42 | 'image': job['spec'].get('pauseImage', 'gcr.io/google_containers/pause'), 43 | 'resources': {'requests': {'cpu': '10m'}} 44 | }] 45 | daemon['spec']['selector'] = {'matchLabels': copy.deepcopy(job['spec']['template']['metadata']['labels'])} 46 | 47 | return daemon 48 | 49 | class Controller(BaseHTTPRequestHandler): 50 | def sync(self, job, children): 51 | desired_status = {} 52 | child = '%s-dj' % (job['metadata']['name']) 53 | 54 | self.log_message(" Children: %s", children) 55 | 56 | # If the job already finished at some point, freeze the status, 57 | # delete children, and take no further action. 58 | if is_job_finished(job): 59 | desired_status = copy.deepcopy(job['status']) 60 | desired_status['conditions'] = [{'type': 'Complete', 'status': 'True'}] 61 | return {'status': desired_status, 'children': []} 62 | 63 | # Compute status based on what we observed, before building desired state. 64 | # Our .status is just a copy of the DaemonSet .status with extra fields. 65 | desired_status = copy.deepcopy(children['DaemonSet.apps/v1'].get(child, {}).get('status',{})) 66 | if is_job_finished(children['DaemonSet.apps/v1'].get(child, {})): 67 | desired_status['conditions'] = [{'type': 'Complete', 'status': 'True'}] 68 | else: 69 | desired_status['conditions'] = [{'type': 'Complete', 'status': 'False'}] 70 | 71 | # Always generate desired state for child if we reach this point. 72 | # We should not delete children until after we know we've recorded 73 | # completion in our status, which was the first check we did above. 74 | desired_child = new_daemon(job) 75 | return {'status': desired_status, 'children': [desired_child]} 76 | 77 | 78 | def do_POST(self): 79 | observed = json.loads(self.rfile.read(int(self.headers.getheader('content-length')))) 80 | desired = self.sync(observed['parent'], observed['children']) 81 | 82 | self.send_response(200) 83 | self.send_header('Content-type', 'application/json') 84 | self.end_headers() 85 | self.wfile.write(json.dumps(desired)) 86 | 87 | HTTPServer(('', 80), Controller).serve_forever() 88 | -------------------------------------------------------------------------------- /examples/daemonjob/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | echo "Clean up..." 6 | kubectl delete -f my-daemonjob.yaml 7 | kubectl delete daemonset hello-world-dj 8 | kubectl delete po -l app=hello-world 9 | kubectl delete -f daemonjob-controller.yaml 10 | kubectl delete configmap daemonjob-controller -n metacontroller 11 | } 12 | trap cleanup EXIT 13 | 14 | set -ex 15 | 16 | dj="daemonjobs" 17 | 18 | echo "Install controller..." 19 | kubectl create configmap daemonjob-controller -n metacontroller --from-file=sync.py 20 | kubectl apply -f daemonjob-controller.yaml 21 | 22 | echo "Wait until CRD is available..." 23 | until kubectl get $dj; do sleep 1; done 24 | 25 | echo "Create an object..." 26 | kubectl apply -f my-daemonjob.yaml 27 | 28 | echo "Wait for successful completion..." 29 | until [[ "$(kubectl get $dj hello-world -o 'jsonpath={.status.conditions[0].status}')" == "True" ]]; do sleep 1; done 30 | 31 | echo "Check that DaemonSet gets cleaned up after finishing..." 32 | until [[ "$(kubectl get daemonset hello-world-dj 2>&1)" =~ NotFound ]]; do sleep 1; done 33 | -------------------------------------------------------------------------------- /examples/go/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /examples/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11 AS build 2 | 3 | COPY . /go/src/thing-controller 4 | WORKDIR /go/src/thing-controller 5 | RUN go get -u github.com/golang/dep/cmd/dep && dep ensure && go build -o /go/bin/thing-controller 6 | 7 | FROM debian:stretch-slim 8 | 9 | COPY --from=build /go/bin/thing-controller /usr/bin/thing-controller 10 | -------------------------------------------------------------------------------- /examples/go/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:0a3f6a0c68ab8f3d455f8892295503b179e571b7fefe47cc6c556405d1f83411" 6 | name = "github.com/gogo/protobuf" 7 | packages = [ 8 | "proto", 9 | "sortkeys", 10 | ] 11 | pruneopts = "" 12 | revision = "1adfc126b41513cc696b209667c8656ea7aac67c" 13 | version = "v1.0.0" 14 | 15 | [[projects]] 16 | branch = "master" 17 | digest = "1:107b233e45174dbab5b1324201d092ea9448e58243ab9f039e4c0f332e121e3a" 18 | name = "github.com/golang/glog" 19 | packages = ["."] 20 | pruneopts = "" 21 | revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" 22 | 23 | [[projects]] 24 | branch = "master" 25 | digest = "1:754f77e9c839b24778a4b64422236d38515301d2baeb63113aa3edc42e6af692" 26 | name = "github.com/google/gofuzz" 27 | packages = ["."] 28 | pruneopts = "" 29 | revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" 30 | 31 | [[projects]] 32 | digest = "1:261bc565833ef4f02121450d74eb88d5ae4bd74bfe5d0e862cddb8550ec35000" 33 | name = "github.com/spf13/pflag" 34 | packages = ["."] 35 | pruneopts = "" 36 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" 37 | version = "v1.0.0" 38 | 39 | [[projects]] 40 | branch = "master" 41 | digest = "1:d6ff496a166a2371bfc9b653632c1a94de1b8cf3bed6ee0f2dd38ba75c8efe2c" 42 | name = "golang.org/x/net" 43 | packages = [ 44 | "http2", 45 | "http2/hpack", 46 | "idna", 47 | "lex/httplex", 48 | ] 49 | pruneopts = "" 50 | revision = "d0aafc73d5cdc42264b0af071c261abac580695e" 51 | 52 | [[projects]] 53 | digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4" 54 | name = "golang.org/x/text" 55 | packages = [ 56 | "collate", 57 | "collate/build", 58 | "internal/colltab", 59 | "internal/gen", 60 | "internal/tag", 61 | "internal/triegen", 62 | "internal/ucd", 63 | "language", 64 | "secure/bidirule", 65 | "transform", 66 | "unicode/bidi", 67 | "unicode/cldr", 68 | "unicode/norm", 69 | "unicode/rangetable", 70 | ] 71 | pruneopts = "" 72 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 73 | version = "v0.3.0" 74 | 75 | [[projects]] 76 | digest = "1:e5d1fb981765b6f7513f793a3fcaac7158408cca77f75f7311ac82cc88e9c445" 77 | name = "gopkg.in/inf.v0" 78 | packages = ["."] 79 | pruneopts = "" 80 | revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4" 81 | version = "v0.9.0" 82 | 83 | [[projects]] 84 | branch = "master" 85 | digest = "1:25a4794663763eacfae30ae399b17a0e203ef514de0d4d8a87613b33f6bb4995" 86 | name = "k8s.io/api" 87 | packages = ["core/v1"] 88 | pruneopts = "" 89 | revision = "fd252c3a3e1debf912ff5b80221a31a6a3c24493" 90 | 91 | [[projects]] 92 | branch = "master" 93 | digest = "1:6e643d2bbfd60bd5a03965fcc0ea5a40947656c5a77727fd62f5bc757fd24b80" 94 | name = "k8s.io/apimachinery" 95 | packages = [ 96 | "pkg/api/resource", 97 | "pkg/apis/meta/v1", 98 | "pkg/conversion", 99 | "pkg/conversion/queryparams", 100 | "pkg/fields", 101 | "pkg/labels", 102 | "pkg/runtime", 103 | "pkg/runtime/schema", 104 | "pkg/selection", 105 | "pkg/types", 106 | "pkg/util/errors", 107 | "pkg/util/intstr", 108 | "pkg/util/json", 109 | "pkg/util/net", 110 | "pkg/util/runtime", 111 | "pkg/util/sets", 112 | "pkg/util/validation", 113 | "pkg/util/validation/field", 114 | "pkg/util/wait", 115 | "pkg/watch", 116 | "third_party/forked/golang/reflect", 117 | ] 118 | pruneopts = "" 119 | revision = "e9ff529c66f83aeac6dff90f11ea0c5b7c4d626a" 120 | 121 | [solve-meta] 122 | analyzer-name = "dep" 123 | analyzer-version = 1 124 | input-imports = [ 125 | "k8s.io/api/core/v1", 126 | "k8s.io/apimachinery/pkg/apis/meta/v1", 127 | "k8s.io/apimachinery/pkg/runtime", 128 | "k8s.io/apimachinery/pkg/util/json", 129 | ] 130 | solver-name = "gps-cdcl" 131 | solver-version = 1 132 | -------------------------------------------------------------------------------- /examples/go/Gopkg.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/metacontroller/ddfc021ec8a1685f17349096b84a432817932884/examples/go/Gopkg.toml -------------------------------------------------------------------------------- /examples/go/README.md: -------------------------------------------------------------------------------- 1 | ## Example Go Controller 2 | 3 | This controller doesn't do anything useful. 4 | It's just an example skeleton for writing Metacontroller hooks with Go. 5 | 6 | **WARNING** 7 | 8 | There's a [known issue](https://github.com/GoogleCloudPlatform/metacontroller/issues/76) 9 | that makes it difficult to produce JSON according to the rules that Metacontroller 10 | requires if you import the official Go structs for Kubernetes APIs. 11 | In particular, some fields will always be emitted, even if you never set them, 12 | which goes against Metacontroller's [apply semantics](https://metacontroller.app/api/apply/). 13 | 14 | ### Prerequisites 15 | 16 | * [Install Metacontroller](https://metacontroller.app/guide/install/) 17 | 18 | ### Install Thing Controller 19 | 20 | ```sh 21 | kubectl apply -f thing-controller.yaml 22 | ``` 23 | 24 | ### Create a Thing 25 | 26 | ```sh 27 | kubectl apply -f my-thing.yaml 28 | ``` 29 | 30 | Look at the thing: 31 | 32 | ```sh 33 | kubectl get thing -o yaml 34 | ``` 35 | 36 | Look at the thing the thing created: 37 | 38 | ```sh 39 | kubectl get pod thing-1 -a 40 | ``` 41 | 42 | Look at what the thing the thing created said: 43 | 44 | ```sh 45 | kubectl logs thing-1 46 | ``` 47 | 48 | ### Clean up 49 | 50 | ```sh 51 | kubectl delete -f thing-controller.yaml 52 | ``` 53 | 54 | ### Building 55 | 56 | You don't need to build to run the example above, 57 | but if you make changes: 58 | 59 | ```sh 60 | go get -u github.com/golang/dep/cmd/dep 61 | dep ensure 62 | go build -o thing-controller 63 | ``` 64 | 65 | Or just make a new container image: 66 | 67 | ```sh 68 | docker build . -t /thing-controller 69 | ``` 70 | -------------------------------------------------------------------------------- /examples/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | 8 | "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/util/json" 12 | ) 13 | 14 | type Controller struct { 15 | metav1.TypeMeta `json:",inline"` 16 | metav1.ObjectMeta `json:"metadata"` 17 | Spec ControllerSpec `json:"spec"` 18 | Status ControllerStatus `json:"status"` 19 | } 20 | 21 | type ControllerSpec struct { 22 | Message string `json:"message"` 23 | } 24 | 25 | type ControllerStatus struct { 26 | Replicas int `json:"replicas"` 27 | Succeeded int `json:"succeeded"` 28 | } 29 | 30 | type SyncRequest struct { 31 | Parent Controller `json:"parent"` 32 | Children SyncRequestChildren `json:"children"` 33 | } 34 | 35 | type SyncRequestChildren struct { 36 | Pods map[string]*v1.Pod `json:"Pod.v1"` 37 | } 38 | 39 | type SyncResponse struct { 40 | Status ControllerStatus `json:"status"` 41 | Children []runtime.Object `json:"children"` 42 | } 43 | 44 | func sync(request *SyncRequest) (*SyncResponse, error) { 45 | response := &SyncResponse{} 46 | 47 | // Compute status based on latest observed state. 48 | for _, pod := range request.Children.Pods { 49 | response.Status.Replicas += 1 50 | if pod.Status.Phase == v1.PodSucceeded { 51 | response.Status.Succeeded += 1 52 | } 53 | } 54 | 55 | // Generate desired children. 56 | pod := &v1.Pod{ 57 | TypeMeta: metav1.TypeMeta{ 58 | APIVersion: "v1", 59 | Kind: "Pod", 60 | }, 61 | ObjectMeta: metav1.ObjectMeta{ 62 | Name: request.Parent.Name, 63 | }, 64 | Spec: v1.PodSpec{ 65 | RestartPolicy: v1.RestartPolicyOnFailure, 66 | Containers: []v1.Container{ 67 | { 68 | Name: "hello", 69 | Image: "busybox", 70 | Command: []string{"echo", request.Parent.Spec.Message}, 71 | }, 72 | }, 73 | }, 74 | } 75 | response.Children = append(response.Children, pod) 76 | 77 | return response, nil 78 | } 79 | 80 | func syncHandler(w http.ResponseWriter, r *http.Request) { 81 | body, err := ioutil.ReadAll(r.Body) 82 | if err != nil { 83 | http.Error(w, err.Error(), http.StatusInternalServerError) 84 | return 85 | } 86 | request := &SyncRequest{} 87 | if err := json.Unmarshal(body, request); err != nil { 88 | http.Error(w, err.Error(), http.StatusBadRequest) 89 | return 90 | } 91 | response, err := sync(request) 92 | if err != nil { 93 | http.Error(w, err.Error(), http.StatusInternalServerError) 94 | return 95 | } 96 | body, err = json.Marshal(&response) 97 | if err != nil { 98 | http.Error(w, err.Error(), http.StatusInternalServerError) 99 | return 100 | } 101 | w.Header().Set("Content-Type", "application/json") 102 | w.Write(body) 103 | } 104 | 105 | func main() { 106 | http.HandleFunc("/sync", syncHandler) 107 | 108 | log.Fatal(http.ListenAndServe(":8080", nil)) 109 | } 110 | -------------------------------------------------------------------------------- /examples/go/my-thing.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ctl.enisoc.com/v1 2 | kind: Thing 3 | metadata: 4 | name: thing-1 5 | spec: 6 | message: Hello, World! 7 | -------------------------------------------------------------------------------- /examples/go/thing-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: things.ctl.enisoc.com 5 | spec: 6 | group: ctl.enisoc.com 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | plural: things 11 | singular: thing 12 | kind: Thing 13 | --- 14 | apiVersion: metacontroller.k8s.io/v1alpha1 15 | kind: CompositeController 16 | metadata: 17 | name: thing-controller 18 | spec: 19 | generateSelector: true 20 | parentResource: 21 | apiVersion: ctl.enisoc.com/v1 22 | resource: things 23 | childResources: 24 | - apiVersion: v1 25 | resource: pods 26 | hooks: 27 | sync: 28 | webhook: 29 | url: http://thing-controller.metacontroller/sync 30 | --- 31 | apiVersion: apps/v1 32 | kind: Deployment 33 | metadata: 34 | name: thing-controller 35 | namespace: metacontroller 36 | spec: 37 | replicas: 1 38 | selector: 39 | matchLabels: 40 | app: thing-controller 41 | template: 42 | metadata: 43 | labels: 44 | app: thing-controller 45 | spec: 46 | containers: 47 | - name: controller 48 | image: enisoc/thing-controller:latest 49 | command: ["thing-controller"] 50 | --- 51 | apiVersion: v1 52 | kind: Service 53 | metadata: 54 | name: thing-controller 55 | namespace: metacontroller 56 | spec: 57 | selector: 58 | app: thing-controller 59 | ports: 60 | - port: 80 61 | targetPort: 8080 62 | -------------------------------------------------------------------------------- /examples/indexedjob/README.md: -------------------------------------------------------------------------------- 1 | ## IndexedJob 2 | 3 | This is an example CompositeController that's similar to Job, 4 | except that each Pod gets assigned a unique index, similar to StatefulSet. 5 | 6 | ### Prerequisites 7 | 8 | * Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller) 9 | 10 | ### Deploy the controller 11 | 12 | ```sh 13 | kubectl create configmap indexedjob-controller -n metacontroller --from-file=sync.py 14 | kubectl apply -f indexedjob-controller.yaml 15 | ``` 16 | 17 | ### Create an IndexedJob 18 | 19 | ```sh 20 | kubectl apply -f my-indexedjob.yaml 21 | ``` 22 | 23 | Each Pod created should print its index: 24 | 25 | ```console 26 | $ kubectl logs print-index-2 27 | 2 28 | ``` 29 | 30 | ### Failure Policy 31 | 32 | Implementing `activeDeadlineSeconds` and `backoffLimit` is left as an exercise for the reader. 33 | -------------------------------------------------------------------------------- /examples/indexedjob/indexedjob-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: indexedjobs.ctl.enisoc.com 5 | spec: 6 | group: ctl.enisoc.com 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | plural: indexedjobs 11 | singular: indexedjob 12 | kind: IndexedJob 13 | shortNames: ["ij", "idxj"] 14 | subresources: 15 | status: {} 16 | --- 17 | apiVersion: metacontroller.k8s.io/v1alpha1 18 | kind: CompositeController 19 | metadata: 20 | name: indexedjob-controller 21 | spec: 22 | generateSelector: true 23 | parentResource: 24 | apiVersion: ctl.enisoc.com/v1 25 | resource: indexedjobs 26 | childResources: 27 | - apiVersion: v1 28 | resource: pods 29 | hooks: 30 | sync: 31 | webhook: 32 | url: http://indexedjob-controller.metacontroller/sync 33 | --- 34 | apiVersion: apps/v1beta1 35 | kind: Deployment 36 | metadata: 37 | name: indexedjob-controller 38 | namespace: metacontroller 39 | spec: 40 | replicas: 1 41 | selector: 42 | matchLabels: 43 | app: indexedjob-controller 44 | template: 45 | metadata: 46 | labels: 47 | app: indexedjob-controller 48 | spec: 49 | containers: 50 | - name: controller 51 | image: python:2.7 52 | command: ["python", "/hooks/sync.py"] 53 | volumeMounts: 54 | - name: hooks 55 | mountPath: /hooks 56 | volumes: 57 | - name: hooks 58 | configMap: 59 | name: indexedjob-controller 60 | --- 61 | apiVersion: v1 62 | kind: Service 63 | metadata: 64 | name: indexedjob-controller 65 | namespace: metacontroller 66 | spec: 67 | selector: 68 | app: indexedjob-controller 69 | ports: 70 | - port: 80 71 | -------------------------------------------------------------------------------- /examples/indexedjob/my-indexedjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ctl.enisoc.com/v1 2 | kind: IndexedJob 3 | metadata: 4 | name: print-index 5 | spec: 6 | completions: 10 7 | parallelism: 2 8 | template: 9 | metadata: 10 | labels: 11 | app: print-index 12 | spec: 13 | restartPolicy: OnFailure 14 | containers: 15 | - name: print-index 16 | image: busybox 17 | command: ["sh", "-c", "echo ${JOB_INDEX}"] 18 | -------------------------------------------------------------------------------- /examples/indexedjob/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | echo "Clean up..." 6 | kubectl delete -f my-indexedjob.yaml 7 | kubectl delete po -l app=print-index 8 | kubectl delete -f indexedjob-controller.yaml 9 | kubectl delete configmap indexedjob-controller -n metacontroller 10 | } 11 | trap cleanup EXIT 12 | 13 | set -ex 14 | 15 | ij="indexedjobs" 16 | 17 | echo "Install controller..." 18 | kubectl create configmap indexedjob-controller -n metacontroller --from-file=sync.py 19 | kubectl apply -f indexedjob-controller.yaml 20 | 21 | echo "Wait until CRD is available..." 22 | until kubectl get $ij; do sleep 1; done 23 | 24 | echo "Create an object..." 25 | kubectl apply -f my-indexedjob.yaml 26 | 27 | echo "Wait for 10 successful completions..." 28 | until [[ "$(kubectl get $ij print-index -o 'jsonpath={.status.succeeded}')" -eq 10 ]]; do sleep 1; done 29 | 30 | echo "Check that correct index is printed..." 31 | if [[ "$(kubectl logs print-index-9)" != "9" ]]; then 32 | exit 1 33 | fi 34 | -------------------------------------------------------------------------------- /examples/jsonnetd/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /jsonnetd 3 | -------------------------------------------------------------------------------- /examples/jsonnetd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.10 AS build 2 | 3 | RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 4 | 5 | COPY . /go/src/jsonnetd/ 6 | WORKDIR /go/src/jsonnetd/ 7 | RUN dep ensure && go install 8 | 9 | FROM debian:stretch-slim 10 | COPY --from=build /go/bin/jsonnetd /jsonnetd/ 11 | WORKDIR /jsonnetd 12 | ENTRYPOINT ["/jsonnetd/jsonnetd"] 13 | EXPOSE 8080 -------------------------------------------------------------------------------- /examples/jsonnetd/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/google/go-jsonnet" 7 | packages = [".","ast","parser"] 8 | revision = "c7a5b68f1c7a2f5e69736e489790f58edd27f7f3" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "dff655412715ccf4f74481ef79f3d20d587d66eb1e1b47aa715890f610302c6e" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /examples/jsonnetd/Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | -------------------------------------------------------------------------------- /examples/jsonnetd/Makefile: -------------------------------------------------------------------------------- 1 | tag = 0.1 2 | 3 | image: 4 | docker build -t metacontroller/jsonnetd:$(tag) . 5 | 6 | push: image 7 | docker push metacontroller/jsonnetd:$(tag) 8 | -------------------------------------------------------------------------------- /examples/jsonnetd/README.md: -------------------------------------------------------------------------------- 1 | ## jsonnetd 2 | 3 | This is an HTTP server that loads a directory of Jsonnet files and 4 | serves each one as a webhook. 5 | 6 | Each hook should evaluate to a Jsonnet function: 7 | 8 | ```js 9 | function(request) { 10 | // response body 11 | } 12 | ``` 13 | 14 | The body of the POST request is itself interpreted as Jsonnet 15 | and given to the hook as a top-level `request` argument. 16 | 17 | The entire result of evaluating the Jsonnet function is returned as 18 | the webhook response body, unless the function returns an error. 19 | -------------------------------------------------------------------------------- /examples/jsonnetd/extensions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "strconv" 23 | 24 | jsonnet "github.com/google/go-jsonnet" 25 | "github.com/google/go-jsonnet/ast" 26 | ) 27 | 28 | var extensions = []*jsonnet.NativeFunction{ 29 | // jsonUnmarshal adds a native function for unmarshaling JSON, 30 | // since there doesn't seem to be one in the standard library. 31 | { 32 | Name: "jsonUnmarshal", 33 | Params: ast.Identifiers{"jsonStr"}, 34 | Func: func(args []interface{}) (interface{}, error) { 35 | jsonStr, ok := args[0].(string) 36 | if !ok { 37 | return nil, fmt.Errorf("unexpected type %T for 'jsonStr' arg", args[0]) 38 | } 39 | val := make(map[string]interface{}) 40 | if err := json.Unmarshal([]byte(jsonStr), &val); err != nil { 41 | return nil, fmt.Errorf("can't unmarshal JSON: %v", err) 42 | } 43 | return val, nil 44 | }, 45 | }, 46 | 47 | // parseInt adds a native function for parsing non-decimal integers, 48 | // since there doesn't seem to be one in the standard library. 49 | { 50 | Name: "parseInt", 51 | Params: ast.Identifiers{"intStr", "base"}, 52 | Func: func(args []interface{}) (interface{}, error) { 53 | str, ok := args[0].(string) 54 | if !ok { 55 | return nil, fmt.Errorf("unexpected type %T for 'intStr' arg", args[0]) 56 | } 57 | base, ok := args[1].(float64) 58 | if !ok { 59 | return nil, fmt.Errorf("unexpected type %T for 'base' arg", args[1]) 60 | } 61 | intVal, err := strconv.ParseInt(str, int(base), 64) 62 | if err != nil { 63 | return nil, fmt.Errorf("can't parse 'intStr': %v", err) 64 | } 65 | return float64(intVal), nil 66 | }, 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /examples/jsonnetd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "io/ioutil" 23 | "log" 24 | "net/http" 25 | "os" 26 | "os/signal" 27 | "strings" 28 | "syscall" 29 | 30 | jsonnet "github.com/google/go-jsonnet" 31 | ) 32 | 33 | func main() { 34 | // Read all Jsonnet files in the working dir. 35 | files, err := ioutil.ReadDir(".") 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | for _, file := range files { 40 | filename := file.Name() 41 | if !strings.HasSuffix(filename, ".jsonnet") { 42 | continue 43 | } 44 | 45 | hookname := strings.TrimSuffix(filename, ".jsonnet") 46 | filedata, err := ioutil.ReadFile(filename) 47 | if err != nil { 48 | log.Fatalf("can't read %q: %v", filename, err) 49 | } 50 | hookcode := string(filedata) 51 | 52 | // Serve the Jsonnet file as a webhook. 53 | http.HandleFunc("/"+hookname, func(w http.ResponseWriter, r *http.Request) { 54 | // Read POST body as jsonnet input. 55 | if r.Method != http.MethodPost { 56 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 57 | return 58 | } 59 | body, err := ioutil.ReadAll(r.Body) 60 | if err != nil { 61 | http.Error(w, "Can't read body", http.StatusInternalServerError) 62 | return 63 | } 64 | 65 | // Evaluate Jsonnet hook, passing request body as a top-level argument. 66 | vm := jsonnet.MakeVM() 67 | for _, ext := range extensions { 68 | vm.NativeFunction(ext) 69 | } 70 | vm.TLACode("request", string(body)) 71 | result, err := vm.EvaluateSnippet(filename, hookcode) 72 | if err != nil { 73 | log.Printf("/%s request: %s", hookname, body) 74 | log.Printf("/%s error: %s", hookname, err) 75 | http.Error(w, err.Error(), http.StatusInternalServerError) 76 | return 77 | } 78 | w.Header().Set("Content-Type", "application/json") 79 | fmt.Fprint(w, result) 80 | }) 81 | } 82 | 83 | server := &http.Server{Addr: ":8080"} 84 | go func() { 85 | log.Fatal(server.ListenAndServe()) 86 | }() 87 | 88 | // Shutdown on SIGTERM. 89 | sigchan := make(chan os.Signal, 2) 90 | signal.Notify(sigchan, os.Interrupt, syscall.SIGTERM) 91 | sig := <-sigchan 92 | log.Printf("Received %v signal. Shutting down...", sig) 93 | server.Shutdown(context.Background()) 94 | } 95 | -------------------------------------------------------------------------------- /examples/nodejs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | COPY server.js /node/ 3 | WORKDIR /node 4 | ENTRYPOINT ["node", "/node/server.js"] 5 | -------------------------------------------------------------------------------- /examples/nodejs/Makefile: -------------------------------------------------------------------------------- 1 | tag = 0.1 2 | 3 | image: 4 | docker build -t metacontroller/nodejs-server:$(tag) . 5 | 6 | push: image 7 | docker push metacontroller/nodejs-server:$(tag) 8 | -------------------------------------------------------------------------------- /examples/nodejs/README.md: -------------------------------------------------------------------------------- 1 | ## nodejs-server 2 | 3 | This is a generic nodejs server that wraps a function written for 4 | fission's nodejs environment, making it deployable without fission. 5 | 6 | This shows that it's possible to deploy a new CompositeController instantly, 7 | without baking it into a Docker image, either with or without a system like fission. 8 | -------------------------------------------------------------------------------- /examples/nodejs/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://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 | // This is a generic nodejs server that wraps a function written for 18 | // fission's nodejs environment, making it deployable without fission. 19 | 20 | const fs = require('fs'); 21 | const path = require('path'); 22 | const http = require('http'); 23 | 24 | let hooks = {}; 25 | for (let file of fs.readdirSync('./hooks')) { 26 | if (file.endsWith('.js')) { 27 | let name = path.basename(file, '.js'); 28 | console.log('loading hook: /' + name); 29 | hooks[name] = require('./hooks/' + name); 30 | } 31 | } 32 | 33 | http.createServer((request, response) => { 34 | let hook = hooks[request.url.split('/')[1]]; 35 | if (!hook) { 36 | response.writeHead(404, {'Content-Type': 'text/plain'}); 37 | response.end('Not found'); 38 | return; 39 | } 40 | 41 | // Read the whole request body. 42 | let body = []; 43 | request.on('error', (err) => { 44 | console.error(err); 45 | }).on('data', (chunk) => { 46 | body.push(chunk); 47 | }).on('end', () => { 48 | body = Buffer.concat(body).toString(); 49 | 50 | if (request.headers['content-type'] === 'application/json') { 51 | body = JSON.parse(body); 52 | } 53 | 54 | // Emulate part of the fission.io nodejs environment, 55 | // so we can use the same sync.js file. 56 | hook({request: {body: body}}).then((result) => { 57 | response.writeHead(result.status, result.headers); 58 | let body = result.body; 59 | if (typeof body !== 'string') { 60 | body = JSON.stringify(body); 61 | } 62 | response.end(body); 63 | }, (err) => { 64 | response.writeHead(500, {'Content-Type': 'text/plain'}); 65 | response.end(err.toString()); 66 | }); 67 | }); 68 | }).listen(80); 69 | -------------------------------------------------------------------------------- /examples/service-per-pod/README.md: -------------------------------------------------------------------------------- 1 | ## Service-Per-Pod Decorator 2 | 3 | This is an example DecoratorController that adds a Service for each Pod in a 4 | StatefulSet, for any StatefulSet that requests this by adding an annotation 5 | that specifies the name of the label containing the Pod name. 6 | 7 | In Kubernetes 1.9+, StatefulSet automatically adds the Pod name as a label on 8 | each of its Pods, so you can enable Service-Per-Pod like this: 9 | 10 | ```yaml 11 | apiVersion: apps/v1beta2 12 | kind: StatefulSet 13 | metadata: 14 | annotations: 15 | service-per-pod-label: "statefulset.kubernetes.io/pod-name" 16 | service-per-pod-ports: "80:8080" 17 | ... 18 | ``` 19 | 20 | For earlier versions, this example also contains a second DecoratorController 21 | that adds the Pod name label since StatefulSet previously didn't do it. 22 | 23 | The Pod name label is only added to Pods that request it with an annotation, 24 | which you can add in the StatefulSet's Pod template: 25 | 26 | ```yaml 27 | apiVersion: apps/v1beta2 28 | kind: StatefulSet 29 | metadata: 30 | annotations: 31 | service-per-pod-label: "pod-name" 32 | service-per-pod-ports: "80:8080" 33 | ... 34 | spec: 35 | template: 36 | metadata: 37 | annotations: 38 | pod-name-label: "pod-name" 39 | ... 40 | ``` 41 | 42 | If the StatefulSet is then deleted, or if the `service-per-pod-label` annotation 43 | is removed to opt out of the decorator, any Services created will be cleaned up. 44 | 45 | ### Prerequisites 46 | 47 | * Kubernetes 1.8+ is recommended for its improved CRD support, 48 | especially garbage collection. 49 | * Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller). 50 | 51 | ### Deploy the DecoratorControllers 52 | 53 | ```sh 54 | kubectl create configmap service-per-pod-hooks -n metacontroller --from-file=hooks 55 | kubectl apply -f service-per-pod.yaml 56 | ``` 57 | 58 | ### Create an Example StatefulSet 59 | 60 | ```sh 61 | kubectl apply -f my-statefulset.yaml 62 | ``` 63 | 64 | Watch for the Services to get created: 65 | 66 | ```sh 67 | kubectl get services --watch 68 | ``` 69 | 70 | Check that the StatefulSet's Pods can be selected by `pod-name` label: 71 | 72 | ```sh 73 | kubectl get pod -l pod-name=nginx-0 74 | kubectl get pod -l pod-name=nginx-1 75 | kubectl get pod -l pod-name=nginx-2 76 | ``` 77 | 78 | Check that the per-Pod Services get cleaned up when the StatefulSet is deleted: 79 | 80 | ```sh 81 | kubectl delete -f my-statefulset.yaml 82 | kubectl get services 83 | ``` 84 | -------------------------------------------------------------------------------- /examples/service-per-pod/hooks/finalize-service-per-pod.jsonnet: -------------------------------------------------------------------------------- 1 | function(request) { 2 | // If the StatefulSet is updated to no longer match our decorator selector, 3 | // or if the StatefulSet is deleted, clean up any attachments we made. 4 | attachments: [], 5 | // Mark as finalized once we observe all Services are gone. 6 | finalized: std.length(request.attachments['Service.v1']) == 0 7 | } 8 | -------------------------------------------------------------------------------- /examples/service-per-pod/hooks/sync-pod-name-label.jsonnet: -------------------------------------------------------------------------------- 1 | function(request) { 2 | local pod = request.object, 3 | local labelKey = pod.metadata.annotations["pod-name-label"], 4 | 5 | // Inject the Pod name as a label with the key requested in the annotation. 6 | labels: { 7 | [labelKey]: pod.metadata.name 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/service-per-pod/hooks/sync-service-per-pod.jsonnet: -------------------------------------------------------------------------------- 1 | function(request) { 2 | local statefulset = request.object, 3 | local labelKey = statefulset.metadata.annotations["service-per-pod-label"], 4 | local ports = statefulset.metadata.annotations["service-per-pod-ports"], 5 | 6 | // Create a service for each Pod, with a selector on the given label key. 7 | attachments: [ 8 | { 9 | apiVersion: "v1", 10 | kind: "Service", 11 | metadata: { 12 | name: statefulset.metadata.name + "-" + index, 13 | labels: {app: "service-per-pod"} 14 | }, 15 | spec: { 16 | selector: { 17 | [labelKey]: statefulset.metadata.name + "-" + index 18 | }, 19 | ports: [ 20 | { 21 | local parts = std.split(portnums, ":"), 22 | port: std.parseInt(parts[0]), 23 | targetPort: std.parseInt(parts[1]), 24 | } 25 | for portnums in std.split(ports, ",") 26 | ] 27 | } 28 | } 29 | for index in std.range(0, statefulset.spec.replicas - 1) 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/service-per-pod/my-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nginx 5 | labels: 6 | app: nginx 7 | spec: 8 | ports: 9 | - port: 80 10 | name: web 11 | clusterIP: None 12 | selector: 13 | app: nginx 14 | --- 15 | apiVersion: apps/v1beta2 16 | kind: StatefulSet 17 | metadata: 18 | name: nginx 19 | annotations: 20 | service-per-pod-label: pod-name 21 | service-per-pod-ports: "80:80" 22 | spec: 23 | selector: 24 | matchLabels: 25 | app: nginx 26 | serviceName: nginx 27 | replicas: 3 28 | template: 29 | metadata: 30 | labels: 31 | app: nginx 32 | annotations: 33 | pod-name-label: pod-name 34 | spec: 35 | terminationGracePeriodSeconds: 1 36 | containers: 37 | - name: nginx 38 | image: gcr.io/google_containers/nginx-slim:0.8 39 | ports: 40 | - containerPort: 80 41 | name: web 42 | -------------------------------------------------------------------------------- /examples/service-per-pod/service-per-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: metacontroller.k8s.io/v1alpha1 2 | kind: DecoratorController 3 | metadata: 4 | name: service-per-pod 5 | spec: 6 | resources: 7 | - apiVersion: apps/v1beta1 8 | resource: statefulsets 9 | annotationSelector: 10 | matchExpressions: 11 | - {key: service-per-pod-label, operator: Exists} 12 | - {key: service-per-pod-ports, operator: Exists} 13 | attachments: 14 | - apiVersion: v1 15 | resource: services 16 | hooks: 17 | sync: 18 | webhook: 19 | url: http://service-per-pod.metacontroller/sync-service-per-pod 20 | finalize: 21 | webhook: 22 | url: http://service-per-pod.metacontroller/finalize-service-per-pod 23 | --- 24 | apiVersion: metacontroller.k8s.io/v1alpha1 25 | kind: DecoratorController 26 | metadata: 27 | name: pod-name-label 28 | spec: 29 | resources: 30 | - apiVersion: v1 31 | resource: pods 32 | labelSelector: 33 | matchExpressions: 34 | - {key: pod-name, operator: DoesNotExist} 35 | annotationSelector: 36 | matchExpressions: 37 | - {key: pod-name-label, operator: Exists} 38 | hooks: 39 | sync: 40 | webhook: 41 | url: http://service-per-pod.metacontroller/sync-pod-name-label 42 | --- 43 | apiVersion: apps/v1beta1 44 | kind: Deployment 45 | metadata: 46 | name: service-per-pod 47 | namespace: metacontroller 48 | spec: 49 | replicas: 1 50 | selector: 51 | matchLabels: 52 | app: service-per-pod 53 | template: 54 | metadata: 55 | labels: 56 | app: service-per-pod 57 | spec: 58 | containers: 59 | - name: hooks 60 | image: metacontroller/jsonnetd:0.1 61 | imagePullPolicy: Always 62 | workingDir: /hooks 63 | volumeMounts: 64 | - name: hooks 65 | mountPath: /hooks 66 | volumes: 67 | - name: hooks 68 | configMap: 69 | name: service-per-pod-hooks 70 | --- 71 | apiVersion: v1 72 | kind: Service 73 | metadata: 74 | name: service-per-pod 75 | namespace: metacontroller 76 | spec: 77 | selector: 78 | app: service-per-pod 79 | ports: 80 | - port: 80 81 | targetPort: 8080 82 | -------------------------------------------------------------------------------- /examples/service-per-pod/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | echo "Clean up..." 6 | kubectl patch statefulset nginx --type=merge -p '{"metadata":{"finalizers":[]}}' 7 | kubectl delete -f my-statefulset.yaml 8 | kubectl delete -f service-per-pod.yaml 9 | kubectl delete svc -l app=service-per-pod 10 | kubectl delete configmap service-per-pod-hooks -n metacontroller 11 | } 12 | trap cleanup EXIT 13 | 14 | set -ex 15 | 16 | finalizer="metacontroller.app/service-per-pod-test" 17 | 18 | echo "Install controller..." 19 | kubectl create configmap service-per-pod-hooks -n metacontroller --from-file=hooks 20 | kubectl apply -f service-per-pod.yaml 21 | 22 | echo "Create a StatefulSet..." 23 | kubectl apply -f my-statefulset.yaml 24 | 25 | echo "Wait for per-pod Service..." 26 | until [[ "$(kubectl get svc nginx-2 -o 'jsonpath={.spec.selector.pod-name}')" == "nginx-2" ]]; do sleep 1; done 27 | 28 | echo "Wait for pod-name label..." 29 | until [[ "$(kubectl get pod nginx-2 -o 'jsonpath={.metadata.labels.pod-name}')" == "nginx-2" ]]; do sleep 1; done 30 | 31 | echo "Remove annotation to opt out of service-per-pod without deleting the StatefulSet..." 32 | kubectl annotate statefulset nginx service-per-pod-label- 33 | 34 | echo "Wait for per-pod Service to get cleaned up by the decorator's finalizer..." 35 | until [[ "$(kubectl get svc nginx-2 2>&1)" == *NotFound* ]]; do sleep 1; done 36 | 37 | echo "Wait for the decorator's finalizer to be removed..." 38 | while [[ "$(kubectl get statefulset nginx -o 'jsonpath={.metadata.finalizers}')" == *decoratorcontroller-service-per-pod* ]]; do sleep 1; done 39 | 40 | echo "Add the annotation back to opt in again..." 41 | kubectl annotate statefulset nginx service-per-pod-label=pod-name 42 | 43 | echo "Wait for per-pod Service to come back..." 44 | until [[ "$(kubectl get svc nginx-2 -o 'jsonpath={.spec.selector.pod-name}')" == "nginx-2" ]]; do sleep 1; done 45 | 46 | echo "Append our own finalizer so we can check deletion ordering..." 47 | kubectl patch statefulset nginx --type=json -p '[{"op":"add","path":"/metadata/finalizers/-","value":"'${finalizer}'"}]' 48 | 49 | echo "Delete the StatefulSet..." 50 | kubectl delete statefulset nginx --wait=false 51 | 52 | echo "Wait for per-pod Service to get cleaned up by the decorator's finalizer..." 53 | until [[ "$(kubectl get svc nginx-2 2>&1)" == *NotFound* ]]; do sleep 1; done 54 | 55 | echo "Wait for the decorator's finalizer to be removed..." 56 | while [[ "$(kubectl get statefulset nginx -o 'jsonpath={.metadata.finalizers}')" == *decoratorcontroller-service-per-pod* ]]; do sleep 1; done 57 | -------------------------------------------------------------------------------- /examples/status/README.md: -------------------------------------------------------------------------------- 1 | ## Noop 2 | 3 | This is an example DecoratorController returning a status 4 | 5 | ### Prerequisites 6 | 7 | * Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller) 8 | 9 | ### Deploy the controller 10 | 11 | ```sh 12 | kubectl create configmap noop-controller -n metacontroller --from-file=sync.js 13 | kubectl apply -f noop-controller.yaml 14 | ``` 15 | 16 | ### Create a Noop 17 | 18 | ```sh 19 | kubectl apply -f my-noop.yaml 20 | ``` 21 | -------------------------------------------------------------------------------- /examples/status/my-noop.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: metacontroller.k8s.io/v1 2 | kind: Noop 3 | metadata: 4 | name: noop 5 | spec: 6 | -------------------------------------------------------------------------------- /examples/status/noop-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: noops.metacontroller.k8s.io 5 | spec: 6 | group: metacontroller.k8s.io 7 | version: v1 8 | scope: Namespaced 9 | names: 10 | plural: noops 11 | singular: noop 12 | kind: Noop 13 | subresources: 14 | status: {} 15 | --- 16 | apiVersion: metacontroller.k8s.io/v1alpha1 17 | kind: DecoratorController 18 | metadata: 19 | name: noop-controller 20 | spec: 21 | resources: 22 | - apiVersion: metacontroller.k8s.io/v1 23 | resource: noops 24 | hooks: 25 | sync: 26 | webhook: 27 | url: http://noop-controller.metacontroller/sync 28 | --- 29 | apiVersion: apps/v1beta1 30 | kind: Deployment 31 | metadata: 32 | name: noop-controller 33 | namespace: metacontroller 34 | spec: 35 | replicas: 1 36 | selector: 37 | matchLabels: 38 | app: noop-controller 39 | template: 40 | metadata: 41 | labels: 42 | app: noop-controller 43 | spec: 44 | containers: 45 | - name: controller 46 | image: metacontroller/nodejs-server:0.1 47 | imagePullPolicy: Always 48 | volumeMounts: 49 | - name: hooks 50 | mountPath: /node/hooks 51 | volumes: 52 | - name: hooks 53 | configMap: 54 | name: noop-controller 55 | 56 | --- 57 | apiVersion: v1 58 | kind: Service 59 | metadata: 60 | name: noop-controller 61 | namespace: metacontroller 62 | spec: 63 | selector: 64 | app: noop-controller 65 | ports: 66 | - port: 80 67 | -------------------------------------------------------------------------------- /examples/status/sync.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | module.exports = async function (context) { 19 | return {status: 200, body: { status: { message: "success"} }, headers: {'Content-Type': 'application/json'}}; 20 | }; 21 | -------------------------------------------------------------------------------- /examples/status/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | echo "Clean up..." 6 | kubectl delete -f my-noop.yaml 7 | kubectl delete rs,svc -l app=noop-controller 8 | kubectl delete -f noop-controller.yaml 9 | kubectl delete configmap noop-controller -n metacontroller 10 | } 11 | trap cleanup EXIT 12 | 13 | set -ex 14 | 15 | np="noops.metacontroller.k8s.io" 16 | 17 | echo "Install controller..." 18 | kubectl create configmap noop-controller -n metacontroller --from-file=sync.js 19 | kubectl apply -f noop-controller.yaml 20 | 21 | echo "Wait until CRD is available..." 22 | until kubectl get $np; do sleep 1; done 23 | 24 | echo "Create an object..." 25 | kubectl apply -f my-noop.yaml 26 | 27 | echo "Wait for status to be updated..." 28 | until [[ "$(kubectl get $np noop -o 'jsonpath={.status.message}')" == "success" ]]; do sleep 1; done 29 | -------------------------------------------------------------------------------- /examples/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script runs the smoke tests that check basic Metacontroller functionality 4 | # by running through each example controller. 5 | # 6 | # * You should only run this in a test cluster. 7 | # * You should already have Metacontroller installed in your test cluster. 8 | # * You should have kubectl in your PATH and configured for the right cluster. 9 | 10 | set -e 11 | 12 | logfile=$(mktemp) 13 | echo "Logging test output to ${logfile}" 14 | 15 | cleanup() { 16 | rm ${logfile} 17 | } 18 | trap cleanup EXIT 19 | 20 | for test in */test.sh; do 21 | echo -n "Running ${test}..." 22 | if ! (cd "$(dirname "${test}")" && ./test.sh > ${logfile} 2>&1); then 23 | echo "FAILED" 24 | cat ${logfile} 25 | echo "Test ${test} failed!" 26 | exit 1 27 | fi 28 | echo "PASSED" 29 | done 30 | -------------------------------------------------------------------------------- /examples/vitess/README.md: -------------------------------------------------------------------------------- 1 | ## Vitess Operator 2 | 3 | **NOTE: The [Vitess Operator][] has moved to its own repository, 4 | and is now maintained by the Vitess project.** 5 | 6 | [Vitess Operator]: https://github.com/vitessio/vitess-operator 7 | 8 | This is an example of an app-specific [Operator](https://coreos.com/operators/), 9 | in this case for [Vitess](http://vitess.io), built with Metacontroller. 10 | 11 | It's meant to demonstrate the following patterns: 12 | 13 | * Building an Operator for a complex, stateful application out of a set of small 14 | Lambda Controllers that each do one thing well. 15 | * In addition to presenting a k8s-style API to users, this Operator uses 16 | custom k8s API objects to coordinate within itself. 17 | * Each controller manages one layer of the hierarchical Vitess cluster topology. 18 | The user only needs to create and manage a single, top-level VitessCluster 19 | object. 20 | * Replacing static, client-side template rendering with Lambda Controllers, 21 | which can adjust based on dynamic cluster state. 22 | * Each controller aggregates status and orchestrates app-specific rolling 23 | updates for its immediate children. 24 | * The top-level object contains a continuously-updated, aggregate "Ready" 25 | condition for the whole app, and can be directly edited to trigger rolling 26 | updates throughout the app. 27 | * Using a functional-style language ([Jsonnet](http://jsonnet.org)) to 28 | define Lambda Controllers in terms of template-like transformations on JSON 29 | objects. 30 | * You can use any language to write a Lambda Controller webhook, but the 31 | functional style is a good fit for a process that conceptually consists of 32 | declarative input, declarative output, and no side effects. 33 | * As a JSON templating language, Jsonnet is a particularly good fit for 34 | generating k8s manifests, providing functionality missing from pure 35 | JavaScript, such as first-class *merge* and *deep equal* operations. 36 | * Using the "Apply" update strategy feature of CompositeController, which 37 | emulates the behavior of `kubectl apply`, except that it attempts to do 38 | pseudo-strategic merges for CRDs. 39 | 40 | See the [Vitess Operator][] repository for details. 41 | -------------------------------------------------------------------------------- /hack/get-kube-binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | # This script downloads etcd and Kubernetes binaries that are 7 | # used as part of the integration test environment, 8 | # and places them in hack/bin/. 9 | # 10 | # The integration test framework expects these binaries to be found in the PATH. 11 | 12 | # This is the kube-apiserver version to test against. 13 | KUBE_VERSION="${KUBE_VERSION:-v1.11.3}" 14 | KUBERNETES_RELEASE_URL="${KUBERNETES_RELEASE_URL:-https://dl.k8s.io}" 15 | 16 | # This should be the etcd version downloaded by kubernetes/hack/lib/etcd.sh 17 | # as of the above Kubernetes version. 18 | ETCD_VERSION="${ETCD_VERSION:-v3.2.18}" 19 | 20 | mkdir -p hack/bin 21 | cd hack/bin 22 | 23 | # Download kubectl. 24 | rm -f kubectl 25 | wget "${KUBERNETES_RELEASE_URL}/${KUBE_VERSION}/bin/linux/amd64/kubectl" 26 | chmod +x kubectl 27 | 28 | # Download kube-apiserver. 29 | rm -f kube-apiserver 30 | wget "${KUBERNETES_RELEASE_URL}/${KUBE_VERSION}/bin/linux/amd64/kube-apiserver" 31 | chmod +x kube-apiserver 32 | 33 | # Download etcd. 34 | rm -f etcd 35 | basename="etcd-${ETCD_VERSION}-linux-amd64" 36 | filename="${basename}.tar.gz" 37 | url="https://github.com/coreos/etcd/releases/download/${ETCD_VERSION}/${filename}" 38 | wget "${url}" 39 | tar -zxf "${filename}" 40 | mv "${basename}/etcd" etcd 41 | rm -rf "${basename}" "${filename}" 42 | -------------------------------------------------------------------------------- /hooks/hooks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package hooks 18 | 19 | import ( 20 | "fmt" 21 | 22 | "metacontroller.app/apis/metacontroller/v1alpha1" 23 | ) 24 | 25 | func Call(hook *v1alpha1.Hook, request interface{}, response interface{}) error { 26 | if hook.Webhook != nil { 27 | return callWebhook(hook.Webhook, request, response) 28 | } 29 | return fmt.Errorf("hook spec not defined") 30 | } 31 | -------------------------------------------------------------------------------- /hooks/webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package hooks 18 | 19 | import ( 20 | "bytes" 21 | gojson "encoding/json" 22 | "fmt" 23 | "io/ioutil" 24 | "net/http" 25 | "time" 26 | 27 | "github.com/golang/glog" 28 | "k8s.io/apimachinery/pkg/util/json" 29 | 30 | "metacontroller.app/apis/metacontroller/v1alpha1" 31 | ) 32 | 33 | func callWebhook(webhook *v1alpha1.Webhook, request interface{}, response interface{}) error { 34 | url, err := webhookURL(webhook) 35 | hookTimeout, err := webhookTimeout(webhook) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // Encode request. 41 | reqBody, err := json.Marshal(request) 42 | if err != nil { 43 | return fmt.Errorf("can't marshal request: %v", err) 44 | } 45 | if glog.V(6) { 46 | reqBodyIndent, _ := gojson.MarshalIndent(request, "", " ") 47 | glog.Infof("DEBUG: webhook url: %s request body: %s", url, reqBodyIndent) 48 | } 49 | 50 | // Send request. 51 | client := &http.Client{Timeout: hookTimeout} 52 | glog.V(6).Infof("DEBUG: webhook timeout: %v", hookTimeout) 53 | resp, err := client.Post(url, "application/json", bytes.NewReader(reqBody)) 54 | if err != nil { 55 | return fmt.Errorf("http error: %v", err) 56 | } 57 | defer resp.Body.Close() 58 | 59 | // Read response. 60 | respBody, err := ioutil.ReadAll(resp.Body) 61 | if err != nil { 62 | return fmt.Errorf("can't read response body: %v", err) 63 | } 64 | glog.V(6).Infof("DEBUG: webhook url: %s response body: %s", url, respBody) 65 | 66 | // Check status code. 67 | if resp.StatusCode != http.StatusOK { 68 | return fmt.Errorf("remote error: %s", respBody) 69 | } 70 | 71 | // Decode response. 72 | if err := json.Unmarshal(respBody, response); err != nil { 73 | return fmt.Errorf("can't unmarshal response: %v", err) 74 | } 75 | return nil 76 | } 77 | 78 | func webhookURL(webhook *v1alpha1.Webhook) (string, error) { 79 | if webhook.URL != nil { 80 | // Full URL overrides everything else. 81 | return *webhook.URL, nil 82 | } 83 | if webhook.Service == nil || webhook.Path == nil { 84 | return "", fmt.Errorf("invalid webhook config: must specify either full 'url', or both 'service' and 'path'") 85 | } 86 | 87 | // For now, just use cluster DNS to resolve Services. 88 | // If necessary, we can use a Lister to get more info about Services. 89 | if webhook.Service.Name == "" || webhook.Service.Namespace == "" { 90 | return "", fmt.Errorf("invalid client config: must specify service 'name' and 'namespace'") 91 | } 92 | port := int32(80) 93 | if webhook.Service.Port != nil { 94 | port = *webhook.Service.Port 95 | } 96 | protocol := "http" 97 | if webhook.Service.Protocol != nil { 98 | protocol = *webhook.Service.Protocol 99 | } 100 | return fmt.Sprintf("%s://%s.%s:%v%s", protocol, webhook.Service.Name, webhook.Service.Namespace, port, *webhook.Path), nil 101 | } 102 | 103 | func webhookTimeout(webhook *v1alpha1.Webhook) (time.Duration, error) { 104 | if webhook.Timeout == nil { 105 | // Defaults to 10 Seconds to preserve current behavior. 106 | return 10 * time.Second, nil 107 | } 108 | 109 | if webhook.Timeout.Duration <= 0 { 110 | // Defaults to 10 Seconds if invalid. 111 | return 10 * time.Second, fmt.Errorf("invalid client config: timeout must be a non-zero positive duration") 112 | } 113 | 114 | return webhook.Timeout.Duration, nil 115 | } 116 | -------------------------------------------------------------------------------- /kustomization.yaml: -------------------------------------------------------------------------------- 1 | commonLabels: 2 | app.kubernetes.io/name: metacontroller 3 | 4 | resources: 5 | - manifests/metacontroller-rbac.yaml 6 | - manifests/metacontroller.yaml 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "flag" 22 | "net/http" 23 | "os" 24 | "os/signal" 25 | "syscall" 26 | "time" 27 | 28 | "github.com/golang/glog" 29 | "go.opencensus.io/exporter/prometheus" 30 | "go.opencensus.io/stats/view" 31 | 32 | "k8s.io/client-go/rest" 33 | "k8s.io/client-go/tools/clientcmd" 34 | 35 | "metacontroller.app/server" 36 | 37 | _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" 38 | ) 39 | 40 | var ( 41 | discoveryInterval = flag.Duration("discovery-interval", 30*time.Second, "How often to refresh discovery cache to pick up newly-installed resources") 42 | informerRelist = flag.Duration("cache-flush-interval", 30*time.Minute, "How often to flush local caches and relist objects from the API server") 43 | debugAddr = flag.String("debug-addr", ":9999", "The address to bind the debug http endpoints") 44 | clientConfigPath = flag.String("client-config-path", "", "Path to kubeconfig file (same format as used by kubectl); if not specified, use in-cluster config") 45 | ) 46 | 47 | func main() { 48 | flag.Parse() 49 | 50 | glog.Infof("Discovery cache flush interval: %v", *discoveryInterval) 51 | glog.Infof("API server object cache flush interval: %v", *informerRelist) 52 | glog.Infof("Debug http server address: %v", *debugAddr) 53 | 54 | var config *rest.Config 55 | var err error 56 | if *clientConfigPath != "" { 57 | glog.Infof("Using current context from kubeconfig file: %v", *clientConfigPath) 58 | config, err = clientcmd.BuildConfigFromFlags("", *clientConfigPath) 59 | } else { 60 | glog.Info("No kubeconfig file specified; trying in-cluster auto-config...") 61 | config, err = rest.InClusterConfig() 62 | } 63 | if err != nil { 64 | glog.Fatal(err) 65 | } 66 | 67 | stopServer, err := server.Start(config, *discoveryInterval, *informerRelist) 68 | if err != nil { 69 | glog.Fatal(err) 70 | } 71 | 72 | exporter, err := prometheus.NewExporter(prometheus.Options{}) 73 | if err != nil { 74 | glog.Fatalf("can't create prometheus exporter: %v", err) 75 | } 76 | view.RegisterExporter(exporter) 77 | 78 | mux := http.NewServeMux() 79 | mux.Handle("/metrics", exporter) 80 | srv := &http.Server{ 81 | Addr: *debugAddr, 82 | Handler: mux, 83 | } 84 | go func() { 85 | glog.Errorf("Error serving debug endpoint: %v", srv.ListenAndServe()) 86 | }() 87 | 88 | // On SIGTERM, stop all controllers gracefully. 89 | sigchan := make(chan os.Signal, 2) 90 | signal.Notify(sigchan, os.Interrupt, syscall.SIGTERM) 91 | sig := <-sigchan 92 | glog.Infof("Received %q signal. Shutting down...", sig) 93 | 94 | stopServer() 95 | srv.Shutdown(context.Background()) 96 | } 97 | -------------------------------------------------------------------------------- /manifests/dev/args.yaml: -------------------------------------------------------------------------------- 1 | # Override args for development mode. 2 | apiVersion: apps/v1 3 | kind: StatefulSet 4 | metadata: 5 | name: metacontroller 6 | namespace: metacontroller 7 | spec: 8 | template: 9 | spec: 10 | containers: 11 | - name: metacontroller 12 | args: 13 | - --logtostderr 14 | - -v=5 15 | - --discovery-interval=5s 16 | -------------------------------------------------------------------------------- /manifests/dev/image.yaml: -------------------------------------------------------------------------------- 1 | # Override image for development mode (skaffold fills in the tag). 2 | apiVersion: apps/v1 3 | kind: StatefulSet 4 | metadata: 5 | name: metacontroller 6 | namespace: metacontroller 7 | spec: 8 | template: 9 | spec: 10 | containers: 11 | - name: metacontroller 12 | image: enisoc/metacontroller 13 | -------------------------------------------------------------------------------- /manifests/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - ../.. 3 | resources: 4 | - ../../manifests/metacontroller-namespace.yaml 5 | patches: 6 | - image.yaml 7 | - args.yaml 8 | -------------------------------------------------------------------------------- /manifests/metacontroller-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: metacontroller 5 | -------------------------------------------------------------------------------- /manifests/metacontroller-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: metacontroller 5 | namespace: metacontroller 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: metacontroller 11 | rules: 12 | - apiGroups: 13 | - "*" 14 | resources: 15 | - "*" 16 | verbs: 17 | - "*" 18 | --- 19 | apiVersion: rbac.authorization.k8s.io/v1 20 | kind: ClusterRoleBinding 21 | metadata: 22 | name: metacontroller 23 | subjects: 24 | - kind: ServiceAccount 25 | name: metacontroller 26 | namespace: metacontroller 27 | roleRef: 28 | kind: ClusterRole 29 | name: metacontroller 30 | apiGroup: rbac.authorization.k8s.io 31 | --- 32 | kind: ClusterRole 33 | apiVersion: rbac.authorization.k8s.io/v1 34 | metadata: 35 | name: aggregate-metacontroller-view 36 | labels: 37 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 38 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 39 | rbac.authorization.k8s.io/aggregate-to-view: "true" 40 | rules: 41 | - apiGroups: 42 | - metacontroller.k8s.io 43 | resources: 44 | - compositecontrollers 45 | - controllerrevisions 46 | - decoratorcontrollers 47 | verbs: 48 | - get 49 | - list 50 | - watch 51 | --- 52 | kind: ClusterRole 53 | apiVersion: rbac.authorization.k8s.io/v1 54 | metadata: 55 | name: aggregate-metacontroller-edit 56 | labels: 57 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 58 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 59 | rules: 60 | - apiGroups: 61 | - metacontroller.k8s.io 62 | resources: 63 | - controllerrevisions 64 | verbs: 65 | - create 66 | - delete 67 | - deletecollection 68 | - get 69 | - list 70 | - patch 71 | - update 72 | - watch 73 | -------------------------------------------------------------------------------- /manifests/metacontroller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: compositecontrollers.metacontroller.k8s.io 5 | spec: 6 | group: metacontroller.k8s.io 7 | version: v1alpha1 8 | scope: Cluster 9 | names: 10 | plural: compositecontrollers 11 | singular: compositecontroller 12 | kind: CompositeController 13 | shortNames: 14 | - cc 15 | - cctl 16 | --- 17 | apiVersion: apiextensions.k8s.io/v1beta1 18 | kind: CustomResourceDefinition 19 | metadata: 20 | name: decoratorcontrollers.metacontroller.k8s.io 21 | spec: 22 | group: metacontroller.k8s.io 23 | version: v1alpha1 24 | scope: Cluster 25 | names: 26 | plural: decoratorcontrollers 27 | singular: decoratorcontroller 28 | kind: DecoratorController 29 | shortNames: 30 | - dec 31 | - decorators 32 | --- 33 | apiVersion: apiextensions.k8s.io/v1beta1 34 | kind: CustomResourceDefinition 35 | metadata: 36 | name: controllerrevisions.metacontroller.k8s.io 37 | spec: 38 | group: metacontroller.k8s.io 39 | version: v1alpha1 40 | scope: Namespaced 41 | names: 42 | plural: controllerrevisions 43 | singular: controllerrevision 44 | kind: ControllerRevision 45 | --- 46 | apiVersion: apps/v1 47 | kind: StatefulSet 48 | metadata: 49 | labels: 50 | app.kubernetes.io/name: metacontroller 51 | name: metacontroller 52 | namespace: metacontroller 53 | spec: 54 | replicas: 1 55 | selector: 56 | matchLabels: 57 | app.kubernetes.io/name: metacontroller 58 | serviceName: "" 59 | template: 60 | metadata: 61 | labels: 62 | app.kubernetes.io/name: metacontroller 63 | spec: 64 | serviceAccountName: metacontroller 65 | containers: 66 | - name: metacontroller 67 | image: metacontroller/metacontroller:v0.4.0 68 | command: ["/usr/bin/metacontroller"] 69 | args: 70 | - --logtostderr 71 | - -v=4 72 | - --discovery-interval=20s 73 | volumeClaimTemplates: [] 74 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [context.production.environment] 2 | JEKYLL_ENV = "production" 3 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "fmt" 21 | "sync" 22 | "time" 23 | 24 | "github.com/0xRLG/ocworkqueue" 25 | "go.opencensus.io/stats/view" 26 | 27 | "k8s.io/client-go/discovery" 28 | "k8s.io/client-go/rest" 29 | "k8s.io/client-go/util/workqueue" 30 | 31 | "metacontroller.app/apis/metacontroller/v1alpha1" 32 | mcclientset "metacontroller.app/client/generated/clientset/internalclientset" 33 | mcinformers "metacontroller.app/client/generated/informer/externalversions" 34 | "metacontroller.app/controller/composite" 35 | "metacontroller.app/controller/decorator" 36 | dynamicclientset "metacontroller.app/dynamic/clientset" 37 | dynamicdiscovery "metacontroller.app/dynamic/discovery" 38 | dynamicinformer "metacontroller.app/dynamic/informer" 39 | 40 | _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" 41 | ) 42 | 43 | type controller interface { 44 | Start() 45 | Stop() 46 | } 47 | 48 | func Start(config *rest.Config, discoveryInterval, informerRelist time.Duration) (stop func(), err error) { 49 | // Periodically refresh discovery to pick up newly-installed resources. 50 | dc := discovery.NewDiscoveryClientForConfigOrDie(config) 51 | resources := dynamicdiscovery.NewResourceMap(dc) 52 | // We don't care about stopping this cleanly since it has no external effects. 53 | resources.Start(discoveryInterval) 54 | 55 | // Create informer factory for metacontroller API objects. 56 | mcClient, err := mcclientset.NewForConfig(config) 57 | if err != nil { 58 | return nil, fmt.Errorf("Can't create client for api %s: %v", v1alpha1.SchemeGroupVersion, err) 59 | } 60 | mcInformerFactory := mcinformers.NewSharedInformerFactory(mcClient, informerRelist) 61 | 62 | // Create dynamic clientset (factory for dynamic clients). 63 | dynClient, err := dynamicclientset.New(config, resources) 64 | if err != nil { 65 | return nil, err 66 | } 67 | // Create dynamic informer factory (for sharing dynamic informers). 68 | dynInformers := dynamicinformer.NewSharedInformerFactory(dynClient, informerRelist) 69 | 70 | workqueue.SetProvider(ocworkqueue.MetricsProvider()) 71 | view.Register(ocworkqueue.DefaultViews...) 72 | 73 | // Start metacontrollers (controllers that spawn controllers). 74 | // Each one requests the informers it needs from the factory. 75 | controllers := []controller{ 76 | composite.NewMetacontroller(resources, dynClient, dynInformers, mcInformerFactory, mcClient), 77 | decorator.NewMetacontroller(resources, dynClient, dynInformers, mcInformerFactory), 78 | } 79 | 80 | // Start all requested informers. 81 | // We don't care about stopping this cleanly since it has no external effects. 82 | mcInformerFactory.Start(nil) 83 | 84 | // Start all controllers. 85 | for _, c := range controllers { 86 | c.Start() 87 | } 88 | 89 | // Return a function that will stop all controllers. 90 | return func() { 91 | var wg sync.WaitGroup 92 | for _, c := range controllers { 93 | wg.Add(1) 94 | go func(c controller) { 95 | defer wg.Done() 96 | c.Stop() 97 | }(c) 98 | } 99 | wg.Wait() 100 | }, nil 101 | } 102 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v1beta2 2 | kind: Config 3 | build: 4 | artifacts: 5 | - image: enisoc/metacontroller 6 | docker: 7 | dockerfile: Dockerfile.dev 8 | deploy: 9 | kustomize: 10 | path: manifests/dev 11 | -------------------------------------------------------------------------------- /test/integration/framework/apiserver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package framework 18 | 19 | /* 20 | This file replaces the mechanism for starting kube-apiserver used in 21 | k8s.io/kubernetes integration tests. In k8s.io/kubernetes, the apiserver is 22 | one of the components being tested, so it makes sense that there we build it 23 | from scratch and link it into the test binary. However, here we treat the 24 | apiserver as an external component just like etcd. This avoids having to vendor 25 | and build all of Kubernetes into our test binary. 26 | */ 27 | 28 | import ( 29 | "context" 30 | "fmt" 31 | "io/ioutil" 32 | "os" 33 | "os/exec" 34 | "strconv" 35 | 36 | "k8s.io/client-go/rest" 37 | "k8s.io/klog" 38 | ) 39 | 40 | var apiserverURL = "" 41 | 42 | const installApiserver = ` 43 | Cannot find kube-apiserver, cannot run integration tests 44 | 45 | Please download kube-apiserver and ensure it is somewhere in the PATH. 46 | See hack/get-kube-binaries.sh 47 | 48 | ` 49 | 50 | // getApiserverPath returns a path to a kube-apiserver executable. 51 | func getApiserverPath() (string, error) { 52 | return exec.LookPath("kube-apiserver") 53 | } 54 | 55 | // startApiserver executes a kube-apiserver instance. 56 | // The returned function will signal the process and wait for it to exit. 57 | func startApiserver() (func(), error) { 58 | apiserverPath, err := getApiserverPath() 59 | if err != nil { 60 | fmt.Fprintf(os.Stderr, installApiserver) 61 | return nil, fmt.Errorf("could not find kube-apiserver in PATH: %v", err) 62 | } 63 | apiserverPort, err := getAvailablePort() 64 | if err != nil { 65 | return nil, fmt.Errorf("could not get a port: %v", err) 66 | } 67 | apiserverURL = fmt.Sprintf("http://127.0.0.1:%d", apiserverPort) 68 | klog.Infof("starting kube-apiserver on %s", apiserverURL) 69 | 70 | apiserverDataDir, err := ioutil.TempDir(os.TempDir(), "integration_test_apiserver_data") 71 | if err != nil { 72 | return nil, fmt.Errorf("unable to make temp kube-apiserver data dir: %v", err) 73 | } 74 | klog.Infof("storing kube-apiserver data in: %v", apiserverDataDir) 75 | 76 | ctx, cancel := context.WithCancel(context.Background()) 77 | cmd := exec.CommandContext( 78 | ctx, 79 | apiserverPath, 80 | "--cert-dir", apiserverDataDir, 81 | // Disable secure port since we don't use it, so we don't conflict with other apiservers. 82 | "--secure-port", "0", 83 | "--insecure-port", strconv.Itoa(apiserverPort), 84 | "--etcd-servers", etcdURL, 85 | ) 86 | 87 | // Uncomment these to see kube-apiserver output in test logs. 88 | // For Metacontroller tests, we generally don't expect problems at this level. 89 | //cmd.Stdout = os.Stdout 90 | //cmd.Stderr = os.Stderr 91 | 92 | stop := func() { 93 | cancel() 94 | err := cmd.Wait() 95 | klog.Infof("kube-apiserver exit status: %v", err) 96 | err = os.RemoveAll(apiserverDataDir) 97 | if err != nil { 98 | klog.Warningf("error during kube-apiserver cleanup: %v", err) 99 | } 100 | } 101 | 102 | if err := cmd.Start(); err != nil { 103 | return nil, fmt.Errorf("failed to run kube-apiserver: %v", err) 104 | } 105 | return stop, nil 106 | } 107 | 108 | // ApiserverURL returns the URL of the kube-apiserver instance started by TestMain. 109 | func ApiserverURL() string { 110 | return apiserverURL 111 | } 112 | 113 | // ApiserverConfig returns a rest.Config to connect to the test instance. 114 | func ApiserverConfig() *rest.Config { 115 | return &rest.Config{ 116 | Host: ApiserverURL(), 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/integration/framework/crd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package framework 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 | "k8s.io/apimachinery/pkg/util/json" 27 | 28 | dynamicclientset "metacontroller.app/dynamic/clientset" 29 | ) 30 | 31 | const ( 32 | // APIGroup is the group used for CRDs created as part of the test. 33 | APIGroup = "test.metacontroller.app" 34 | // APIVersion is the group-version used for CRDs created as part of the test. 35 | APIVersion = APIGroup + "/v1" 36 | ) 37 | 38 | // CreateCRD generates a quick-and-dirty CRD for use in tests, 39 | // and installs it in the test environment's API server. 40 | func (f *Fixture) CreateCRD(kind string, scope v1beta1.ResourceScope) (*v1beta1.CustomResourceDefinition, *dynamicclientset.ResourceClient) { 41 | singular := strings.ToLower(kind) 42 | plural := singular + "s" 43 | crd := &v1beta1.CustomResourceDefinition{ 44 | ObjectMeta: metav1.ObjectMeta{ 45 | Name: fmt.Sprintf("%s.%s", plural, APIGroup), 46 | }, 47 | Spec: v1beta1.CustomResourceDefinitionSpec{ 48 | Group: APIGroup, 49 | Scope: scope, 50 | Names: v1beta1.CustomResourceDefinitionNames{ 51 | Singular: singular, 52 | Plural: plural, 53 | Kind: kind, 54 | }, 55 | Versions: []v1beta1.CustomResourceDefinitionVersion{ 56 | { 57 | Name: "v1", 58 | Served: true, 59 | Storage: true, 60 | }, 61 | }, 62 | }, 63 | } 64 | crd, err := f.apiextensions.CustomResourceDefinitions().Create(crd) 65 | if err != nil { 66 | f.t.Fatal(err) 67 | } 68 | f.deferTeardown(func() error { 69 | return f.apiextensions.CustomResourceDefinitions().Delete(crd.Name, nil) 70 | }) 71 | 72 | f.t.Logf("Waiting for %v CRD to appear in API server discovery info...", kind) 73 | err = f.Wait(func() (bool, error) { 74 | return resourceMap.Get(APIVersion, plural) != nil, nil 75 | }) 76 | if err != nil { 77 | f.t.Fatal(err) 78 | } 79 | 80 | client, err := f.dynamic.Resource(APIVersion, plural) 81 | if err != nil { 82 | f.t.Fatal(err) 83 | } 84 | 85 | f.t.Logf("Waiting for %v CRD client List() to succeed...", kind) 86 | err = f.Wait(func() (bool, error) { 87 | _, err := client.List(metav1.ListOptions{}) 88 | return err == nil, err 89 | }) 90 | if err != nil { 91 | f.t.Fatal(err) 92 | } 93 | 94 | return crd, client 95 | } 96 | 97 | // UnstructuredCRD creates a new Unstructured object for the given CRD. 98 | func UnstructuredCRD(crd *v1beta1.CustomResourceDefinition, name string) *unstructured.Unstructured { 99 | obj := &unstructured.Unstructured{} 100 | obj.SetAPIVersion(crd.Spec.Group + "/" + crd.Spec.Versions[0].Name) 101 | obj.SetKind(crd.Spec.Names.Kind) 102 | obj.SetName(name) 103 | return obj 104 | } 105 | 106 | // UnstructuredJSON creates a new Unstructured object from the given JSON. 107 | // It panics on a decode error because it's meant for use with hard-coded test 108 | // data. 109 | func UnstructuredJSON(apiVersion, kind, name, jsonStr string) *unstructured.Unstructured { 110 | obj := map[string]interface{}{} 111 | if err := json.Unmarshal([]byte(jsonStr), &obj); err != nil { 112 | panic(err) 113 | } 114 | u := &unstructured.Unstructured{Object: obj} 115 | u.SetAPIVersion(apiVersion) 116 | u.SetKind(kind) 117 | u.SetName(name) 118 | return u 119 | } 120 | -------------------------------------------------------------------------------- /test/integration/framework/etcd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 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 | // This file is copied from k8s.io/kubernetes/test/integration/framework/ 18 | // to avoid vendoring the rest of the package, which depends on all of k8s. 19 | 20 | package framework 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "io/ioutil" 26 | "net" 27 | "os" 28 | "os/exec" 29 | "path/filepath" 30 | 31 | "k8s.io/klog" 32 | ) 33 | 34 | var etcdURL = "" 35 | 36 | const installEtcd = ` 37 | Cannot find etcd, cannot run integration tests 38 | 39 | Please download kube-apiserver and ensure it is somewhere in the PATH. 40 | See hack/get-kube-binaries.sh 41 | 42 | ` 43 | 44 | // getEtcdPath returns a path to an etcd executable. 45 | func getEtcdPath() (string, error) { 46 | bazelPath := filepath.Join(os.Getenv("RUNFILES_DIR"), "com_coreos_etcd/etcd") 47 | p, err := exec.LookPath(bazelPath) 48 | if err == nil { 49 | return p, nil 50 | } 51 | return exec.LookPath("etcd") 52 | } 53 | 54 | // getAvailablePort returns a TCP port that is available for binding. 55 | func getAvailablePort() (int, error) { 56 | l, err := net.Listen("tcp", ":0") 57 | if err != nil { 58 | return 0, fmt.Errorf("could not bind to a port: %v", err) 59 | } 60 | // It is possible but unlikely that someone else will bind this port before we 61 | // get a chance to use it. 62 | defer l.Close() 63 | return l.Addr().(*net.TCPAddr).Port, nil 64 | } 65 | 66 | // startEtcd executes an etcd instance. The returned function will signal the 67 | // etcd process and wait for it to exit. 68 | func startEtcd() (func(), error) { 69 | etcdPath, err := getEtcdPath() 70 | if err != nil { 71 | fmt.Fprintf(os.Stderr, installEtcd) 72 | return nil, fmt.Errorf("could not find etcd in PATH: %v", err) 73 | } 74 | etcdPort, err := getAvailablePort() 75 | if err != nil { 76 | return nil, fmt.Errorf("could not get a port: %v", err) 77 | } 78 | etcdURL = fmt.Sprintf("http://127.0.0.1:%d", etcdPort) 79 | klog.Infof("starting etcd on %s", etcdURL) 80 | 81 | etcdDataDir, err := ioutil.TempDir(os.TempDir(), "integration_test_etcd_data") 82 | if err != nil { 83 | return nil, fmt.Errorf("unable to make temp etcd data dir: %v", err) 84 | } 85 | klog.Infof("storing etcd data in: %v", etcdDataDir) 86 | 87 | ctx, cancel := context.WithCancel(context.Background()) 88 | cmd := exec.CommandContext( 89 | ctx, 90 | etcdPath, 91 | "--data-dir", etcdDataDir, 92 | "--listen-client-urls", etcdURL, 93 | "--advertise-client-urls", etcdURL, 94 | "--listen-peer-urls", "http://127.0.0.1:0", 95 | ) 96 | 97 | // Uncomment these to see etcd output in test logs. 98 | // For Metacontroller tests, we generally don't expect problems at this level. 99 | //cmd.Stdout = os.Stdout 100 | //cmd.Stderr = os.Stderr 101 | 102 | stop := func() { 103 | cancel() 104 | err := cmd.Wait() 105 | klog.Infof("etcd exit status: %v", err) 106 | err = os.RemoveAll(etcdDataDir) 107 | if err != nil { 108 | klog.Warningf("error during etcd cleanup: %v", err) 109 | } 110 | } 111 | 112 | if err := cmd.Start(); err != nil { 113 | return nil, fmt.Errorf("failed to run etcd: %v", err) 114 | } 115 | return stop, nil 116 | } 117 | -------------------------------------------------------------------------------- /test/integration/framework/webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package framework 18 | 19 | import ( 20 | "io/ioutil" 21 | "net/http" 22 | "net/http/httptest" 23 | ) 24 | 25 | // ServeWebhook is a helper for quickly creating a webhook server in tests. 26 | func (f *Fixture) ServeWebhook(handler func(request []byte) (response []byte, err error)) *httptest.Server { 27 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | if r.Method != http.MethodPost { 29 | http.Error(w, "unsupported method", http.StatusMethodNotAllowed) 30 | return 31 | } 32 | 33 | body, err := ioutil.ReadAll(r.Body) 34 | r.Body.Close() 35 | if err != nil { 36 | http.Error(w, "can't read body", http.StatusBadRequest) 37 | return 38 | } 39 | 40 | resp, err := handler(body) 41 | if err != nil { 42 | http.Error(w, err.Error(), http.StatusInternalServerError) 43 | return 44 | } 45 | w.Header().Set("Content-Type", "application/json") 46 | w.Write(resp) 47 | })) 48 | f.deferTeardown(func() error { 49 | srv.Close() 50 | return nil 51 | }) 52 | return srv 53 | } 54 | -------------------------------------------------------------------------------- /third_party/kubernetes/controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package kubernetes 15 | 16 | // This is copied from k8s.io/kubernetes to avoid a dependency on all of Kubernetes. 17 | // TODO(enisoc): Move the upstream code to somewhere better. 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/golang/glog" 23 | 24 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 25 | "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // WaitForCacheSync is a wrapper around cache.WaitForCacheSync that generates log messages 29 | // indicating that the controller identified by controllerName is waiting for syncs, followed by 30 | // either a successful or failed sync. 31 | func WaitForCacheSync(controllerName string, stopCh <-chan struct{}, cacheSyncs ...cache.InformerSynced) bool { 32 | glog.Infof("Waiting for caches to sync for %s controller", controllerName) 33 | 34 | if !cache.WaitForCacheSync(stopCh, cacheSyncs...) { 35 | utilruntime.HandleError(fmt.Errorf("Unable to sync caches for %s controller", controllerName)) 36 | return false 37 | } 38 | 39 | glog.Infof("Caches are synced for %s controller", controllerName) 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /third_party/kubernetes/pointer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package kubernetes 15 | 16 | // This is copied from k8s.io/kubernetes to avoid a dependency on all of Kubernetes. 17 | // TODO(enisoc): Move the upstream code to somewhere better. 18 | 19 | // BoolPtr returns a pointer to a bool 20 | func BoolPtr(b bool) *bool { 21 | o := b 22 | return &o 23 | } 24 | --------------------------------------------------------------------------------