Skip to content

Commit 9b62916

Browse files
committed
Run tests in a pod in the ephemeral namespace
This way we don't have to work around the networking with explicit kubectl exec around each request. Assisted-By: Claude
1 parent d6019c8 commit 9b62916

3 files changed

Lines changed: 98 additions & 127 deletions

File tree

.tekton/README-INTEGRATION-TESTS.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,11 @@ service with a real PostgreSQL database.
2929
### Test Scripts
3030

3131
- **`../tests/test_integration_api.py`** - Integration tests using pytest
32-
- Supports both local testing and CI pipeline testing
3332
- Uses pytest fixtures for clean test setup
34-
- Two modes:
35-
- **Direct HTTP**: `CTS_URL=http://localhost:5005 pytest tests/test_integration_api.py -v`
36-
- **Kubectl exec**: `KUBECTL_POD=<pod-name> pytest tests/test_integration_api.py -v`
37-
- In CI, uses kubectl exec mode to run curl commands inside the CTS pod
38-
- Tests all major API endpoints
33+
- Makes direct HTTP requests to CTS API
34+
- Run with: `CTS_URL=http://localhost:5005 pytest tests/test_integration_api.py -v`
35+
- In CI, runs in a pod deployed to the same namespace as CTS
36+
- Tests all major API endpoints and workflows
3937
- Edit this file to add or modify integration tests
4038

4139
## What Gets Tested
@@ -83,12 +81,14 @@ The integration tests validate:
8381
└─────────────┬───────────────┘
8482
8583
┌─────────────▼───────────────┐
86-
│ 5. Run Integration Tests │ Clone repo, run tests via kubectl exec
87-
│ │
84+
│ 5. Run Integration Tests │ Create test runner pod in ephemeral ns
85+
│ │ Install pytest, clone repo
86+
│ │ Run tests with direct HTTP to CTS service
8887
└─────────────┬───────────────┘
8988
9089
┌─────────────▼───────────────┐
9190
│ 6. Automatic Cleanup │ Ephemeral namespace deleted by EaaS
91+
│ │ (includes test runner, CTS, and database)
9292
└─────────────────────────────┘
9393
```
9494

.tekton/integration-test-eaas.yaml

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -387,30 +387,6 @@ spec:
387387
- name: test-result
388388
description: Test execution result
389389
steps:
390-
- name: clone-repository
391-
image: quay.io/konflux-ci/appstudio-utils:latest
392-
script: |
393-
#!/usr/bin/env bash
394-
set -euo pipefail
395-
396-
GIT_URL="$(params.git-url)"
397-
GIT_REV="$(params.git-revision)"
398-
399-
echo "=========================================="
400-
echo "Cloning Repository"
401-
echo "=========================================="
402-
echo "URL: $GIT_URL"
403-
echo "Revision: $GIT_REV"
404-
echo ""
405-
406-
# Shallow clone for faster cloning (only get the specific revision)
407-
git clone --depth 1 "$GIT_URL" /workspace/cts-repo
408-
cd /workspace/cts-repo
409-
git fetch --depth 1 origin "$GIT_REV"
410-
git checkout "$GIT_REV"
411-
412-
echo "✓ Repository cloned successfully"
413-
414390
- name: execute-tests
415391
image: quay.io/konflux-ci/appstudio-utils:latest
416392
script: |
@@ -426,38 +402,79 @@ spec:
426402
kubectl get secret $(params.kubeconfig-secret) -o jsonpath='{.data.kubeconfig}' | base64 -d > $KUBECONFIG
427403
export KUBECONFIG
428404
429-
# Get the namespace and CTS pod
405+
# Get the namespace
430406
NAMESPACE=$(kubectl config view --minify -o jsonpath='{..namespace}')
431407
echo "Ephemeral namespace: $NAMESPACE"
432-
433-
CTS_POD=$(kubectl get pod -l app=cts -n $NAMESPACE -o jsonpath='{.items[0].metadata.name}')
434-
echo "CTS Pod: $CTS_POD"
435408
echo ""
436409
437-
# Bootstrap pip and install pytest
438-
python3 -m ensurepip --default-pip || dnf install -y python3-pip
439-
python3 -m pip install --user pytest
410+
echo "Creating test runner pod in namespace: $NAMESPACE"
411+
412+
# Create a test runner pod in the target namespace
413+
kubectl run cts-test-runner \
414+
--image=registry.fedoraproject.org/fedora:latest \
415+
--namespace=$NAMESPACE \
416+
--restart=Never \
417+
--overrides='{"spec":{"securityContext":{"runAsUser":0}}}' \
418+
--command -- sleep 3600
419+
420+
# Wait for pod to be ready
421+
echo "Waiting for test runner pod to be ready..."
422+
kubectl wait --for=condition=Ready pod/cts-test-runner -n $NAMESPACE --timeout=60s
440423
441-
# Run the tests using kubectl exec mode (with properly quoted URLs to handle & in query params)
442-
# Temporarily disable exit-on-error to capture test result
424+
GIT_URL='$(params.git-url)'
425+
GIT_REV='$(params.git-revision)'
426+
427+
echo ""
428+
echo "Running tests in pod..."
429+
echo "CTS URL: http://cts:5005"
430+
echo ""
431+
432+
# Run the tests - temporarily disable exit-on-error to capture result
443433
set +e
444-
KUBECTL_POD="$CTS_POD" python3 -m pytest /workspace/cts-repo/tests/test_integration_api.py -v -s -o addopts=""
434+
kubectl exec cts-test-runner -n $NAMESPACE -- bash -c "
435+
set -e
436+
export HOME=/tmp
437+
438+
echo 'Installing dependencies...'
439+
dnf install -y python3 python3-pip git-core
440+
python3 -m pip install pytest
441+
442+
echo ''
443+
echo 'Cloning repository...'
444+
cd /tmp
445+
git clone --depth 1 '$GIT_URL' cts-repo
446+
cd cts-repo
447+
git fetch --depth 1 origin '$GIT_REV'
448+
git checkout '$GIT_REV'
449+
450+
echo ''
451+
echo 'Running pytest...'
452+
CTS_URL=http://cts:5005 python3 -m pytest tests/test_integration_api.py -v -s
453+
"
445454
TEST_RESULT=$?
446455
set -e
447456
457+
# Clean up test runner pod
458+
echo ""
459+
echo "Cleaning up test runner pod..."
460+
kubectl delete pod cts-test-runner -n $NAMESPACE --ignore-not-found=true
461+
448462
if [ $TEST_RESULT -eq 0 ]; then
463+
echo ""
464+
echo "✓ All tests passed!"
449465
echo "passed" | tee $(results.test-result.path)
450466
exit 0
451467
else
468+
echo ""
469+
echo "✗ Tests failed!"
452470
echo "failed" | tee $(results.test-result.path)
453471
454-
# Show pod logs for debugging
472+
# Show CTS pod logs for debugging
455473
echo ""
456474
echo "=========================================="
457475
echo "CTS Pod Logs (last 50 lines):"
458476
echo "=========================================="
459477
460-
# kubeconfig is already set up above
461478
CTS_POD=$(kubectl get pod -l app=cts -n $NAMESPACE -o jsonpath='{.items[0].metadata.name}')
462479
kubectl logs $CTS_POD -n $NAMESPACE --tail=50 || true
463480

tests/test_integration_api.py

Lines changed: 37 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,55 @@
11
"""
22
Integration tests for CTS API endpoints.
33
4-
Run with pytest in one of two modes:
4+
Run with pytest by setting the CTS_URL environment variable:
55
6-
1. Direct HTTP mode (local testing):
7-
CTS_URL=http://localhost:5005 pytest tests/test_integration_api.py -v
6+
CTS_URL=http://localhost:5005 pytest tests/test_integration_api.py -v
87
9-
2. Kubectl exec mode (CI testing):
10-
KUBECTL_POD=cts-abc123 pytest tests/test_integration_api.py -v
11-
12-
Uses kubectl exec to run curl inside the CTS pod. URLs are properly quoted
13-
to handle special characters like & in query parameters.
8+
In CI, the tests run in a pod deployed to the same namespace as the CTS service,
9+
so they can access it directly via: http://cts:5005
1410
"""
1511

1612
import json
1713
import os
18-
import subprocess
1914
from urllib.request import urlopen, Request
2015
from urllib.error import HTTPError, URLError
2116

2217
import pytest
2318

2419

2520
class HTTPClient:
26-
"""HTTP client that works in both direct and kubectl exec modes"""
21+
"""Simple HTTP client for making requests to the CTS API"""
2722

28-
def __init__(self, base_url=None, kubectl_pod=None):
29-
self.base_url = base_url.rstrip("/") if base_url else None
30-
self.kubectl_pod = kubectl_pod
23+
def __init__(self, base_url):
24+
self.base_url = base_url.rstrip("/")
3125

3226
def _request(self, method, path, json_data=None):
3327
"""Make HTTP request with specified method"""
34-
if self.kubectl_pod:
35-
# Kubectl exec mode - use curl
36-
# Important: Quote the URL to prevent shell interpretation of & and other special chars
37-
url = f"http://localhost:5005{path}"
38-
if json_data:
39-
json_str = json.dumps(json_data).replace("'", "'\\''")
40-
cmd = f"curl -s -w '\\n%{{http_code}}' -X {method} -H 'Content-Type: application/json' -d '{json_str}' '{url}'"
41-
else:
42-
cmd = f"curl -s -w '\\n%{{http_code}}' -X {method} '{url}'"
43-
44-
result = subprocess.run(
45-
["kubectl", "exec", "-i", self.kubectl_pod, "--", "sh", "-c", cmd],
46-
capture_output=True,
47-
text=True,
48-
)
49-
50-
# Parse output: last line is status code, rest is body
51-
lines = result.stdout.rsplit("\n", 1)
52-
if len(lines) == 2:
53-
body, status_code = lines
54-
try:
55-
status = int(status_code)
56-
except ValueError:
57-
body = result.stdout
58-
status = 200 if result.returncode == 0 else 500
59-
else:
60-
body = result.stdout
61-
status = 200 if result.returncode == 0 else 500
62-
63-
# Try to parse as JSON
64-
try:
65-
return status, json.loads(body) if body.strip() else {}
66-
except json.JSONDecodeError:
67-
return status, body
68-
else:
69-
# Direct HTTP mode
70-
url = f"{self.base_url}{path}"
71-
req = Request(url, method=method)
72-
if json_data:
73-
req.add_header("Content-Type", "application/json")
74-
req.data = json.dumps(json_data).encode("utf-8")
75-
28+
url = f"{self.base_url}{path}"
29+
req = Request(url, method=method)
30+
if json_data:
31+
req.add_header("Content-Type", "application/json")
32+
req.data = json.dumps(json_data).encode("utf-8")
33+
34+
try:
35+
with urlopen(req, timeout=10) as response:
36+
data = response.read()
37+
if response.headers.get("Content-Type", "").startswith(
38+
"application/json"
39+
):
40+
return response.status, json.loads(data)
41+
return response.status, data.decode("utf-8")
42+
except HTTPError as e:
43+
# Try to read error body
7644
try:
77-
with urlopen(req, timeout=10) as response:
78-
data = response.read()
79-
if response.headers.get("Content-Type", "").startswith(
80-
"application/json"
81-
):
82-
return response.status, json.loads(data)
83-
return response.status, data.decode("utf-8")
84-
except HTTPError as e:
85-
# Try to read error body
86-
try:
87-
error_data = e.read()
88-
if e.headers.get("Content-Type", "").startswith("application/json"):
89-
return e.code, json.loads(error_data)
90-
return e.code, error_data.decode("utf-8")
91-
except:
92-
return e.code, None
93-
except URLError as e:
94-
raise Exception(f"Failed to connect to {url}: {e}")
45+
error_data = e.read()
46+
if e.headers.get("Content-Type", "").startswith("application/json"):
47+
return e.code, json.loads(error_data)
48+
return e.code, error_data.decode("utf-8")
49+
except:
50+
return e.code, None
51+
except URLError as e:
52+
raise Exception(f"Failed to connect to {url}: {e}")
9553

9654
def get(self, path):
9755
"""Make HTTP GET request"""
@@ -112,18 +70,14 @@ def delete(self, path):
11270

11371
@pytest.fixture(scope="module")
11472
def http_client():
115-
"""HTTP client fixture that auto-detects mode from environment"""
116-
kubectl_pod = os.environ.get("KUBECTL_POD")
73+
"""HTTP client fixture that reads CTS_URL from environment"""
11774
base_url = os.environ.get("CTS_URL")
11875

119-
if kubectl_pod:
120-
print(f"\nUsing kubectl exec mode (pod: {kubectl_pod})")
121-
return HTTPClient(kubectl_pod=kubectl_pod)
122-
elif base_url:
123-
print(f"\nUsing direct HTTP mode (URL: {base_url})")
124-
return HTTPClient(base_url=base_url)
125-
else:
126-
pytest.skip("Must set either CTS_URL or KUBECTL_POD environment variable")
76+
if not base_url:
77+
pytest.skip("Must set CTS_URL environment variable")
78+
79+
print(f"\nConnecting to CTS at: {base_url}")
80+
return HTTPClient(base_url=base_url)
12781

12882

12983
def _create_compose_info(

0 commit comments

Comments
 (0)