Skip to content

Commit fff664a

Browse files
authored
Merge pull request #7 from d4rkstar/main
feat: improved builder
2 parents 3aa0611 + 132c717 commit fff664a

7 files changed

Lines changed: 443 additions & 88 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ Available APIs at the moment:
3636

3737
### Build API
3838

39-
`POST /system/api/v1/build` - Perform the build of a custom image and push it to repository.
39+
`POST /system/api/v1/build/start` - Perform the build of a custom image and push it to repository.
40+
41+
`POST /system/api/v1/build/cleanup` - Perform cleanup of build jobs older than 24 hours (or different number of hours if otherwise specified)
4042

4143
More informations [Here](docs/DEPLOYER.md)
4244

@@ -71,6 +73,7 @@ Taskfile supports the following tasks:
7173
* buildx: Build the docker image using buildx. Set PUSH=1 to push the image to the registry.
7274
* docker-login: Login to the docker registry. Set REGISTRY=ghcr or REGISTRY=dockerhub in .env to use the respective registry.
7375
* image-tag: Create a new tag for the current git commit.
76+
* builder:clean: Cleanup old build jobs via api
7477
* builder:cleanjobs: Clean up old jobs
7578
* builder:delete-image: Delete an image from the registry
7679
* builder:get-image: Get an image from the registry

TODO.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
# TODO
2121

2222
## Tests
23-
Add integration and unit tests
23+
- [ ] Add integration tests
24+
- [X] Add unit tests
25+
- [ ] Add more unit tests
2426

2527
## Various
2628

2729
- [ ] `openserverless.common.whis_user_data.py` - Add `with_` blocks for other new OpenServerless Services
2830
- [ ] `openserverless.common.whisk_user_generator` - Check if `generate_whisk_user_yaml` is complete
29-
- [ ] cleanup config maps and builds
31+
- [X] cleanup config maps and builds

TaskfileBuilder.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,32 @@ tasks:
3131
- if test -z "{{.KIND}}"; then echo "KIND IS NOT SET" && exit 1; fi
3232
- |
3333
echo '{"source": "{{.SOURCE}}", "target": "{{.TARGET}}", "kind": "{{.KIND}}", "file": "{{.REQUIREMENTS}}" }' | \
34-
curl -X POST $ADMIN_API_URL/api/v1/build -H "Content-Type: application/json" -H "Authorization: {{.AUTH}}" -d @-
34+
curl -X POST $ADMIN_API_URL/api/v1/build/start -H "Content-Type: application/json" -H "Authorization: {{.AUTH}}" -d @-
3535
- sleep 5
3636
- task: logs
3737
deps:
3838
- cleanjobs
3939
# - updatetoml
4040
silent: true
4141

42+
clean:
43+
desc: Cleanup old build jobs via api
44+
vars:
45+
AUTH:
46+
sh: cat ~/.wskprops | grep "AUTH" | cut -d'=' -f2 | xargs -I {}
47+
MAX_AGE_HOURS:
48+
sh: |
49+
if test -z "{{.MAX_AGE_HOURS}}";
50+
then echo "24";
51+
else echo "{{.MAX_AGE_HOURS}}";
52+
fi
53+
cmds:
54+
- |
55+
echo '{"max_age_hours": "{{.MAX_AGE_HOURS}}" }' | \
56+
curl -X POST $ADMIN_API_URL/api/v1/build/cleanup -H "Content-Type: application/json" -H "Authorization: {{.AUTH}}" -d @-
57+
silent: false
58+
59+
4260
logs:
4361
desc: Show logs of the last build job
4462
cmds:
@@ -66,7 +84,7 @@ tasks:
6684
desc: List catalogs in the registry
6785
cmds:
6886
- curl -u $REGISTRY_USER:$REGISTRY_PASS $REGISTRY_HOST/v2/_catalog
69-
silent: false
87+
silent: true
7088

7189
list-images:
7290
desc: List images in a specific catalog
@@ -75,7 +93,7 @@ tasks:
7593
cmds:
7694
- if test -z "{{.CATALOG}}"; then echo "CATALOG IS NOT SET" && exit 1; fi
7795
- curl -u $REGISTRY_USER:$REGISTRY_PASS $REGISTRY_HOST/v2/{{.CATALOG}}/tags/list
78-
silent: false
96+
silent: true
7997

8098
get-image:
8199
desc: Get an image from the registry

docs/DEPLOYER.md

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,75 @@
1919
-->
2020
# Deployer
2121

22-
These tasks are useful to interact with OpenServerless Admin Api Builder
22+
Deployer is the implementation of the feature described
23+
in [OpenServerless Issue 156](https://github.com/apache/openserverless/issues/156).
2324

24-
There are some tasks to interact with OpenServerless internal registry too.
25+
Specifically, the deployer can extend a default runtime with user-defined
26+
"requirements" by generating a new "extended" user runtime and pushing it to
27+
OpenServerless’ internal Docker registry.
28+
29+
Actually, the supported "requirements" are listed in the following table:
30+
31+
| kind | requirement file |
32+
|:-------|:-----------------|
33+
| go | go.mod |
34+
| java | pom.xml |
35+
| nodejs | package.json |
36+
| php | composer.json |
37+
| python | requirements.txt |
38+
| ruby | Gemfile |
39+
| dotnet | project.json |
40+
41+
*NOTE*: this list will be improved when new extendible runtimes will be ready.
42+
43+
The "requirement" can be passed as base64 encoded string inside the `file` attribute
44+
of the json body payload:
45+
46+
```json
47+
{
48+
"source": "apache/openserverless-runtime-python:v3.13-2506091954",
49+
"target": "devel:python3.12-custom",
50+
"kind": "python",
51+
"file": "Z25ld3MKYmVhdXRpZnVsc291cDQ="
52+
}
53+
```
54+
55+
By default the deployer will push to OpenServerless internal docker registry.
56+
To detect the host, it will use the `registry_host` inside the Operator's config
57+
map.
58+
To authenticate, it will use the imagePullSecret named `registry-pull-secret`
59+
(these credentials are valid to push and pull from the internal registry).
60+
61+
The deployer supports also pushing to an external private docker registry, using
62+
ops env:
63+
64+
- `REGISTRY_HOST` - put here the hostname:port of the external private registry.
65+
- `REGISTRY_SECRET` - put here the name of a kubernetes secret containing an
66+
imagePullSecret able to push to the registry specified by `REGISTRY_HOST`.
67+
68+
This project has also support tasks:
69+
70+
- to test the build.
71+
- to interact with OpenServerless internal registry too.
72+
73+
See [Examples](#examples) section
74+
75+
## Endpoints
76+
77+
`POST /system/api/v1/build/start` - Perform the build of a custom image and push it to repository.
78+
79+
`POST /system/api/v1/build/cleanup` - Perform cleanup of build jobs older than 24 hours (or different number of hours if otherwise specified)
80+
81+
Both endpoints requires the wsk token in an `authorization` header.
82+
The token will be used to check the user (the target image hash needs to be
83+
always in the format `user:image-tag`).
2584

2685
## Available tasks
2786

2887
task: Available tasks for this project:
2988

3089
```
90+
* builder:clean: Cleanup old build jobs via api
3191
* builder:cleanjobs: Clean up old jobs
3292
* builder:delete-image: Delete an image from the registry
3393
* builder:get-image: Get an image from the registry
@@ -44,6 +104,12 @@ task: Available tasks for this project:
44104

45105
`task builder:send SOURCE=apache/openserverless-runtime-python:v3.13-2506091954 TARGET=devel:python3.13-custom KIND=python REQUIREMENTS=$(base64 -i deploy/samples/requirements.txt)`
46106

107+
### Clenaup of old jobs via API
108+
109+
`task builder:clean MAX_AGE_HOURS=2`
110+
111+
MAX_AGE_HOURS, if not specified, has a default value of 24.
112+
47113
### List images for the user
48114

49115
`task builder:list-images CATALOG=devel`

openserverless/common/kube_api_client.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def create_whisk_user(self, whisk_user_dict, namespace="nuvolaris"):
118118
logging.error("create_whisk_user %s", ex)
119119
return False
120120

121-
def delete_whisk_user(self, username, namespace="nuvolaris"):
121+
def delete_whisk_user(self, username: str, namespace="nuvolaris"):
122122
""" "
123123
Delete a whisk user using a DELETE operation
124124
param: username of the whisksusers resource to delete
@@ -147,7 +147,7 @@ def delete_whisk_user(self, username, namespace="nuvolaris"):
147147
logging.error(f"delete_whisk_user {ex}")
148148
return False
149149

150-
def get_whisk_user(self, username, namespace="nuvolaris"):
150+
def get_whisk_user(self, username: str, namespace="nuvolaris"):
151151
""" "
152152
Get a whisk user using a GET operation
153153
param: username of the whisksusers resource to delete
@@ -210,7 +210,7 @@ def update_whisk_user(self, whisk_user_dict, namespace="nuvolaris"):
210210
logging.error(f"update_whisk_user {ex}")
211211
return False
212212

213-
def get_config_map(self, cm_name, namespace="nuvolaris"):
213+
def get_config_map(self, cm_name: str, namespace="nuvolaris"):
214214
"""
215215
Get a ConfigMap by name.
216216
:param cm_name: Name of the ConfigMap.
@@ -238,7 +238,7 @@ def get_config_map(self, cm_name, namespace="nuvolaris"):
238238
logging.error(f"get_config_map {ex}")
239239
return None
240240

241-
def post_config_map(self, cm_name, file_or_dir, namespace="nuvolaris"):
241+
def post_config_map(self, cm_name: str, file_or_dir: str, namespace="nuvolaris"):
242242
"""
243243
Create a ConfigMap from a file or directory.
244244
:param cm_name: Name of the ConfigMap.
@@ -291,7 +291,7 @@ def post_config_map(self, cm_name, file_or_dir, namespace="nuvolaris"):
291291
logging.error(f"post_config_map {ex}")
292292
return None
293293

294-
def delete_config_map(self, cm_name, namespace="nuvolaris"):
294+
def delete_config_map(self, cm_name: str, namespace="nuvolaris"):
295295
"""
296296
Delete a ConfigMap by name.
297297
:param cm_name: Name of the ConfigMap to delete.
@@ -320,7 +320,7 @@ def delete_config_map(self, cm_name, namespace="nuvolaris"):
320320
logging.error(f"delete_config_map {ex}")
321321
return False
322322

323-
def get_secret(self, secret_name, namespace="nuvolaris"):
323+
def get_secret(self, secret_name: str, namespace="nuvolaris"):
324324
"""
325325
Get a Kubernetes secret by name.
326326
:param secret_name: Name of the secret.
@@ -348,7 +348,7 @@ def get_secret(self, secret_name, namespace="nuvolaris"):
348348
logging.error(f"get_secret {ex}")
349349
return None
350350

351-
def post_secret(self, secret_name, secret_data, namespace="nuvolaris"):
351+
def post_secret(self, secret_name: str, secret_data: dict, namespace="nuvolaris"):
352352
"""
353353
Create a Kubernetes secret.
354354
:param secret_name: Name of the secret.
@@ -385,7 +385,7 @@ def post_secret(self, secret_name, secret_data, namespace="nuvolaris"):
385385
logging.error(f"post_secret {ex}")
386386
return None
387387

388-
def delete_secret(self, secret_name, namespace="nuvolaris"):
388+
def delete_secret(self, secret_name: str, namespace="nuvolaris"):
389389
"""
390390
Delete a Kubernetes secret.
391391
:param secret_name: Name of the secret to delete.
@@ -412,11 +412,70 @@ def delete_secret(self, secret_name, namespace="nuvolaris"):
412412
except Exception as ex:
413413
logging.error(f"delete_secret {ex}")
414414
return False
415+
416+
def get_jobs(self, name_filter: str = None, namespace="nuvolaris"):
417+
"""
418+
Get all Kubernetes jobs in a specific namespace.
419+
:param namespace: Namespace to list jobs from.
420+
:return: List of jobs or None if failed.
421+
"""
422+
url = f"{self.host}/apis/batch/v1/namespaces/{namespace}/jobs"
423+
headers = {"Authorization": self.token}
424+
try:
425+
logging.info(f"GET request to {url}")
426+
response = req.get(url, headers=headers, verify=self.ssl_ca_cert)
427+
428+
if response.status_code in [200, 202]:
429+
logging.debug(
430+
f"GET to {url} succeeded with {response.status_code}. Body {response.text}"
431+
)
432+
433+
if name_filter:
434+
jobs = json.loads(response.text)["items"]
435+
filtered_jobs = [job for job in jobs if name_filter in job["metadata"]["name"]]
436+
return filtered_jobs
437+
438+
return json.loads(response.text)["items"]
415439

416-
def post_job(self, job_name, job_manifest, namespace="nuvolaris"):
440+
logging.error(
441+
f"GET to {url} failed with {response.status_code}. Body {response.text}"
442+
)
443+
return None
444+
except Exception as ex:
445+
logging.error(f"get_jobs {ex}")
446+
return None
447+
448+
def delete_job(self, job_name: str, namespace="nuvolaris"):
449+
"""
450+
Delete a Kubernetes job by name.
451+
:param job_name: Name of the job to delete.
452+
:param namespace: Namespace where the job is located.
453+
:return: True if deletion was successful, False otherwise.
454+
"""
455+
url = f"{self.host}/apis/batch/v1/namespaces/{namespace}/jobs/{job_name}"
456+
headers = {"Authorization": self.token}
457+
458+
try:
459+
logging.info(f"DELETE request to {url}")
460+
response = req.delete(url, headers=headers, verify=self.ssl_ca_cert)
461+
462+
if response.status_code in [200, 202]:
463+
logging.debug(
464+
f"DELETE to {url} succeeded with {response.status_code}. Body {response.text}"
465+
)
466+
return True
467+
468+
logging.error(
469+
f"DELETE to {url} failed with {response.status_code}. Body {response.text}"
470+
)
471+
return False
472+
except Exception as ex:
473+
logging.error(f"delete_job {ex}")
474+
return False
475+
476+
def post_job(self, job_manifest: json, namespace="nuvolaris"):
417477
"""
418478
Create a Kubernetes job.
419-
:param job_name: Name of the job.
420479
:param job_manifest: Dictionary containing the job manifest.
421480
:param namespace: Namespace where the job will be created.
422481
:return: The created job or None if failed.
@@ -440,7 +499,7 @@ def post_job(self, job_name, job_manifest, namespace="nuvolaris"):
440499
logging.error(f"post_job {ex}")
441500
return None
442501

443-
def get_pod_by_job_name(self, job_name, namespace="nuvolaris"):
502+
def get_pod_by_job_name(self, job_name: str, namespace="nuvolaris"):
444503
"""
445504
Get the pod name associated with a job by its name.
446505
:param job_name: Name of the job.
@@ -474,7 +533,7 @@ def get_pod_by_job_name(self, job_name, namespace="nuvolaris"):
474533
logging.error(f"get_pod_by_job_name {ex}")
475534
return None
476535

477-
def stream_pod_logs(self, pod_name, namespace="nuvolaris"):
536+
def stream_pod_logs(self, pod_name: str, namespace="nuvolaris"):
478537
"""
479538
Stream logs from a specific pod.
480539
:param pod_name: Name of the pod to stream logs from.
@@ -487,7 +546,7 @@ def stream_pod_logs(self, pod_name, namespace="nuvolaris"):
487546
if line:
488547
print(line.decode())
489548

490-
def check_job_status(self, job_name, namespace="nuvolaris"):
549+
def check_job_status(self, job_name: str, namespace="nuvolaris"):
491550
"""
492551
Check the status of a job by its name.
493552
:param job_name: Name of the job to check.

0 commit comments

Comments
 (0)