397 lines
11 KiB
JavaScript
397 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const assert = require("assert");
|
|
const cp = require("child_process");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
|
|
const repo = path.resolve(__dirname, "..");
|
|
const releaseRoot = path.resolve(
|
|
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
|
|
path.join(repo, "target/public-release-dryrun")
|
|
);
|
|
const acceptanceRoot = path.join(repo, "target/acceptance");
|
|
const manifestPath = path.join(releaseRoot, "public-release-manifest.json");
|
|
const reportPath = path.join(acceptanceRoot, "cli-happy-path-live.json");
|
|
const serviceEndpoint = "https://disasmer.michelpaulissen.com:9443";
|
|
const serviceAddr =
|
|
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR ||
|
|
"disasmer.michelpaulissen.com:9443";
|
|
const oidcIssuer = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL;
|
|
const oidcCode = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE;
|
|
const enabled = process.env.DISASMER_CLI_HAPPY_PATH_LIVE === "1";
|
|
const commands = [];
|
|
|
|
function requireEnabled() {
|
|
if (!enabled) {
|
|
throw new Error(
|
|
"DISASMER_CLI_HAPPY_PATH_LIVE=1 is required because this runs the live CLI happy path"
|
|
);
|
|
}
|
|
if (!oidcIssuer || !oidcCode) {
|
|
throw new Error(
|
|
"DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL and DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE are required"
|
|
);
|
|
}
|
|
}
|
|
|
|
function readJson(file) {
|
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
}
|
|
|
|
function ensureDir(dir) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
function nonInteractiveGitEnv(extra = {}) {
|
|
return {
|
|
...process.env,
|
|
GIT_TERMINAL_PROMPT: "0",
|
|
GIT_ASKPASS: process.env.GIT_ASKPASS || "/bin/false",
|
|
SSH_ASKPASS: process.env.SSH_ASKPASS || "/bin/false",
|
|
GIT_SSH_COMMAND:
|
|
process.env.GIT_SSH_COMMAND ||
|
|
"ssh -o BatchMode=yes -o NumberOfPasswordPrompts=0",
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function commandEnv(command, env) {
|
|
if (command !== "git") return env;
|
|
return nonInteractiveGitEnv(env);
|
|
}
|
|
|
|
function run(command, args, options = {}) {
|
|
commands.push([command, ...args].join(" "));
|
|
return cp.execFileSync(command, args, {
|
|
cwd: repo,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
timeout: 15 * 60 * 1000,
|
|
...options,
|
|
env: commandEnv(command, options.env),
|
|
});
|
|
}
|
|
|
|
function runJson(command, args, options = {}) {
|
|
const output = run(command, args, options);
|
|
const line = output
|
|
.trim()
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)
|
|
.at(-1);
|
|
return JSON.parse(line);
|
|
}
|
|
|
|
function executable(root, name) {
|
|
return path.join(root, "bin", process.platform === "win32" ? `${name}.exe` : name);
|
|
}
|
|
|
|
function binaryArchive(manifest) {
|
|
const platform = `${os.platform()}-${os.arch()}`;
|
|
const asset = manifest.assets.find(
|
|
(candidate) =>
|
|
candidate.name.startsWith("disasmer-public-binaries-") &&
|
|
candidate.name.includes(platform)
|
|
);
|
|
assert(asset, `missing released binary archive for ${platform}`);
|
|
assert(fs.existsSync(asset.file), `missing released binary archive ${asset.file}`);
|
|
return asset.file;
|
|
}
|
|
|
|
function publicRepoUrl(manifest) {
|
|
const url =
|
|
process.env.DISASMER_PUBLIC_REPO_URL ||
|
|
process.env.DISASMER_PUBLIC_REPO_REMOTE ||
|
|
manifest.public_repo_url ||
|
|
manifest.public_repo_remote;
|
|
assert(url, "public repository URL is required");
|
|
assert(url.includes("git.michelpaulissen.com"));
|
|
return url;
|
|
}
|
|
|
|
function waitForJsonLine(child, label) {
|
|
return new Promise((resolve, reject) => {
|
|
let stdout = "";
|
|
let stderr = "";
|
|
const timer = setTimeout(() => {
|
|
reject(new Error(`${label} did not produce a JSON line\n${stderr}`));
|
|
}, 120000);
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk.toString();
|
|
for (const line of stdout.split(/\r?\n/)) {
|
|
if (!line.trim()) continue;
|
|
try {
|
|
const parsed = JSON.parse(line);
|
|
clearTimeout(timer);
|
|
resolve(parsed);
|
|
return;
|
|
} catch (_) {
|
|
// keep waiting
|
|
}
|
|
}
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
child.once("error", (error) => {
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
});
|
|
child.once("exit", (code) => {
|
|
if (code !== 0) {
|
|
clearTimeout(timer);
|
|
reject(new Error(`${label} exited with ${code}\n${stderr}`));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function stopChild(child) {
|
|
if (!child || child.exitCode !== null) return;
|
|
child.kill("SIGTERM");
|
|
await new Promise((resolve) => child.once("exit", resolve));
|
|
}
|
|
|
|
async function main() {
|
|
requireEnabled();
|
|
const manifest = readJson(manifestPath);
|
|
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
|
|
assert.strictEqual(manifest.default_operator_endpoint, serviceEndpoint);
|
|
assert.strictEqual(serviceAddr, "disasmer.michelpaulissen.com:9443");
|
|
|
|
const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-cli-happy-"));
|
|
const installDir = path.join(workRoot, "install");
|
|
const checkout = path.join(workRoot, "public-repo");
|
|
ensureDir(installDir);
|
|
run("tar", ["-xzf", binaryArchive(manifest), "-C", installDir]);
|
|
run("git", ["clone", "--depth", "1", publicRepoUrl(manifest), checkout]);
|
|
|
|
const disasmer = executable(installDir, "disasmer");
|
|
const disasmerNode = executable(installDir, "disasmer-node");
|
|
const projectDir = path.join(checkout, "examples/launch-build-demo");
|
|
const suffix = String(Date.now());
|
|
const tenant = `cli-${suffix}`;
|
|
const project = `project-${suffix}`;
|
|
const user = "user";
|
|
const cliNode = `cli-node-${suffix}`;
|
|
const workerNode = `worker-${suffix}`;
|
|
const scope = [
|
|
"--coordinator",
|
|
serviceEndpoint,
|
|
"--tenant",
|
|
tenant,
|
|
"--project-id",
|
|
project,
|
|
"--user",
|
|
user,
|
|
"--json",
|
|
];
|
|
|
|
const login = runJson(
|
|
disasmer,
|
|
[
|
|
"login",
|
|
"--browser",
|
|
"--json",
|
|
"--complete-browser-code",
|
|
oidcCode,
|
|
"--oidc-issuer-url",
|
|
oidcIssuer,
|
|
"--tenant",
|
|
tenant,
|
|
"--project-id",
|
|
project,
|
|
"--user",
|
|
user,
|
|
],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert.strictEqual(login.plan.coordinator, serviceEndpoint);
|
|
assert.strictEqual(login.boundary.scoped_cli_session_received, true);
|
|
|
|
const projectInit = runJson(
|
|
disasmer,
|
|
[
|
|
"project",
|
|
"init",
|
|
...scope,
|
|
"--new-project",
|
|
project,
|
|
"--name",
|
|
"CLI Happy Path",
|
|
"--yes",
|
|
],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert.strictEqual(projectInit.command, "project init");
|
|
assert.strictEqual(projectInit.project_config_written, true);
|
|
assert.strictEqual(projectInit.coordinator_response.type, "project");
|
|
|
|
const projectList = runJson(disasmer, ["project", "list", ...scope], {
|
|
cwd: projectDir,
|
|
});
|
|
assert(projectList.project_count >= 1);
|
|
const projectSelect = runJson(
|
|
disasmer,
|
|
["project", "select", ...scope, project],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert.strictEqual(projectSelect.command, "project select");
|
|
|
|
const inspection = runJson(disasmer, ["inspect", "--project", ".", "--json"], {
|
|
cwd: projectDir,
|
|
});
|
|
assert(
|
|
inspection.discovered_environments.some((environment) => environment.name === "linux")
|
|
);
|
|
assert(inspection.source_providers);
|
|
|
|
const cliGrant = runJson(
|
|
disasmer,
|
|
["node", "enroll", ...scope, "--grant", `cli-grant-${suffix}`],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert.strictEqual(cliGrant.command, "node enroll");
|
|
const attach = runJson(
|
|
disasmer,
|
|
[
|
|
"node",
|
|
"attach",
|
|
"--coordinator",
|
|
serviceEndpoint,
|
|
"--tenant",
|
|
tenant,
|
|
"--project-id",
|
|
project,
|
|
"--node",
|
|
cliNode,
|
|
"--public-key",
|
|
`${cliNode}-public-key`,
|
|
"--enrollment-grant",
|
|
cliGrant.enrollment_grant.grant,
|
|
"--json",
|
|
],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert.strictEqual(attach.command, "node attach");
|
|
assert.strictEqual(attach.boundary.used_enrollment_exchange, true);
|
|
|
|
const workerGrant = runJson(
|
|
disasmer,
|
|
["node", "enroll", ...scope, "--grant", `worker-grant-${suffix}`],
|
|
{ cwd: projectDir }
|
|
);
|
|
const workerArgs = [
|
|
"--coordinator",
|
|
serviceEndpoint,
|
|
"--tenant",
|
|
tenant,
|
|
"--project-id",
|
|
project,
|
|
"--node",
|
|
workerNode,
|
|
"--public-key",
|
|
`${workerNode}-public-key`,
|
|
"--enrollment-grant",
|
|
workerGrant.enrollment_grant.grant,
|
|
"--worker",
|
|
"--assignment-poll-ms",
|
|
"500",
|
|
"--emit-ready",
|
|
];
|
|
commands.push([disasmerNode, ...workerArgs].join(" "));
|
|
const worker = cp.spawn(disasmerNode, workerArgs, { cwd: projectDir });
|
|
try {
|
|
const ready = await waitForJsonLine(worker, "CLI happy-path worker ready");
|
|
assert.strictEqual(ready.node_status, "ready");
|
|
|
|
const runReport = runJson(disasmer, ["run", "build", "--project", ".", "--json"], {
|
|
cwd: projectDir,
|
|
});
|
|
assert.strictEqual(runReport.command, "run");
|
|
assert.strictEqual(runReport.status, "task_launched");
|
|
assert.strictEqual(runReport.task_launch.type, "task_launched");
|
|
assert.strictEqual(runReport.task_launch.placement.node, workerNode);
|
|
|
|
const completion = await waitForJsonLine(worker, "CLI happy-path worker completion");
|
|
assert.strictEqual(completion.node_status, "completed");
|
|
|
|
const processStatus = runJson(
|
|
disasmer,
|
|
["process", "status", ...scope, "--process", "vp-current"],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert.strictEqual(processStatus.command, "process status");
|
|
assert(processStatus.current_task_count >= 1);
|
|
|
|
const logs = runJson(disasmer, ["logs", ...scope, "--process", "vp-current"], {
|
|
cwd: projectDir,
|
|
});
|
|
assert(logs.log_entries.length >= 1);
|
|
|
|
const artifacts = runJson(
|
|
disasmer,
|
|
["artifact", "list", ...scope, "--process", "vp-current"],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert(artifacts.artifacts.length >= 1);
|
|
const artifact = artifacts.artifacts[0].artifact;
|
|
|
|
const download = runJson(disasmer, ["artifact", "download", ...scope, artifact], {
|
|
cwd: projectDir,
|
|
});
|
|
assert.strictEqual(download.download_session.link_issued, true);
|
|
|
|
const restart = runJson(
|
|
disasmer,
|
|
["process", "restart", ...scope, "--process", "vp-current", "--yes"],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert.strictEqual(restart.restart_request.accepted, true);
|
|
|
|
const cancel = runJson(
|
|
disasmer,
|
|
["process", "cancel", ...scope, "--process", "vp-current", "--yes"],
|
|
{ cwd: projectDir }
|
|
);
|
|
assert.strictEqual(cancel.cancel_request.accepted, true);
|
|
|
|
const report = {
|
|
kind: "disasmer-cli-happy-path-live",
|
|
release_name: manifest.release_name,
|
|
source_commit: manifest.source_commit,
|
|
service_addr: serviceAddr,
|
|
default_operator_endpoint: serviceEndpoint,
|
|
public_repository_url: publicRepoUrl(manifest),
|
|
tenant,
|
|
project,
|
|
cli_node: cliNode,
|
|
worker_node: workerNode,
|
|
signed_in: true,
|
|
cli_installed_and_login_ran: true,
|
|
project_initialized_or_selected: true,
|
|
inspect_discovered_linux: true,
|
|
node_attached_with_cli: true,
|
|
run_launched_task: true,
|
|
work_ran_on_attached_node: true,
|
|
status_and_logs_seen: true,
|
|
artifact_download_requested: true,
|
|
process_restart_requested: true,
|
|
process_cancel_requested: true,
|
|
commands,
|
|
acceptance_result: "passed",
|
|
};
|
|
ensureDir(path.dirname(reportPath));
|
|
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
console.log(`CLI happy-path live smoke passed: ${reportPath}`);
|
|
} finally {
|
|
await stopChild(worker);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error.message);
|
|
process.exit(1);
|
|
});
|