Skip to content

Commit

Permalink
Support for cluster scoped parent resources
Browse files Browse the repository at this point in the history
This CL adds initial support for clustered parents the decorator
controller. The composite controller will work as long as you're not
using rolling updates.

This CL changes the behavior of the client map which is sent as
`attachments` or `children` in the DecoratorController and
CompositeController, respectively. The new keys can be thought of as a
relative path to the child. When both the parent and child are at the
same scope - either both namespaced or both clustered - the key is just
the child's name. When the parent is clustered and the child is
namespaced the relative are relative - the children's keys will always
be prefaced with the namespace - this is to disambiguate between two
children with the same name in different namespaces.

To test this change this CL also adds two examples. The first example is
of a decorator controller that creates a "reader" ClusterRole, similar
to the default roles and rolebindings for each CRD with the
`enable-default-roles` annotation.

The second is a decorator controller which creates role bindings in the
default namespace that bind the default service account to clusterrolebindings.

Closes GoogleCloudPlatform#2
  • Loading branch information
rlguarino committed Sep 17, 2018
1 parent 8b53f27 commit 3b932a6
Show file tree
Hide file tree
Showing 17 changed files with 441 additions and 35 deletions.
54 changes: 43 additions & 11 deletions controller/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/tools/cache"

Expand All @@ -37,14 +38,15 @@ func (m ChildMap) InitGroup(apiVersion, kind string) {
m[childMapKey(apiVersion, kind)] = make(map[string]*unstructured.Unstructured)
}

func (m ChildMap) Insert(obj *unstructured.Unstructured) {
func (m ChildMap) Insert(parent metav1.Object, obj *unstructured.Unstructured) {
key := childMapKey(obj.GetAPIVersion(), obj.GetKind())
group := m[key]
if group == nil {
group = make(map[string]*unstructured.Unstructured)
m[key] = group
}
group[obj.GetName()] = obj
name := relativeName(parent, obj)
group[name] = obj
}

func (m ChildMap) FindGroupKindName(apiGroup, kind, name string) *unstructured.Unstructured {
Expand All @@ -64,28 +66,58 @@ func (m ChildMap) FindGroupKindName(apiGroup, kind, name string) *unstructured.U
return nil
}

func (m ChildMap) ReplaceChild(child *unstructured.Unstructured) {

// relativeName returns the name of the child relative to the parent.
// If the parent is cluster scoped and the child namespaced scoped the
// name is of the format <namespace>/<name>. Otherwise the name of the child
// is returned.
func relativeName(parent metav1.Object, child *unstructured.Unstructured) string {
if parent.GetNamespace() == "" && child.GetNamespace() != "" {
return fmt.Sprintf("%s/%s", child.GetNamespace(), child.GetName())
} else {
return child.GetName()
}
}

// ReplaceChild replaces the child object with the same name & namespace as
// the given child with the contents of the given child. If no child exists
// in the existing map then no action is taken.
func (m ChildMap) ReplaceChild(parent metav1.Object, child *unstructured.Unstructured) {
key := childMapKey(child.GetAPIVersion(), child.GetKind())
children := m[key]
if children == nil {
// We only want to replace if it already exists, so do nothing.
return
}
name := child.GetName()

name := relativeName(parent, child)
if _, found := children[name]; found {
children[name] = child
}
}

func MakeChildMap(list []*unstructured.Unstructured) ChildMap {
// MakeChildMap builds the map of children resources that is suitable for use
// in the `children` field of a CompositeController SyncRequest or
// `attachments` field of the DecoratorControllers SyncRequest.
//
// This function returns a ChildMap which is a map of maps. The outer most map
// is keyed using the child's type and the inner map is keyed using the
// child's name. If the parent resource is clustered and the child resource
// is namespaced the inner map's keys are prefixed by the namespace of the
// child resource.
//
// This function requires parent resources has the meta.Namespace accurately
// set. If the namespace of the pareent is empty it's considered a clustered
// resource.
//
// If a user returns a namespaced as a child of a clustered resources without
// the namespace set this is considered a user error but it's not handled here
// since the api errorstrying to create the child is clear.
func MakeChildMap(parent metav1.Object, list []*unstructured.Unstructured) ChildMap {
children := make(ChildMap)
for _, child := range list {
key := childMapKey(child.GetAPIVersion(), child.GetKind())

if children[key] == nil {
children[key] = make(map[string]*unstructured.Unstructured)
}
children[key][child.GetName()] = child
for _, child := range list {
children.Insert(parent, child)
}
return children
}
Expand Down
15 changes: 10 additions & 5 deletions controller/common/manage_children.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func deleteChildren(client *dynamicclientset.ResourceClient, parent *unstructure
// This observed object wasn't listed as desired.
glog.Infof("%v %v/%v: deleting %v %v", parent.GetKind(), parent.GetNamespace(), parent.GetName(), obj.GetKind(), obj.GetName())
uid := obj.GetUID()
err := client.Namespace(parent.GetNamespace()).Delete(name, &metav1.DeleteOptions{
err := client.Namespace(obj.GetNamespace()).Delete(name, &metav1.DeleteOptions{
Preconditions: &metav1.Preconditions{UID: &uid},
})
if err != nil {
Expand All @@ -126,6 +126,11 @@ func deleteChildren(client *dynamicclientset.ResourceClient, parent *unstructure
func updateChildren(client *dynamicclientset.ResourceClient, updateStrategy ChildUpdateStrategy, parent *unstructured.Unstructured, observed, desired map[string]*unstructured.Unstructured) error {
var errs []error
for name, obj := range desired {
ns := obj.GetNamespace()
if ns == "" {
ns = parent.GetNamespace()
}
nsClient := client.Namespace(ns)
if oldObj := observed[name]; oldObj != nil {
// Update
newObj, err := ApplyUpdate(oldObj, obj)
Expand Down Expand Up @@ -160,7 +165,7 @@ func updateChildren(client *dynamicclientset.ResourceClient, updateStrategy Chil
// Delete the object (now) and recreate it (on the next sync).
glog.Infof("%v %v/%v: deleting %v %v for update", parent.GetKind(), parent.GetNamespace(), parent.GetName(), obj.GetKind(), obj.GetName())
uid := oldObj.GetUID()
err := client.Namespace(parent.GetNamespace()).Delete(name, &metav1.DeleteOptions{
err := nsClient.Delete(name, &metav1.DeleteOptions{
Preconditions: &metav1.Preconditions{UID: &uid},
})
if err != nil {
Expand All @@ -170,7 +175,7 @@ func updateChildren(client *dynamicclientset.ResourceClient, updateStrategy Chil
case v1alpha1.ChildUpdateInPlace, v1alpha1.ChildUpdateRollingInPlace:
// Update the object in-place.
glog.Infof("%v %v/%v: updating %v %v", parent.GetKind(), parent.GetNamespace(), parent.GetName(), obj.GetKind(), obj.GetName())
if _, err := client.Namespace(parent.GetNamespace()).Update(newObj); err != nil {
if _, err := nsClient.Update(newObj); err != nil {
errs = append(errs, err)
continue
}
Expand All @@ -180,7 +185,7 @@ func updateChildren(client *dynamicclientset.ResourceClient, updateStrategy Chil
}
} else {
// Create
glog.Infof("%v %v/%v: creating %v %v", parent.GetKind(), parent.GetNamespace(), parent.GetName(), obj.GetKind(), obj.GetName())
glog.Infof("%v %v/%v: creating %v %v/%v", parent.GetKind(), parent.GetNamespace(), parent.GetName(), obj.GetKind(), obj.GetNamespace(), obj.GetName())

// The controller should return a partial object containing only the
// fields it cares about. We save this partial object so we can do
Expand All @@ -198,7 +203,7 @@ func updateChildren(client *dynamicclientset.ResourceClient, updateStrategy Chil
ownerRefs = append(ownerRefs, *controllerRef)
obj.SetOwnerReferences(ownerRefs)

if _, err := client.Namespace(parent.GetNamespace()).Create(obj); err != nil {
if _, err := nsClient.Create(obj); err != nil {
fmt.Printf("\n\n6: %+v\n\n", errs)
errs = append(errs, err)
continue
Expand Down
2 changes: 1 addition & 1 deletion controller/composite/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ func (pc *parentController) claimChildren(parent *unstructured.Unstructured) (co
// Add children to map by name.
// Note that we limit each parent to only working within its own namespace.
for _, obj := range children {
childMap.Insert(obj)
childMap.Insert(parent, obj)
}
}
return childMap, nil
Expand Down
6 changes: 3 additions & 3 deletions controller/composite/controller_revision.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (pc *parentController) syncRevisions(parent *unstructured.Unstructured, obs
if err != nil {
return nil, nil, fmt.Errorf("sync hook failed for %v %v/%v: %v", pc.parentResource.Kind, parent.GetNamespace(), parent.GetName(), err)
}
return syncResult.Status, common.MakeChildMap(syncResult.Children), nil
return syncResult.Status, common.MakeChildMap(parent, syncResult.Children), nil
}

// Claim all matching ControllerRevisions for the parent.
Expand Down Expand Up @@ -160,7 +160,7 @@ func (pc *parentController) syncRevisions(parent *unstructured.Unstructured, obs
}
pr.status = syncResult.Status
pr.desiredChildList = syncResult.Children
pr.desiredChildMap = common.MakeChildMap(syncResult.Children)
pr.desiredChildMap = common.MakeChildMap(parent, syncResult.Children)
}(pr)
}
wg.Wait()
Expand Down Expand Up @@ -206,7 +206,7 @@ func (pc *parentController) syncRevisions(parent *unstructured.Unstructured, obs
for _, name := range ck.Names {
child := pr.desiredChildMap.FindGroupKindName(ck.APIGroup, ck.Kind, name)
if child != nil {
desiredChildren.ReplaceChild(child)
desiredChildren.ReplaceChild(parent, child)
}
}
}
Expand Down
6 changes: 2 additions & 4 deletions controller/decorator/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ func (c *decoratorController) syncParentObject(parent *unstructured.Unstructured
if err != nil {
return err
}
desiredChildren = common.MakeChildMap(syncResult.Attachments)
desiredChildren = common.MakeChildMap(parent, syncResult.Attachments)

// Set desired labels and annotations on parent.
// Make a copy since parent is from the cache.
Expand Down Expand Up @@ -516,9 +516,7 @@ func (c *decoratorController) getChildren(parent *unstructured.Unstructured) (co
if obj.GetAnnotations()[decoratorControllerAnnotation] != c.dc.Name {
continue
}
// Add children to map by name.
// Note that we limit each parent to only working within its own namespace.
childMap.Insert(obj)
childMap.Insert(parent, obj)
}
}
return childMap, nil
Expand Down
26 changes: 21 additions & 5 deletions docs/_api/compositecontroller.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,28 @@ because it does conversion for you as needed, so your hook doesn't need
to know how to convert between different versions of a given resource.

Within each child type (e.g. in `children['Pod.v1']`),
there is another associative array that maps from the child's
`metadata.name` to the JSON representation, like what you might get
from `kubectl get <child-resource> <child-name> -o json`.

For example, a Pod named `my-pod` could be accessed as:
there is another associative array that maps from the child's path relative
to the parent to the JSON representation. If the parent and child are of the
same scope - both cluser or both namespaced - then the key is simply the
object's `.metadata.name`, like what you might get from
`kubectl get <child-resource> <child-name> -o json`. If the parent is
cluster scoped and the child is namespaced scope then the key will be of the
form: `{.metadata.namespace}/{.metadata.name}`. This is to disambiguate between
two children with the same name in different namespaces.

For example, a Pod named `my-pod` in the `default` namespace could be accessed
as:

```js
request.children['Pod.v1']['my-pod']
```

or if the parent resource is cluster scoped as:

```js
request.children['Pod.v1']['default/my-pod']
```

Note that you will only be sent children that you "own" according to the
[ControllerRef rules][controller-ref].
That means, for a given parent object, **you will only see children whose
Expand Down Expand Up @@ -361,6 +373,10 @@ you return, and also to ensure that you list every type of
[child resource][child resources] you plan to create in the
CompositeController spec.

If the parent resource is cluster scoped and the child resouce is namespaced
it's important to include the `.metadata.namespace` otherwise the namespace is
inferred from the parent's namespce.

Any objects sent as children in the request that you decline to return
in your response list **will be deleted**.
However, you shouldn't directly copy children from the request into the
Expand Down
28 changes: 22 additions & 6 deletions docs/_api/decoratorcontroller.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,17 +229,29 @@ Metacontroller requires you to be explicit about the version you expect
because it does conversion for you as needed, so your hook doesn't need
to know how to convert between different versions of a given resource.

Within each attachment type (e.g. in `attachments['Pod.v1']`),
there is another associative array that maps from the attachment's
`metadata.name` to the JSON representation, like what you might get
from `kubectl get <attachment-resource> <attachment-name> -o json`.

For example, a Pod named `my-pod` could be accessed as:
Within each child type (e.g. in `children['Pod.v1']`),
there is another associative array that maps from the child's path relative
to the parent to the JSON representation. If the parent and child are of the
same scope - both cluser or both namespaced - then the key is simply the
object's `.metadata.name`, like what you might get from
`kubectl get <child-resource> <child-name> -o json`. If the parent is
cluster scoped and the child is namespaced scope then the key will be of the
form: `{.metadata.namespace}/{.metadata.name}`. This is to disambiguate between
two children with the same name in different namespaces.

For example, a Pod named `my-pod` in the `default` namespace could be accessed
as:

```js
request.attachments['Pod.v1']['my-pod']
```

or if the parent resource is cluster scoped as:

```js
request.attachments['Pod.v1']['default/my-pod']
`

Note that you will only be sent objects that are owned by the target
(i.e. objects you attached), not all objects of that resource type.

Expand Down Expand Up @@ -296,6 +308,10 @@ you return, and also to ensure that you list every type of
[attachment resource](#attachments) you plan to create in the
DecoratorController spec.

If the parent resource is cluster scoped and the child resouce is namespaced
it's important to include the `.metadata.namespace` otherwise the namespace is
inferred from the parent's namespce.

Any objects sent as attachments in the request that you decline to return
in your response list **will be deleted**.
However, you shouldn't directly copy attachments from the request into the
Expand Down
32 changes: 32 additions & 0 deletions examples/clusteredparent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## ClusterRole service account binding

This is an example DecoratorController that creates a namespaced resources from a
cluster scoped parent resource.

This controller will bind any ClusterRole with the "default-service-account-binding"
annotation to the default service account in the default namespace.

### Prerequisites

* Install [Metacontroller](https://github.com/GoogleCloudPlatform/metacontroller)

### Deploy the controller

```sh
kubectl create configmap cluster-parent-controller -n metacontroller --from-file=sync.py
kubectl apply -f cluster-parent.yaml
```

### Create a ClusterRole

```sh
kubectl apply -f my-clusterole.yaml
```

A RoleBinding should be created for the ClusterRole:

```console
$ kubectl get rolebinding -n default my-clusterrole -o wide
NAME AGE ROLE USERS GROUPS SERVICEACCOUNTS
my-clusterrole 40s ClusterRole/my-clusterrole default/default
```
56 changes: 56 additions & 0 deletions examples/clusteredparent/cluster-parent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
apiVersion: metacontroller.k8s.io/v1alpha1
kind: DecoratorController
metadata:
name: cluster-parent
spec:
resources:
- apiVersion: rbac.authorization.k8s.io/v1
resource: clusterroles
annotationSelector:
matchExpressions:
- {key: default-service-account-binding, operator: Exists}
attachments:
- apiVersion: rbac.authorization.k8s.io/v1
resource: rolebindings
hooks:
sync:
webhook:
url: http://cluster-parent-controller.metacontroller/sync
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: clusterparent-controller
namespace: metacontroller
spec:
replicas: 1
selector:
matchLabels:
app: cluster-parent-controller
template:
metadata:
labels:
app: cluster-parent-controller
spec:
containers:
- name: controller
image: python:2.7
command: ["python", "/hooks/sync.py"]
volumeMounts:
- name: hooks
mountPath: /hooks
volumes:
- name: hooks
configMap:
name: cluster-parent-controller
---
apiVersion: v1
kind: Service
metadata:
name: cluster-parent-controller
namespace: metacontroller
spec:
selector:
app: cluster-parent-controller
ports:
- port: 80
Loading

0 comments on commit 3b932a6

Please sign in to comment.