Skip to content

Commit f4ea670

Browse files
authored
New Feature - Downscaling Jobs Using Admission Controller (Kyverno And Gatekeeper) (#42)
* added downscaling jobs feature using admission controller (kyverno and gatekeeper). Refactored docs in order to explain how to use the new feature and how to use matching_labels args * added downscaling jobs feature using admission controller (kyverno and gatekeeper). Refactored docs in order to explain how to use the new feature and how to use matching_labels. Rebased args * small syntax fix after rebase * added doc for downscaling daemonset feature
1 parent 11735c6 commit f4ea670

11 files changed

Lines changed: 1360 additions & 23 deletions

File tree

README.md

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ Available command line options:
267267
`--include-resources`
268268
269269
: Downscale resources of this kind as comma separated list.
270-
\[deployments, statefulsets, stacks, horizontalpodautoscalers, cronjobs, daemonsets, rollouts, scaledobjects\]
270+
\[deployments, statefulsets, stacks, horizontalpodautoscalers, cronjobs, daemonsets, rollouts, scaledobjects, jobs\]
271271
(default: deployments)
272272
273273
`--grace-period`
@@ -340,6 +340,134 @@ Available command line options:
340340
For backwards compatibility, if this argument is not specified,
341341
py-kube-downscaler will apply to all resources.
342342
343+
`--admission-controller`
344+
345+
: Optional: admission controller used by the kube-downscaler to downscale and upscale
346+
jobs. Required only if "jobs" are specified inside "--include-resources" arg.
347+
Supported Admission Controllers are
348+
\[gatekeeper, kyverno*\]
349+
350+
*Make sure to read the dedicated section below to understand how to use the
351+
--admission-controller feature correctly
352+
353+
### Scaling Jobs
354+
355+
Before scaling jobs make sure the Admission Controller of your choice is correctly installed inside the cluster.
356+
Kube-Downscaler performs some health checks that are displayed inside logs when the `--debug` arg is present.
357+
If you are using Gatekeeper, Kube-Downscaler will install a new Custom Resource Definition
358+
called `kubedownscalerjobsconstraint`
359+
360+
**When using this feature you need to exclude Kyverno or Gatekeeper resources from downscaling otherwise
361+
the admission controller pods won't be able to donwscale jobs.**
362+
You can use `EXCLUDE_NAMESPACES` environment variable or `--exclude-namespaces` arg to exclude `"kyverno"` or `"gatekeeper-system"` namespaces.
363+
To have a more fine-grained control you can use `EXCLUDE_DEPLOYMENTS` environment variable
364+
or `--exclude-deployments` arg to exclude only certain resources inside `"kyverno"` or `"gatekeeper-system"` namespaces
365+
366+
**<u>Important</u>:** Jobs started from CronJobs are excluded by default unless you have included `cronjobs` inside `--include-resources` argument
367+
368+
**Annotations:** both the `downscaler/exclude` and `downscaler/exclude-until` annotations are fully supported
369+
inside jobs to exclude them from downscaling. However, when using `downscaler/exclude-until`, the time <u>**must**</u> be specified in the RFC format `YYYY-MM-DDT00:00:00Z`
370+
otherwise the exclusion won't work.
371+
Please check the example below
372+
373+
```yaml {.sourceCode .yaml}
374+
apiVersion: batch/v1
375+
kind: Job
376+
metadata:
377+
namespace: default
378+
name: testjob
379+
annotations:
380+
downscaler/exclude-until: "2024-01-31T00:00:00Z"
381+
spec:
382+
template:
383+
spec:
384+
containers:
385+
- image: nginx
386+
name: testjob
387+
restartPolicy: Never
388+
```
389+
390+
**Arguments and Env:** you can also use `EXCLUDE_DEPLOYMENTS` environment variable or the argument `--exclude-deployments`
391+
to exclude jobs. As described above, despite their names, these variables work for any type of workload
392+
393+
**<u>Important</u>:**
394+
`downscaler/downscale-period`, `downscaler/downtime`, `downscaler/upscale-period`, `downscaler/uptime`
395+
annotations are not supported if specified directly inside the Job definition due to limitations
396+
on computing days of the week inside the policies. However you can still use
397+
these annotations at Namespace level to downscale/upscale Jobs
398+
399+
400+
**Deleting Policies:** if for some reason you want to delete all resources blocking jobs, you can use these commands:
401+
402+
Gatekeeper
403+
404+
``` {.sourceCode .sh}
405+
kubectl delete constraints -A -l origin=kube-downscaler
406+
```
407+
408+
Kyverno
409+
410+
``` {.sourceCode .sh}
411+
kubectl delete policies -A -l origin=kube-downscaler
412+
```
413+
414+
### Scaling DaemonSet
415+
416+
The feature to scale DaemonSets can be very useful for reducing the base occupancy of a node. If enabled, the DaemonSets downscaling algorithm works like this:
417+
418+
1) Downtime Hours: Kube Downscaler will add to each targeted DaemonSet a Node Selector that cannot be satisfied `kube-downscaler-non-existent=true`
419+
2) Uptime Hours: Kube Downscaler will remove the `kube-downscaler-non-existent=true` Node Selector from each targeted DaemonSet
420+
421+
### Matching Labels Argument
422+
423+
Labels, in Kubernetes, are key-value pairs that can be used to identify and group resources.
424+
425+
You can use the `--matching-labels` argument to include only certain resources in the namespaces
426+
that are targeted by the Kube Downscaler. inside this argument you can specify:
427+
- labels written in this format [key=value]
428+
- regular expressions that target this format [key=value].
429+
430+
Each entry must be separated by a comma (`,`). If multiple entries are specified, the Kube Downscaler evaluates them as an OR condition
431+
432+
To make it more clear, given the following resource
433+
434+
```yaml {.sourceCode .yaml}
435+
kind: Deployment
436+
metadata:
437+
labels:
438+
app: nginx
439+
type: example
440+
name: nginx
441+
spec:
442+
replicas: 1
443+
selector:
444+
matchLabels:
445+
app: nginx
446+
template:
447+
metadata:
448+
labels:
449+
app: nginx
450+
spec:
451+
containers:
452+
- image: nginx
453+
name: nginx
454+
```
455+
456+
Kube-Downscaler will evaluate the input of the `--matching-labels` argument against _app=nginx_
457+
and _type=example_. If at least one of the two key-value pairs matches the resource will be downscaled
458+
459+
Example of valid inputs are:
460+
461+
`--matching-labels=hello=world`: if the resource has a label "hello" equals to "world" it will be downscaled
462+
463+
`--matching-labels=hello=world,version=2.0`: if the resource has a label "hello" equals to "world"
464+
or a label "version" equal to "2.0" it will be downscaled
465+
466+
`--matching-labels=^it-plt.*`: if the resource has a label that starts with "it-plt" it will be downscaled
467+
468+
`--matching-labels=^it-plt.*,not-critical=true`: if the resource has a label that starts with "it-plt" or a label
469+
"not-critical" equals to "true" it will be downscaled
470+
343471
### Namespace Defaults
344472
345473
`DEFAULT_UPTIME`, `DEFAULT_DOWNTIME`, `FORCE_UPTIME` and exclusion can

chart/templates/rbac.yaml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,59 @@ rules:
100100
- list
101101
- update
102102
- patch
103+
- apiGroups:
104+
- constraints.gatekeeper.sh
105+
resources:
106+
- kubedownscalerjobsconstraint
107+
verbs:
108+
- get
109+
- create
110+
- watch
111+
- list
112+
- update
113+
- patch
114+
- delete
115+
- apiGroups:
116+
- kyverno.io
117+
resources:
118+
- policies
119+
resourceNames:
120+
- kube-downscaler-jobs-policy
121+
verbs:
122+
- get
123+
- create
124+
- watch
125+
- list
126+
- update
127+
- patch
128+
- delete
129+
- apiGroups:
130+
- kyverno.io
131+
resources:
132+
- policies
133+
verbs:
134+
- get
135+
- create
136+
- watch
137+
- list
138+
- apiGroups:
139+
- templates.gatekeeper.sh
140+
resources:
141+
- constrainttemplate
142+
verbs:
143+
- create
144+
- get
145+
- list
146+
- watch
147+
- apiGroups:
148+
- apiextensions.k8s.io
149+
resources:
150+
- customresourcedefinitions
151+
verbs:
152+
- create
153+
- get
154+
- list
155+
- watch
103156
---
104157
apiVersion: rbac.authorization.k8s.io/v1
105158
kind: ClusterRoleBinding

kube_downscaler/cmd.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"horizontalpodautoscalers",
1111
"rollouts",
1212
"scaledobjects",
13+
"jobs",
1314
"daemonsets",
1415
"poddisruptionbudgets",
1516
]
@@ -106,4 +107,9 @@ def get_parser():
106107
default=os.getenv("MATCHING_LABELS", ""),
107108
help="Apply downscaling to resources with the supplied labels. This is a comma-separated list of regex patterns. This is optional, downscaling will be applied to all resources by default.",
108109
)
110+
parser.add_argument(
111+
"--admission-controller",
112+
default=os.getenv("ADMISSION_CONTROLLER", ""),
113+
help="Apply downscaling to jobs using the supplied admission controller. Jobs should be included inside --include-resources if you want to use this parameter. kyverno and gatekeeper are supported.",
114+
)
109115
return parser

kube_downscaler/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def main(args=None):
3131
args.namespace,
3232
args.include_resources,
3333
args.matching_labels,
34+
args.admission_controller,
3435
args.upscale_period,
3536
args.downscale_period,
3637
args.default_uptime,
@@ -51,6 +52,7 @@ def run_loop(
5152
namespace,
5253
include_resources,
5354
matching_labels,
55+
admission_controller,
5456
upscale_period,
5557
downscale_period,
5658
default_uptime,
@@ -80,6 +82,7 @@ def run_loop(
8082
exclude_deployments=frozenset(exclude_deployments.split(",")),
8183
dry_run=dry_run,
8284
grace_period=grace_period,
85+
admission_controller=admission_controller,
8386
downtime_replicas=downtime_replicas,
8487
deployment_time_annotation=deployment_time_annotation,
8588
enable_events=enable_events,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pykube.objects import APIObject
2+
3+
4+
class KubeDownscalerJobsConstraint(APIObject):
5+
6+
"""Support the Gatakeeper Admission Controller Custom CRDs (https://open-policy-agent.github.io/gatekeeper/website/docs)."""
7+
8+
version = "constraints.gatekeeper.sh/v1beta1"
9+
endpoint = "kubedownscalerjobsconstraint"
10+
kind = "KubeDownscalerJobsConstraint"
11+
12+
@property
13+
def namespaces_list(self):
14+
return self.obj["spec"]["match"].get("namespaces", [])
15+
16+
@namespaces_list.setter
17+
def namespaces_list(self, updated_list):
18+
self.obj["spec"]["match"]["namespaces"] = updated_list
19+
20+
@staticmethod
21+
def create_job_constraint(resource_name):
22+
obj = {
23+
"apiVersion": "constraints.gatekeeper.sh/v1beta1",
24+
"kind": "KubeDownscalerJobsConstraint",
25+
"metadata": {
26+
"name": resource_name,
27+
"labels": {
28+
"origin": "kube-downscaler"
29+
}
30+
},
31+
"spec": {
32+
"match": {
33+
"namespaces": [resource_name]
34+
}
35+
}
36+
}
37+
38+
return obj
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from pykube.objects import APIObject
2+
3+
from kube_downscaler.helper import logger
4+
5+
6+
class ConstraintTemplate(APIObject):
7+
8+
"""Support the Gatakeeper Admission Controller Custom CRDs (https://open-policy-agent.github.io/gatekeeper/website/docs)."""
9+
10+
version = "templates.gatekeeper.sh/v1"
11+
endpoint = "constrainttemplates"
12+
kind = "ConstraintTemplate"
13+
14+
@staticmethod
15+
def create_constraint_template_crd(excluded_jobs, matching_labels):
16+
17+
excluded_jobs_regex = '^(' + '|'.join(excluded_jobs) + ')$'
18+
19+
# For backwards compatibility, if the matching_labels FrozenSet has an empty string as the first element,
20+
# we don't ignore anything
21+
first_element = next(iter(matching_labels), None)
22+
first_element_str = first_element.pattern
23+
24+
if first_element_str == "":
25+
logger.debug("Matching_labels arg set to empty string: all resources are considered in the scaling process")
26+
matching_labels_arg_is_present = False
27+
else:
28+
matching_labels_arg_is_present = True
29+
30+
if matching_labels_arg_is_present:
31+
matching_labels_rego_string: str = "\n"
32+
for pattern in matching_labels:
33+
matching_labels_rego_string = matching_labels_rego_string + " has_matched_labels(\"" + pattern.pattern + "\", input.review.object.metadata.labels)\n"
34+
else:
35+
matching_labels_rego_string: str = ""
36+
37+
rego = """
38+
package kubedownscalerjobsconstraint
39+
40+
violation[{"msg": msg}] {
41+
input.review.kind.kind == "Job"
42+
not exist_owner_reference
43+
not exact_match(\"""" + excluded_jobs_regex + """\", input.review.object.metadata.name)
44+
not has_exclude_annotation
45+
not is_exclude_until_date_reached""" + matching_labels_rego_string + """
46+
msg := "Job creation is not allowed in this namespace during a kube-downscaler downtime period."
47+
}
48+
49+
exact_match(pattern, name) {
50+
regex.match(pattern, name)
51+
}
52+
53+
exist_owner_reference {
54+
input.review.object.metadata.ownerReferences
55+
}
56+
57+
has_exclude_annotation {
58+
input.review.object.metadata.annotations["downscaler/exclude"] = "true"
59+
}
60+
61+
is_exclude_until_date_reached {
62+
until_date_str := input.review.object.metadata.annotations["downscaler/exclude-until"]
63+
parsed_until_date := time.parse_rfc3339_ns(until_date_str)
64+
current_utc := time.now_ns()
65+
parsed_until_date >= current_utc
66+
}
67+
68+
has_matched_labels(pattern, labels) {
69+
some k
70+
value := labels[k]
71+
key_equals_contact := concat("", [k, "="])
72+
equals_value_contact := concat("", [key_equals_contact, value])
73+
regex.match(pattern, equals_value_contact)
74+
}
75+
"""
76+
77+
obj = {
78+
"apiVersion": "templates.gatekeeper.sh/v1",
79+
"kind": "ConstraintTemplate",
80+
"metadata": {
81+
"name": "kubedownscalerjobsconstraint",
82+
"annotations": {
83+
"metadata.gatekeeper.sh/title": "Kube Downscaler Jobs Constraint",
84+
"metadata.gatekeeper.sh/version": "1.0.0",
85+
"description": "Policy to downscale jobs in certain namespaces."
86+
}
87+
},
88+
"spec": {
89+
"crd": {
90+
"spec": {
91+
"names": {
92+
"kind": "KubeDownscalerJobsConstraint"
93+
}
94+
}
95+
},
96+
"targets": [
97+
{
98+
"target": "admission.k8s.gatekeeper.sh",
99+
"rego": rego
100+
}
101+
]
102+
}
103+
}
104+
105+
return obj

0 commit comments

Comments
 (0)