test: add regression scenario coverage
This commit is contained in:
+7
-6
@@ -6,9 +6,10 @@
|
|||||||
"description": "Whole-system integration scenarios for serve.zone components.",
|
"description": "Whole-system integration scenarios for serve.zone components.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bootstrap:components": "pnpm --dir ../interfaces install && pnpm --dir ../api install && pnpm --dir ../cloudly install && pnpm --dir ../corebuild install && pnpm --dir ../coreflow install && pnpm --dir ../coretraffic install && pnpm --dir ../corestore install && pnpm --dir ../onebox exec deno install --config deno.json && deno cache --config ../isocreator/deno.json ../isocreator/mod.ts && deno cache --config ../baseos/deno.json ../baseos/mod.ts && pnpm install",
|
"bootstrap:components": "pnpm --dir ../interfaces install && pnpm --dir ../api install && pnpm --dir ../cloudly install && pnpm --dir ../corebuild install && pnpm --dir ../coreflow install && pnpm --dir ../coretraffic install && pnpm --dir ../corestore install && pnpm --dir ../onebox exec deno install --config deno.json && deno cache --config ../isocreator/deno.json ../isocreator/mod.ts && deno cache --config ../baseos/deno.json ../baseos/mod.ts && pnpm install",
|
||||||
"test": "pnpm scenario:corestore-volume-driver && pnpm scenario:registry-deploy-on-push && pnpm scenario:onebox-basic-lifecycle && pnpm scenario:onebox-backup-restore",
|
"test": "pnpm scenario:codebase-regressions && pnpm scenario:corestore-volume-driver && pnpm scenario:registry-deploy-on-push && pnpm scenario:onebox-basic-lifecycle && pnpm scenario:onebox-backup-restore",
|
||||||
"test:full": "pnpm test && pnpm scenario:onebox-cloudly-appstore-worker",
|
"test:full": "pnpm test && pnpm scenario:onebox-cloudly-appstore-worker",
|
||||||
"scenario:corestore-volume-driver": "tsx scenarios/corestore-volume-driver/scenario.ts",
|
"scenario:corestore-volume-driver": "tsx scenarios/corestore-volume-driver/scenario.ts",
|
||||||
|
"scenario:codebase-regressions": "tsx scenarios/codebase-regressions/scenario.ts",
|
||||||
"scenario:registry-deploy-on-push": "tsx --tsconfig ../cloudly/tsconfig.json scenarios/registry-deploy-on-push/scenario.ts",
|
"scenario:registry-deploy-on-push": "tsx --tsconfig ../cloudly/tsconfig.json scenarios/registry-deploy-on-push/scenario.ts",
|
||||||
"scenario:baseos-image-pipeline": "tsx --tsconfig ../corebuild/tsconfig.json scenarios/baseos-image-pipeline/scenario.ts",
|
"scenario:baseos-image-pipeline": "tsx --tsconfig ../corebuild/tsconfig.json scenarios/baseos-image-pipeline/scenario.ts",
|
||||||
"scenario:baseos-qemu-enrollment": "tsx scenarios/baseos-qemu-enrollment/scenario.ts",
|
"scenario:baseos-qemu-enrollment": "tsx scenarios/baseos-qemu-enrollment/scenario.ts",
|
||||||
@@ -22,11 +23,11 @@
|
|||||||
"vagrant:destroy": "vagrant destroy -f"
|
"vagrant:destroy": "vagrant destroy -f"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tstest": "2.3.8",
|
"@git.zone/tstest": "3.6.3",
|
||||||
"@push.rocks/smartnetwork": "4.4.0",
|
"@push.rocks/smartnetwork": "4.7.1",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^25.6.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
Generated
+685
-2656
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ Component repositories keep their fast unit and package tests locally. This repo
|
|||||||
|
|
||||||
| Scenario | Command | What it validates |
|
| Scenario | Command | What it validates |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| `codebase-regressions` | `pnpm scenario:codebase-regressions` | Static cross-package regression checks for security defaults, registry/image stream fixes, package metadata, and ContainerArchive prune safety. |
|
||||||
| `corestore-volume-driver` | `pnpm scenario:corestore-volume-driver` | Corestore control API, Docker VolumeDriver protocol, snapshot/restore through `containerarchive`, Coreflow volume mount generation, backup/restore orchestration, and remote replication against a fake Cloudly API. |
|
| `corestore-volume-driver` | `pnpm scenario:corestore-volume-driver` | Corestore control API, Docker VolumeDriver protocol, snapshot/restore through `containerarchive`, Coreflow volume mount generation, backup/restore orchestration, and remote replication against a fake Cloudly API. |
|
||||||
| `registry-deploy-on-push` | `pnpm scenario:registry-deploy-on-push` | Cloudly startup with isolated Mongo/S3 helpers, cluster and registry setup, Docker image push, Cloudly metadata update, Coreflow workload provisioning, Coretraffic HTTPS routing, and same-tag redeploy after digest changes. |
|
| `registry-deploy-on-push` | `pnpm scenario:registry-deploy-on-push` | Cloudly startup with isolated Mongo/S3 helpers, cluster and registry setup, Docker image push, Cloudly metadata update, Coreflow workload provisioning, Coretraffic HTTPS routing, and same-tag redeploy after digest changes. |
|
||||||
| `onebox-basic-lifecycle` | `pnpm scenario:onebox-basic-lifecycle` | Onebox startup in dev mode, SmartProxy ingress, local registry status, workload deploy/remove, HTTP route check, and HTTPS route check with a temporary certificate. |
|
| `onebox-basic-lifecycle` | `pnpm scenario:onebox-basic-lifecycle` | Onebox startup in dev mode, SmartProxy ingress, local registry status, workload deploy/remove, HTTP route check, and HTTPS route check with a temporary certificate. |
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import assert from 'assert/strict';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { dirname, resolve } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||||
|
|
||||||
|
const readWorkspaceFile = (pathFromRoot: string) => {
|
||||||
|
return readFileSync(resolve(root, pathFromRoot), 'utf8');
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertIncludes = (filePath: string, expected: string) => {
|
||||||
|
const content = readWorkspaceFile(filePath);
|
||||||
|
assert.ok(content.includes(expected), `${filePath} should include ${expected}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertExcludes = (filePath: string, forbidden: string) => {
|
||||||
|
const content = readWorkspaceFile(filePath);
|
||||||
|
assert.ok(!content.includes(forbidden), `${filePath} should not include ${forbidden}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
assertIncludes('containerarchive/rust/src/prune.rs', 'rewritten_offsets');
|
||||||
|
assertExcludes('containerarchive/rust/src/prune.rs', 'offset: entry.offset, // Note: offset in the new pack may differ');
|
||||||
|
assertIncludes('containerarchive/test/test.ts', 'partial-pack GC rewrites chunks');
|
||||||
|
|
||||||
|
assertExcludes('onebox/ts/classes/onebox.ts', "hashPassword('admin')");
|
||||||
|
assertIncludes('onebox/ts/classes/onebox.ts', 'ONEBOX_ADMIN_PASSWORD');
|
||||||
|
assertExcludes('dcrouter/ts/opsserver/handlers/admin.handler.ts', "password: 'admin'");
|
||||||
|
assertIncludes('dcrouter/ts/opsserver/handlers/admin.handler.ts', 'DCROUTER_ADMIN_PASSWORD');
|
||||||
|
assertExcludes('corerender/ts/rendertron.db.ts', 'wxW4LBa3sxPjyXGf');
|
||||||
|
assertExcludes('corerender/ts/rendertron.db.ts', 'losslessone-main.zee8suk.mongodb.net');
|
||||||
|
|
||||||
|
assertIncludes('api/ts/classes.cloudlyapiclient.ts', 'return response;');
|
||||||
|
assertIncludes('api/ts/classes.image.ts', 'versionString: imageVersion');
|
||||||
|
assertIncludes('cloudly/ts/manager.image/classes.imagemanager.ts', 'readFromWebstream(readable)');
|
||||||
|
assertIncludes('cloudly/ts/manager.service/classes.servicemanager.ts', 'adminIdentityGuard');
|
||||||
|
assertIncludes('cloudly/ts/manager.cluster/classes.clustermanager.ts', 'getClusterById');
|
||||||
|
|
||||||
|
assertIncludes('platformclient/ts/email/classes.emailconnector.ts', 'return response.responseId');
|
||||||
|
assertIncludes('platformclient/ts/email/classes.letterconnector.ts', 'return response.processId');
|
||||||
|
assertIncludes('platformclient/ts/email/classes.pushnotificationconnector.ts', "typeof process !== 'undefined'");
|
||||||
|
|
||||||
|
assertIncludes('siprouter/ts/sipproxy.ts', 'return { id: callId };');
|
||||||
|
assertIncludes('onebox/ts/opsserver/classes.opsserver.ts', "'/v2/*'");
|
||||||
|
assertIncludes('spark/ts/spark.cli.ts', 'CLOUDLY_URL: cloudlyUrl');
|
||||||
|
|
||||||
|
const rustdeskConfig = JSON.parse(readWorkspaceFile('appstore-apptemplates/apps/rustdesk-server/versions/1.0.0/config.json'));
|
||||||
|
const relay = rustdeskConfig.envVars.find((entry: { key: string }) => entry.key === 'RELAY');
|
||||||
|
assert.equal(relay?.value, '${SERVICE_DOMAIN}:21116');
|
||||||
|
|
||||||
|
console.log('codebase regression checks passed');
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as net from 'node:net';
|
import * as net from 'node:net';
|
||||||
|
import * as crypto from 'node:crypto';
|
||||||
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
@@ -120,6 +121,10 @@ const pathExists = async (pathArg: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sha256Base64 = (base64Arg: string) => {
|
||||||
|
return crypto.createHash('sha256').update(Buffer.from(base64Arg, 'base64')).digest('hex');
|
||||||
|
};
|
||||||
|
|
||||||
const assertCoreflowVolumeMounts = () => {
|
const assertCoreflowVolumeMounts = () => {
|
||||||
const coreflow = new Coreflow();
|
const coreflow = new Coreflow();
|
||||||
const service: IService = {
|
const service: IService = {
|
||||||
@@ -439,12 +444,207 @@ const assertCoreflowBackupOrchestration = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const assertCoreflowRemoteReplication = async () => {
|
||||||
|
const socketPath = join(buildDir, 'replication-plugins', 'corestore.sock');
|
||||||
|
const dataDir = join(buildDir, 'replication-data');
|
||||||
|
await mkdir(dirname(socketPath), { recursive: true });
|
||||||
|
|
||||||
|
const controlPort = await getFreePort();
|
||||||
|
const s3Port = await getFreePort();
|
||||||
|
const dbPort = await getFreePort();
|
||||||
|
let corestore = new CoreStore({
|
||||||
|
dataDir,
|
||||||
|
bindAddress: '127.0.0.1',
|
||||||
|
publicHost: '127.0.0.1',
|
||||||
|
controlPort,
|
||||||
|
s3Port,
|
||||||
|
dbPort,
|
||||||
|
apiToken,
|
||||||
|
volumePluginSocketPath: socketPath,
|
||||||
|
});
|
||||||
|
const previousControlUrl = process.env.CORESTORE_CONTROL_URL;
|
||||||
|
const previousApiToken = process.env.CORESTORE_API_TOKEN;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await corestore.start();
|
||||||
|
process.env.CORESTORE_CONTROL_URL = `http://127.0.0.1:${controlPort}`;
|
||||||
|
process.env.CORESTORE_API_TOKEN = apiToken;
|
||||||
|
|
||||||
|
const volumeName = 'remote-replication-data';
|
||||||
|
const service: IService = {
|
||||||
|
id: 'svc-remote-replication',
|
||||||
|
data: {
|
||||||
|
name: 'remote-replication',
|
||||||
|
description: 'Coreflow remote replication service',
|
||||||
|
imageId: 'image-remote-replication',
|
||||||
|
imageVersion: 'latest',
|
||||||
|
environment: {},
|
||||||
|
secretBundleId: 'secret-remote-replication',
|
||||||
|
serviceCategory: 'workload',
|
||||||
|
deploymentStrategy: 'custom',
|
||||||
|
scaleFactor: 1,
|
||||||
|
balancingStrategy: 'round-robin',
|
||||||
|
ports: {
|
||||||
|
web: 80,
|
||||||
|
},
|
||||||
|
volumes: [
|
||||||
|
{
|
||||||
|
source: volumeName,
|
||||||
|
mountPath: '/data',
|
||||||
|
backup: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
domains: [],
|
||||||
|
deploymentIds: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createVolumeResponse = await controlPost<any>(controlPort, '/volumes/create', {
|
||||||
|
name: volumeName,
|
||||||
|
serviceId: service.id,
|
||||||
|
serviceName: service.data.name,
|
||||||
|
mountPath: '/data',
|
||||||
|
backup: true,
|
||||||
|
});
|
||||||
|
const mountpoint = createVolumeResponse.volume?.Mountpoint;
|
||||||
|
assert(typeof mountpoint === 'string', `Could not create replication test volume: ${JSON.stringify(createVolumeResponse)}`);
|
||||||
|
await writeFile(join(mountpoint, 'state.txt'), 'before remote backup\n');
|
||||||
|
|
||||||
|
const remoteObjects = new Map<string, { object: any; contentsBase64: string }>();
|
||||||
|
let remoteManifest: any;
|
||||||
|
const coreflow = new Coreflow();
|
||||||
|
const fakeCloudlyApiClient = {
|
||||||
|
platform: {
|
||||||
|
getPlatformDesiredState: async () => ({ capabilities: [], providerConfigs: [], bindings: [] }),
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
getServices: async () => [service],
|
||||||
|
},
|
||||||
|
typedsocketClient: {
|
||||||
|
createTypedRequest: (methodArg: string) => ({
|
||||||
|
fire: async (payloadArg: any) => {
|
||||||
|
if (methodArg === 'prepareBackupReplication') {
|
||||||
|
remoteManifest = payloadArg.manifest;
|
||||||
|
return {
|
||||||
|
missingObjects: payloadArg.manifest.objects.filter((objectArg: any) => {
|
||||||
|
const existing = remoteObjects.get(objectArg.path);
|
||||||
|
return !existing || existing.object.sha256 !== objectArg.sha256;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (methodArg === 'uploadBackupArchiveObject') {
|
||||||
|
assert(payloadArg.object.sha256 === sha256Base64(payloadArg.contentsBase64), `Remote upload checksum mismatch for ${payloadArg.object.path}`);
|
||||||
|
remoteObjects.set(payloadArg.object.path, {
|
||||||
|
object: payloadArg.object,
|
||||||
|
contentsBase64: payloadArg.contentsBase64,
|
||||||
|
});
|
||||||
|
return { accepted: true };
|
||||||
|
}
|
||||||
|
if (methodArg === 'completeBackupReplication') {
|
||||||
|
remoteManifest = payloadArg.manifest;
|
||||||
|
for (const object of remoteManifest.objects) {
|
||||||
|
assert(remoteObjects.has(object.path), `Missing replicated object ${object.path}`);
|
||||||
|
}
|
||||||
|
const manifestBody = JSON.stringify(remoteManifest);
|
||||||
|
return {
|
||||||
|
replication: {
|
||||||
|
targetType: 's3',
|
||||||
|
targetPath: 'fake-target',
|
||||||
|
manifestPath: 'fake-target/manifest.json',
|
||||||
|
manifestSha256: crypto.createHash('sha256').update(manifestBody).digest('hex'),
|
||||||
|
objectCount: remoteManifest.objects.length,
|
||||||
|
totalSize: remoteManifest.totalSize,
|
||||||
|
completedAt: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (methodArg === 'getBackupArchiveManifest') {
|
||||||
|
return { manifest: remoteManifest };
|
||||||
|
}
|
||||||
|
if (methodArg === 'downloadBackupArchiveObject') {
|
||||||
|
const remoteObject = remoteObjects.get(payloadArg.object.path);
|
||||||
|
assert(remoteObject, `Remote object ${payloadArg.object.path} not found`);
|
||||||
|
return remoteObject;
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected fake Cloudly request ${methodArg}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
(coreflow.cloudlyConnector as any).cloudlyApiClient = fakeCloudlyApiClient;
|
||||||
|
(coreflow.cloudlyConnector as any).identity = {
|
||||||
|
name: 'replication-test-coreflow',
|
||||||
|
role: 'cluster',
|
||||||
|
type: 'machine',
|
||||||
|
userId: 'cluster-remote-replication',
|
||||||
|
expiresAt: Date.now() + 3600 * 1000,
|
||||||
|
jwt: '',
|
||||||
|
clusterId: 'cluster-remote-replication',
|
||||||
|
};
|
||||||
|
|
||||||
|
const backupResult = await coreflow.backupManager.executeServiceBackup({
|
||||||
|
backupId: 'remote-replication-smoke',
|
||||||
|
service,
|
||||||
|
replication: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
scenario: scenarioName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert(backupResult.replication?.objectCount === remoteObjects.size, `Unexpected replication result: ${JSON.stringify(backupResult.replication)}`);
|
||||||
|
assert(remoteObjects.size > 0, 'Remote replication did not upload archive objects');
|
||||||
|
|
||||||
|
await corestore.stop();
|
||||||
|
await rm(join(dataDir, 'volume-archive'), { recursive: true, force: true });
|
||||||
|
await writeFile(join(mountpoint, 'state.txt'), 'after local archive loss\n');
|
||||||
|
|
||||||
|
corestore = new CoreStore({
|
||||||
|
dataDir,
|
||||||
|
bindAddress: '127.0.0.1',
|
||||||
|
publicHost: '127.0.0.1',
|
||||||
|
controlPort,
|
||||||
|
s3Port,
|
||||||
|
dbPort,
|
||||||
|
apiToken,
|
||||||
|
volumePluginSocketPath: socketPath,
|
||||||
|
});
|
||||||
|
await corestore.start();
|
||||||
|
|
||||||
|
await coreflow.backupManager.executeServiceRestore({
|
||||||
|
backupId: 'remote-replication-smoke',
|
||||||
|
service,
|
||||||
|
snapshots: backupResult.snapshots,
|
||||||
|
clear: true,
|
||||||
|
replication: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert(await readFile(join(mountpoint, 'state.txt'), 'utf8') === 'before remote backup\n', 'Remote archive restore did not restore volume data');
|
||||||
|
} finally {
|
||||||
|
if (previousControlUrl === undefined) {
|
||||||
|
delete process.env.CORESTORE_CONTROL_URL;
|
||||||
|
} else {
|
||||||
|
process.env.CORESTORE_CONTROL_URL = previousControlUrl;
|
||||||
|
}
|
||||||
|
if (previousApiToken === undefined) {
|
||||||
|
delete process.env.CORESTORE_API_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.CORESTORE_API_TOKEN = previousApiToken;
|
||||||
|
}
|
||||||
|
await corestore.stop().catch((errorArg) => {
|
||||||
|
console.log(`[${scenarioName}] Failed to stop replication Corestore: ${(errorArg as Error).message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
await mkdir(buildDir, { recursive: true });
|
await mkdir(buildDir, { recursive: true });
|
||||||
assertCoreflowVolumeMounts();
|
assertCoreflowVolumeMounts();
|
||||||
await assertCorestoreVolumeDriver();
|
await assertCorestoreVolumeDriver();
|
||||||
await assertCoreflowBackupOrchestration();
|
await assertCoreflowBackupOrchestration();
|
||||||
|
await assertCoreflowRemoteReplication();
|
||||||
console.log(`[${scenarioName}] PASS`);
|
console.log(`[${scenarioName}] PASS`);
|
||||||
} finally {
|
} finally {
|
||||||
await rm(buildDir, { recursive: true, force: true });
|
await rm(buildDir, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -15,13 +15,36 @@ const delayFor = async (millisecondsArg: number) => {
|
|||||||
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
|
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = async (commandArg: string, argsArg: string[]) => {
|
const outputCommand = async (
|
||||||
|
commandArg: string,
|
||||||
|
argsArg: string[],
|
||||||
|
optionsArg: Pick<Deno.CommandOptions, 'stdout' | 'stderr'>,
|
||||||
|
timeoutMsArg = 30000,
|
||||||
|
) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMsArg);
|
||||||
|
try {
|
||||||
const command = new Deno.Command(commandArg, {
|
const command = new Deno.Command(commandArg, {
|
||||||
args: argsArg,
|
args: argsArg,
|
||||||
|
signal: controller.signal,
|
||||||
|
...optionsArg,
|
||||||
|
});
|
||||||
|
return await command.output();
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).name === 'AbortError') {
|
||||||
|
throw new Error(`${commandArg} ${argsArg.join(' ')} timed out after ${timeoutMsArg}ms`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const run = async (commandArg: string, argsArg: string[]) => {
|
||||||
|
const output = await outputCommand(commandArg, argsArg, {
|
||||||
stdout: 'piped',
|
stdout: 'piped',
|
||||||
stderr: 'piped',
|
stderr: 'piped',
|
||||||
});
|
});
|
||||||
const output = await command.output();
|
|
||||||
const stdout = new TextDecoder().decode(output.stdout).trim();
|
const stdout = new TextDecoder().decode(output.stdout).trim();
|
||||||
const stderr = new TextDecoder().decode(output.stderr).trim();
|
const stderr = new TextDecoder().decode(output.stderr).trim();
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
@@ -48,12 +71,11 @@ const waitFor = async (checkFunctionArg: () => boolean | Promise<boolean>, messa
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dockerServiceExists = async (serviceNameArg: string) => {
|
const dockerServiceExists = async (serviceNameArg: string) => {
|
||||||
const command = new Deno.Command('docker', {
|
const output = await outputCommand('docker', ['service', 'inspect', serviceNameArg], {
|
||||||
args: ['service', 'inspect', serviceNameArg],
|
|
||||||
stdout: 'null',
|
stdout: 'null',
|
||||||
stderr: 'null',
|
stderr: 'null',
|
||||||
});
|
}, 15000);
|
||||||
return (await command.output()).success;
|
return output.success;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeDockerService = async (serviceNameArg: string) => {
|
const removeDockerService = async (serviceNameArg: string) => {
|
||||||
@@ -71,12 +93,16 @@ const assertNoPreexistingOneboxIngress = async () => {
|
|||||||
|
|
||||||
const waitForDockerServiceRunning = async (serviceNameArg: string) => {
|
const waitForDockerServiceRunning = async (serviceNameArg: string) => {
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const command = new Deno.Command('docker', {
|
const output = await outputCommand('docker', [
|
||||||
args: ['service', 'ps', serviceNameArg, '--format', '{{.CurrentState}}'],
|
'service',
|
||||||
|
'ps',
|
||||||
|
serviceNameArg,
|
||||||
|
'--format',
|
||||||
|
'{{.CurrentState}}',
|
||||||
|
], {
|
||||||
stdout: 'piped',
|
stdout: 'piped',
|
||||||
stderr: 'null',
|
stderr: 'null',
|
||||||
});
|
}, 15000);
|
||||||
const output = await command.output();
|
|
||||||
if (!output.success) {
|
if (!output.success) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -205,4 +231,10 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await main();
|
try {
|
||||||
|
await main();
|
||||||
|
Deno.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,13 +14,36 @@ const delayFor = async (millisecondsArg: number) => {
|
|||||||
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
|
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = async (commandArg: string, argsArg: string[]) => {
|
const outputCommand = async (
|
||||||
|
commandArg: string,
|
||||||
|
argsArg: string[],
|
||||||
|
optionsArg: Pick<Deno.CommandOptions, 'stdout' | 'stderr'>,
|
||||||
|
timeoutMsArg = 30000,
|
||||||
|
) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMsArg);
|
||||||
|
try {
|
||||||
const command = new Deno.Command(commandArg, {
|
const command = new Deno.Command(commandArg, {
|
||||||
args: argsArg,
|
args: argsArg,
|
||||||
|
signal: controller.signal,
|
||||||
|
...optionsArg,
|
||||||
|
});
|
||||||
|
return await command.output();
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).name === 'AbortError') {
|
||||||
|
throw new Error(`${commandArg} ${argsArg.join(' ')} timed out after ${timeoutMsArg}ms`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const run = async (commandArg: string, argsArg: string[]) => {
|
||||||
|
const output = await outputCommand(commandArg, argsArg, {
|
||||||
stdout: 'piped',
|
stdout: 'piped',
|
||||||
stderr: 'piped',
|
stderr: 'piped',
|
||||||
});
|
});
|
||||||
const output = await command.output();
|
|
||||||
const stdout = new TextDecoder().decode(output.stdout).trim();
|
const stdout = new TextDecoder().decode(output.stdout).trim();
|
||||||
const stderr = new TextDecoder().decode(output.stderr).trim();
|
const stderr = new TextDecoder().decode(output.stderr).trim();
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
@@ -47,12 +70,11 @@ const waitFor = async (checkFunctionArg: () => boolean | Promise<boolean>, messa
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dockerServiceExists = async (serviceNameArg: string) => {
|
const dockerServiceExists = async (serviceNameArg: string) => {
|
||||||
const command = new Deno.Command('docker', {
|
const output = await outputCommand('docker', ['service', 'inspect', serviceNameArg], {
|
||||||
args: ['service', 'inspect', serviceNameArg],
|
|
||||||
stdout: 'null',
|
stdout: 'null',
|
||||||
stderr: 'null',
|
stderr: 'null',
|
||||||
});
|
}, 15000);
|
||||||
return (await command.output()).success;
|
return output.success;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeDockerService = async (serviceNameArg: string) => {
|
const removeDockerService = async (serviceNameArg: string) => {
|
||||||
@@ -70,12 +92,16 @@ const assertNoPreexistingOneboxIngress = async () => {
|
|||||||
|
|
||||||
const waitForDockerServiceRunning = async (serviceNameArg: string) => {
|
const waitForDockerServiceRunning = async (serviceNameArg: string) => {
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const command = new Deno.Command('docker', {
|
const output = await outputCommand('docker', [
|
||||||
args: ['service', 'ps', serviceNameArg, '--format', '{{.CurrentState}}'],
|
'service',
|
||||||
|
'ps',
|
||||||
|
serviceNameArg,
|
||||||
|
'--format',
|
||||||
|
'{{.CurrentState}}',
|
||||||
|
], {
|
||||||
stdout: 'piped',
|
stdout: 'piped',
|
||||||
stderr: 'null',
|
stderr: 'null',
|
||||||
});
|
}, 15000);
|
||||||
const output = await command.output();
|
|
||||||
if (!output.success) {
|
if (!output.success) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -107,12 +133,10 @@ const requestRoute = async (protocolArg: 'http' | 'https', portArg: number) => {
|
|||||||
curlArgs.unshift('-k');
|
curlArgs.unshift('-k');
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new Deno.Command('curl', {
|
const output = await outputCommand('curl', curlArgs, {
|
||||||
args: curlArgs,
|
|
||||||
stdout: 'piped',
|
stdout: 'piped',
|
||||||
stderr: 'piped',
|
stderr: 'piped',
|
||||||
});
|
}, 15000);
|
||||||
const output = await command.output();
|
|
||||||
const stdout = new TextDecoder().decode(output.stdout).trim();
|
const stdout = new TextDecoder().decode(output.stdout).trim();
|
||||||
const stderr = new TextDecoder().decode(output.stderr).trim();
|
const stderr = new TextDecoder().decode(output.stderr).trim();
|
||||||
if (!output.success) {
|
if (!output.success) {
|
||||||
@@ -277,4 +301,10 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await main();
|
try {
|
||||||
|
await main();
|
||||||
|
Deno.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -329,4 +329,10 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await main();
|
try {
|
||||||
|
await main();
|
||||||
|
Deno.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { dirname, join, resolve } from 'node:path';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
|
||||||
|
|
||||||
import * as cloudlyApiClient from '../../../api/ts/index.js';
|
import * as cloudlyApiClient from '../../../api/ts/index.js';
|
||||||
import { Cloudly, type ICloudlyConfig } from '../../../cloudly/ts/index.js';
|
import { Cloudly, type ICloudlyConfig } from '../../../cloudly/ts/index.js';
|
||||||
@@ -20,6 +19,13 @@ const buildDir = join(testingDir, '.nogit', 'registry-deploy-on-push', smokeId);
|
|||||||
const coretrafficServiceName = `${smokeId}-coretraffic`;
|
const coretrafficServiceName = `${smokeId}-coretraffic`;
|
||||||
const coreflowProxyServiceName = 'coreflow';
|
const coreflowProxyServiceName = 'coreflow';
|
||||||
const stopFunctions: Array<() => Promise<void>> = [];
|
const stopFunctions: Array<() => Promise<void>> = [];
|
||||||
|
const tapNodeTools = new TapNodeTools({
|
||||||
|
registerCleanup: (cleanupFunctionArg) => {
|
||||||
|
stopFunctions.push(async () => {
|
||||||
|
await cleanupFunctionArg();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const delayFor = async (millisecondsArg: number) => {
|
const delayFor = async (millisecondsArg: number) => {
|
||||||
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
|
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
|
||||||
@@ -367,15 +373,18 @@ const createCloudlyConfig = async (): Promise<ICloudlyConfig> => {
|
|||||||
stopFunctions.push(async () => {
|
stopFunctions.push(async () => {
|
||||||
await smartmongo.stopAndDumpToDir(join(testingDir, '.nogit', 'mongodump', smokeId));
|
await smartmongo.stopAndDumpToDir(join(testingDir, '.nogit', 'mongodump', smokeId));
|
||||||
});
|
});
|
||||||
const smarts3 = await tapNodeTools.createSmarts3();
|
const smarts3 = await tapNodeTools.createSmartStorage();
|
||||||
stopFunctions.push(async () => {
|
stopFunctions.push(async () => {
|
||||||
await smarts3.stop();
|
await smarts3.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
const bucketName = `${smokeId}-bucket`;
|
const bucketName = `${smokeId}-bucket`;
|
||||||
await smarts3.createBucket(bucketName);
|
await smarts3.createBucket(bucketName);
|
||||||
const smartnetwork = new SmartNetwork();
|
const publicPort = await tapNodeTools.findFreePort({
|
||||||
const publicPort = await smartnetwork.findFreePort(30000, 40000, { randomize: true });
|
startPort: 30000,
|
||||||
|
endPort: 40000,
|
||||||
|
randomize: true,
|
||||||
|
});
|
||||||
if (!publicPort) {
|
if (!publicPort) {
|
||||||
throw new Error('Could not find a free Cloudly scenario port');
|
throw new Error('Could not find a free Cloudly scenario port');
|
||||||
}
|
}
|
||||||
@@ -386,7 +395,7 @@ const createCloudlyConfig = async (): Promise<ICloudlyConfig> => {
|
|||||||
publicUrl: '127.0.0.1',
|
publicUrl: '127.0.0.1',
|
||||||
publicPort: String(publicPort),
|
publicPort: String(publicPort),
|
||||||
mongoDescriptor: await smartmongo.getMongoDescriptor(),
|
mongoDescriptor: await smartmongo.getMongoDescriptor(),
|
||||||
s3Descriptor: await smarts3.getS3Descriptor({
|
s3Descriptor: await smarts3.getStorageDescriptor({
|
||||||
bucketName,
|
bucketName,
|
||||||
}),
|
}),
|
||||||
sslMode: 'none',
|
sslMode: 'none',
|
||||||
@@ -533,8 +542,11 @@ const main = async () => {
|
|||||||
createdCorechatNetwork = true;
|
createdCorechatNetwork = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const smartnetwork = new SmartNetwork();
|
const coretrafficHttpsPort = await tapNodeTools.findFreePort({
|
||||||
const coretrafficHttpsPort = await smartnetwork.findFreePort(41000, 43000, { randomize: true });
|
startPort: 41000,
|
||||||
|
endPort: 43000,
|
||||||
|
randomize: true,
|
||||||
|
});
|
||||||
if (!coretrafficHttpsPort) {
|
if (!coretrafficHttpsPort) {
|
||||||
throw new Error('Could not find a free Coretraffic HTTPS test port');
|
throw new Error('Could not find a free Coretraffic HTTPS test port');
|
||||||
}
|
}
|
||||||
@@ -547,7 +559,14 @@ const main = async () => {
|
|||||||
coretrafficHttpsPort,
|
coretrafficHttpsPort,
|
||||||
);
|
);
|
||||||
createdCoretrafficService = true;
|
createdCoretrafficService = true;
|
||||||
await coreflow.cloudlyConnector.start();
|
const clusterIdentity = await testClient.getIdentityByToken(clusterToken, {
|
||||||
|
statefullIdentity: true,
|
||||||
|
tagConnection: true,
|
||||||
|
});
|
||||||
|
coreflow.cloudlyConnector.cloudlyApiClient = testClient as any;
|
||||||
|
coreflow.cloudlyConnector.identity = clusterIdentity;
|
||||||
|
coreflow.cloudlyConnector.coreflowJumpCode = clusterToken;
|
||||||
|
coreflow.cloudlyConnector.stop = async () => {};
|
||||||
console.log('[registry-deploy-on-push] Coreflow connector authenticated and tagged');
|
console.log('[registry-deploy-on-push] Coreflow connector authenticated and tagged');
|
||||||
try {
|
try {
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user