├── examples ├── jsonnetd │ ├── .gitignore │ ├── Makefile │ ├── Dockerfile │ ├── Gopkg.lock │ ├── README.md │ ├── Gopkg.toml │ ├── extensions.go │ └── main.go ├── status │ ├── my-noop.yaml │ ├── README.md │ ├── sync.js │ ├── test.sh │ └── noop-controller.yaml ├── nodejs │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ └── server.js ├── clusteredparent │ ├── my-clusterrole.yaml │ ├── README.md │ ├── test.sh │ ├── cluster-parent.yaml │ └── sync.py ├── service-per-pod │ ├── hooks │ │ ├── sync-pod-name-label.jsonnet │ │ ├── finalize-service-per-pod.jsonnet │ │ └── sync-service-per-pod.jsonnet │ ├── my-statefulset.yaml │ ├── service-per-pod.yaml │ ├── README.md │ └── test.sh ├── crd-roles │ ├── my-crd.yaml │ ├── test.sh │ ├── README.md │ ├── crd-role-controller.yaml │ └── sync.py ├── indexedjob │ ├── my-indexedjob.yaml │ ├── README.md │ ├── test.sh │ ├── indexedjob-controller.yaml │ └── sync.py ├── bluegreen │ ├── my-bluegreen.yaml │ ├── README.md │ ├── bluegreen-controller.yaml │ └── test.sh ├── test.sh ├── catset │ ├── README.md │ ├── my-catset.yaml │ ├── catset-controller.yaml │ └── test.sh └── vitess │ └── README.md ├── docs ├── .gitignore ├── _redirects ├── design.md ├── pronunciation.md ├── api.md ├── guide.md ├── _includes │ └── footer.html ├── go-import.html ├── 404.html ├── contrib.md ├── assets │ └── css │ │ └── main.scss ├── _data │ └── navigation.yml ├── _api │ ├── hook.md │ └── controllerrevision.md ├── _guide │ ├── install.md │ ├── troubleshooting.md │ └── best-practices.md ├── _config.yml └── examples.md ├── netlify.toml ├── .gitignore ├── manifests ├── dev │ ├── kustomization.yaml │ ├── image.yaml │ └── args.yaml ├── metacontroller-rbac.yaml └── metacontroller.yaml ├── kustomization.yaml ├── .dockerignore ├── code-of-conduct.md ├── skaffold.yaml ├── .travis.yml ├── Dockerfile ├── Dockerfile.dev ├── apis └── metacontroller │ └── v1alpha1 │ ├── doc.go │ ├── register.go │ └── roundtrip_test.go ├── client └── generated │ ├── clientset │ └── internalclientset │ │ ├── doc.go │ │ ├── scheme │ │ ├── doc.go │ │ └── register.go │ │ ├── typed │ │ └── metacontroller │ │ │ └── v1alpha1 │ │ │ ├── doc.go │ │ │ ├── generated_expansion.go │ │ │ ├── controllerrevision_expansion.go │ │ │ └── metacontroller_client.go │ │ └── clientset.go │ ├── lister │ └── metacontroller │ │ └── v1alpha1 │ │ ├── expansion_generated.go │ │ ├── compositecontroller.go │ │ ├── decoratorcontroller.go │ │ └── controllerrevision.go │ └── informer │ └── externalversions │ ├── internalinterfaces │ └── factory_interfaces.go │ ├── metacontroller │ ├── interface.go │ └── v1alpha1 │ │ ├── interface.go │ │ ├── compositecontroller.go │ │ ├── decoratorcontroller.go │ │ └── controllerrevision.go │ └── generic.go ├── third_party └── kubernetes │ ├── pointer.go │ └── controller.go ├── hooks ├── hooks.go └── webhook.go ├── Gemfile ├── CONTRIBUTING.md ├── hack └── get-kube-binaries.sh ├── README.md ├── test └── integration │ ├── framework │ ├── webhook.go │ ├── metacontroller.go │ ├── crd.go │ ├── etcd.go │ ├── apiserver.go │ └── main.go │ └── composite │ └── composite_test.go ├── dynamic ├── controllerref │ └── controller_ref.go ├── object │ ├── metadata.go │ └── status.go └── lister │ └── lister.go ├── Gopkg.toml ├── controller ├── common │ ├── manage_children_test.go │ └── finalizer │ │ └── finalizer.go ├── composite │ └── hooks.go └── decorator │ ├── hooks.go │ └── selector.go ├── Makefile ├── main.go ├── Gemfile.lock └── server └── server.go /examples/jsonnetd/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /jsonnetd 3 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [context.production.environment] 2 | JEKYLL_ENV = "production" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | /metacontroller 4 | /hack/bin/ 5 | .*.swp 6 | .history 7 | -------------------------------------------------------------------------------- /manifests/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - ../.. 3 | patches: 4 | - image.yaml 5 | - args.yaml 6 | -------------------------------------------------------------------------------- /examples/status/my-noop.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: metacontroller.k8s.io/v1 2 | kind: Noop 3 | metadata: 4 | name: noop 5 | spec: 6 | -------------------------------------------------------------------------------- /examples/nodejs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | COPY server.js /node/ 3 | WORKDIR /node 4 | ENTRYPOINT ["node", "/node/server.js"] 5 | -------------------------------------------------------------------------------- /docs/_redirects: -------------------------------------------------------------------------------- 1 | https://metacontroller.netlify.com/* https://metacontroller.app/:splat 301! 2 | /* go-get=1 /go-import.html 200! 3 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Design Docs 3 | layout: collection 4 | collection: design 5 | sort_by: title 6 | permalink: /design/ 7 | --- 8 | -------------------------------------------------------------------------------- /kustomization.yaml: -------------------------------------------------------------------------------- 1 | commonLabels: 2 | app: metacontroller 3 | resources: 4 | - manifests/metacontroller-rbac.yaml 5 | - manifests/metacontroller.yaml 6 | -------------------------------------------------------------------------------- /docs/pronunciation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to pronounce Metacontroller 3 | permalink: /pronunciation/ 4 | --- 5 | *Metacontroller* is pronounced as *me-ta-con-trol-ler*. -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /docs/go-import.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /manifests/metacontroller-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: metacontroller 5 | --- 6 | apiVersion: v1 7 | kind: ServiceAccount 8 | metadata: 9 | name: metacontroller 10 | namespace: metacontroller 11 | --- 12 | apiVersion: rbac.authorization.k8s.io/v1 13 | kind: ClusterRole 14 | metadata: 15 | name: metacontroller 16 | rules: 17 | - apiGroups: 18 | - "*" 19 | resources: 20 | - "*" 21 | verbs: 22 | - "*" 23 | --- 24 | apiVersion: rbac.authorization.k8s.io/v1 25 | kind: ClusterRoleBinding 26 | metadata: 27 | name: metacontroller 28 | subjects: 29 | - kind: ServiceAccount 30 | name: metacontroller 31 | namespace: metacontroller 32 | roleRef: 33 | kind: ClusterRole 34 | name: metacontroller 35 | apiGroup: rbac.authorization.k8s.io 36 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Metacontroller 2 | 3 | Metacontroller is an add-on for Kubernetes 4 | that makes it easy to write and deploy [custom controllers](https://kubernetes.io/docs/concepts/api-extension/custom-resources/#custom-controllers) 5 | in the form of [simple scripts](https://metacontroller.app). 6 | 7 | This is not an officially supported Google product. 8 | Although this open-source project was started by [GKE](https://cloud.google.com/kubernetes-engine/), 9 | the add-on works the same in any Kubernetes cluster. 10 | 11 | ## Documentation 12 | 13 | Please see the [documentation site](https://metacontroller.app) for details 14 | on how to install, use, or contribute to Metacontroller. 15 | 16 | ## Contact 17 | 18 | Please file [GitHub issues](issues) for bugs, feature requests, and proposals. 19 | 20 | Use the [mailing list](https://groups.google.com/forum/#!forum/metacontroller) 21 | for questions and comments, or join the 22 | [#metacontroller](https://kubernetes.slack.com/messages/metacontroller/) channel on 23 | [Kubernetes Slack](http://slack.kubernetes.io). 24 | 25 | Subscribe to the [announce list](https://groups.google.com/forum/#!forum/metacontroller-announce) 26 | for low-frequency project updates like new releases. 27 | 28 | ## Contributing 29 | 30 | See [CONTRIBUTING.md](CONTRIBUTING.md) and the 31 | [contributor guide](https://metacontroller.app/contrib/). 32 | 33 | ## Licensing 34 | 35 | This project is licensed under the [Apache License 2.0](LICENSE). 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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`. | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | [[constraint]] 32 | name = "k8s.io/client-go" 33 | version = "8.0.0" 34 | 35 | [[constraint]] 36 | name = "k8s.io/apimachinery" 37 | version = "kubernetes-1.11.0" 38 | 39 | [[constraint]] 40 | name = "k8s.io/apiextensions-apiserver" 41 | version = "kubernetes-1.11.0" 42 | 43 | [[constraint]] 44 | name = "k8s.io/code-generator" 45 | version = "kubernetes-1.11.0" 46 | 47 | [[override]] 48 | name = "github.com/json-iterator/go" 49 | version = "1.1.5" # same minor version track as used by apimachinery@kubernetes-1.11.0 50 | 51 | [[override]] 52 | name = "github.com/prometheus/client_model" 53 | revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" 54 | # last version compatable with golang/protobuf@v1.0.0 (the version used by apimachinery@kubernetes-1.9.9) 55 | 56 | [prune] 57 | go-tests = true 58 | unused-packages = true 59 | 60 | [[prune.project]] 61 | name = "k8s.io/code-generator" 62 | unused-packages = false 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | name: metacontroller 50 | namespace: metacontroller 51 | labels: 52 | app: metacontroller 53 | spec: 54 | replicas: 1 55 | selector: 56 | matchLabels: 57 | app: metacontroller 58 | serviceName: "" 59 | template: 60 | metadata: 61 | labels: 62 | app: metacontroller 63 | spec: 64 | serviceAccountName: metacontroller 65 | containers: 66 | - name: metacontroller 67 | image: metacontroller/metacontroller:v0.3.1 68 | command: ["/usr/bin/metacontroller"] 69 | args: 70 | - --logtostderr 71 | - -v=4 72 | - --discovery-interval=20s 73 | volumeClaimTemplates: [] 74 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/)" ; \ 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 | -------------------------------------------------------------------------------- /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, service account, and role/binding. 30 | kubectl apply -f {{ site.repo_raw }}/manifests/metacontroller-rbac.yaml 31 | # Create CRDs for Metacontroller APIs, and the Metacontroller StatefulSet. 32 | kubectl apply -f {{ site.repo_raw }}/manifests/metacontroller.yaml 33 | ``` 34 | 35 | If you prefer to build and host your own images, please see the 36 | [build instructions](/contrib/build/) in the contributor guide. 37 | 38 | ## Configuration 39 | 40 | The Metacontroller server has a few settings that can be configured 41 | with command-line flags (by editing the Metacontroller StatefulSet 42 | in `manifests/metacontroller.yaml`): 43 | 44 | | Flag | Description | 45 | | ---- | ----------- | 46 | | `-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. | 47 | | `--discovery-interval` | How often to refresh discovery cache to pick up newly-installed resources (e.g. `--discovery-interval=10s`). | 48 | | `--cache-flush-interval` | How often to flush local caches and relist objects from the API server (e.g. `--cache-flush-interval=30m`). | 49 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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-116602056-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 | -------------------------------------------------------------------------------- /test/integration/framework/metacontroller.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 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 23 | 24 | "metacontroller.app/apis/metacontroller/v1alpha1" 25 | ) 26 | 27 | // CreateCompositeController generates a test CompositeController and installs 28 | // it in the test API server. 29 | func (f *Fixture) CreateCompositeController(name, syncHookURL string, parentCRD, childCRD *apiextensions.CustomResourceDefinition) *v1alpha1.CompositeController { 30 | cc := &v1alpha1.CompositeController{ 31 | ObjectMeta: metav1.ObjectMeta{ 32 | Namespace: "default", 33 | Name: "cc", 34 | }, 35 | Spec: v1alpha1.CompositeControllerSpec{ 36 | ParentResource: v1alpha1.CompositeControllerParentResourceRule{ 37 | ResourceRule: v1alpha1.ResourceRule{ 38 | APIVersion: parentCRD.Spec.Group + "/" + parentCRD.Spec.Versions[0].Name, 39 | Resource: parentCRD.Spec.Names.Plural, 40 | }, 41 | }, 42 | ChildResources: []v1alpha1.CompositeControllerChildResourceRule{ 43 | { 44 | ResourceRule: v1alpha1.ResourceRule{ 45 | APIVersion: childCRD.Spec.Group + "/" + childCRD.Spec.Versions[0].Name, 46 | Resource: childCRD.Spec.Names.Plural, 47 | }, 48 | }, 49 | }, 50 | Hooks: &v1alpha1.CompositeControllerHooks{ 51 | Sync: &v1alpha1.Hook{ 52 | Webhook: &v1alpha1.Webhook{ 53 | URL: &syncHookURL, 54 | }, 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | cc, err := f.metacontroller.MetacontrollerV1alpha1().CompositeControllers().Create(cc) 61 | if err != nil { 62 | f.t.Fatal(err) 63 | } 64 | f.deferTeardown(func() error { 65 | return f.metacontroller.MetacontrollerV1alpha1().CompositeControllers().Delete(cc.Name, nil) 66 | }) 67 | 68 | return cc 69 | } 70 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | // Finalized is only used by the finalize hook. 43 | Finalized bool `json:"finalized"` 44 | } 45 | 46 | func callSyncHook(cc *v1alpha1.CompositeController, request *SyncHookRequest) (*SyncHookResponse, error) { 47 | if cc.Spec.Hooks == nil { 48 | return nil, fmt.Errorf("no hooks defined") 49 | } 50 | 51 | var response SyncHookResponse 52 | 53 | // First check if we should instead call the finalize hook, 54 | // which has the same API as the sync hook except that it's 55 | // called while the object is pending deletion. 56 | if request.Parent.GetDeletionTimestamp() != nil && cc.Spec.Hooks.Finalize != nil { 57 | // Finalize 58 | request.Finalizing = true 59 | if err := hooks.Call(cc.Spec.Hooks.Finalize, request, &response); err != nil { 60 | return nil, fmt.Errorf("finalize hook failed: %v", err) 61 | } 62 | } else { 63 | // Sync 64 | request.Finalizing = false 65 | if cc.Spec.Hooks.Sync == nil { 66 | return nil, fmt.Errorf("sync hook not defined") 67 | } 68 | 69 | if err := hooks.Call(cc.Spec.Hooks.Sync, request, &response); err != nil { 70 | return nil, fmt.Errorf("sync hook failed: %v", err) 71 | } 72 | } 73 | 74 | return &response, nil 75 | } 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/integration/composite/composite_test.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 composite 18 | 19 | import ( 20 | "testing" 21 | 22 | apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "k8s.io/apimachinery/pkg/util/json" 26 | 27 | "metacontroller.app/controller/composite" 28 | "metacontroller.app/test/integration/framework" 29 | ) 30 | 31 | func TestMain(m *testing.M) { 32 | framework.TestMain(m.Run) 33 | } 34 | 35 | // TestSyncWebhook tests that the sync webhook triggers and passes the 36 | // resquest/response properly. 37 | func TestSyncWebhook(t *testing.T) { 38 | ns := "test-sync-webhook" 39 | labels := map[string]string{ 40 | "test": "test", 41 | } 42 | 43 | f := framework.NewFixture(t) 44 | defer f.TearDown() 45 | 46 | f.CreateNamespace(ns) 47 | parentCRD, parentClient := f.CreateCRD("Parent", apiextensions.NamespaceScoped) 48 | childCRD, childClient := f.CreateCRD("Child", apiextensions.NamespaceScoped) 49 | 50 | hook := f.ServeWebhook(func(body []byte) ([]byte, error) { 51 | req := composite.SyncHookRequest{} 52 | if err := json.Unmarshal(body, &req); err != nil { 53 | return nil, err 54 | } 55 | // As a simple test of request/response content, 56 | // just create a child with the same name as the parent. 57 | child := framework.UnstructuredCRD(childCRD, req.Parent.GetName()) 58 | child.SetLabels(labels) 59 | resp := composite.SyncHookResponse{ 60 | Children: []*unstructured.Unstructured{child}, 61 | } 62 | return json.Marshal(resp) 63 | }) 64 | 65 | f.CreateCompositeController("cc", hook.URL, parentCRD, childCRD) 66 | 67 | parent := framework.UnstructuredCRD(parentCRD, "test-sync-webhook") 68 | unstructured.SetNestedStringMap(parent.Object, labels, "spec", "selector", "matchLabels") 69 | _, err := parentClient.Namespace(ns).Create(parent) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | t.Logf("Waiting for child object to be created...") 75 | err = f.Wait(func() (bool, error) { 76 | _, err = childClient.Namespace(ns).Get("test-sync-webhook", metav1.GetOptions{}) 77 | return err == nil, err 78 | }) 79 | if err != nil { 80 | t.Errorf("didn't find expected child: %v", err) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | // Finalized is only used by the finalize hook. 45 | Finalized bool `json:"finalized"` 46 | } 47 | 48 | func (c *decoratorController) callSyncHook(request *SyncHookRequest) (*SyncHookResponse, error) { 49 | if c.dc.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 | // 59 | // In addition to finalizing when the object is deleted, we also finalize 60 | // when the object no longer matches our decorator selector. 61 | // This allows the decorator to clean up after itself if the object has been 62 | // updated to disable the functionality added by the decorator. 63 | if c.dc.Spec.Hooks.Finalize != nil && 64 | (request.Object.GetDeletionTimestamp() != nil || !c.parentSelector.Matches(request.Object)) { 65 | // Finalize 66 | request.Finalizing = true 67 | if err := hooks.Call(c.dc.Spec.Hooks.Finalize, request, &response); err != nil { 68 | return nil, fmt.Errorf("finalize hook failed: %v", err) 69 | } 70 | } else { 71 | // Sync 72 | request.Finalizing = false 73 | if c.dc.Spec.Hooks.Sync == nil { 74 | return nil, fmt.Errorf("sync hook not defined") 75 | } 76 | 77 | if err := hooks.Call(c.dc.Spec.Hooks.Sync, request, &response); err != nil { 78 | return nil, fmt.Errorf("sync hook failed: %v", err) 79 | } 80 | } 81 | 82 | return &response, nil 83 | } 84 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | 27 | dynamicclientset "metacontroller.app/dynamic/clientset" 28 | ) 29 | 30 | const ( 31 | // APIGroup is the group used for CRDs created as part of the test. 32 | APIGroup = "test.metacontroller.app" 33 | // APIVersion is the group-version used for CRDs created as part of the test. 34 | APIVersion = APIGroup + "/v1" 35 | ) 36 | 37 | // CreateCRD generates a quick-and-dirty CRD for use in tests, 38 | // and installs it in the test environment's API server. 39 | func (f *Fixture) CreateCRD(kind string, scope v1beta1.ResourceScope) (*v1beta1.CustomResourceDefinition, *dynamicclientset.ResourceClient) { 40 | singular := strings.ToLower(kind) 41 | plural := singular + "s" 42 | crd := &v1beta1.CustomResourceDefinition{ 43 | ObjectMeta: metav1.ObjectMeta{ 44 | Name: fmt.Sprintf("%s.%s", plural, APIGroup), 45 | }, 46 | Spec: v1beta1.CustomResourceDefinitionSpec{ 47 | Group: APIGroup, 48 | Scope: scope, 49 | Names: v1beta1.CustomResourceDefinitionNames{ 50 | Singular: singular, 51 | Plural: plural, 52 | Kind: kind, 53 | }, 54 | Versions: []v1beta1.CustomResourceDefinitionVersion{ 55 | { 56 | Name: "v1", 57 | Served: true, 58 | Storage: true, 59 | }, 60 | }, 61 | }, 62 | } 63 | crd, err := f.apiextensions.CustomResourceDefinitions().Create(crd) 64 | if err != nil { 65 | f.t.Fatal(err) 66 | } 67 | f.deferTeardown(func() error { 68 | return f.apiextensions.CustomResourceDefinitions().Delete(crd.Name, nil) 69 | }) 70 | 71 | f.t.Logf("Waiting for %v CRD to appear in API server discovery info...", kind) 72 | err = f.Wait(func() (bool, error) { 73 | return resourceMap.Get(APIVersion, plural) != nil, nil 74 | }) 75 | if err != nil { 76 | f.t.Fatal(err) 77 | } 78 | 79 | client, err := f.dynamic.Resource(APIVersion, plural) 80 | if err != nil { 81 | f.t.Fatal(err) 82 | } 83 | 84 | f.t.Logf("Waiting for %v CRD client List() to succeed...", kind) 85 | err = f.Wait(func() (bool, error) { 86 | _, err := client.List(metav1.ListOptions{}) 87 | return err == nil, err 88 | }) 89 | if err != nil { 90 | f.t.Fatal(err) 91 | } 92 | 93 | return crd, client 94 | } 95 | 96 | // UnstructuredCRD creates a new Unstructured object for the given CRD. 97 | func UnstructuredCRD(crd *v1beta1.CustomResourceDefinition, name string) *unstructured.Unstructured { 98 | obj := &unstructured.Unstructured{} 99 | obj.SetAPIVersion(crd.Spec.Group + "/" + crd.Spec.Versions[0].Name) 100 | obj.SetKind(crd.Spec.Names.Kind) 101 | obj.SetName(name) 102 | return obj 103 | } 104 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "--insecure-port", strconv.Itoa(apiserverPort), 82 | "--etcd-servers", etcdURL, 83 | ) 84 | 85 | // Uncomment these to see kube-apiserver output in test logs. 86 | // For Metacontroller tests, we generally don't expect problems at this level. 87 | //cmd.Stdout = os.Stdout 88 | //cmd.Stderr = os.Stderr 89 | 90 | stop := func() { 91 | cancel() 92 | err := cmd.Wait() 93 | klog.Infof("kube-apiserver exit status: %v", err) 94 | err = os.RemoveAll(apiserverDataDir) 95 | if err != nil { 96 | klog.Warningf("error during kube-apiserver cleanup: %v", err) 97 | } 98 | } 99 | 100 | if err := cmd.Start(); err != nil { 101 | return nil, fmt.Errorf("failed to run kube-apiserver: %v", err) 102 | } 103 | return stop, nil 104 | } 105 | 106 | // ApiserverURL returns the URL of the kube-apiserver instance started by TestMain. 107 | func ApiserverURL() string { 108 | return apiserverURL 109 | } 110 | 111 | // ApiserverConfig returns a rest.Config to connect to the test instance. 112 | func ApiserverConfig() *rest.Config { 113 | return &rest.Config{ 114 | Host: ApiserverURL(), 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /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/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/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 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 | // DecoratorControllerInformer provides access to a shared informer and lister for 35 | // DecoratorControllers. 36 | type DecoratorControllerInformer interface { 37 | Informer() cache.SharedIndexInformer 38 | Lister() v1alpha1.DecoratorControllerLister 39 | } 40 | 41 | type decoratorControllerInformer struct { 42 | factory internalinterfaces.SharedInformerFactory 43 | tweakListOptions internalinterfaces.TweakListOptionsFunc 44 | } 45 | 46 | // NewDecoratorControllerInformer constructs a new informer for DecoratorController 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 NewDecoratorControllerInformer(client internalclientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 50 | return NewFilteredDecoratorControllerInformer(client, resyncPeriod, indexers, nil) 51 | } 52 | 53 | // NewFilteredDecoratorControllerInformer constructs a new informer for DecoratorController 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 NewFilteredDecoratorControllerInformer(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().DecoratorControllers().List(options) 64 | }, 65 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 66 | if tweakListOptions != nil { 67 | tweakListOptions(&options) 68 | } 69 | return client.MetacontrollerV1alpha1().DecoratorControllers().Watch(options) 70 | }, 71 | }, 72 | &metacontrollerv1alpha1.DecoratorController{}, 73 | resyncPeriod, 74 | indexers, 75 | ) 76 | } 77 | 78 | func (f *decoratorControllerInformer) defaultInformer(client internalclientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 79 | return NewFilteredDecoratorControllerInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 80 | } 81 | 82 | func (f *decoratorControllerInformer) Informer() cache.SharedIndexInformer { 83 | return f.factory.InformerFor(&metacontrollerv1alpha1.DecoratorController{}, f.defaultInformer) 84 | } 85 | 86 | func (f *decoratorControllerInformer) Lister() v1alpha1.DecoratorControllerLister { 87 | return v1alpha1.NewDecoratorControllerLister(f.Informer().GetIndexer()) 88 | } 89 | -------------------------------------------------------------------------------- /examples/indexedjob/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 | import copy 20 | import re 21 | 22 | def is_job_finished(job): 23 | for condition in job.get('status', {}).get('conditions', []): 24 | if (condition['type'] == 'Complete' or condition['type'] == 'Failed') and condition['status'] == 'True': 25 | return True 26 | return False 27 | 28 | def get_index(base_name, name): 29 | m = re.match(r'^(.*)-(\d+)$', name) 30 | if m and m.group(1) == base_name: 31 | return int(m.group(2)) 32 | return -1 33 | 34 | def new_pod(job, index): 35 | pod = copy.deepcopy(job['spec']['template']) 36 | pod['apiVersion'] = 'v1' 37 | pod['kind'] = 'Pod' 38 | pod['metadata'] = pod.get('metadata', {}) 39 | pod['metadata']['name'] = '%s-%d' % (job['metadata']['name'], index) 40 | 41 | # Add env var to every container. 42 | for container in pod['spec']['containers']: 43 | env = container.get('env', []) 44 | env.append({'name': 'JOB_INDEX', 'value': str(index)}) 45 | container['env'] = env 46 | 47 | return pod 48 | 49 | class Controller(BaseHTTPRequestHandler): 50 | def sync(self, job, children): 51 | # Arrange observed Pods by index, and count by phase. 52 | observed_pods = {} 53 | (active, succeeded, failed) = (0, 0, 0) 54 | for pod_name, pod in children['Pod.v1'].iteritems(): 55 | pod_index = get_index(job['metadata']['name'], pod_name) 56 | if pod_index >= 0: 57 | phase = pod.get('status', {}).get('phase') 58 | if phase == 'Succeeded': 59 | succeeded += 1 60 | elif phase == 'Failed': 61 | failed += 1 62 | else: 63 | active += 1 64 | observed_pods[pod_index] = pod 65 | 66 | # If the job already finished (either completed or failed) at some point, 67 | # stop actively managing Pods since they might get deleted by Pod GC. 68 | # Just generate a desired state for any observed Pods and return status. 69 | if is_job_finished(job): 70 | return { 71 | 'status': job['status'], 72 | 'children': [new_pod(job, i) for i, pod in observed_pods.iteritems()] 73 | } 74 | 75 | # Compute status based on what we observed, before building desired state. 76 | spec_completions = job['spec'].get('completions', 1) 77 | desired_status = {'active': active, 'succeeded': succeeded, 'failed': failed} 78 | desired_status['conditions'] = [{'type': 'Complete', 'status': str(succeeded == spec_completions)}] 79 | 80 | # Generate desired state for existing Pods. 81 | desired_pods = {} 82 | for pod_index, pod in observed_pods.iteritems(): 83 | desired_pods[pod_index] = new_pod(job, pod_index) 84 | 85 | # Create more Pods as needed. 86 | spec_parallelism = job['spec'].get('parallelism', 1) 87 | for pod_index in xrange(spec_completions): 88 | if pod_index not in desired_pods and active < spec_parallelism: 89 | desired_pods[pod_index] = new_pod(job, pod_index) 90 | active += 1 91 | 92 | return {'status': desired_status, 'children': desired_pods.values()} 93 | 94 | 95 | def do_POST(self): 96 | observed = json.loads(self.rfile.read(int(self.headers.getheader('content-length')))) 97 | desired = self.sync(observed['parent'], observed['children']) 98 | 99 | self.send_response(200) 100 | self.send_header('Content-type', 'application/json') 101 | self.end_headers() 102 | self.wfile.write(json.dumps(desired)) 103 | 104 | HTTPServer(('', 80), Controller).serve_forever() 105 | -------------------------------------------------------------------------------- /client/generated/informer/externalversions/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 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 | // ControllerRevisionInformer provides access to a shared informer and lister for 35 | // ControllerRevisions. 36 | type ControllerRevisionInformer interface { 37 | Informer() cache.SharedIndexInformer 38 | Lister() v1alpha1.ControllerRevisionLister 39 | } 40 | 41 | type controllerRevisionInformer struct { 42 | factory internalinterfaces.SharedInformerFactory 43 | tweakListOptions internalinterfaces.TweakListOptionsFunc 44 | namespace string 45 | } 46 | 47 | // NewControllerRevisionInformer constructs a new informer for ControllerRevision type. 48 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 49 | // one. This reduces memory footprint and number of connections to the server. 50 | func NewControllerRevisionInformer(client internalclientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 51 | return NewFilteredControllerRevisionInformer(client, namespace, resyncPeriod, indexers, nil) 52 | } 53 | 54 | // NewFilteredControllerRevisionInformer constructs a new informer for ControllerRevision type. 55 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 56 | // one. This reduces memory footprint and number of connections to the server. 57 | func NewFilteredControllerRevisionInformer(client internalclientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 58 | return cache.NewSharedIndexInformer( 59 | &cache.ListWatch{ 60 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 61 | if tweakListOptions != nil { 62 | tweakListOptions(&options) 63 | } 64 | return client.MetacontrollerV1alpha1().ControllerRevisions(namespace).List(options) 65 | }, 66 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 67 | if tweakListOptions != nil { 68 | tweakListOptions(&options) 69 | } 70 | return client.MetacontrollerV1alpha1().ControllerRevisions(namespace).Watch(options) 71 | }, 72 | }, 73 | &metacontrollerv1alpha1.ControllerRevision{}, 74 | resyncPeriod, 75 | indexers, 76 | ) 77 | } 78 | 79 | func (f *controllerRevisionInformer) defaultInformer(client internalclientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 80 | return NewFilteredControllerRevisionInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 81 | } 82 | 83 | func (f *controllerRevisionInformer) Informer() cache.SharedIndexInformer { 84 | return f.factory.InformerFor(&metacontrollerv1alpha1.ControllerRevision{}, f.defaultInformer) 85 | } 86 | 87 | func (f *controllerRevisionInformer) Lister() v1alpha1.ControllerRevisionLister { 88 | return v1alpha1.NewControllerRevisionLister(f.Informer().GetIndexer()) 89 | } 90 | -------------------------------------------------------------------------------- /controller/decorator/selector.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 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | "k8s.io/apimachinery/pkg/labels" 25 | 26 | "metacontroller.app/apis/metacontroller/v1alpha1" 27 | "metacontroller.app/controller/common" 28 | dynamicdiscovery "metacontroller.app/dynamic/discovery" 29 | ) 30 | 31 | type decoratorSelector struct { 32 | labelSelectors map[string]labels.Selector 33 | annotationSelectors map[string]labels.Selector 34 | } 35 | 36 | func newDecoratorSelector(resources *dynamicdiscovery.ResourceMap, dc *v1alpha1.DecoratorController) (*decoratorSelector, error) { 37 | ds := &decoratorSelector{ 38 | labelSelectors: make(map[string]labels.Selector), 39 | annotationSelectors: make(map[string]labels.Selector), 40 | } 41 | var err error 42 | 43 | for _, parent := range dc.Spec.Resources { 44 | // Keep the map by Group and Kind. Ignore Version. 45 | resource := resources.Get(parent.APIVersion, parent.Resource) 46 | if resource == nil { 47 | return nil, fmt.Errorf("can't find resource %q in apiVersion %q", parent.Resource, parent.APIVersion) 48 | } 49 | key := selectorMapKey(resource.Group, resource.Kind) 50 | 51 | // Convert the label selector to the internal form. 52 | if parent.LabelSelector != nil { 53 | ds.labelSelectors[key], err = metav1.LabelSelectorAsSelector(parent.LabelSelector) 54 | if err != nil { 55 | return nil, fmt.Errorf("can't convert label selector for parent resource %q in apiVersion %q: %v", parent.Resource, parent.APIVersion, err) 56 | } 57 | } else { 58 | // Add an explicit selector so we can tell the difference between 59 | // missing (not a type we care about) and empty (select everything). 60 | ds.labelSelectors[key] = labels.Everything() 61 | } 62 | 63 | // Convert the annotation selector to a label selector, then to internal form. 64 | if parent.AnnotationSelector != nil { 65 | labelSelector := &metav1.LabelSelector{ 66 | MatchLabels: parent.AnnotationSelector.MatchAnnotations, 67 | MatchExpressions: parent.AnnotationSelector.MatchExpressions, 68 | } 69 | ds.annotationSelectors[key], err = metav1.LabelSelectorAsSelector(labelSelector) 70 | if err != nil { 71 | return nil, fmt.Errorf("can't convert annotation selector for parent resource %q in apiVersion %q: %v", parent.Resource, parent.APIVersion, err) 72 | } 73 | } else { 74 | // Add an explicit selector so we can tell the difference between 75 | // missing (not a type we care about) and empty (select everything). 76 | ds.annotationSelectors[key] = labels.Everything() 77 | } 78 | } 79 | 80 | return ds, nil 81 | } 82 | 83 | func (ds *decoratorSelector) Matches(obj *unstructured.Unstructured) bool { 84 | // Look up the label and annotation selectors for this object. 85 | // Use only Group and Kind. Ignore Version. 86 | apiGroup, _ := common.ParseAPIVersion(obj.GetAPIVersion()) 87 | key := selectorMapKey(apiGroup, obj.GetKind()) 88 | 89 | labelSelector := ds.labelSelectors[key] 90 | annotationSelector := ds.annotationSelectors[key] 91 | if labelSelector == nil || annotationSelector == nil { 92 | // This object is not a kind we care about, so it doesn't match. 93 | return false 94 | } 95 | 96 | // It must match both selectors. 97 | return labelSelector.Matches(labels.Set(obj.GetLabels())) && 98 | annotationSelector.Matches(labels.Set(obj.GetAnnotations())) 99 | } 100 | 101 | func selectorMapKey(apiGroup, kind string) string { 102 | return fmt.Sprintf("%s.%s", kind, apiGroup) 103 | } 104 | -------------------------------------------------------------------------------- /test/integration/framework/main.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 | "os" 22 | "os/exec" 23 | "path" 24 | "time" 25 | 26 | "k8s.io/client-go/discovery" 27 | "k8s.io/klog" 28 | 29 | dynamicdiscovery "metacontroller.app/dynamic/discovery" 30 | "metacontroller.app/server" 31 | ) 32 | 33 | var resourceMap *dynamicdiscovery.ResourceMap 34 | 35 | const installKubectl = ` 36 | Cannot find kubectl, cannot run integration tests 37 | 38 | Please download kubectl and ensure it is somewhere in the PATH. 39 | See hack/get-kube-binaries.sh 40 | 41 | ` 42 | 43 | // manifestDir is the path from the integration test binary working dir to the 44 | // directory containing manifests to install Metacontroller. 45 | const manifestDir = "../../../manifests" 46 | 47 | // getKubectlPath returns a path to a kube-apiserver executable. 48 | func getKubectlPath() (string, error) { 49 | return exec.LookPath("kubectl") 50 | } 51 | 52 | // TestMain starts etcd, kube-apiserver, and metacontroller before running tests. 53 | func TestMain(tests func() int) { 54 | result := 1 55 | defer func() { 56 | os.Exit(result) 57 | }() 58 | 59 | if _, err := getKubectlPath(); err != nil { 60 | klog.Fatal(installKubectl) 61 | } 62 | 63 | stopEtcd, err := startEtcd() 64 | if err != nil { 65 | klog.Fatalf("cannot run integration tests: unable to start etcd: %v", err) 66 | } 67 | defer stopEtcd() 68 | 69 | stopApiserver, err := startApiserver() 70 | if err != nil { 71 | klog.Fatalf("cannot run integration tests: unable to start kube-apiserver: %v", err) 72 | } 73 | defer stopApiserver() 74 | 75 | klog.Info("Waiting for kube-apiserver to be ready...") 76 | start := time.Now() 77 | for { 78 | if err := execKubectl("version"); err == nil { 79 | break 80 | } 81 | if time.Since(start) > defaultWaitTimeout { 82 | klog.Fatalf("timed out waiting for kube-apiserver to be ready: %v", err) 83 | } 84 | time.Sleep(time.Second) 85 | } 86 | 87 | // Install Metacontroller RBAC. 88 | if err := execKubectl("apply", "-f", path.Join(manifestDir, "metacontroller-rbac.yaml")); err != nil { 89 | klog.Fatalf("can't install metacontroller RBAC: %v", err) 90 | } 91 | 92 | // Install Metacontroller CRDs. 93 | if err := execKubectl("apply", "-f", path.Join(manifestDir, "metacontroller.yaml")); err != nil { 94 | klog.Fatalf("can't install metacontroller CRDs: %v", err) 95 | } 96 | 97 | // In this integration test environment, there are no Nodes, so the 98 | // metacontroller StatefulSet will not actually run anything. 99 | // Instead, we start the Metacontroller server locally inside the test binary, 100 | // since that's part of the code under test. 101 | stopServer, err := server.Start(ApiserverConfig(), 500*time.Millisecond, 30*time.Minute) 102 | if err != nil { 103 | klog.Fatalf("can't start metacontroller server: %v", err) 104 | } 105 | defer stopServer() 106 | 107 | // Periodically refresh discovery to pick up newly-installed resources. 108 | discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(ApiserverConfig()) 109 | resourceMap = dynamicdiscovery.NewResourceMap(discoveryClient) 110 | // We don't care about stopping this cleanly since it has no external effects. 111 | resourceMap.Start(500 * time.Millisecond) 112 | 113 | result = tests() 114 | } 115 | 116 | func execKubectl(args ...string) error { 117 | execPath, err := exec.LookPath("kubectl") 118 | if err != nil { 119 | return fmt.Errorf("can't exec kubectl: %v", err) 120 | } 121 | cmdline := append([]string{"--server", ApiserverURL()}, args...) 122 | cmd := exec.Command(execPath, cmdline...) 123 | return cmd.Run() 124 | } 125 | --------------------------------------------------------------------------------