695 lines
24 KiB
TypeScript
695 lines
24 KiB
TypeScript
import { execFile } from 'node:child_process';
|
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { promisify } from 'node:util';
|
|
|
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
|
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
|
|
|
import * as cloudlyApiClient from '../../../api/ts/index.js';
|
|
import { Cloudly, type ICloudlyConfig } from '../../../cloudly/ts/index.js';
|
|
import { Coreflow } from '../../../coreflow/ts/coreflow.classes.coreflow.js';
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const scenarioDir = dirname(fileURLToPath(import.meta.url));
|
|
const testingDir = resolve(scenarioDir, '../..');
|
|
const repoRoot = resolve(testingDir, '..');
|
|
const smokeId = `szn-e2e-smoke-${Date.now().toString(36)}`;
|
|
const buildDir = join(testingDir, '.nogit', 'registry-deploy-on-push', smokeId);
|
|
const coretrafficServiceName = `${smokeId}-coretraffic`;
|
|
const coreflowProxyServiceName = 'coreflow';
|
|
const stopFunctions: Array<() => Promise<void>> = [];
|
|
|
|
const delayFor = async (millisecondsArg: number) => {
|
|
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
|
|
};
|
|
|
|
const run = async (commandArg: string, argsArg: string[]) => {
|
|
const { stdout, stderr } = await execFileAsync(commandArg, argsArg, {
|
|
maxBuffer: 1024 * 1024 * 20,
|
|
});
|
|
if (stdout.trim()) {
|
|
console.log(stdout.trim());
|
|
}
|
|
if (stderr.trim()) {
|
|
console.log(stderr.trim());
|
|
}
|
|
};
|
|
|
|
const waitFor = async (checkFunctionArg: () => boolean | Promise<boolean>, messageArg: string) => {
|
|
const startTime = Date.now();
|
|
while (Date.now() - startTime < 90000) {
|
|
if (await checkFunctionArg()) {
|
|
return;
|
|
}
|
|
await delayFor(500);
|
|
}
|
|
throw new Error(`Timed out waiting for ${messageArg}`);
|
|
};
|
|
|
|
const getDockerSafeName = (valueArg: string, maxLengthArg = 64) => {
|
|
const safeName = valueArg
|
|
.replace(/[^a-zA-Z0-9_.-]+/g, '-')
|
|
.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, '')
|
|
.slice(0, maxLengthArg)
|
|
.replace(/[^a-zA-Z0-9]+$/g, '');
|
|
return safeName || 'resource';
|
|
};
|
|
|
|
const getWorkloadSecretName = (serviceArg: { id: string; data: { name: string } }) => {
|
|
const serviceName = getDockerSafeName(serviceArg.data.name, 36);
|
|
const serviceId = getDockerSafeName(serviceArg.id, 20);
|
|
return getDockerSafeName(`${serviceName}-${serviceId}-secret`);
|
|
};
|
|
|
|
const dockerServiceExists = async (serviceNameArg: string) => {
|
|
try {
|
|
await execFileAsync('docker', ['service', 'inspect', serviceNameArg]);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const removeDockerService = async (serviceNameArg: string) => {
|
|
if (await dockerServiceExists(serviceNameArg)) {
|
|
await execFileAsync('docker', ['service', 'rm', serviceNameArg]).catch(() => null);
|
|
await delayFor(2000);
|
|
}
|
|
};
|
|
|
|
const printDockerServiceLogs = async (serviceNameArg: string) => {
|
|
if (!(await dockerServiceExists(serviceNameArg))) {
|
|
return;
|
|
}
|
|
console.log(`[registry-deploy-on-push] Logs for Docker service ${serviceNameArg}:`);
|
|
await execFileAsync('docker', ['service', 'logs', '--raw', '--tail', '120', serviceNameArg], {
|
|
maxBuffer: 1024 * 1024 * 5,
|
|
})
|
|
.then(({ stdout, stderr }) => {
|
|
if (stdout.trim()) {
|
|
console.log(stdout.trim());
|
|
}
|
|
if (stderr.trim()) {
|
|
console.log(stderr.trim());
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.log(`[registry-deploy-on-push] Could not read ${serviceNameArg} logs: ${(error as Error).message}`);
|
|
});
|
|
};
|
|
|
|
const printDockerServicePs = async (serviceNameArg: string) => {
|
|
if (!(await dockerServiceExists(serviceNameArg))) {
|
|
return;
|
|
}
|
|
console.log(`[registry-deploy-on-push] Tasks for Docker service ${serviceNameArg}:`);
|
|
await execFileAsync('docker', ['service', 'ps', '--no-trunc', serviceNameArg], {
|
|
maxBuffer: 1024 * 1024 * 5,
|
|
})
|
|
.then(({ stdout, stderr }) => {
|
|
if (stdout.trim()) {
|
|
console.log(stdout.trim());
|
|
}
|
|
if (stderr.trim()) {
|
|
console.log(stderr.trim());
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.log(`[registry-deploy-on-push] Could not read ${serviceNameArg} tasks: ${(error as Error).message}`);
|
|
});
|
|
};
|
|
|
|
const printDockerNetworkContainers = async (networkNameArg: string) => {
|
|
console.log(`[registry-deploy-on-push] Containers on Docker network ${networkNameArg}:`);
|
|
await execFileAsync('docker', ['network', 'inspect', networkNameArg, '--format', '{{json .Containers}}'], {
|
|
maxBuffer: 1024 * 1024 * 5,
|
|
})
|
|
.then(({ stdout, stderr }) => {
|
|
if (stdout.trim()) {
|
|
console.log(stdout.trim());
|
|
}
|
|
if (stderr.trim()) {
|
|
console.log(stderr.trim());
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.log(`[registry-deploy-on-push] Could not inspect ${networkNameArg}: ${(error as Error).message}`);
|
|
});
|
|
};
|
|
|
|
const getDockerGwbridgeGatewayIp = async () => {
|
|
const { stdout } = await execFileAsync('docker', [
|
|
'network',
|
|
'inspect',
|
|
'docker_gwbridge',
|
|
'--format',
|
|
'{{(index .IPAM.Config 0).Gateway}}',
|
|
]);
|
|
const gatewayIp = stdout.trim();
|
|
if (!gatewayIp) {
|
|
throw new Error('Could not determine docker_gwbridge gateway IP');
|
|
}
|
|
return gatewayIp;
|
|
};
|
|
|
|
const createSelfSignedCertificate = async (domainNameArg: string) => {
|
|
mkdirSync(buildDir, { recursive: true });
|
|
const keyPath = join(buildDir, 'route.key');
|
|
const certPath = join(buildDir, 'route.crt');
|
|
await run('openssl', [
|
|
'req',
|
|
'-x509',
|
|
'-newkey',
|
|
'rsa:2048',
|
|
'-nodes',
|
|
'-keyout',
|
|
keyPath,
|
|
'-out',
|
|
certPath,
|
|
'-subj',
|
|
`/CN=${domainNameArg}`,
|
|
'-days',
|
|
'1',
|
|
]);
|
|
return {
|
|
privateKey: readFileSync(keyPath, 'utf8'),
|
|
publicKey: readFileSync(certPath, 'utf8'),
|
|
};
|
|
};
|
|
|
|
const waitForWorkloadContainer = async (
|
|
networkArg: Awaited<ReturnType<Coreflow['dockerHost']['getNetworkByName']>>,
|
|
serviceArg: Awaited<ReturnType<Coreflow['dockerHost']['getServiceByName']>>,
|
|
) => {
|
|
if (!networkArg) {
|
|
throw new Error('Missing Docker network while waiting for workload container');
|
|
}
|
|
await waitFor(async () => {
|
|
const containers = await networkArg.getContainersOnNetworkForService(serviceArg);
|
|
return containers.length > 0;
|
|
}, 'workload container on web gateway network');
|
|
};
|
|
|
|
const createCoreflowProxyService = async (corechatNetworkNameArg: string) => {
|
|
if (await dockerServiceExists(coreflowProxyServiceName)) {
|
|
throw new Error(`Docker service ${coreflowProxyServiceName} already exists; refusing to overwrite it`);
|
|
}
|
|
mkdirSync(buildDir, { recursive: true });
|
|
const gatewayIp = await getDockerGwbridgeGatewayIp();
|
|
const caddyfilePath = join(buildDir, 'coreflow-proxy.Caddyfile');
|
|
writeFileSync(caddyfilePath, `:3000 {\n reverse_proxy ${gatewayIp}:3000\n}\n`);
|
|
await run('docker', [
|
|
'service',
|
|
'create',
|
|
'--name',
|
|
coreflowProxyServiceName,
|
|
'--label',
|
|
`serve.zone.testing.id=${smokeId}`,
|
|
'--network',
|
|
corechatNetworkNameArg,
|
|
'--mount',
|
|
`type=bind,source=${caddyfilePath},target=/etc/caddy/Caddyfile,readonly`,
|
|
'caddy:2-alpine',
|
|
]);
|
|
};
|
|
|
|
const createCoretrafficService = async (
|
|
corechatNetworkNameArg: string,
|
|
webGatewayNetworkNameArg: string,
|
|
httpsPortArg: number,
|
|
) => {
|
|
const coretrafficDir = join(repoRoot, 'coretraffic');
|
|
await run('pnpm', ['--dir', coretrafficDir, 'build']);
|
|
await run('docker', [
|
|
'service',
|
|
'create',
|
|
'--name',
|
|
coretrafficServiceName,
|
|
'--label',
|
|
`serve.zone.testing.id=${smokeId}`,
|
|
'--network',
|
|
corechatNetworkNameArg,
|
|
'--network',
|
|
webGatewayNetworkNameArg,
|
|
'--publish',
|
|
`published=${httpsPortArg},target=8000,protocol=tcp`,
|
|
'--mount',
|
|
`type=bind,source=${coretrafficDir},target=/app`,
|
|
'node:22-trixie-slim',
|
|
'sh',
|
|
'-lc',
|
|
'cd /app && node cli.js',
|
|
]);
|
|
};
|
|
|
|
const requestCoretrafficRoute = async (domainNameArg: string, httpsPortArg: number) => {
|
|
const curlArgs = [
|
|
'-k',
|
|
'-sS',
|
|
'--noproxy',
|
|
'*',
|
|
'--max-time',
|
|
'10',
|
|
'--resolve',
|
|
`${domainNameArg}:${httpsPortArg}:127.0.0.1`,
|
|
'-o',
|
|
'-',
|
|
'-w',
|
|
'\n%{http_code}',
|
|
`https://${domainNameArg}:${httpsPortArg}/`,
|
|
];
|
|
const { stdout, stderr } = await execFileAsync('curl', curlArgs);
|
|
const lines = stdout.trim().split('\n');
|
|
const statusCode = lines[lines.length - 1];
|
|
const body = lines.slice(0, -1).join('\n');
|
|
return {
|
|
statusCode,
|
|
body,
|
|
stderr,
|
|
};
|
|
};
|
|
|
|
const waitForCoretrafficRoute = async (
|
|
domainNameArg: string,
|
|
httpsPortArg: number,
|
|
messageArg: string,
|
|
backendServiceNameArg?: string,
|
|
) => {
|
|
let lastResponse: Awaited<ReturnType<typeof requestCoretrafficRoute>> | undefined;
|
|
let lastError: Error | undefined;
|
|
try {
|
|
await waitFor(async () => {
|
|
try {
|
|
lastResponse = await requestCoretrafficRoute(domainNameArg, httpsPortArg);
|
|
lastError = undefined;
|
|
return lastResponse.statusCode === '200' && /Caddy|serve/i.test(lastResponse.body);
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
return false;
|
|
}
|
|
}, messageArg);
|
|
} catch (error) {
|
|
console.log(`[registry-deploy-on-push] Last route response: ${JSON.stringify(lastResponse)}`);
|
|
if (lastError) {
|
|
console.log(`[registry-deploy-on-push] Last route error: ${lastError.message}`);
|
|
}
|
|
if (backendServiceNameArg) {
|
|
await printDockerServicePs(backendServiceNameArg);
|
|
await printDockerServiceLogs(backendServiceNameArg);
|
|
}
|
|
await printDockerServicePs(coretrafficServiceName);
|
|
await printDockerServiceLogs(coretrafficServiceName);
|
|
await printDockerServicePs(coreflowProxyServiceName);
|
|
await printDockerServiceLogs(coreflowProxyServiceName);
|
|
await printDockerNetworkContainers('sznwebgateway');
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const ensureDockerReady = async () => {
|
|
await run('docker', ['version']);
|
|
const { stdout } = await execFileAsync('docker', ['info', '--format', '{{.Swarm.LocalNodeState}}']);
|
|
if (stdout.trim() !== 'active') {
|
|
throw new Error('Docker Swarm must be active. In Vagrant this is handled by scripts/provision-vm.sh.');
|
|
}
|
|
};
|
|
|
|
const buildSmokeImage = async (revisionArg: string) => {
|
|
mkdirSync(buildDir, { recursive: true });
|
|
const imageTag = `${smokeId}:${revisionArg}`;
|
|
writeFileSync(
|
|
join(buildDir, 'Dockerfile'),
|
|
`FROM caddy:2-alpine\nLABEL serve.zone.smoke.id="${smokeId}"\nLABEL serve.zone.smoke.revision="${revisionArg}"\n`,
|
|
);
|
|
await run('docker', ['build', '-t', imageTag, buildDir]);
|
|
return imageTag;
|
|
};
|
|
|
|
const dockerImageRemove = async (imageArg: string) => {
|
|
await execFileAsync('docker', ['image', 'rm', imageArg]).catch(() => null);
|
|
};
|
|
|
|
const dockerLogin = async (registryHostArg: string, usernameArg: string, passwordArg: string) => {
|
|
await new Promise<void>((resolveArg, rejectArg) => {
|
|
const childProcess = execFile('docker', [
|
|
'login',
|
|
registryHostArg,
|
|
'-u',
|
|
usernameArg,
|
|
'--password-stdin',
|
|
]);
|
|
childProcess.stdin?.write(passwordArg);
|
|
childProcess.stdin?.end();
|
|
let output = '';
|
|
childProcess.stdout?.on('data', (dataArg) => {
|
|
output += dataArg.toString();
|
|
});
|
|
childProcess.stderr?.on('data', (dataArg) => {
|
|
output += dataArg.toString();
|
|
});
|
|
childProcess.on('error', rejectArg);
|
|
childProcess.on('exit', (codeArg) => {
|
|
console.log(output.trim());
|
|
if (codeArg === 0) {
|
|
resolveArg();
|
|
} else {
|
|
rejectArg(new Error(`docker login exited with ${codeArg}`));
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const createCloudlyConfig = async (): Promise<ICloudlyConfig> => {
|
|
console.log('[registry-deploy-on-push] Starting isolated MongoDB and S3 helpers');
|
|
const smartmongo = await tapNodeTools.createSmartmongo();
|
|
stopFunctions.push(async () => {
|
|
await smartmongo.stopAndDumpToDir(join(testingDir, '.nogit', 'mongodump', smokeId));
|
|
});
|
|
const smarts3 = await tapNodeTools.createSmarts3();
|
|
stopFunctions.push(async () => {
|
|
await smarts3.stop();
|
|
});
|
|
|
|
const bucketName = `${smokeId}-bucket`;
|
|
await smarts3.createBucket(bucketName);
|
|
const smartnetwork = new SmartNetwork();
|
|
const publicPort = await smartnetwork.findFreePort(30000, 40000, { randomize: true });
|
|
if (!publicPort) {
|
|
throw new Error('Could not find a free Cloudly scenario port');
|
|
}
|
|
|
|
return {
|
|
environment: 'integration',
|
|
letsEncryptEmail: 'test@serve.zone',
|
|
publicUrl: '127.0.0.1',
|
|
publicPort: String(publicPort),
|
|
mongoDescriptor: await smartmongo.getMongoDescriptor(),
|
|
s3Descriptor: await smarts3.getS3Descriptor({
|
|
bucketName,
|
|
}),
|
|
sslMode: 'none',
|
|
servezoneAdminaccount: 'smokeadmin:smokepassword',
|
|
};
|
|
};
|
|
|
|
const main = async () => {
|
|
let testCloudly: Cloudly | undefined;
|
|
let testClient: cloudlyApiClient.CloudlyApiClient | undefined;
|
|
let coreflow: Coreflow | undefined;
|
|
let subscription: { unsubscribe: () => void } | undefined;
|
|
let createdWebGatewayNetwork = false;
|
|
let createdCorechatNetwork = false;
|
|
let createdCoreflowProxyService = false;
|
|
let createdCoretrafficService = false;
|
|
let startedCoreflowInternalServer = false;
|
|
let serviceName = '';
|
|
let registryImageUrl = '';
|
|
let localImageRevision1 = '';
|
|
let localImageRevision2 = '';
|
|
let serviceForCleanup: { id: string; data: { name: string } } | undefined;
|
|
const routeDomain = `${smokeId}.test`;
|
|
|
|
try {
|
|
await ensureDockerReady();
|
|
|
|
const cloudlyConfig = await createCloudlyConfig();
|
|
testCloudly = new Cloudly(cloudlyConfig);
|
|
console.log('[registry-deploy-on-push] Starting Cloudly');
|
|
await testCloudly.start();
|
|
|
|
const machineUser = new testCloudly.authManager.CUser();
|
|
machineUser.id = await testCloudly.authManager.CUser.getNewId();
|
|
machineUser.data = {
|
|
type: 'machine',
|
|
username: 'smoke-admin',
|
|
password: 'smoke-admin-token',
|
|
tokens: [
|
|
{
|
|
token: 'smoke-admin-token',
|
|
expiresAt: Date.now() + 3600 * 1000,
|
|
assignedRoles: ['admin'],
|
|
},
|
|
],
|
|
role: 'admin',
|
|
};
|
|
await machineUser.save();
|
|
|
|
const cloudlyUrl = `http://${cloudlyConfig.publicUrl}:${cloudlyConfig.publicPort}`;
|
|
testClient = new cloudlyApiClient.CloudlyApiClient({
|
|
registerAs: 'api',
|
|
cloudlyUrl,
|
|
});
|
|
await testClient.start();
|
|
await testClient.getIdentityByToken('smoke-admin-token');
|
|
console.log(`[registry-deploy-on-push] Cloudly started at ${cloudlyUrl}`);
|
|
|
|
const cluster = await testClient.cluster.createCluster(`${smokeId} cluster`);
|
|
const persistedCluster = await testCloudly.clusterManager.getConfigBy_ConfigID(cluster.id);
|
|
const clusterUser = await testCloudly.authManager.CUser.getInstance({
|
|
id: persistedCluster.data.userId,
|
|
});
|
|
const clusterToken = clusterUser.data.tokens?.[0]?.token;
|
|
if (!clusterToken) {
|
|
throw new Error('Cluster token was not created');
|
|
}
|
|
|
|
const image = await testClient.image.createImage({
|
|
name: `${smokeId} image`,
|
|
description: 'End-to-end registry/Coreflow smoke image',
|
|
});
|
|
const secretBundle = await testClient.secretbundle.createSecretBundle({
|
|
name: `${smokeId} secrets`,
|
|
description: 'End-to-end registry/Coreflow smoke secrets',
|
|
type: 'service',
|
|
includedSecretGroupIds: [],
|
|
includedTags: [],
|
|
imageClaims: [],
|
|
authorizations: [
|
|
{
|
|
environment: 'production',
|
|
secretAccessKey: `${smokeId}-secret-access`,
|
|
},
|
|
],
|
|
});
|
|
|
|
serviceName = smokeId;
|
|
const service = await testClient.services.createService({
|
|
name: serviceName,
|
|
description: 'End-to-end registry/Coreflow smoke service',
|
|
imageId: image.id,
|
|
imageVersion: 'latest',
|
|
environment: {},
|
|
secretBundleId: secretBundle.id,
|
|
serviceCategory: 'workload',
|
|
deploymentStrategy: 'custom',
|
|
scaleFactor: 1,
|
|
balancingStrategy: 'round-robin',
|
|
ports: {
|
|
web: 80,
|
|
},
|
|
domains: [
|
|
{
|
|
name: routeDomain,
|
|
port: 80,
|
|
protocol: 'https',
|
|
},
|
|
],
|
|
deploymentIds: [],
|
|
});
|
|
serviceForCleanup = service;
|
|
|
|
const registryTarget = await testClient.services.getRegistryTarget(service.id, 'latest');
|
|
registryImageUrl = registryTarget.imageUrl;
|
|
console.log(`[registry-deploy-on-push] Registry target: ${registryImageUrl}`);
|
|
|
|
process.env.CLOUDLY_URL = cloudlyUrl;
|
|
process.env.JUMPCODE = clusterToken;
|
|
coreflow = new Coreflow();
|
|
await coreflow.dockerHost.start();
|
|
await coreflow.internalServer.start();
|
|
startedCoreflowInternalServer = true;
|
|
coreflow.cloudlyConnector.getCertificateForDomainFromCloudly = (async () => {
|
|
return await createSelfSignedCertificate(routeDomain);
|
|
}) as any;
|
|
|
|
let webGatewayNetwork = await coreflow.dockerHost.getNetworkByName(
|
|
coreflow.clusterManager.commonDockerData.networkNames.sznWebgateway,
|
|
);
|
|
if (!webGatewayNetwork) {
|
|
webGatewayNetwork = await coreflow.dockerHost.createNetwork({
|
|
Name: coreflow.clusterManager.commonDockerData.networkNames.sznWebgateway,
|
|
});
|
|
createdWebGatewayNetwork = true;
|
|
}
|
|
let corechatNetwork = await coreflow.dockerHost.getNetworkByName(
|
|
coreflow.clusterManager.commonDockerData.networkNames.sznCorechat,
|
|
);
|
|
if (!corechatNetwork) {
|
|
corechatNetwork = await coreflow.dockerHost.createNetwork({
|
|
Name: coreflow.clusterManager.commonDockerData.networkNames.sznCorechat,
|
|
});
|
|
createdCorechatNetwork = true;
|
|
}
|
|
|
|
const smartnetwork = new SmartNetwork();
|
|
const coretrafficHttpsPort = await smartnetwork.findFreePort(41000, 43000, { randomize: true });
|
|
if (!coretrafficHttpsPort) {
|
|
throw new Error('Could not find a free Coretraffic HTTPS test port');
|
|
}
|
|
|
|
await createCoreflowProxyService(coreflow.clusterManager.commonDockerData.networkNames.sznCorechat);
|
|
createdCoreflowProxyService = true;
|
|
await createCoretrafficService(
|
|
coreflow.clusterManager.commonDockerData.networkNames.sznCorechat,
|
|
coreflow.clusterManager.commonDockerData.networkNames.sznWebgateway,
|
|
coretrafficHttpsPort,
|
|
);
|
|
createdCoretrafficService = true;
|
|
await coreflow.cloudlyConnector.start();
|
|
console.log('[registry-deploy-on-push] Coreflow connector authenticated and tagged');
|
|
try {
|
|
await waitFor(async () => {
|
|
try {
|
|
await coreflow!.corechatConnector.setReverseConfigs([]);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}, 'coretraffic connection to coreflow');
|
|
} catch (error) {
|
|
await printDockerServiceLogs(coretrafficServiceName);
|
|
await printDockerServiceLogs(coreflowProxyServiceName);
|
|
throw error;
|
|
}
|
|
|
|
const configUpdates: Array<Record<string, any>> = [];
|
|
subscription = coreflow.cloudlyConnector.cloudlyApiClient.configUpdateSubject.subscribe((updateArg) => {
|
|
if (updateArg.services?.some((serviceArg: any) => serviceArg.id === service.id)) {
|
|
configUpdates.push(updateArg);
|
|
}
|
|
});
|
|
|
|
await dockerLogin(registryTarget.registryHost, 'smoke-admin', 'smoke-admin-token');
|
|
localImageRevision1 = await buildSmokeImage('revision1');
|
|
await run('docker', ['tag', localImageRevision1, registryImageUrl]);
|
|
await run('docker', ['push', registryImageUrl]);
|
|
await waitFor(() => configUpdates.length >= 1, 'first registry config update');
|
|
console.log('[registry-deploy-on-push] First docker push produced a Cloudly config update');
|
|
|
|
let refreshedService = await testClient.services.getServiceById(service.id);
|
|
await coreflow.clusterManager.provisionWorkloadService(refreshedService as any);
|
|
let dockerService = await coreflow.dockerHost.getServiceByName(serviceName);
|
|
await waitForWorkloadContainer(webGatewayNetwork, dockerService);
|
|
await coreflow.clusterManager.updateTrafficRouting(persistedCluster as any);
|
|
await waitForCoretrafficRoute(
|
|
routeDomain,
|
|
coretrafficHttpsPort,
|
|
'coretraffic HTTPS route to first deployment',
|
|
serviceName,
|
|
);
|
|
console.log(`[registry-deploy-on-push] Coretraffic routed ${routeDomain} to first deployment`);
|
|
const firstDockerServiceId = dockerService.ID;
|
|
const firstDigest = dockerService.Spec.Labels['serve.zone.registryDigest'];
|
|
if (!firstDigest) {
|
|
throw new Error('First deployment did not record a registry digest label');
|
|
}
|
|
console.log(`[registry-deploy-on-push] First deployment: ${firstDockerServiceId} digest=${firstDigest}`);
|
|
|
|
localImageRevision2 = await buildSmokeImage('revision2');
|
|
await run('docker', ['tag', localImageRevision2, registryImageUrl]);
|
|
await run('docker', ['push', registryImageUrl]);
|
|
await waitFor(() => configUpdates.length >= 2, 'second registry config update');
|
|
console.log('[registry-deploy-on-push] Second docker push produced a Cloudly config update');
|
|
|
|
refreshedService = await testClient.services.getServiceById(service.id);
|
|
await coreflow.clusterManager.provisionWorkloadService(refreshedService as any);
|
|
dockerService = await coreflow.dockerHost.getServiceByName(serviceName);
|
|
await waitForWorkloadContainer(webGatewayNetwork, dockerService);
|
|
await coreflow.clusterManager.updateTrafficRouting(persistedCluster as any);
|
|
await waitForCoretrafficRoute(
|
|
routeDomain,
|
|
coretrafficHttpsPort,
|
|
'coretraffic HTTPS route to redeployment',
|
|
serviceName,
|
|
);
|
|
const secondDockerServiceId = dockerService.ID;
|
|
const secondDigest = dockerService.Spec.Labels['serve.zone.registryDigest'];
|
|
if (firstDockerServiceId === secondDockerServiceId) {
|
|
throw new Error('Docker service ID did not change after same-tag digest update');
|
|
}
|
|
if (firstDigest === secondDigest) {
|
|
throw new Error('Registry digest label did not change after second push');
|
|
}
|
|
console.log(`[registry-deploy-on-push] Redeployment: ${secondDockerServiceId} digest=${secondDigest}`);
|
|
console.log(`[registry-deploy-on-push] Coretraffic routed ${routeDomain} to redeployment`);
|
|
console.log('[registry-deploy-on-push] PASS');
|
|
} finally {
|
|
subscription?.unsubscribe();
|
|
if (coreflow) {
|
|
if (createdCoretrafficService) {
|
|
await removeDockerService(coretrafficServiceName);
|
|
}
|
|
if (createdCoreflowProxyService) {
|
|
await removeDockerService(coreflowProxyServiceName);
|
|
}
|
|
if (serviceName) {
|
|
const dockerService = await coreflow.dockerHost.getServiceByName(serviceName).catch(() => null);
|
|
if (dockerService) {
|
|
await dockerService.remove();
|
|
await delayFor(5000);
|
|
}
|
|
}
|
|
if (serviceForCleanup) {
|
|
const dockerSecret = await coreflow.dockerHost
|
|
.getSecretByName(getWorkloadSecretName(serviceForCleanup))
|
|
.catch(() => null);
|
|
if (dockerSecret) {
|
|
await dockerSecret.remove();
|
|
}
|
|
}
|
|
if (createdWebGatewayNetwork) {
|
|
const webGatewayNetwork = await coreflow.dockerHost
|
|
.getNetworkByName(coreflow.clusterManager.commonDockerData.networkNames.sznWebgateway)
|
|
.catch(() => null);
|
|
if (webGatewayNetwork) {
|
|
await webGatewayNetwork.remove();
|
|
}
|
|
}
|
|
if (createdCorechatNetwork) {
|
|
const corechatNetwork = await coreflow.dockerHost
|
|
.getNetworkByName(coreflow.clusterManager.commonDockerData.networkNames.sznCorechat)
|
|
.catch(() => null);
|
|
if (corechatNetwork) {
|
|
await corechatNetwork.remove();
|
|
}
|
|
}
|
|
await coreflow.cloudlyConnector.stop().catch(() => null);
|
|
if (startedCoreflowInternalServer) {
|
|
await coreflow.internalServer.stop().catch(() => null);
|
|
}
|
|
await coreflow.dockerHost.stop().catch(() => null);
|
|
}
|
|
if (testClient) {
|
|
await testClient.stop().catch(() => null);
|
|
}
|
|
if (testCloudly) {
|
|
await testCloudly.stop().catch(() => null);
|
|
}
|
|
await Promise.all(stopFunctions.map((stopFunction) => stopFunction().catch(() => null)));
|
|
if (registryImageUrl) {
|
|
await dockerImageRemove(registryImageUrl);
|
|
}
|
|
if (localImageRevision1) {
|
|
await dockerImageRemove(localImageRevision1);
|
|
}
|
|
if (localImageRevision2) {
|
|
await dockerImageRemove(localImageRevision2);
|
|
}
|
|
rmSync(buildDir, { force: true, recursive: true });
|
|
}
|
|
};
|
|
|
|
await main();
|