Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ tasks:
silent: true
desc: Build the image locally
cmds:
- uv lock --upgrade
- |
BASEIMG=$(task base-image-name)
IMG="$BASEIMG:{{.TAG}}"
Expand Down
2 changes: 1 addition & 1 deletion TaskfileBuilder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ tasks:
HASH:
sh: echo '{{.IMAGE}}' | cut -d':' -f2
MANIFEST_DIGEST:
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'
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'
cmds:
- echo 'Deleting image {{.IMAGE}}'
- echo "Deleting manifest {{.MANIFEST_DIGEST}} for image {{.IMAGE_NAME}}"
Expand Down
18 changes: 14 additions & 4 deletions TaskfileDev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@ tasks:
silent: true
cmds:
- mkdir -p tokens
- kubectl get secret nuvolaris-wsku-secret -o jsonpath='{.data.token}' | base64 --decode > tokens/token
- kubectl get secret nuvolaris-wsku-secret -o jsonpath='{.data.ca\.crt}' | base64 --decode > tokens/ca.crt
- rm -f tokens/token && kubectl -n nuvolaris exec -it nuvolaris-operator-0 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token > tokens/token
- 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

setup-developer:
desc: "Setup developer environment"
silent: true
silent: false
vars:
REGISTRY_PASS:
sh: ops util kubectl -- -n nuvolaris get cm/config -o jsonpath='{.metadata.annotations.registry_password}'
COUCHDB_PASS:
sh: ops util kubectl -- -n nuvolaris get whisk -o jsonpath='{.items[0].spec.couchdb.admin.password}'
KUB_SERVICE_PORT:
sh: "docker port nuvolaris-control-plane | grep 6443 | cut -d: -f2"
cmds:
- task: get-tokens
- |
Expand All @@ -41,7 +48,10 @@ tasks:
if [ ! -d .venv ];
then uv venv
fi
- uv pip install -r pyproject.toml 2>/dev/null
- uv pip install -r pyproject.toml 2>/dev/null
- sed -i '' 's/^KUBERNETES_SERVICE_PORT=.*/KUBERNETES_SERVICE_PORT={{.KUB_SERVICE_PORT}}/' .env
- sed -i '' 's/^REGISTRY_PASS=.*/REGISTRY_PASS={{.REGISTRY_PASS}}/' .env
- sed -i '' 's/^COUCHDB_ADMIN_PASSWORD=.*/COUCHDB_ADMIN_PASSWORD={{.COUCHDB_PASS}}/' .env

run:
desc: |
Expand Down
4 changes: 2 additions & 2 deletions deploy/buildkit/buildkitd.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
# Registry HTTP insicuro
# =========================
[registry."nuvolaris-registry-svc:5000"]
insecure = true
#insecure = true
http = true

# =========================
# Logging
# =========================
[log]
level = "debug"
level = "trace"
21 changes: 21 additions & 0 deletions deploy/samples/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM d4rkstar/ops-runtime-python:v313
COPY requirements.txt /tmp/requirements.txt
USER root
RUN /bin/extend
USER nobody
4 changes: 2 additions & 2 deletions deploy/samples/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
gnews
beautifulsoup4
beautifulsoup4
setuptools
54 changes: 45 additions & 9 deletions openserverless/common/kube_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@
SERVICE_CERT_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
class KubeApiClient:

@staticmethod
def build_dockerconfigjson(username: str, password: str, registry: str = "https://index.docker.io/v1/") -> str:
"""
Crea una stringa .dockerconfigjson per un secret docker-registry.
:param username: Username del registry
:param password: Password del registry
:param registry: URL del registry (default Docker Hub)
:return: Stringa JSON da usare come valore per .dockerconfigjson
"""
import base64
import json as _json
auth = base64.b64encode(f"{username}:{password}".encode()).decode()
dockerconfig = {
"auths": {
registry: {
"auth": auth,
"username": username,
"password": password
}
}
}
return _json.dumps(dockerconfig)

def __init__(self, environ=os.environ):
self._environ = environ
self.SERVICE_TOKEN_FILENAME = self._environ.get("KUBERNETES_TOKEN_FILENAME") or SERVICE_TOKEN_FILENAME
Expand Down Expand Up @@ -348,23 +371,39 @@ def get_secret(self, secret_name: str, namespace="nuvolaris"):
logging.error(f"get_secret {ex}")
return None

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

if type == "kubernetes.io/dockerconfigjson":
# secret_data should contain a key '.dockerconfigjson' with the JSON string value
manifest_data = {
".dockerconfigjson": b64encode(secret_data[".dockerconfigjson"].encode()).decode()
}
elif type == "kubernetes.io/tls":
# secret_data should contain 'tls.crt' and 'tls.key'
manifest_data = {
"tls.crt": b64encode(secret_data["tls.crt"].encode()).decode(),
"tls.key": b64encode(secret_data["tls.key"].encode()).decode()
}
else:
# Default: Opaque or other types
manifest_data = {k: b64encode(v.encode()).decode() for k, v in secret_data.items()}

secret_manifest = {
"apiVersion": "v1",
"kind": "Secret",
"metadata": {"name": secret_name},
"data": {k: b64encode(v.encode()).decode() for k, v in secret_data.items()},
"type": "Opaque"
"data": manifest_data,
"type": type
}

try:
Expand Down Expand Up @@ -511,17 +550,14 @@ def get_pod_by_job_name(self, job_name: str, namespace="nuvolaris"):
try:
while True:
resp = req.get(url, headers=headers, verify=self.ssl_ca_cert)

if not response.status_code in [200, 202]:
if not resp.status_code in [200, 202]:
logging.error(
f"POST to {url} failed with {response.status_code}. Body {response.text}"
f"POST to {url} failed with {resp.status_code}. Body {resp.text}"
)
return None

logging.debug(
f"POST to {url} succeeded with {response.status_code}. Body {response.text}"
f"POST to {url} succeeded with {resp.status_code}. Body {resp.text}"
)

pods = resp.json()["items"]
for pod in pods:
labels = pod["metadata"].get("labels", {})
Expand Down
106 changes: 76 additions & 30 deletions openserverless/impl/builder/build_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
# under the License.
#
import shutil
import time
from openserverless.common.kube_api_client import KubeApiClient
import os
import uuid
import logging
from datetime import datetime, timezone, timedelta
from types import SimpleNamespace
import random
import string

JOB_NAME = "build"
CM_NAME = "cm"
Expand Down Expand Up @@ -59,11 +62,8 @@ def __init__(self, user_env=None):

# define registry auth
self.registry_auth = self.get_registry_auth()
self.custom_registry_auth = False
logging.info(f"Using registry auth: {self.registry_auth}")

# define demo mode
self.demo_mode = int(os.environ.get("DEMO_MODE", 0)) == 1
logging.info(f"Using demo mode: {self.demo_mode}")

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


def create_registry_secret(self, username: str, password: str, registry: str):
randompart = ''.join(random.choices(string.ascii_lowercase + string.digits, k=5))
random_name = f"reg-{self.user}-{randompart}"
conf = KubeApiClient.build_dockerconfigjson(username=username, password=password,registry=registry)

data = {".dockerconfigjson": conf}
secret = self.kube_client.post_secret(secret_name=random_name, secret_data=data, type="kubernetes.io/dockerconfigjson")
return secret

def get_registry_host(self) -> str:
"""
Retrieve the registry host
- firstly, check if the user environment has a registry host set
- otherwise retrieve the OpenServerless config map
- if not present use a default value
"""
registry_host = 'nuvolaris-registry-svc:5000'
"""
# if customized by the user
if (self.user_env.get('REGISTRY_HOST') is not None):
return self.build_config.get('REGISTRY_HOST')

return self.user_env.get('REGISTRY_HOST')

# the default
registry_host = 'nuvolaris-registry-svc:5000'
ops_config_map = self.kube_client.get_config_map('config')
if ops_config_map is not None:
if 'annotations' in ops_config_map.get('metadata', {}):
Expand All @@ -108,29 +118,44 @@ def get_registry_host(self) -> str:

def get_registry_auth(self) -> str:
"""
Get the name of the registry auth secret. If the user environment has a registry auth set, use it.
Otherwise, use the default 'registry-pull-secret'.
Get the name of the registry auth secret. If the user environment has a registry auth set, use it.
"""
if (self.user_env.get('REGISTRY_SECRET') is not None):
return self.build_config.get('REGISTRY_SECRET')
custom_credentials = self.user_env.get('REGISTRY_SECRET')
# if not ':' it means that the user is referencing an already created custom secret
if ":" not in custom_credentials:
return custom_credentials

username, password = custom_credentials.split(":")
registry_secret = self.create_registry_secret(
username=username, password=password,
registry=self.registry_host
)
if registry_secret is not None:
self.registry_auth = registry_secret['metadata']['name']
# is custom only when is not equal to the default
if self.registry_auth != "registry-pull-secret":
self.custom_registry_auth = True

return 'registry-pull-secret'
return self.user_env.get('REGISTRY_SECRET')

return 'registry-pull-secret-int'

def create_docker_file(self) -> str:
def create_docker_file(self, requirements=None) -> str:
"""
Create a Dockerfile in the current directory.
"""
source = self.build_config.get("source")

dockerfile_content = f"FROM {source}\n"
if 'file' in self.build_config:
requirement_file = self.get_requirements_file_from_kind()
dockerfile_content += f"COPY ./{requirement_file} /tmp/{requirement_file}\n"
if self.demo_mode:
dockerfile_content += "RUN echo \"/bin/extend\"\n"
else:
dockerfile_content += "RUN \"/bin/extend\"\n"
dockerfile_content = f"FROM {source}\n\n"

if 'file' in self.build_config:
if requirements is not None:
dockerfile_content += f"COPY {requirements} /tmp/{requirements}\n"
dockerfile_content += "USER root\n"
dockerfile_content += "RUN /bin/extend\n"
dockerfile_content += "USER nobody\n"


return dockerfile_content

Expand Down Expand Up @@ -163,30 +188,42 @@ def build(self, image_name: str) -> str:
"""
import tempfile
import base64


# define registry host
self.registry_host = self.get_registry_host()
if self.registry_host is None:
return None
logging.info(f"Using registry host: {self.registry_host}")

# define registry auth
self.registry_auth = self.get_registry_auth()

logging.info(f"Using registry auth: {self.registry_auth}")

# firstly remove old build jobs
self.delete_old_build_jobs()

tmpdirname = tempfile.mkdtemp()
logging.info(f"Starting the build to: {tmpdirname}")
requirements_file = None
if 'file' in self.build_config:
logging.info("Decoding the requirements file from base64")
# decode base64 self.build_config.get('file')
try:
requirements = base64.b64decode(self.build_config.get('file')).decode('utf-8')

requirement_file = self.get_requirements_file_from_kind()
with open(os.path.join(tmpdirname, requirement_file), 'w') as f:
requirements_file = self.get_requirements_file_from_kind()

with open(os.path.join(tmpdirname,requirements_file), 'w') as f:
f.write(requirements)

except Exception as e:
logging.error(f"Failed to decode the requirements file: {e}")
return None

dockerfile_path = os.path.join(tmpdirname, "Dockerfile")
logging.info(f"Creating Dockerfile at: {dockerfile_path}")
with open(dockerfile_path, "w") as dockerfile:
dockerfile.write(self.create_docker_file())
dockerfile.write(self.create_docker_file(requirements=requirements_file))

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

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


if self.custom_registry_auth:
if not self.kube_client.delete_secret(secret_name=self.registry_auth):
logging.error(f"Failed to delete Secret {self.custom_registry_auth}")

return job

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

def create_build_job(self, image_name: str) -> dict:
"""Create a Kubernetes job manifest for building the Docker image."""
registry_image_name = f"{self.registry_host}/{image_name}"
if not self.custom_registry_auth:
registry_image_name = f"{self.registry_host}/{image_name}"
else:
registry_image_name = f"{image_name}"

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