Skip to content

Commit 983464b

Browse files
Merge pull request #8 from d4rkstar/main
feat: image builder
2 parents fff664a + d936b5e commit 983464b

10 files changed

Lines changed: 480 additions & 282 deletions

File tree

Taskfile.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ tasks:
112112
silent: true
113113
desc: Build the image locally
114114
cmds:
115+
- uv lock --upgrade
115116
- |
116117
BASEIMG=$(task base-image-name)
117118
IMG="$BASEIMG:{{.TAG}}"

TaskfileBuilder.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ tasks:
117117
HASH:
118118
sh: echo '{{.IMAGE}}' | cut -d':' -f2
119119
MANIFEST_DIGEST:
120-
sh: curl --silent -u $REGISTRY_USER:$REGISTRY_PASS $REGISTRY_HOST/v2/{{.IMAGE_NAME}}/manifests/{{.HASH}} | grep -i 'Docker-Content-Digest:' | awk '{print $2}' | tr -d '\r'
120+
sh: curl -u $REGISTRY_USER:$REGISTRY_PASS -s -D - -o /dev/null $REGISTRY_HOST/v2/{{.IMAGE_NAME}}/manifests/{{.HASH}} | grep -i 'Docker-Content-Digest:' | awk '{print $2}' | tr -d '\r'
121121
cmds:
122122
- echo 'Deleting image {{.IMAGE}}'
123123
- echo "Deleting manifest {{.MANIFEST_DIGEST}} for image {{.IMAGE_NAME}}"

TaskfileDev.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,19 @@ tasks:
2424
silent: true
2525
cmds:
2626
- mkdir -p tokens
27-
- kubectl get secret nuvolaris-wsku-secret -o jsonpath='{.data.token}' | base64 --decode > tokens/token
28-
- kubectl get secret nuvolaris-wsku-secret -o jsonpath='{.data.ca\.crt}' | base64 --decode > tokens/ca.crt
27+
- rm -f tokens/token && kubectl -n nuvolaris exec -it nuvolaris-operator-0 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token > tokens/token
28+
- rm -f tokens/ca.crt && kubectl -n nuvolaris exec -it nuvolaris-operator-0 -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt > tokens/ca.crt
2929

3030
setup-developer:
3131
desc: "Setup developer environment"
32-
silent: true
32+
silent: false
33+
vars:
34+
REGISTRY_PASS:
35+
sh: ops util kubectl -- -n nuvolaris get cm/config -o jsonpath='{.metadata.annotations.registry_password}'
36+
COUCHDB_PASS:
37+
sh: ops util kubectl -- -n nuvolaris get whisk -o jsonpath='{.items[0].spec.couchdb.admin.password}'
38+
KUB_SERVICE_PORT:
39+
sh: "docker port nuvolaris-control-plane | grep 6443 | cut -d: -f2"
3340
cmds:
3441
- task: get-tokens
3542
- |
@@ -41,7 +48,10 @@ tasks:
4148
if [ ! -d .venv ];
4249
then uv venv
4350
fi
44-
- uv pip install -r pyproject.toml 2>/dev/null
51+
- uv pip install -r pyproject.toml 2>/dev/null
52+
- sed -i '' 's/^KUBERNETES_SERVICE_PORT=.*/KUBERNETES_SERVICE_PORT={{.KUB_SERVICE_PORT}}/' .env
53+
- sed -i '' 's/^REGISTRY_PASS=.*/REGISTRY_PASS={{.REGISTRY_PASS}}/' .env
54+
- sed -i '' 's/^COUCHDB_ADMIN_PASSWORD=.*/COUCHDB_ADMIN_PASSWORD={{.COUCHDB_PASS}}/' .env
4555

4656
run:
4757
desc: |

deploy/buildkit/buildkitd.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
# Registry HTTP insicuro
3232
# =========================
3333
[registry."nuvolaris-registry-svc:5000"]
34-
insecure = true
34+
#insecure = true
3535
http = true
3636

3737
# =========================
3838
# Logging
3939
# =========================
4040
[log]
41-
level = "debug"
41+
level = "trace"

deploy/samples/Dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
FROM d4rkstar/ops-runtime-python:v313
18+
COPY requirements.txt /tmp/requirements.txt
19+
USER root
20+
RUN /bin/extend
21+
USER nobody

deploy/samples/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
gnews
2-
beautifulsoup4
1+
beautifulsoup4
2+
setuptools

openserverless/common/kube_api_client.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,29 @@
3333
SERVICE_CERT_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
3434
class KubeApiClient:
3535

36+
@staticmethod
37+
def build_dockerconfigjson(username: str, password: str, registry: str = "https://index.docker.io/v1/") -> str:
38+
"""
39+
Crea una stringa .dockerconfigjson per un secret docker-registry.
40+
:param username: Username del registry
41+
:param password: Password del registry
42+
:param registry: URL del registry (default Docker Hub)
43+
:return: Stringa JSON da usare come valore per .dockerconfigjson
44+
"""
45+
import base64
46+
import json as _json
47+
auth = base64.b64encode(f"{username}:{password}".encode()).decode()
48+
dockerconfig = {
49+
"auths": {
50+
registry: {
51+
"auth": auth,
52+
"username": username,
53+
"password": password
54+
}
55+
}
56+
}
57+
return _json.dumps(dockerconfig)
58+
3659
def __init__(self, environ=os.environ):
3760
self._environ = environ
3861
self.SERVICE_TOKEN_FILENAME = self._environ.get("KUBERNETES_TOKEN_FILENAME") or SERVICE_TOKEN_FILENAME
@@ -348,23 +371,39 @@ def get_secret(self, secret_name: str, namespace="nuvolaris"):
348371
logging.error(f"get_secret {ex}")
349372
return None
350373

351-
def post_secret(self, secret_name: str, secret_data: dict, namespace="nuvolaris"):
374+
def post_secret(self, secret_name: str, secret_data: dict, type: str="Opaque", namespace="nuvolaris"):
352375
"""
353376
Create a Kubernetes secret.
354377
:param secret_name: Name of the secret.
355378
:param secret_data: Dictionary containing the secret data.
379+
:param type: Type of the secret (Opaque, kubernetes.io/dockerconfigjson, kubernetes.io/tls)
356380
:param namespace: Namespace where the secret will be created.
357381
:return: The created secret or None if failed.
358382
"""
359383
url = f"{self.host}/api/v1/namespaces/{namespace}/secrets"
360384
headers = {"Authorization": self.token, "Content-Type": "application/json"}
361385

386+
if type == "kubernetes.io/dockerconfigjson":
387+
# secret_data should contain a key '.dockerconfigjson' with the JSON string value
388+
manifest_data = {
389+
".dockerconfigjson": b64encode(secret_data[".dockerconfigjson"].encode()).decode()
390+
}
391+
elif type == "kubernetes.io/tls":
392+
# secret_data should contain 'tls.crt' and 'tls.key'
393+
manifest_data = {
394+
"tls.crt": b64encode(secret_data["tls.crt"].encode()).decode(),
395+
"tls.key": b64encode(secret_data["tls.key"].encode()).decode()
396+
}
397+
else:
398+
# Default: Opaque or other types
399+
manifest_data = {k: b64encode(v.encode()).decode() for k, v in secret_data.items()}
400+
362401
secret_manifest = {
363402
"apiVersion": "v1",
364403
"kind": "Secret",
365404
"metadata": {"name": secret_name},
366-
"data": {k: b64encode(v.encode()).decode() for k, v in secret_data.items()},
367-
"type": "Opaque"
405+
"data": manifest_data,
406+
"type": type
368407
}
369408

370409
try:
@@ -511,17 +550,14 @@ def get_pod_by_job_name(self, job_name: str, namespace="nuvolaris"):
511550
try:
512551
while True:
513552
resp = req.get(url, headers=headers, verify=self.ssl_ca_cert)
514-
515-
if not response.status_code in [200, 202]:
553+
if not resp.status_code in [200, 202]:
516554
logging.error(
517-
f"POST to {url} failed with {response.status_code}. Body {response.text}"
555+
f"POST to {url} failed with {resp.status_code}. Body {resp.text}"
518556
)
519557
return None
520-
521558
logging.debug(
522-
f"POST to {url} succeeded with {response.status_code}. Body {response.text}"
559+
f"POST to {url} succeeded with {resp.status_code}. Body {resp.text}"
523560
)
524-
525561
pods = resp.json()["items"]
526562
for pod in pods:
527563
labels = pod["metadata"].get("labels", {})

openserverless/impl/builder/build_service.py

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
# under the License.
1717
#
1818
import shutil
19+
import time
1920
from openserverless.common.kube_api_client import KubeApiClient
2021
import os
2122
import uuid
2223
import logging
2324
from datetime import datetime, timezone, timedelta
2425
from types import SimpleNamespace
26+
import random
27+
import string
2528

2629
JOB_NAME = "build"
2730
CM_NAME = "cm"
@@ -59,11 +62,8 @@ def __init__(self, user_env=None):
5962

6063
# define registry auth
6164
self.registry_auth = self.get_registry_auth()
65+
self.custom_registry_auth = False
6266
logging.info(f"Using registry auth: {self.registry_auth}")
63-
64-
# define demo mode
65-
self.demo_mode = int(os.environ.get("DEMO_MODE", 0)) == 1
66-
logging.info(f"Using demo mode: {self.demo_mode}")
6767

6868
def init(self, build_config: dict):
6969
"""
@@ -85,18 +85,28 @@ def init(self, build_config: dict):
8585
if status is None:
8686
logging.error("Failed to create nuvolaris-buildkitd-conf ConfigMap")
8787

88-
88+
def create_registry_secret(self, username: str, password: str, registry: str):
89+
randompart = ''.join(random.choices(string.ascii_lowercase + string.digits, k=5))
90+
random_name = f"reg-{self.user}-{randompart}"
91+
conf = KubeApiClient.build_dockerconfigjson(username=username, password=password,registry=registry)
92+
93+
data = {".dockerconfigjson": conf}
94+
secret = self.kube_client.post_secret(secret_name=random_name, secret_data=data, type="kubernetes.io/dockerconfigjson")
95+
return secret
96+
8997
def get_registry_host(self) -> str:
9098
"""
9199
Retrieve the registry host
92100
- firstly, check if the user environment has a registry host set
93101
- otherwise retrieve the OpenServerless config map
94102
- if not present use a default value
95-
"""
96-
registry_host = 'nuvolaris-registry-svc:5000'
103+
"""
104+
# if customized by the user
97105
if (self.user_env.get('REGISTRY_HOST') is not None):
98-
return self.build_config.get('REGISTRY_HOST')
99-
106+
return self.user_env.get('REGISTRY_HOST')
107+
108+
# the default
109+
registry_host = 'nuvolaris-registry-svc:5000'
100110
ops_config_map = self.kube_client.get_config_map('config')
101111
if ops_config_map is not None:
102112
if 'annotations' in ops_config_map.get('metadata', {}):
@@ -108,29 +118,44 @@ def get_registry_host(self) -> str:
108118

109119
def get_registry_auth(self) -> str:
110120
"""
111-
Get the name of the registry auth secret. If the user environment has a registry auth set, use it.
112-
Otherwise, use the default 'registry-pull-secret'.
121+
Get the name of the registry auth secret. If the user environment has a registry auth set, use it.
113122
"""
114123
if (self.user_env.get('REGISTRY_SECRET') is not None):
115-
return self.build_config.get('REGISTRY_SECRET')
124+
custom_credentials = self.user_env.get('REGISTRY_SECRET')
125+
# if not ':' it means that the user is referencing an already created custom secret
126+
if ":" not in custom_credentials:
127+
return custom_credentials
116128

129+
username, password = custom_credentials.split(":")
130+
registry_secret = self.create_registry_secret(
131+
username=username, password=password,
132+
registry=self.registry_host
133+
)
134+
if registry_secret is not None:
135+
self.registry_auth = registry_secret['metadata']['name']
136+
# is custom only when is not equal to the default
137+
if self.registry_auth != "registry-pull-secret":
138+
self.custom_registry_auth = True
117139

118-
return 'registry-pull-secret'
140+
return self.user_env.get('REGISTRY_SECRET')
141+
142+
return 'registry-pull-secret-int'
119143

120-
def create_docker_file(self) -> str:
144+
def create_docker_file(self, requirements=None) -> str:
121145
"""
122146
Create a Dockerfile in the current directory.
123147
"""
124148
source = self.build_config.get("source")
125149

126-
dockerfile_content = f"FROM {source}\n"
127-
if 'file' in self.build_config:
128-
requirement_file = self.get_requirements_file_from_kind()
129-
dockerfile_content += f"COPY ./{requirement_file} /tmp/{requirement_file}\n"
130-
if self.demo_mode:
131-
dockerfile_content += "RUN echo \"/bin/extend\"\n"
132-
else:
133-
dockerfile_content += "RUN \"/bin/extend\"\n"
150+
dockerfile_content = f"FROM {source}\n\n"
151+
152+
if 'file' in self.build_config:
153+
if requirements is not None:
154+
dockerfile_content += f"COPY {requirements} /tmp/{requirements}\n"
155+
dockerfile_content += "USER root\n"
156+
dockerfile_content += "RUN /bin/extend\n"
157+
dockerfile_content += "USER nobody\n"
158+
134159

135160
return dockerfile_content
136161

@@ -163,30 +188,42 @@ def build(self, image_name: str) -> str:
163188
"""
164189
import tempfile
165190
import base64
166-
191+
192+
# define registry host
193+
self.registry_host = self.get_registry_host()
194+
if self.registry_host is None:
195+
return None
196+
logging.info(f"Using registry host: {self.registry_host}")
197+
198+
# define registry auth
199+
self.registry_auth = self.get_registry_auth()
200+
201+
logging.info(f"Using registry auth: {self.registry_auth}")
202+
167203
# firstly remove old build jobs
168204
self.delete_old_build_jobs()
169205

170206
tmpdirname = tempfile.mkdtemp()
171207
logging.info(f"Starting the build to: {tmpdirname}")
208+
requirements_file = None
172209
if 'file' in self.build_config:
173210
logging.info("Decoding the requirements file from base64")
174211
# decode base64 self.build_config.get('file')
175212
try:
176213
requirements = base64.b64decode(self.build_config.get('file')).decode('utf-8')
177-
178-
requirement_file = self.get_requirements_file_from_kind()
179-
with open(os.path.join(tmpdirname, requirement_file), 'w') as f:
214+
requirements_file = self.get_requirements_file_from_kind()
215+
216+
with open(os.path.join(tmpdirname,requirements_file), 'w') as f:
180217
f.write(requirements)
181-
218+
182219
except Exception as e:
183220
logging.error(f"Failed to decode the requirements file: {e}")
184221
return None
185222

186223
dockerfile_path = os.path.join(tmpdirname, "Dockerfile")
187224
logging.info(f"Creating Dockerfile at: {dockerfile_path}")
188225
with open(dockerfile_path, "w") as dockerfile:
189-
dockerfile.write(self.create_docker_file())
226+
dockerfile.write(self.create_docker_file(requirements=requirements_file))
190227

191228
# check if the directory contains a Dockerfile and is not empty.
192229
if not self.check_build_dir(tmpdirname):
@@ -212,9 +249,15 @@ def build(self, image_name: str) -> str:
212249
logging.error(f"Failed to create job {self.job_name}")
213250
return None
214251

252+
time.sleep(3)
215253
if not self.kube_client.delete_config_map(cm_name=self.cm):
216254
logging.error(f"Failed to delete ConfigMap {self.cm}")
217255

256+
257+
if self.custom_registry_auth:
258+
if not self.kube_client.delete_secret(secret_name=self.registry_auth):
259+
logging.error(f"Failed to delete Secret {self.custom_registry_auth}")
260+
218261
return job
219262

220263
def delete_old_build_jobs(self, max_age_hours: int = 24) -> int:
@@ -282,7 +325,10 @@ def check_build_dir(self, unzip_dir: str) -> bool:
282325

283326
def create_build_job(self, image_name: str) -> dict:
284327
"""Create a Kubernetes job manifest for building the Docker image."""
285-
registry_image_name = f"{self.registry_host}/{image_name}"
328+
if not self.custom_registry_auth:
329+
registry_image_name = f"{self.registry_host}/{image_name}"
330+
else:
331+
registry_image_name = f"{image_name}"
286332

287333
# --- MANIFEST DEL JOB ---
288334
job_manifest = {
@@ -341,7 +387,7 @@ def create_build_job(self, image_name: str) -> dict:
341387
"command": ["sh", "-c"],
342388
"args": [
343389
"rootlesskit buildkitd --config /config/buildkitd.toml & sleep 3 && "
344-
f"buildctl build --frontend=dockerfile.v0 --local context=/workspace --local dockerfile=/workspace --output=type=image,name={registry_image_name},push=true"
390+
f"buildctl build --progress=plain --frontend=dockerfile.v0 --local context=/workspace --local dockerfile=/workspace --output=type=image,name={registry_image_name},push=true"
345391
],
346392
"securityContext": {
347393
"runAsUser": 1000,

0 commit comments

Comments
 (0)