mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
229 lines
7.4 KiB
JavaScript
229 lines
7.4 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { createHash } = require('node:crypto');
|
|
const { test } = require('node:test');
|
|
const {
|
|
CreateBucketCommand,
|
|
DeleteBucketCommand,
|
|
DeleteObjectsCommand,
|
|
ListObjectVersionsCommand,
|
|
ListObjectsV2Command,
|
|
PutBucketVersioningCommand,
|
|
S3Client,
|
|
} = require('@aws-sdk/client-s3');
|
|
const {
|
|
S3ClusterRemoteWorkerArtifactStore,
|
|
S3ClusterRemoteWorkerArtifactStoreError,
|
|
} = require('@qinglong/cluster-control/s3-artifact-store');
|
|
|
|
const endpoint = process.env.QL3_TEST_S3_ENDPOINT;
|
|
const accessKeyId = process.env.QL3_TEST_S3_ACCESS_KEY_ID;
|
|
const secretAccessKey = process.env.QL3_TEST_S3_SECRET_ACCESS_KEY;
|
|
|
|
test(
|
|
'real S3-compatible service preserves immutable Artifact evidence',
|
|
{
|
|
skip:
|
|
endpoint && accessKeyId && secretAccessKey
|
|
? false
|
|
: 'requires QL3_TEST_S3_ENDPOINT and credentials',
|
|
},
|
|
async () => {
|
|
const client = new S3Client({
|
|
endpoint,
|
|
region: 'us-east-1',
|
|
forcePathStyle: true,
|
|
credentials: { accessKeyId, secretAccessKey },
|
|
});
|
|
const bucket = `ql3-artifact-${process.pid}-${Date.now()}`.slice(0, 63);
|
|
const versionedBucket = `${bucket}-v`.slice(0, 63);
|
|
const command = Object.freeze({
|
|
projectId: 'project-s3-integration',
|
|
runId: 'run-s3-integration',
|
|
attemptId: 'attempt-s3-integration',
|
|
logArtifactId: `wlog-${'c'.repeat(30)}`,
|
|
byteLength: 17,
|
|
truncated: true,
|
|
});
|
|
const content = Buffer.from('real object bytes');
|
|
const body = (value) =>
|
|
Object.freeze({
|
|
async *[Symbol.asyncIterator]() {
|
|
yield value.subarray(0, 4);
|
|
yield value.subarray(4);
|
|
},
|
|
});
|
|
|
|
try {
|
|
await client.send(new CreateBucketCommand({ Bucket: bucket }));
|
|
const store = new S3ClusterRemoteWorkerArtifactStore({
|
|
client,
|
|
bucket,
|
|
prefix: 'qinglong/integration',
|
|
encryption: { mode: 's3' },
|
|
});
|
|
const stored = await store.put(command, body(content));
|
|
assert.equal(stored.status, 'stored');
|
|
assert.equal(
|
|
stored.sha256,
|
|
createHash('sha256').update(content).digest('hex'),
|
|
);
|
|
const replay = await store.put(command, body(content));
|
|
assert.equal(replay.status, 'already_stored');
|
|
const range = await store.readLogRange(
|
|
{
|
|
projectId: command.projectId,
|
|
runId: command.runId,
|
|
attemptId: command.attemptId,
|
|
logArtifactId: command.logArtifactId,
|
|
},
|
|
{
|
|
offset: 5,
|
|
length: 6,
|
|
},
|
|
);
|
|
assert.equal(range.status, 'available');
|
|
assert.equal(Buffer.from(range.content).toString(), 'object');
|
|
assert.equal(range.start, 5);
|
|
assert.equal(range.endExclusive, 11);
|
|
assert.equal(range.totalBytes, content.byteLength);
|
|
assert.equal(range.nextOffset, 11);
|
|
assert.deepEqual(range.truncation, { truncated: true });
|
|
await assert.rejects(
|
|
store.put(command, body(Buffer.from('REAL OBJECT BYTES'))),
|
|
(error) =>
|
|
error instanceof S3ClusterRemoteWorkerArtifactStoreError &&
|
|
error.reason === 'integrity_mismatch',
|
|
);
|
|
const objects = await client.send(
|
|
new ListObjectsV2Command({
|
|
Bucket: bucket,
|
|
Prefix: 'qinglong/integration/',
|
|
}),
|
|
);
|
|
assert.equal(objects.KeyCount, 1);
|
|
assert.match(objects.Contents[0].Key, /\/objects\//);
|
|
const retired = await store.retire({
|
|
projectId: command.projectId,
|
|
runId: command.runId,
|
|
attemptId: command.attemptId,
|
|
logArtifactId: command.logArtifactId,
|
|
executorType: 'remote_worker',
|
|
finishedAtMs: 1,
|
|
});
|
|
assert.deepEqual(retired, {
|
|
disposition: 'deleted',
|
|
byteLength: content.byteLength,
|
|
truncation: { truncated: true },
|
|
});
|
|
assert.equal(
|
|
(
|
|
await client.send(
|
|
new ListObjectsV2Command({
|
|
Bucket: bucket,
|
|
Prefix: 'qinglong/integration/',
|
|
}),
|
|
)
|
|
).KeyCount,
|
|
0,
|
|
);
|
|
assert.deepEqual(
|
|
await store.retire({
|
|
projectId: command.projectId,
|
|
runId: command.runId,
|
|
attemptId: command.attemptId,
|
|
logArtifactId: command.logArtifactId,
|
|
executorType: 'remote_worker',
|
|
finishedAtMs: 1,
|
|
}),
|
|
{
|
|
disposition: 'already_absent',
|
|
byteLength: 0,
|
|
truncation: { truncated: 'unknown' },
|
|
},
|
|
);
|
|
|
|
await client.send(new CreateBucketCommand({ Bucket: versionedBucket }));
|
|
await client.send(
|
|
new PutBucketVersioningCommand({
|
|
Bucket: versionedBucket,
|
|
VersioningConfiguration: { Status: 'Enabled' },
|
|
}),
|
|
);
|
|
const versionedStore = new S3ClusterRemoteWorkerArtifactStore({
|
|
client,
|
|
bucket: versionedBucket,
|
|
prefix: 'qinglong/integration',
|
|
encryption: { mode: 's3' },
|
|
});
|
|
assert.equal(
|
|
(await versionedStore.put(command, body(content))).status,
|
|
'stored',
|
|
);
|
|
const beforeVersionedRetirement = await client.send(
|
|
new ListObjectVersionsCommand({ Bucket: versionedBucket }),
|
|
);
|
|
assert.equal(beforeVersionedRetirement.Versions?.length, 1);
|
|
assert.equal(beforeVersionedRetirement.DeleteMarkers?.length ?? 0, 0);
|
|
assert.match(beforeVersionedRetirement.Versions[0].Key, /\/objects\//);
|
|
assert.equal(
|
|
(
|
|
await versionedStore.retire({
|
|
projectId: command.projectId,
|
|
runId: command.runId,
|
|
attemptId: command.attemptId,
|
|
logArtifactId: command.logArtifactId,
|
|
executorType: 'remote_worker',
|
|
finishedAtMs: 1,
|
|
})
|
|
).disposition,
|
|
'deleted',
|
|
);
|
|
const afterVersionedRetirement = await client.send(
|
|
new ListObjectVersionsCommand({ Bucket: versionedBucket }),
|
|
);
|
|
assert.equal(afterVersionedRetirement.Versions?.length ?? 0, 0);
|
|
assert.equal(afterVersionedRetirement.DeleteMarkers?.length ?? 0, 0);
|
|
} finally {
|
|
for (const cleanupBucket of [versionedBucket, bucket]) {
|
|
try {
|
|
const versions = await client.send(
|
|
new ListObjectVersionsCommand({ Bucket: cleanupBucket }),
|
|
);
|
|
const versionedObjects = [
|
|
...(versions.Versions ?? []),
|
|
...(versions.DeleteMarkers ?? []),
|
|
].map(({ Key, VersionId }) => ({ Key, VersionId }));
|
|
if (versionedObjects.length) {
|
|
await client.send(
|
|
new DeleteObjectsCommand({
|
|
Bucket: cleanupBucket,
|
|
Delete: { Objects: versionedObjects, Quiet: true },
|
|
}),
|
|
);
|
|
}
|
|
const objects = await client.send(
|
|
new ListObjectsV2Command({ Bucket: cleanupBucket }),
|
|
);
|
|
if (objects.Contents?.length) {
|
|
await client.send(
|
|
new DeleteObjectsCommand({
|
|
Bucket: cleanupBucket,
|
|
Delete: {
|
|
Objects: objects.Contents.map(({ Key }) => ({ Key })),
|
|
Quiet: true,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
await client.send(new DeleteBucketCommand({ Bucket: cleanupBucket }));
|
|
} catch {
|
|
// Preserve the integration assertion; the ephemeral container is removed.
|
|
}
|
|
}
|
|
client.destroy();
|
|
}
|
|
},
|
|
);
|