Skip to content

Commit a1f8d90

Browse files
committed
fix: resourceVersion on ksvc replace + in-cluster curl for E2E
- Fetch existing ksvc resourceVersion before replace (PUT requires it) - Switch HTTP test from Kourier port-forward to kubectl run + in-cluster curl - Remove Kourier port-forward from workflow (not needed) - Fix knative.ts metadata type to allow optional resourceVersion
1 parent 051bd2d commit a1f8d90

5 files changed

Lines changed: 69 additions & 57 deletions

File tree

.github/workflows/test-provisioning-e2e.yaml

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -141,35 +141,17 @@ jobs:
141141
psql -c "SELECT name FROM metaschema_public.namespace;"
142142
psql -c "SELECT name, image FROM constructive_compute_public.platform_function_definitions;"
143143
144-
# ── Start proxies ──────────────────────────────────────────────────
145-
- name: Start kubectl proxy and Kourier port-forward
144+
# ── Start kubectl proxy ─────────────────────────────────────────────
145+
- name: Start kubectl proxy
146146
run: |
147-
# K8s API proxy for @kubernetesjs/ops
148147
kubectl proxy --port=8001 &
149148
echo "kubectl proxy started on :8001"
150-
151-
# Wait for Kourier service to exist
152-
echo "Waiting for Kourier service..."
153-
for i in $(seq 1 30); do
154-
if kubectl get svc kourier -n kourier-system &>/dev/null; then
155-
echo " Kourier service found"
156-
break
157-
fi
158-
echo " attempt $i: waiting..."
159-
sleep 5
160-
done
161-
162-
# Port-forward Kourier for function invocation
163-
kubectl port-forward -n kourier-system svc/kourier 8090:80 &
164-
echo "Kourier port-forward started on :8090"
165-
166-
sleep 3
149+
sleep 2
167150
168151
# ── Run E2E tests ──────────────────────────────────────────────────
169152
- name: Run provisioning Knative E2E tests
170153
env:
171154
K8S_API_URL: http://localhost:8001
172-
KOURIER_URL: http://localhost:8090
173155
PGHOST: localhost
174156
PGPORT: "5432"
175157
PGUSER: postgres

packages/provisioning-handlers/src/handlers/function-sync-resources.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ export const handleFunctionSyncResources: ProvisioningHandler = async (
6767
const serviceSpec = buildKnativeServiceSpec(fnRow, namespaceName);
6868

6969
try {
70+
// GET the existing service to retrieve its resourceVersion (required for PUT)
71+
const existing = await client.readServingKnativeDevV1NamespacedService({
72+
query: {},
73+
path: { name: fnName, namespace: namespaceName },
74+
});
75+
const resourceVersion = existing?.metadata?.resourceVersion;
76+
if (resourceVersion && serviceSpec.metadata) {
77+
serviceSpec.metadata.resourceVersion = resourceVersion;
78+
}
79+
7080
const svc = await client.replaceServingKnativeDevV1NamespacedService({
7181
query: {},
7282
path: { name: fnName, namespace: namespaceName },

packages/provisioning-handlers/src/knative.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export function buildKnativeServiceSpec(
2727
return {
2828
apiVersion: 'serving.knative.dev/v1',
2929
kind: 'Service',
30-
metadata: { name: fnName, namespace: namespaceName },
30+
metadata: { name: fnName, namespace: namespaceName } as {
31+
name: string;
32+
namespace: string;
33+
resourceVersion?: string;
34+
},
3135
spec: {
3236
template: {
3337
metadata: {

packages/provisioning-handlers/src/seed.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,16 @@ export async function provision(opts: ProvisionSeedOptions): Promise<ProvisionSe
191191
result.functions.push({ name: fnName, namespace: namespaceName, serviceUrl, status: 'created' });
192192
} catch (err: unknown) {
193193
if (isConflict(err)) {
194+
// GET the existing service to retrieve its resourceVersion (required for PUT)
195+
const existing = await client.readServingKnativeDevV1NamespacedService({
196+
query: {},
197+
path: { name: fnName, namespace: namespaceName },
198+
});
199+
const resourceVersion = existing?.metadata?.resourceVersion;
200+
if (resourceVersion && serviceSpec.metadata) {
201+
serviceSpec.metadata.resourceVersion = resourceVersion;
202+
}
203+
194204
const svc = await client.replaceServingKnativeDevV1NamespacedService({
195205
query: {},
196206
path: { name: fnName, namespace: namespaceName },

tests/e2e/__tests__/provisioning-knative-e2e.test.ts

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,30 @@
33
*
44
* Tests the full provisioning flow end-to-end:
55
* 1. Calls provision() to create K8s namespaces, secrets, and Knative Services
6-
* 2. Verifies Knative Service reaches Ready state
7-
* 3. Calls the Knative Service via Kourier gateway
8-
* 4. Verifies the service_url was written back to the DB
6+
* 2. Verifies K8s namespace + secret were created correctly
7+
* 3. Verifies Knative Service reaches Ready state
8+
* 4. Calls the function via in-cluster curl (kubectl run)
9+
* 5. Verifies the service_url was written back to the DB
10+
* 6. Re-runs seed to verify idempotency
911
*
1012
* Requires:
1113
* - K8S_API_URL pointing to kubectl proxy (e.g. http://localhost:8001)
1214
* - PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE for a bootstrapped DB
1315
* - Knative Serving + Kourier installed in the kind cluster
14-
* - KOURIER_URL for the Kourier gateway (e.g. http://localhost:8090)
1516
*
1617
* DB must be bootstrapped with:
1718
* tests/e2e/fixtures/provisioning-knative-bootstrap.sql
1819
*/
1920

21+
import { execSync } from 'child_process';
22+
2023
import { provision } from '@constructive-io/provisioning-handlers';
2124
import { InterwebClient } from '@kubernetesjs/ops';
2225
import pg from 'pg';
2326

2427
const { Pool } = pg;
2528

2629
const K8S_API_URL = process.env.K8S_API_URL;
27-
const KOURIER_URL = process.env.KOURIER_URL || 'http://localhost:8090';
2830
const DATABASE_ID = '00000000-0000-0000-0000-000000000001';
2931

3032
const describeKnative = K8S_API_URL ? describe : describe.skip;
@@ -164,56 +166,62 @@ describeKnative('Provisioning E2E — Knative in kind', () => {
164166
expect(ready).toBe(true);
165167
}, 360_000);
166168

167-
// ─── Phase 4: Call the Knative Service ───────────────────────────────────
169+
// ─── Phase 4: Call the function from inside the cluster ─────────────────
168170

169-
it('responds to HTTP requests through Kourier gateway', async () => {
170-
// Get the ksvc URL to determine the Host header
171+
it('responds to HTTP requests (in-cluster curl)', async () => {
172+
// Get the ksvc URL from the K8s API
171173
const resp = await fetch(
172174
`${K8S_API_URL}/apis/serving.knative.dev/v1/namespaces/test-ns/services/hello-provisioned`
173175
);
174176
expect(resp.ok).toBe(true);
175177
const svc = (await resp.json()) as Record<string, any>;
176178
const ksvcUrl = (svc?.status?.url || '') as string;
179+
console.log(`ksvc URL: ${ksvcUrl}`);
177180

178-
// Extract hostname from the ksvc URL
179-
let hostname: string;
180-
try {
181-
hostname = new URL(ksvcUrl).hostname;
182-
} catch {
183-
// Fallback: construct the expected hostname
184-
hostname = 'hello-provisioned.test-ns.svc.cluster.local';
185-
}
186-
console.log(`Calling ksvc via Kourier: Host=${hostname}, URL=${KOURIER_URL}`);
187-
188-
// Call through Kourier with Host header
181+
// Curl from inside the cluster using kubectl run.
182+
// The Knative Service has an in-cluster K8s Service that resolves via DNS.
183+
const inClusterUrl = `http://hello-provisioned.test-ns.svc.cluster.local`;
189184
let body = '';
190185
let success = false;
191-
const maxRetries = 10;
186+
const maxRetries = 12;
187+
const suffix = Math.random().toString(36).slice(2, 8);
192188

193189
for (let i = 0; i < maxRetries; i++) {
194190
try {
195-
const fnResp = await fetch(KOURIER_URL, {
196-
headers: { Host: hostname },
197-
});
198-
body = await fnResp.text();
199-
console.log(` attempt ${i + 1}: status=${fnResp.status} body="${body.slice(0, 200)}"`);
200-
201-
if (fnResp.ok) {
191+
const podName = `curl-e2e-${suffix}-${i}`;
192+
body = execSync(
193+
`kubectl run ${podName} --rm -i --restart=Never ` +
194+
`--image=curlimages/curl --request-timeout=60s -- ` +
195+
`curl -s --max-time 30 ${inClusterUrl}`,
196+
{ encoding: 'utf-8', timeout: 120_000 }
197+
).trim();
198+
199+
console.log(` attempt ${i + 1}: body="${body.slice(0, 200)}"`);
200+
if (body.includes('Hello')) {
202201
success = true;
203202
break;
204203
}
205-
} catch (err) {
206-
console.log(` attempt ${i + 1}: fetch error — ${err}`);
204+
} catch (err: unknown) {
205+
const msg = err instanceof Error ? err.message : String(err);
206+
// Extract stdout from the error if available
207+
const errWithOutput = err as { stdout?: string; stderr?: string };
208+
if (errWithOutput.stdout) {
209+
body = errWithOutput.stdout.trim();
210+
if (body.includes('Hello')) {
211+
console.log(` attempt ${i + 1}: got body from stdout="${body.slice(0, 200)}"`);
212+
success = true;
213+
break;
214+
}
215+
}
216+
console.log(` attempt ${i + 1}: error — ${msg.slice(0, 200)}`);
207217
}
208-
await sleep(5_000);
218+
await sleep(10_000);
209219
}
210220

211221
expect(success).toBe(true);
212-
// The helloworld-go container reads TARGET from env (mounted via K8s secret)
213-
// and returns "Hello {TARGET}!"
214222
expect(body).toContain('Hello');
215223
expect(body).toContain('Provisioning E2E');
216-
}, 120_000);
224+
}, 240_000);
217225

218226
// ─── Phase 5: Verify DB writeback ────────────────────────────────────────
219227

@@ -224,8 +232,6 @@ describeKnative('Provisioning E2E — Knative in kind', () => {
224232
);
225233

226234
expect(rows).toHaveLength(1);
227-
// service_url may or may not be set depending on whether the create response
228-
// included it. At minimum, we verify the row still exists.
229235
console.log(`service_url in DB: ${rows[0].service_url || '(not set yet)'}`);
230236
});
231237

0 commit comments

Comments
 (0)