mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
feat(ql3): add opaque cluster environment bundle delivery
This commit is contained in:
@@ -11,9 +11,11 @@ const {
|
||||
const {
|
||||
digestRunDispatchLeaseToken,
|
||||
} = require('@qinglong/runtime-core/run-dispatch-lease');
|
||||
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||
const {
|
||||
createSecretRef,
|
||||
} = require('@qinglong/runtime-core/secret-reference');
|
||||
ENVIRONMENT_BUNDLE_SCHEMA,
|
||||
serializeEnvironmentBundle,
|
||||
} = require('@qinglong/runtime-core/environment-bundle');
|
||||
const {
|
||||
BoundedWorkerRemoteExecutionContextMaterializer,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
@@ -27,7 +29,7 @@ function secret(name) {
|
||||
return createSecretRef({ projectId: 'project-1', name });
|
||||
}
|
||||
|
||||
function offer(environment) {
|
||||
function offer(environment, environmentBundleRef) {
|
||||
const executionRevision = createClusterTaskExecutionRevision({
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
@@ -38,6 +40,7 @@ function offer(environment) {
|
||||
planSchema: 'qinglong/command-execution@v1',
|
||||
command: { kind: 'argv', file: '/bin/true', args: [] },
|
||||
environment,
|
||||
...(environmentBundleRef === undefined ? {} : { environmentBundleRef }),
|
||||
createdAtMs: 1,
|
||||
});
|
||||
return createClusterRemoteExecutionOffer({
|
||||
@@ -100,7 +103,10 @@ test('resolves deduplicated Secrets before allocating one Attempt log', async ()
|
||||
secretRequest = request;
|
||||
return {
|
||||
values: [{ secretRef, value: 'resolved-value' }],
|
||||
dispose() { events.push('dispose-secrets'); },
|
||||
environmentBundles: [],
|
||||
dispose() {
|
||||
events.push('dispose-secrets');
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -110,8 +116,13 @@ test('resolves deduplicated Secrets before allocating one Attempt log', async ()
|
||||
artifactRequest = request;
|
||||
return {
|
||||
logArtifactId: 'remote-log-1',
|
||||
takeOutput() { events.push('take-output'); return output; },
|
||||
release() { events.push('release-artifact'); },
|
||||
takeOutput() {
|
||||
events.push('take-output');
|
||||
return output;
|
||||
},
|
||||
release() {
|
||||
events.push('release-artifact');
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -129,6 +140,7 @@ test('resolves deduplicated Secrets before allocating one Attempt log', async ()
|
||||
offerId: 'offer-materializer-1',
|
||||
executionDigest: acceptedOffer.executionDigest,
|
||||
secretRefs: [secretRef],
|
||||
environmentBundleRefs: [],
|
||||
});
|
||||
assert.deepEqual(artifactRequest, {
|
||||
projectId: 'project-1',
|
||||
@@ -136,7 +148,10 @@ test('resolves deduplicated Secrets before allocating one Attempt log', async ()
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-materializer-1',
|
||||
});
|
||||
assert.equal(JSON.stringify([secretRequest, artifactRequest]).includes(LEASE_TOKEN), false);
|
||||
assert.equal(
|
||||
JSON.stringify([secretRequest, artifactRequest]).includes(LEASE_TOKEN),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(context.environment, [
|
||||
{ name: 'PUBLIC', value: 'visible' },
|
||||
{ name: 'SECRET_A', value: 'resolved-value' },
|
||||
@@ -149,18 +164,26 @@ test('resolves deduplicated Secrets before allocating one Attempt log', async ()
|
||||
await context.dispose();
|
||||
await context.dispose();
|
||||
assert.deepEqual(events.slice(2).sort(), [
|
||||
'dispose-secrets', 'release-artifact', 'take-output',
|
||||
'dispose-secrets',
|
||||
'release-artifact',
|
||||
'take-output',
|
||||
]);
|
||||
});
|
||||
|
||||
test('fails before Artifact allocation when Secret authority is unavailable', async () => {
|
||||
let artifacts = 0;
|
||||
const materializer = new BoundedWorkerRemoteExecutionContextMaterializer({
|
||||
artifacts: { async prepare() { artifacts += 1; } },
|
||||
artifacts: {
|
||||
async prepare() {
|
||||
artifacts += 1;
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
materializer.prepare({
|
||||
offer: offer([{ name: 'SECRET', kind: 'secret', secretRef: secret('one') }]),
|
||||
offer: offer([
|
||||
{ name: 'SECRET', kind: 'secret', secretRef: secret('one') },
|
||||
]),
|
||||
}),
|
||||
/secret_unavailable/,
|
||||
);
|
||||
@@ -179,11 +202,18 @@ test('disposes malformed Secret and Artifact responses without exposing values',
|
||||
{ secretRef, value: 'first' },
|
||||
{ secretRef, value: 'duplicate' },
|
||||
],
|
||||
dispose() { disposedSecrets += 1; },
|
||||
environmentBundles: [],
|
||||
dispose() {
|
||||
disposedSecrets += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
artifacts: { async prepare() { throw new Error('must not allocate'); } },
|
||||
artifacts: {
|
||||
async prepare() {
|
||||
throw new Error('must not allocate');
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
malformedSecrets.prepare({
|
||||
@@ -196,16 +226,20 @@ test('disposes malformed Secret and Artifact responses without exposing values',
|
||||
);
|
||||
assert.equal(disposedSecrets, 1);
|
||||
|
||||
const malformedArtifact = new BoundedWorkerRemoteExecutionContextMaterializer({
|
||||
artifacts: {
|
||||
async prepare() {
|
||||
return {
|
||||
logArtifactId: 'x'.repeat(37),
|
||||
release() { releasedArtifact += 1; },
|
||||
};
|
||||
const malformedArtifact = new BoundedWorkerRemoteExecutionContextMaterializer(
|
||||
{
|
||||
artifacts: {
|
||||
async prepare() {
|
||||
return {
|
||||
logArtifactId: 'x'.repeat(37),
|
||||
release() {
|
||||
releasedArtifact += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
await assert.rejects(
|
||||
malformedArtifact.prepare({
|
||||
offer: offer([{ name: 'PUBLIC', kind: 'public', value: 'visible' }]),
|
||||
@@ -231,11 +265,18 @@ test('enforces the resolved environment byte budget before Artifact allocation',
|
||||
secretRef,
|
||||
value: 'x'.repeat(16 * 1024),
|
||||
})),
|
||||
dispose() { disposed += 1; },
|
||||
environmentBundles: [],
|
||||
dispose() {
|
||||
disposed += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
artifacts: { async prepare() { artifacts += 1; } },
|
||||
artifacts: {
|
||||
async prepare() {
|
||||
artifacts += 1;
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
materializer.prepare({ offer: offer(bindings) }),
|
||||
@@ -244,3 +285,69 @@ test('enforces the resolved environment byte budget before Artifact allocation',
|
||||
assert.equal(disposed, 1);
|
||||
assert.equal(artifacts, 0);
|
||||
});
|
||||
|
||||
test('expands one opaque bundle in memory and rejects name collisions', async () => {
|
||||
const environmentBundleRef = createSecretRef({
|
||||
projectId: 'project-1',
|
||||
name: 'legacy-env-bundle',
|
||||
version: 2,
|
||||
});
|
||||
const bundle = serializeEnvironmentBundle({
|
||||
schema: ENVIRONMENT_BUNDLE_SCHEMA,
|
||||
entries: [
|
||||
{ name: 'LEGACY_TOKEN', value: 'private' },
|
||||
{ name: 'LEGACY_MODE', value: 'compat' },
|
||||
],
|
||||
});
|
||||
const materializer = new BoundedWorkerRemoteExecutionContextMaterializer({
|
||||
secrets: {
|
||||
async resolve(request) {
|
||||
assert.deepEqual(request.secretRefs, []);
|
||||
assert.deepEqual(request.environmentBundleRefs, [environmentBundleRef]);
|
||||
return {
|
||||
values: [],
|
||||
environmentBundles: [
|
||||
{ secretRef: environmentBundleRef, value: bundle },
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
artifacts: {
|
||||
async prepare() {
|
||||
return {
|
||||
logArtifactId: 'remote-log-bundle',
|
||||
takeOutput() {
|
||||
return {
|
||||
logArtifactId: 'remote-log-bundle',
|
||||
async write() {},
|
||||
async close() {},
|
||||
};
|
||||
},
|
||||
async release() {},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const context = await materializer.prepare({
|
||||
offer: offer(
|
||||
[{ name: 'PUBLIC', kind: 'public', value: 'visible' }],
|
||||
environmentBundleRef,
|
||||
),
|
||||
});
|
||||
assert.deepEqual(context.environment, [
|
||||
{ name: 'PUBLIC', value: 'visible' },
|
||||
{ name: 'LEGACY_MODE', value: 'compat' },
|
||||
{ name: 'LEGACY_TOKEN', value: 'private' },
|
||||
]);
|
||||
await context.dispose();
|
||||
|
||||
await assert.rejects(
|
||||
materializer.prepare({
|
||||
offer: offer(
|
||||
[{ name: 'LEGACY_MODE', kind: 'public', value: 'current' }],
|
||||
environmentBundleRef,
|
||||
),
|
||||
}),
|
||||
/secret_response_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,31 +24,52 @@ const SECRET_REF = createSecretRef({ projectId: 'project-1', name: 'token' });
|
||||
|
||||
function acceptedOffer() {
|
||||
const executionRevision = createClusterTaskExecutionRevision({
|
||||
projectId: 'project-1', taskId: 'task-1', taskRevision: TASK_REVISION,
|
||||
sourceRevision: 1, sourceContentDigest: SOURCE_DIGEST,
|
||||
executorType: 'remote_worker', planSchema: 'qinglong/command-execution@v1',
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
sourceRevision: 1,
|
||||
sourceContentDigest: SOURCE_DIGEST,
|
||||
executorType: 'remote_worker',
|
||||
planSchema: 'qinglong/command-execution@v1',
|
||||
command: { kind: 'argv', file: '/bin/true', args: [] },
|
||||
environment: [{ name: 'TOKEN', kind: 'secret', secretRef: SECRET_REF }],
|
||||
createdAtMs: 1,
|
||||
});
|
||||
return createClusterRemoteExecutionOffer({
|
||||
offerId: 'offer-1', deliveryKind: 'new_claim',
|
||||
offerId: 'offer-1',
|
||||
deliveryKind: 'new_claim',
|
||||
executionDigest: executionRevision.contentDigest,
|
||||
candidate: {
|
||||
runId: 'run-1', attemptId: 'attempt-1', projectId: 'project-1',
|
||||
taskId: 'task-1', taskRevision: TASK_REVISION, priority: 1,
|
||||
queuedAtMs: 10, attemptCreatedAtMs: 11, attemptNumber: 1,
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
priority: 1,
|
||||
queuedAtMs: 10,
|
||||
attemptCreatedAtMs: 11,
|
||||
attemptNumber: 1,
|
||||
executorType: 'remote_worker',
|
||||
},
|
||||
worker: { workerId: 'edge-1', sessionId: SESSION_ID, generation: 2 },
|
||||
lease: {
|
||||
attemptId: 'attempt-1', runId: 'run-1', status: 'leased', version: 4,
|
||||
leaseGeneration: 3, workerId: 'edge-1', workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2, leaseTokenDigest: digestRunDispatchLeaseToken(LEASE_TOKEN),
|
||||
acquiredAtMs: 20, renewedAtMs: 20, expiresAtMs: 30_020,
|
||||
attemptId: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
status: 'leased',
|
||||
version: 4,
|
||||
leaseGeneration: 3,
|
||||
workerId: 'edge-1',
|
||||
workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2,
|
||||
leaseTokenDigest: digestRunDispatchLeaseToken(LEASE_TOKEN),
|
||||
acquiredAtMs: 20,
|
||||
renewedAtMs: 20,
|
||||
expiresAtMs: 30_020,
|
||||
updatedAtMs: 20,
|
||||
},
|
||||
leaseToken: LEASE_TOKEN, executionRevision, placementScore: 0,
|
||||
leaseToken: LEASE_TOKEN,
|
||||
executionRevision,
|
||||
placementScore: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,6 +83,7 @@ function requestFor(offer) {
|
||||
offerId: offer.offerId,
|
||||
executionDigest: offer.executionDigest,
|
||||
secretRefs: [SECRET_REF],
|
||||
environmentBundleRefs: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,12 +100,17 @@ test('rehydrates lease authority from inbox and delivers one exact Secret batch'
|
||||
client: {
|
||||
async postJson(request) {
|
||||
transport = request;
|
||||
return Buffer.from(JSON.stringify({
|
||||
schema: 'qinglong/remote-secret-delivery@v1',
|
||||
runId: 'run-1', attemptId: 'attempt-1', offerId: 'offer-1',
|
||||
executionDigest: offer.executionDigest,
|
||||
values: [{ secretRef: SECRET_REF, value: 'resolved-value' }],
|
||||
}));
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
schema: 'qinglong/remote-secret-delivery@v2',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-1',
|
||||
executionDigest: offer.executionDigest,
|
||||
values: [{ secretRef: SECRET_REF, value: 'resolved-value' }],
|
||||
environmentBundles: [],
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -91,7 +118,11 @@ test('rehydrates lease authority from inbox and delivers one exact Secret batch'
|
||||
assert.deepEqual(resolution.values, [
|
||||
{ secretRef: SECRET_REF, value: 'resolved-value' },
|
||||
]);
|
||||
assert.equal(transport.path.endsWith(`/sessions/${SESSION_ID}/secrets`), true);
|
||||
assert.deepEqual(resolution.environmentBundles, []);
|
||||
assert.equal(
|
||||
transport.path.endsWith(`/sessions/${SESSION_ID}/secrets`),
|
||||
true,
|
||||
);
|
||||
assert.equal(transport.body.leaseToken, LEASE_TOKEN);
|
||||
assert.equal(transport.maximumRequestBytes, 64 * 1024);
|
||||
assert.equal(JSON.stringify(requestFor(offer)).includes(LEASE_TOKEN), false);
|
||||
@@ -102,9 +133,15 @@ test('rejects a stale inbox identity before sending the capability', async () =>
|
||||
let calls = 0;
|
||||
const provider = new WorkerRemoteSecretHttpsProvider({
|
||||
inbox: {
|
||||
async readOffer() { return { state: 'starting_acknowledged', offer }; },
|
||||
async readOffer() {
|
||||
return { state: 'starting_acknowledged', offer };
|
||||
},
|
||||
},
|
||||
client: {
|
||||
async postJson() {
|
||||
calls += 1;
|
||||
},
|
||||
},
|
||||
client: { async postJson() { calls += 1; } },
|
||||
});
|
||||
await assert.rejects(
|
||||
provider.resolve({ ...requestFor(offer), executionDigest: 'b'.repeat(64) }),
|
||||
@@ -117,16 +154,23 @@ test('rejects response authority drift and does not return plaintext', async ()
|
||||
const offer = acceptedOffer();
|
||||
const provider = new WorkerRemoteSecretHttpsProvider({
|
||||
inbox: {
|
||||
async readOffer() { return { state: 'starting_acknowledged', offer }; },
|
||||
async readOffer() {
|
||||
return { state: 'starting_acknowledged', offer };
|
||||
},
|
||||
},
|
||||
client: {
|
||||
async postJson() {
|
||||
return Buffer.from(JSON.stringify({
|
||||
schema: 'qinglong/remote-secret-delivery@v1',
|
||||
runId: 'run-other', attemptId: 'attempt-1', offerId: 'offer-1',
|
||||
executionDigest: offer.executionDigest,
|
||||
values: [{ secretRef: SECRET_REF, value: 'must-not-escape' }],
|
||||
}));
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
schema: 'qinglong/remote-secret-delivery@v2',
|
||||
runId: 'run-other',
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-1',
|
||||
executionDigest: offer.executionDigest,
|
||||
values: [{ secretRef: SECRET_REF, value: 'must-not-escape' }],
|
||||
environmentBundles: [],
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -138,10 +182,21 @@ test('does not fetch Secrets before starting ACK or after the launch barrier', a
|
||||
for (const state of ['accepted', 'launching']) {
|
||||
let calls = 0;
|
||||
const provider = new WorkerRemoteSecretHttpsProvider({
|
||||
inbox: { async readOffer() { return { state, offer }; } },
|
||||
client: { async postJson() { calls += 1; } },
|
||||
inbox: {
|
||||
async readOffer() {
|
||||
return { state, offer };
|
||||
},
|
||||
},
|
||||
client: {
|
||||
async postJson() {
|
||||
calls += 1;
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(provider.resolve(requestFor(offer)), /offer_unavailable/);
|
||||
await assert.rejects(
|
||||
provider.resolve(requestFor(offer)),
|
||||
/offer_unavailable/,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -267,11 +267,20 @@ test('disposes provider-owned credential material on success and rejection', asy
|
||||
rejected.postJson({
|
||||
path: COMPLETION_PATH,
|
||||
body: {},
|
||||
maximumResponseBytes: 1024,
|
||||
maximumResponseBytes: 256 * 1024,
|
||||
}),
|
||||
/credentials_unavailable/,
|
||||
);
|
||||
assert.equal(rejectedDisposals, 1);
|
||||
await assert.rejects(
|
||||
rejected.postJson({
|
||||
path: COMPLETION_PATH,
|
||||
body: {},
|
||||
maximumResponseBytes: 256 * 1024 + 1,
|
||||
}),
|
||||
/request_rejected/,
|
||||
);
|
||||
assert.equal(rejectedDisposals, 1);
|
||||
} finally {
|
||||
rejected.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user