Skip to content

Commit

Permalink
c7n_kube - k8s-admission - add label and auto-label-user actions for …
Browse files Browse the repository at this point in the history
…k8s-admission mode (cloud-custodian#7925)
  • Loading branch information
thisisshi authored Feb 15, 2023
1 parent 8a91f17 commit 867e597
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 45 deletions.
4 changes: 3 additions & 1 deletion docs/source/kubernetes/gettingstarted.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ There are three main components to a policy:
* Filters: criteria to produce a specific subset of resources
* Actions: directives to take on the filtered set of resources

In the example below, we will write a policy that filters for pods with a label "custodian"
and deletes it:

First, lets create a pod resource that we want to target with the policy:

.. code-block:: bash
Expand Down Expand Up @@ -220,4 +223,3 @@ attributes that way:
"phase": "Available"
}
}
10 changes: 10 additions & 0 deletions tools/c7n_kube/c7n_kube/actions/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ class Action(BaseAction):
pass


class EventAction(BaseAction):

def validate(self):
modes = ('k8s-admission',)
policy = self.manager.data
if policy.get('mode', {}).get('type') not in modes:
raise PolicyValidationError(
"Event Actions are only supported for k8s-admission mode policies")


class MethodAction(Action):
method_spec = ()
chunk_size = 20
Expand Down
93 changes: 92 additions & 1 deletion tools/c7n_kube/c7n_kube/actions/labels.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Copyright The Cloud Custodian Authors.
# SPDX-License-Identifier: Apache-2.0

import copy
import logging

from c7n_kube.actions.core import PatchAction
import jsonpatch

from c7n_kube.actions.core import PatchAction, EventAction
from c7n.utils import type_schema
log = logging.getLogger('custodian.k8s.labels')

Expand Down Expand Up @@ -56,3 +59,91 @@ def register_resources(klass, registry, resource_class):
model = resource_class.resource_type
if hasattr(model, 'patch') and hasattr(model, 'namespaced'):
resource_class.action_registry.register('label', klass)


class EventLabelAction(EventAction):
"""
Label a resource on event
.. code-block:: yaml
policies:
- name: 'label-foo-on-creation'
resource: 'k8s.deployment'
mode:
type: k8s-admission
on-match: allow
operations:
- CREATE
actions:
- type: event-label
labels:
foo: bar
"""

schema = type_schema('event-label', labels={'type': 'object'}, required=['labels'])

def get_labels(self, event):
return self.data['labels']

def process_labels(self, resource, labels):
remove = []
for k, v in labels.items():
if v is None:
remove.append(k)
resource.setdefault('c7n:patches', [])
src = copy.deepcopy(resource)
dst = copy.deepcopy(src)
# if the key doesnt exist we need to set it else it wont appear
# in the patch
if dst.get('metadata', {}).get('labels') is None:
dst['metadata']['labels'] = labels
else:
dst.get('metadata', {}).get('labels', {}).update(labels)
for r in remove:
if r in dst.get('metadata', {}).get('labels', {}):
dst.get('metadata', {}).get('labels', {}).pop(r)
patch = jsonpatch.make_patch(src, dst)
resource['c7n:patches'].extend(patch.patch)

def process(self, resources, event):
for r in resources:
labels = self.get_labels(event)
self.process_labels(r, labels)

@classmethod
def register_resources(klass, registry, resource_class):
resource_class.action_registry.register('event-label', klass)


class AutoLabelUser(EventLabelAction):
"""
Label the user that triggered the event
Default label key is OwnerContact, set 'key' to specify a different one
.. code-block:: yaml
policies:
- name: 'auto-label-creator'
resource: 'k8s.deployment'
mode:
type: k8s-admission
on-match: allow
operations:
- CREATE
actions:
- type: auto-label-user
key: owner
"""

schema = type_schema('auto-label-user', key={'type': 'string'})

def get_labels(self, event):
label_key = self.data.get('key', 'OwnerContact')
event_owner = event['request']['userInfo']['username']
return {label_key: event_owner}

@classmethod
def register_resources(klass, registry, resource_class):
resource_class.action_registry.register('auto-label-user', klass)
4 changes: 2 additions & 2 deletions tools/c7n_kube/c7n_kube/actions/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# SPDX-License-Identifier: Apache-2.0

from c7n_kube.actions.core import DeleteResource, PatchResource
from c7n_kube.actions.labels import LabelAction
from c7n_kube.actions.labels import LabelAction, EventLabelAction, AutoLabelUser
from c7n_kube.provider import resources as kube_resources

SHARED_ACTIONS = (DeleteResource, LabelAction, PatchResource)
SHARED_ACTIONS = (DeleteResource, LabelAction, PatchResource, EventLabelAction, AutoLabelUser)


for action in SHARED_ACTIONS:
Expand Down
23 changes: 21 additions & 2 deletions tools/c7n_kube/c7n_kube/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
# SPDX-License-Identifier: Apache-2.0
import logging

from c7n.exceptions import PolicyValidationError
from c7n.policy import PolicyExecutionMode, execution
from c7n.utils import type_schema, dumps

from c7n_kube.exceptions import EventNotMatchedException, PolicyNotRunnableException


log = logging.getLogger('custodian.k8s.policy')


Expand Down Expand Up @@ -63,6 +63,17 @@ class ValidatingControllerMode(K8sEventMode):
}
)

def validate(self):
from c7n_kube.actions.core import EventAction
actions = self.policy.resource_manager.actions
errors = []
for a in actions:
if not isinstance(a, EventAction):
errors.append(a.type)
if errors:
raise PolicyValidationError(
f"Only Event Based actions are allowed: {errors} are not compatible")

def _handle_scope(self, request, value):
if request.get('namespace') and value == 'Namespaced':
return True
Expand Down Expand Up @@ -172,7 +183,15 @@ def run_resource_set(self, event, resources):
)

ctx.output.write_file('resources.json', dumps(resources, indent=2))
# we dont run any actions for validating admission controllers
for action in self.policy.resource_manager.actions:
self.policy.log.info(
"policy:%s invoking action:%s resources:%d",
self.policy.name,
action.name,
len(resources),
)
results = action.process(resources, event)
ctx.output.write_file("action-%s" % action.name, dumps(results))
return resources

def run(self, event, _):
Expand Down
33 changes: 26 additions & 7 deletions tools/c7n_kube/c7n_kube/server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright The Cloud Custodian Authors.
# SPDX-License-Identifier: Apache-2.0
import base64
import json
import os
import http.server
Expand Down Expand Up @@ -37,10 +38,12 @@ class AdmissionControllerHandler(http.server.BaseHTTPRequestHandler):
def run_policies(self, req):
failed_policies = []
warn_policies = []
patches = []
for p in self.server.policy_collection.policies:
# fail_message and warning_message are set on exception
warning_message = None
deny_message = None
resources = None
try:
resources = p.push(req)
action = p.data['mode'].get('on-match', 'deny')
Expand Down Expand Up @@ -76,7 +79,9 @@ def run_policies(self, req):
"description": warning_message or p.data.get('description', '')
}
)
return failed_policies, warn_policies
if resources:
patches.extend(resources[0].get('c7n:patches', []))
return failed_policies, warn_policies, patches

def get_request_body(self):
token = self.rfile.read(int(self.headers["Content-length"]))
Expand Down Expand Up @@ -109,20 +114,27 @@ def do_POST(self):
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
return

failed_policies, warn_policies = self.run_policies(req)
failed_policies, warn_policies, patches = self.run_policies(req)

self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()

if patches:
patches = base64.b64encode(json.dumps(patches).encode('utf-8')).decode()

response = self.create_admission_response(
uid=req['request']['uid'],
failed_policies=failed_policies,
warn_policies=warn_policies
warn_policies=warn_policies,
patches=patches
)
log.info(response)
self.wfile.write(response.encode('utf-8'))

def create_admission_response(self, uid, failed_policies=None, warn_policies=None):
def create_admission_response(
self, uid, failed_policies=None, warn_policies=None, patches=None
):
code = 200 if len(failed_policies) == 0 else 400
message = 'OK'
warnings = []
Expand All @@ -132,7 +144,7 @@ def create_admission_response(self, uid, failed_policies=None, warn_policies=Non
for p in warn_policies:
warnings.append(f"{p['name']}:{p['description']}")

return json.dumps({
response = {
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
Expand All @@ -144,7 +156,15 @@ def create_admission_response(self, uid, failed_policies=None, warn_policies=Non
"message": message
}
}
})
}

if patches:
patch = {
"patchType": "JSONPatch",
"patch": patches
}
response['response'].update(patch)
return json.dumps(response)


def init(
Expand All @@ -163,7 +183,6 @@ def init(
policy_dir=policy_dir,
on_exception=on_exception,
)

if use_tls:
import ssl
server.socket = ssl.wrap_socket(
Expand Down
1 change: 1 addition & 0 deletions tools/c7n_kube/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ classifiers = [
[tool.poetry.dependencies]
python = "^3.7"
kubernetes = "^10.0.1"
jsonpatch = "^1.32"

[tool.poetry.dev-dependencies]
c7n = {path = "../..", develop = true}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"kind": "AdmissionReview", "apiVersion": "admission.k8s.io/v1", "request": {"uid": "7cdb7612-f297-45e3-841b-80a97a27a9ad", "kind": {"group": "", "version": "v1", "kind": "PodAttachOptions"}, "resource": {"group": "", "version": "v1", "resource": "pods"}, "subResource": "attach", "requestKind": {"group": "", "version": "v1", "kind": "PodAttachOptions"}, "requestResource": {"group": "", "version": "v1", "resource": "pods"}, "requestSubResource": "attach", "name": "dockerhost-7d9f4c9b57-rsggv", "namespace": "default", "operation": "CONNECT", "userInfo": {"username": "kubernetes-admin", "groups": ["system:masters", "system:authenticated"]}, "object": {"kind": "PodAttachOptions", "apiVersion": "v1", "stdout": true, "stderr": true, "container": "dockerhost"}, "oldObject": "None", "dryRun": false, "options": "None"}}
{"kind": "AdmissionReview", "apiVersion": "admission.k8s.io/v1", "request": {"uid": "7cdb7612-f297-45e3-841b-80a97a27a9ad", "kind": {"group": "", "version": "v1", "kind": "PodAttachOptions"}, "resource": {"group": "", "version": "v1", "resource": "pods"}, "subResource": "attach", "requestKind": {"group": "", "version": "v1", "kind": "PodAttachOptions"}, "requestResource": {"group": "", "version": "v1", "resource": "pods"}, "requestSubResource": "attach", "name": "dockerhost-7d9f4c9b57-rsggv", "namespace": "default", "operation": "CONNECT", "userInfo": {"username": "kubernetes-admin", "groups": ["system:masters", "system:authenticated"]}, "object": {"kind": "PodAttachOptions", "apiVersion": "v1", "stdout": true, "stderr": true, "container": "dockerhost"}, "oldObject": "None", "dryRun": false, "options": "None"}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"kind": "AdmissionReview", "apiVersion": "admission.k8s.io/v1", "request": {"uid": "6aadf2e9-471e-48df-9f84-730a491561e8", "kind": {"group": "", "version": "v1", "kind": "PodExecOptions"}, "resource": {"group": "", "version": "v1", "resource": "pods"}, "subResource": "exec", "requestKind": {"group": "", "version": "v1", "kind": "PodExecOptions"}, "requestResource": {"group": "", "version": "v1", "resource": "pods"}, "requestSubResource": "exec", "name": "dockerhost-7d9f4c9b57-rsggv", "namespace": "default", "operation": "CONNECT", "userInfo": {"username": "kubernetes-admin", "groups": ["system:masters", "system:authenticated"]}, "object": {"kind": "PodExecOptions", "apiVersion": "v1", "stdin": true, "stdout": true, "tty": true, "container": "dockerhost", "command": ["echo", "hello"]}, "oldObject": "None", "dryRun": false, "options": "None"}}
{"kind": "AdmissionReview", "apiVersion": "admission.k8s.io/v1", "request": {"uid": "6aadf2e9-471e-48df-9f84-730a491561e8", "kind": {"group": "", "version": "v1", "kind": "PodExecOptions"}, "resource": {"group": "", "version": "v1", "resource": "pods"}, "subResource": "exec", "requestKind": {"group": "", "version": "v1", "kind": "PodExecOptions"}, "requestResource": {"group": "", "version": "v1", "resource": "pods"}, "requestSubResource": "exec", "name": "dockerhost-7d9f4c9b57-rsggv", "namespace": "default", "operation": "CONNECT", "userInfo": {"username": "kubernetes-admin", "groups": ["system:masters", "system:authenticated"]}, "object": {"kind": "PodExecOptions", "apiVersion": "v1", "stdin": true, "stdout": true, "tty": true, "container": "dockerhost", "command": ["echo", "hello"]}, "oldObject": "None", "dryRun": false, "options": "None"}}
2 changes: 1 addition & 1 deletion tools/c7n_kube/tests/data/events/create_pod.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"kind": "AdmissionReview", "apiVersion": "admission.k8s.io/v1", "request": {"uid": "662c3df2-ade6-4165-b395-770857bc17b7", "kind": {"group": "", "version": "v1", "kind": "Pod"}, "resource": {"group": "", "version": "v1", "resource": "pods"}, "requestKind": {"group": "", "version": "v1", "kind": "Pod"}, "requestResource": {"group": "", "version": "v1", "resource": "pods"}, "name": "static-web", "namespace": "default", "operation": "CREATE", "userInfo": {"username": "kubernetes-admin", "groups": ["system:masters", "system:authenticated"]}, "object": {"kind": "Pod", "apiVersion": "v1", "metadata": {"name": "static-web", "namespace": "default", "uid": "e96b4e07-633e-426d-9a7f-db39676cf0b4", "creationTimestamp": "2022-08-25T22:08:33Z", "labels": {"role": "myrole"}, "annotations": {"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"role\":\"myrole\"},\"name\":\"static-web\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx\",\"name\":\"web\",\"ports\":[{\"containerPort\":80,\"name\":\"web\",\"protocol\":\"TCP\"}]}]}}\n"}, "managedFields": [{"manager": "kubectl-client-side-apply", "operation": "Update", "apiVersion": "v1", "time": "2022-08-25T22:08:33Z", "fieldsType": "FieldsV1", "fieldsV1": {"f:metadata": {"f:annotations": {".": {}, "f:kubectl.kubernetes.io/last-applied-configuration": {}}, "f:labels": {".": {}, "f:role": {}}}, "f:spec": {"f:containers": {"k:{\"name\":\"web\"}": {".": {}, "f:image": {}, "f:imagePullPolicy": {}, "f:name": {}, "f:ports": {".": {}, "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": {".": {}, "f:containerPort": {}, "f:name": {}, "f:protocol": {}}}, "f:resources": {}, "f:terminationMessagePath": {}, "f:terminationMessagePolicy": {}}}, "f:dnsPolicy": {}, "f:enableServiceLinks": {}, "f:restartPolicy": {}, "f:schedulerName": {}, "f:securityContext": {}, "f:terminationGracePeriodSeconds": {}}}}]}, "spec": {"volumes": [{"name": "kube-api-access-7pc2d", "projected": {"sources": [{"serviceAccountToken": {"expirationSeconds": 3607, "path": "token"}}, {"configMap": {"name": "kube-root-ca.crt", "items": [{"key": "ca.crt", "path": "ca.crt"}]}}, {"downwardAPI": {"items": [{"path": "namespace", "fieldRef": {"apiVersion": "v1", "fieldPath": "metadata.namespace"}}]}}], "defaultMode": 420}}], "containers": [{"name": "web", "image": "nginx", "ports": [{"name": "web", "containerPort": 80, "protocol": "TCP"}], "resources": {}, "volumeMounts": [{"name": "kube-api-access-7pc2d", "readOnly": true, "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"}], "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "imagePullPolicy": "Always"}], "restartPolicy": "Always", "terminationGracePeriodSeconds": 30, "dnsPolicy": "ClusterFirst", "serviceAccountName": "default", "serviceAccount": "default", "securityContext": {}, "schedulerName": "default-scheduler", "tolerations": [{"key": "node.kubernetes.io/not-ready", "operator": "Exists", "effect": "NoExecute", "tolerationSeconds": 300}, {"key": "node.kubernetes.io/unreachable", "operator": "Exists", "effect": "NoExecute", "tolerationSeconds": 300}], "priority": 0, "enableServiceLinks": true, "preemptionPolicy": "PreemptLowerPriority"}, "status": {"phase": "Pending", "qosClass": "BestEffort"}}, "oldObject": "None", "dryRun": false, "options": {"kind": "CreateOptions", "apiVersion": "meta.k8s.io/v1", "fieldManager": "kubectl-client-side-apply"}}}
{"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"uid":"662c3df2-ade6-4165-b395-770857bc17b7","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"name":"static-web","namespace":"default","operation":"CREATE","userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]},"object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"static-web","namespace":"default","uid":"e96b4e07-633e-426d-9a7f-db39676cf0b4","creationTimestamp":"2022-08-25T22:08:33Z","labels":{"role":"myrole","test":"blah"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"role\":\"myrole\"},\"name\":\"static-web\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx\",\"name\":\"web\",\"ports\":[{\"containerPort\":80,\"name\":\"web\",\"protocol\":\"TCP\"}]}]}}\n"},"managedFields":[{"manager":"kubectl-client-side-apply","operation":"Update","apiVersion":"v1","time":"2022-08-25T22:08:33Z","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}},"f:labels":{".":{},"f:role":{}}},"f:spec":{"f:containers":{"k:{\"name\":\"web\"}":{".":{},"f:image":{},"f:imagePullPolicy":{},"f:name":{},"f:ports":{".":{},"k:{\"containerPort\":80,\"protocol\":\"TCP\"}":{".":{},"f:containerPort":{},"f:name":{},"f:protocol":{}}},"f:resources":{},"f:terminationMessagePath":{},"f:terminationMessagePolicy":{}}},"f:dnsPolicy":{},"f:enableServiceLinks":{},"f:restartPolicy":{},"f:schedulerName":{},"f:securityContext":{},"f:terminationGracePeriodSeconds":{}}}}]},"spec":{"volumes":[{"name":"kube-api-access-7pc2d","projected":{"sources":[{"serviceAccountToken":{"expirationSeconds":3607,"path":"token"}},{"configMap":{"name":"kube-root-ca.crt","items":[{"key":"ca.crt","path":"ca.crt"}]}},{"downwardAPI":{"items":[{"path":"namespace","fieldRef":{"apiVersion":"v1","fieldPath":"metadata.namespace"}}]}}],"defaultMode":420}}],"containers":[{"name":"web","image":"nginx","ports":[{"name":"web","containerPort":80,"protocol":"TCP"}],"resources":{},"volumeMounts":[{"name":"kube-api-access-7pc2d","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true,"preemptionPolicy":"PreemptLowerPriority"},"status":{"phase":"Pending","qosClass":"BestEffort"}},"oldObject":"None","dryRun":false,"options":{"kind":"CreateOptions","apiVersion":"meta.k8s.io/v1","fieldManager":"kubectl-client-side-apply"}}}
Loading

0 comments on commit 867e597

Please sign in to comment.