├── 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 |
--------------------------------------------------------------------------------