Skip to content

Commit 4ad2230

Browse files
authored
Merge pull request #11 from d4rkstar/main
feat: improve build service with init container cleanup and dev tooling
2 parents c9a7b4a + 58946b2 commit 4ad2230

File tree

15 files changed

+1143
-604
lines changed

15 files changed

+1143
-604
lines changed

.licenserc.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ header:
3333
- '**/*.service'
3434
- '**/*.txt'
3535
- 'uv.lock'
36-
- '.env.example'
36+
- '.env.example'
37+
- '.vscode/*.json'

.vscode/launch.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
"name": "Debug OpenServerless Admin API",
66
"type": "debugpy",
77
"request": "launch",
8-
"module": "openserverless"
8+
"module": "openserverless",
9+
"preLaunchTask": "Start Port Forwards",
10+
"postDebugTask": "Stop Port Forwards"
911
}
10-
12+
1113
]
1214
}

.vscode/start-port-forwards.sh

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/bin/bash
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
#
19+
set -e
20+
21+
# Log start time for debugging
22+
echo "$(date): Starting port forwards..." >> /tmp/pf-start.log
23+
24+
# Clean up any existing port forwards first
25+
pkill -f 'kubectl port-forward -n nuvolaris registry-0' 2>/dev/null || true
26+
pkill -f 'kubectl -n nuvolaris port-forward couchdb-0' 2>/dev/null || true
27+
rm -f /tmp/pf-registry.pid /tmp/pf-couchdb.pid /tmp/pf-registry.log /tmp/pf-couchdb.log
28+
sleep 1
29+
30+
# Start port forwards in background with nohup to detach from terminal
31+
nohup kubectl port-forward -n nuvolaris registry-0 5000:5000 > /tmp/pf-registry.log 2>&1 &
32+
REGISTRY_PID=$!
33+
echo $REGISTRY_PID > /tmp/pf-registry.pid
34+
echo "$(date): Registry PID: $REGISTRY_PID" >> /tmp/pf-start.log
35+
36+
nohup kubectl -n nuvolaris port-forward couchdb-0 5984:5984 > /tmp/pf-couchdb.log 2>&1 &
37+
COUCHDB_PID=$!
38+
echo $COUCHDB_PID > /tmp/pf-couchdb.pid
39+
echo "$(date): CouchDB PID: $COUCHDB_PID" >> /tmp/pf-start.log
40+
41+
# Disown the processes so they don't get killed when the script exits
42+
disown -a
43+
44+
# Wait a moment for port forwards to establish
45+
sleep 2
46+
47+
# Verify processes are still running
48+
if ps -p $REGISTRY_PID > /dev/null 2>&1; then
49+
echo "$(date): Registry port-forward is running" >> /tmp/pf-start.log
50+
else
51+
echo "$(date): WARNING: Registry port-forward died!" >> /tmp/pf-start.log
52+
fi
53+
54+
if ps -p $COUCHDB_PID > /dev/null 2>&1; then
55+
echo "$(date): CouchDB port-forward is running" >> /tmp/pf-start.log
56+
else
57+
echo "$(date): WARNING: CouchDB port-forward died!" >> /tmp/pf-start.log
58+
fi
59+
60+
echo "Port forwards started: registry=$REGISTRY_PID, couchdb=$COUCHDB_PID"

.vscode/tasks.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "Start Port Forwards",
6+
"type": "shell",
7+
"command": "${workspaceFolder}/.vscode/start-port-forwards.sh",
8+
"isBackground": true,
9+
"problemMatcher": {
10+
"pattern": {
11+
"regexp": "^Port forwards started: (.*)$",
12+
"message": 1
13+
},
14+
"background": {
15+
"activeOnStart": true,
16+
"beginsPattern": "^.*$",
17+
"endsPattern": "^Port forwards started:.*$"
18+
}
19+
},
20+
"presentation": {
21+
"echo": false,
22+
"reveal": "never",
23+
"focus": false,
24+
"panel": "shared",
25+
"showReuseMessage": false,
26+
"clear": false
27+
}
28+
},
29+
{
30+
"label": "Stop Port Forwards",
31+
"type": "shell",
32+
"command": "test -f /tmp/pf-registry.pid && kill $(cat /tmp/pf-registry.pid) 2>/dev/null; test -f /tmp/pf-couchdb.pid && kill $(cat /tmp/pf-couchdb.pid) 2>/dev/null; rm -f /tmp/pf-registry.pid /tmp/pf-registry.log /tmp/pf-couchdb.pid /tmp/pf-couchdb.log; pkill -f 'kubectl port-forward -n nuvolaris registry-0' 2>/dev/null; pkill -f 'kubectl -n nuvolaris port-forward couchdb-0' 2>/dev/null; echo 'Port forwards stopped'; exit 0",
33+
"presentation": {
34+
"echo": true,
35+
"reveal": "silent",
36+
"focus": false,
37+
"panel": "dedicated",
38+
"showReuseMessage": false
39+
},
40+
"options": {
41+
"shell": {
42+
"executable": "/bin/bash",
43+
"args": ["-c"]
44+
}
45+
}
46+
}
47+
]
48+
}

.vscode/test-task.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
#
19+
echo "Testing from VSCode task..."
20+
echo "PATH: $PATH"
21+
echo "SHELL: $SHELL"
22+
echo "PWD: $PWD"
23+
which kubectl
24+
kubectl version --client --short 2>/dev/null || kubectl version --client 2>/dev/null

TaskfileDev.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,25 @@ tasks:
5252
- sed -i '' 's/^KUBERNETES_SERVICE_PORT=.*/KUBERNETES_SERVICE_PORT={{.KUB_SERVICE_PORT}}/' .env
5353
- sed -i '' 's/^REGISTRY_PASS=.*/REGISTRY_PASS={{.REGISTRY_PASS}}/' .env
5454
- sed -i '' 's/^COUCHDB_ADMIN_PASSWORD=.*/COUCHDB_ADMIN_PASSWORD={{.COUCHDB_PASS}}/' .env
55+
- task: setup-secret
5556

57+
setup-secret:
58+
desc: "Create docker-registry secret for development using credentials from .env"
59+
dotenv: ['.env']
60+
cmds:
61+
- kubectl delete secret registry-pull-secret-dev -n nuvolaris 2>/dev/null || true
62+
- kubectl create secret docker-registry registry-pull-secret-dev -n nuvolaris --docker-server=${REGISTRY_HOST} --docker-username=${REGISTRY_USER} --docker-password=${REGISTRY_PASS}
63+
- echo "Secret 'registry-pull-secret-dev' created successfully in namespace 'nuvolaris'"
64+
- ops env add REGISTRY_SECRET=registry-pull-secret-dev
65+
- echo "Environment variable REGISTRY_SECRET set to 'registry-pull-secret-dev'"
66+
- |
67+
if ops env list | rg REGISTRY_SECRET > /dev/null 2>&1; then
68+
echo "✓ Verified: REGISTRY_SECRET is configured"
69+
ops env list | rg REGISTRY_SECRET
70+
else
71+
echo "⚠ Warning: REGISTRY_SECRET not found in ops env list"
72+
fi
73+
5674
run:
5775
desc: |
5876
Run the admin api locally, using configuration from .env file

openserverless/common/kube_api_client.py

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
#
18-
from datetime import time
18+
import time
1919
import requests as req
2020
import json
2121
import os
@@ -452,9 +452,10 @@ def delete_secret(self, secret_name: str, namespace="nuvolaris"):
452452
logging.error(f"delete_secret {ex}")
453453
return False
454454

455-
def get_jobs(self, name_filter: str = None, namespace="nuvolaris"):
455+
def get_jobs(self, name_filter: str | None = None, namespace="nuvolaris"):
456456
"""
457457
Get all Kubernetes jobs in a specific namespace.
458+
:param name_filter: Optional filter to match job names.
458459
:param namespace: Namespace to list jobs from.
459460
:return: List of jobs or None if failed.
460461
"""
@@ -512,7 +513,7 @@ def delete_job(self, job_name: str, namespace="nuvolaris"):
512513
logging.error(f"delete_job {ex}")
513514
return False
514515

515-
def post_job(self, job_manifest: json, namespace="nuvolaris"):
516+
def post_job(self, job_manifest: dict, namespace="nuvolaris"):
516517
"""
517518
Create a Kubernetes job.
518519
:param job_manifest: Dictionary containing the job manifest.
@@ -601,4 +602,104 @@ def check_job_status(self, job_name: str, namespace="nuvolaris"):
601602
return False
602603
except Exception as ex:
603604
logging.error(f"check_job_status {ex}")
604-
return False
605+
return False
606+
607+
def get_pod(self, pod_name: str, namespace="nuvolaris"):
608+
"""
609+
Get pod details by name.
610+
:param pod_name: Name of the pod.
611+
:param namespace: Namespace where the pod is located.
612+
:return: Pod object or None if not found.
613+
"""
614+
url = f"{self.host}/api/v1/namespaces/{namespace}/pods/{pod_name}"
615+
headers = {"Authorization": self.token}
616+
617+
try:
618+
logging.info(f"GET request to {url}")
619+
response = req.get(url, headers=headers, verify=self.ssl_ca_cert)
620+
621+
if response.status_code == 200:
622+
logging.debug(
623+
f"GET to {url} succeeded with {response.status_code}. Body {response.text}"
624+
)
625+
return json.loads(response.text)
626+
627+
logging.error(
628+
f"GET to {url} failed with {response.status_code}. Body {response.text}"
629+
)
630+
return None
631+
except Exception as ex:
632+
logging.error(f"get_pod {ex}")
633+
return None
634+
635+
def wait_for_init_container_completion(self, job_name: str, init_container_name: str, namespace="nuvolaris", timeout_seconds=300):
636+
"""
637+
Wait for a specific init container in a job's pod to complete (successfully or with error).
638+
:param job_name: Name of the job.
639+
:param init_container_name: Name of the init container to wait for.
640+
:param namespace: Namespace where the job is located.
641+
:param timeout_seconds: Maximum time to wait in seconds (default: 300 = 5 minutes).
642+
:return: True if init container completed (success or error), False if timeout or other failure.
643+
"""
644+
import time
645+
646+
start_time = time.time()
647+
pod_name = None
648+
649+
logging.info(f"Waiting for init container '{init_container_name}' in job '{job_name}' to complete")
650+
651+
# First, wait for the pod to be created
652+
while time.time() - start_time < timeout_seconds:
653+
pod_name = self.get_pod_by_job_name(job_name, namespace)
654+
if pod_name:
655+
logging.info(f"Found pod '{pod_name}' for job '{job_name}'")
656+
break
657+
logging.debug(f"Pod for job '{job_name}' not yet created, waiting...")
658+
time.sleep(2)
659+
660+
if not pod_name:
661+
logging.error(f"Timeout waiting for pod to be created for job '{job_name}'")
662+
return False
663+
664+
# Now wait for the init container to complete
665+
while time.time() - start_time < timeout_seconds:
666+
pod = self.get_pod(pod_name, namespace)
667+
668+
if not pod:
669+
logging.error(f"Failed to get pod '{pod_name}'")
670+
return False
671+
672+
# Check init container status
673+
init_container_statuses = pod.get("status", {}).get("initContainerStatuses", [])
674+
675+
for status in init_container_statuses:
676+
if status.get("name") == init_container_name:
677+
state = status.get("state", {})
678+
679+
# Check if terminated (completed or failed)
680+
if "terminated" in state:
681+
terminated = state["terminated"]
682+
exit_code = terminated.get("exitCode", -1)
683+
reason = terminated.get("reason", "Unknown")
684+
685+
if exit_code == 0:
686+
logging.info(f"Init container '{init_container_name}' completed successfully")
687+
else:
688+
logging.warning(f"Init container '{init_container_name}' terminated with exit code {exit_code}, reason: {reason}")
689+
690+
return True
691+
692+
# Check if still running
693+
if "running" in state:
694+
logging.debug(f"Init container '{init_container_name}' is still running")
695+
696+
# Check if waiting
697+
if "waiting" in state:
698+
waiting = state["waiting"]
699+
reason = waiting.get("reason", "Unknown")
700+
logging.debug(f"Init container '{init_container_name}' is waiting, reason: {reason}")
701+
702+
time.sleep(2)
703+
704+
logging.error(f"Timeout waiting for init container '{init_container_name}' to complete")
705+
return False

openserverless/common/response_builder.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,56 @@
1919

2020

2121
def build_error_message(
22-
message: str, status_code=400, headers={"Content-Type": "application/json"}
22+
message: str, status_code=400, headers=None
2323
):
24+
if headers is None:
25+
headers = {"Content-Type": "application/json"}
2426
return make_response(
2527
jsonify({"message": message, "status": "ko"}), status_code, headers
2628
)
2729

2830

2931
def build_response_message(
30-
message: str, status_code=200, headers={"Content-Type": "application/json"}
32+
message: str, data=None, status_code=200, headers=None
3133
):
34+
if headers is None:
35+
headers = {"Content-Type": "application/json"}
36+
37+
payload = {"message": message, "status": "ok"}
38+
if data:
39+
# If caller passed a dict, merge into payload. Otherwise attach under 'data'.
40+
if isinstance(data, dict):
41+
payload.update(data)
42+
else:
43+
payload["data"] = data
44+
3245
return make_response(
33-
jsonify({"message": message, "status": "ok"}), status_code, headers
46+
jsonify(payload), status_code, headers
3447
)
3548

3649

3750
def build_response_with_data(
38-
data, status_code=200, headers={"Content-Type": "application/json"}
51+
data, status_code=200, headers=None
3952
):
53+
if headers is None:
54+
headers = {"Content-Type": "application/json"}
55+
4056
if isinstance(data, dict):
4157
return make_response(jsonify(data), status_code, headers)
4258
return make_response(data, status_code, headers)
4359

4460

4561
def build_response_raw(
46-
message: str, status_code=200, headers={"Content-Type": "application/json"}
62+
message: str, status_code=200, headers=None
4763
):
64+
if headers is None:
65+
headers = {"Content-Type": "application/json"}
4866
return make_response(message, status_code, headers)
4967

5068

5169
def build_error_raw(
52-
message: str, status_code=400, headers={"Content-Type": "application/json"}
70+
message: str, status_code=400, headers=None
5371
):
72+
if headers is None:
73+
headers = {"Content-Type": "application/json"}
5474
return make_response(message, status_code, headers)

0 commit comments

Comments
 (0)