Skip to content
This repository was archived by the owner on Jun 12, 2024. It is now read-only.

Commit a171917

Browse files
authored
Merge pull request #8 from LF-Engineering/feature/cronjob
Add support for CronJob/Job
2 parents d47b2c4 + 6418088 commit a171917

File tree

9 files changed

+345
-0
lines changed

9 files changed

+345
-0
lines changed

features/cronjob.feature

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Feature: Manage CronJobs
2+
3+
Scenario: Try retrieve a CronJob that does not exist
4+
Given a CronJob called hello does not exist
5+
When the user attempts to retrieve the CronJob hello
6+
Then None is returned instead of the CronJob
7+
8+
9+
Scenario: Create a CronJob
10+
Given a CronJob called salamander does not exist
11+
When the CronJob called salamander is created
12+
Then a valid CronJob called salamander can be found
13+
14+
15+
Scenario: Retrieve a CronJob that exists
16+
Given the CronJob called hello exists
17+
When the user attempts to retrieve the CronJob hello
18+
Then Results for the CronJob hello are returned

features/environment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ def before_all(context):
66
config.load_kube_config(context="minikube")
77
context.k8s_v1_core_client = client.CoreV1Api()
88
context.k8s_v1_apps_client = client.AppsV1Api()
9+
context.k8s_v1_batch_client = client.BatchV1Api()

features/job.feature

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Feature: Manage Jobs
2+
3+
Scenario: Try retrieve a Job that does not exist
4+
Given a Job called sleepy-bohr does not exist
5+
When the user attempts to retrieve the Job sleepy-bohr
6+
Then None is returned instead of the Job
7+
8+
9+
Scenario: Create a Job
10+
Given a Job called strange-bhaskara does not exist
11+
When the Job called strange-bhaskara is created
12+
Then a valid Job called strange-bhaskara can be found
13+
14+
15+
Scenario: Retrieve a Job that exists
16+
Given the Job called lucid-lovelace exists
17+
When the user attempts to retrieve the Job lucid-lovelace
18+
Then Results for the Job lucid-lovelace are returned

features/steps/cronjob.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from behave import *
2+
3+
from kelpy import cronjob
4+
from kubernetes import config, client
5+
from jinja2 import Environment, FileSystemLoader
6+
7+
8+
@when("the CronJob called {cronjob_name} is created")
9+
@given("the CronJob called {cronjob_name} exists")
10+
def step_impl(context, cronjob_name):
11+
cronjob_namespace = "default"
12+
13+
env = Environment(
14+
loader=FileSystemLoader("./templates"), trim_blocks=True, lstrip_blocks=True
15+
)
16+
cronjob_tmpl = env.get_template("cronjob.j2")
17+
cronjob_spec_file = cronjob_tmpl.render(
18+
cronjob_name=cronjob_name, cronjob_namespace=cronjob_namespace
19+
)
20+
context.create_resp = cronjob.create(
21+
context.k8s_v1_batch_client, cronjob_spec_file, cronjob_namespace
22+
)
23+
24+
25+
@when("the user attempts to retrieve the CronJob {cronjob_name}")
26+
def step_impl(context, cronjob_name):
27+
context.get_resp = cronjob.get(context.k8s_v1_batch_client, cronjob_name)
28+
29+
30+
@given("a CronJob called {cronjob_name} does not exist")
31+
def step_impl(context, cronjob_name):
32+
cronjob.delete(context.k8s_v1_batch_client, cronjob_name)
33+
34+
35+
@then("None is returned instead of the CronJob")
36+
def step_impl(context):
37+
assert context.get_resp is None, "Did not return None"
38+
39+
40+
@then("Results for the CronJob {cronjob_name} are returned")
41+
def step_impl(context, cronjob_name):
42+
assert context.get_resp, "Should've returned a valid response"
43+
44+
45+
@then("a valid CronJob called {cronjob_name} can be found")
46+
def step_impl(context, cronjob_name):
47+
assert context.create_resp, f"Should've found {cronjob_name} CronJob"

features/steps/job.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from behave import *
2+
3+
from kelpy import job
4+
from kubernetes import config, client
5+
from jinja2 import Environment, FileSystemLoader
6+
7+
8+
@given("a Job called {job_name} does not exist")
9+
def step_impl(context, job_name):
10+
job.delete(context.k8s_v1_batch_client, job_name)
11+
12+
13+
@given("the Job called {job_name} exists")
14+
@when("the Job called {job_name} is created")
15+
def step_impl(context, job_name):
16+
job_namespace = "default"
17+
18+
env = Environment(
19+
loader=FileSystemLoader("./templates"), trim_blocks=True, lstrip_blocks=True
20+
)
21+
job_tmpl = env.get_template("job.j2")
22+
job_spec_file = job_tmpl.render(job_name=job_name, job_namespace=job_namespace)
23+
context.create_resp = job.create(
24+
context.k8s_v1_batch_client, job_spec_file, job_namespace
25+
)
26+
27+
28+
@when("the user attempts to retrieve the Job {job_name}")
29+
def step_impl(context, job_name):
30+
context.get_resp = job.get(context.k8s_v1_batch_client, job_name)
31+
32+
33+
@then("None is returned instead of the Job")
34+
def step_impl(context):
35+
assert context.get_resp is None, "Did not return None"
36+
37+
38+
@then("a valid Job called {job_name} can be found")
39+
def step_impl(context, job_name):
40+
assert context.create_resp, f"Should've found {job_name} Job"
41+
42+
43+
@then("Results for the Job {job_name} are returned")
44+
def step_impl(context, job_name):
45+
assert context.get_resp, "Should've returned a valid response"

kelpy/cronjob.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import yaml
2+
from kubernetes.client.exceptions import ApiException
3+
from kubernetes import watch
4+
5+
6+
def create(client, spec: str, namespace: str = "default", timeout=100):
7+
"""Create a CronJob.
8+
9+
:batch_v1_api: The Batch V1 API object.
10+
:spec: A valid CronJob YAML manifest.
11+
:namespace: The namespace of the CronJob.
12+
:timeout: Timeout in seconds to wait for object creation/modification
13+
14+
:returns: True on creation, False if it already exists.
15+
"""
16+
body = yaml.safe_load(spec)
17+
18+
try:
19+
response = client.create_namespaced_cron_job(namespace, body)
20+
except ApiException as e:
21+
# If the object already exists, return False.
22+
if e.reason == "Conflict":
23+
return False
24+
raise e
25+
26+
name = body["metadata"]["name"]
27+
if get(client, name, namespace) is None:
28+
w = watch.Watch()
29+
for event in w.stream(
30+
client.list_cron_job_for_all_namespaces, timeout_seconds=timeout
31+
):
32+
if (
33+
(event["type"] == "ADDED" or event["type"] == "MODIFIED")
34+
and event["object"].kind == "CronJob"
35+
and event["object"].metadata.name == response.metadata.name
36+
and event["object"].metadata.namespace == response.metadata.namespace
37+
):
38+
break
39+
40+
return response
41+
42+
43+
def get(client, name: str, namespace: str = "default"):
44+
"""Get a CronJob if it does exist, if it doesn't, return None.
45+
46+
:batch_v1_api: The Batch V1 API object.
47+
:name: The name of the CronJob.
48+
:namespace: The namespace of the CronJob.
49+
50+
:returns: CronJob if it does exist, None if it doesn't exist.
51+
"""
52+
field_selector = "metadata.name={0}".format(name)
53+
54+
response = client.list_namespaced_cron_job(namespace, field_selector=field_selector)
55+
56+
if len(response.items) == 1:
57+
return response
58+
59+
return None
60+
61+
62+
def delete(client, name: str, namespace: str = "default", timeout=100):
63+
"""Delete a CronJob if it does exist, if it doesn't, return False.
64+
65+
:batch_v1_api: The Batch V1 API object.
66+
:name: The name of the CronJob.
67+
:namespace: The namespace of the CronJob.
68+
:timeout: Timeout in seconds to wait for object deletion
69+
70+
:returns: Batch API Response if it does exist, False if it doesn't exist.
71+
"""
72+
try:
73+
response = client.delete_namespaced_cron_job(name, namespace)
74+
except ApiException as e:
75+
if e.reason == "Not Found":
76+
return False
77+
raise e
78+
79+
if get(client, name, namespace) is not None:
80+
w = watch.Watch()
81+
for event in w.stream(
82+
client.list_cron_job_for_all_namespaces, timeout_seconds=timeout
83+
):
84+
if (
85+
event["type"] == "DELETED"
86+
and event["object"].metadata.name == name
87+
and event["object"].metadata.namespace == namespace
88+
):
89+
break
90+
91+
return response

kelpy/job.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import yaml
2+
from kubernetes.client.exceptions import ApiException
3+
from kubernetes import watch
4+
5+
6+
def create(client, spec: str, namespace: str = "default", timeout=100):
7+
"""Create a Job.
8+
9+
:batch_v1_api: The Batch V1 API object.
10+
:spec: A valid Job YAML manifest.
11+
:namespace: The namespace of the Job.
12+
:timeout: Timeout in seconds to wait for object creation/modification
13+
14+
:returns: True on creation, False if it already exists.
15+
"""
16+
body = yaml.safe_load(spec)
17+
18+
try:
19+
response = client.create_namespaced_job(namespace, body)
20+
except ApiException as e:
21+
# If the object already exists, return False.
22+
if e.reason == "Conflict":
23+
return False
24+
raise e
25+
26+
name = body["metadata"]["name"]
27+
if get(client, name, namespace) is None:
28+
w = watch.Watch()
29+
for event in w.stream(
30+
client.list_job_for_all_namespaces, timeout_seconds=timeout
31+
):
32+
if (
33+
(event["type"] == "ADDED" or event["type"] == "MODIFIED")
34+
and event["object"].kind == "Job"
35+
and event["object"].metadata.name == response.metadata.name
36+
and event["object"].metadata.namespace == response.metadata.namespace
37+
):
38+
break
39+
40+
return response
41+
42+
43+
def get(client, name: str, namespace: str = "default"):
44+
"""Get a Job if it does exist, if it doesn't, return None.
45+
46+
:batch_v1_api: The Batch V1 API object.
47+
:name: The name of the Job.
48+
:namespace: The namespace of the Job.
49+
50+
:returns: Job if it does exist, None if it doesn't exist.
51+
"""
52+
field_selector = "metadata.name={0}".format(name)
53+
54+
response = client.list_namespaced_job(namespace, field_selector=field_selector)
55+
56+
if len(response.items) == 1:
57+
return response
58+
59+
return None
60+
61+
62+
def delete(client, name: str, namespace: str = "default", timeout=100):
63+
"""Delete a Job if it does exist, if it doesn't, return False.
64+
65+
:batch_v1_api: The Batch V1 API object.
66+
:name: The name of the Job.
67+
:namespace: The namespace of the Job.
68+
:timeout: Timeout in seconds to wait for object deletion
69+
70+
:returns: Batch API Response if it does exist, False if it doesn't exist.
71+
"""
72+
try:
73+
response = client.delete_namespaced_job(name, namespace)
74+
except ApiException as e:
75+
if e.reason == "Not Found":
76+
return False
77+
raise e
78+
79+
if get(client, name, namespace) is not None:
80+
w = watch.Watch()
81+
for event in w.stream(
82+
client.list_job_for_all_namespaces, timeout_seconds=timeout
83+
):
84+
if (
85+
event["type"] == "DELETED"
86+
and event["object"].metadata.name == name
87+
and event["object"].metadata.namespace == namespace
88+
):
89+
break
90+
91+
return response

templates/cronjob.j2

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
apiVersion: batch/v1
2+
kind: CronJob
3+
metadata:
4+
name: {{ cronjob_name }}
5+
namespace: {{ cronjob_namespace }}
6+
spec:
7+
schedule: "*/1 * * * *"
8+
jobTemplate:
9+
spec:
10+
template:
11+
spec:
12+
containers:
13+
- name: hello
14+
image: busybox
15+
imagePullPolicy: IfNotPresent
16+
command:
17+
- /bin/sh
18+
- -c
19+
- date; echo Hello from the Kubernetes cluster
20+
restartPolicy: OnFailure

templates/job.j2

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: batch/v1
2+
kind: Job
3+
metadata:
4+
name: {{ job_name }}
5+
namespace: {{ job_namespace }}
6+
spec:
7+
template:
8+
spec:
9+
containers:
10+
- name: pi
11+
image: perl
12+
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
13+
restartPolicy: Never
14+
backoffLimit: 4

0 commit comments

Comments
 (0)