Files
skipper/scripts/skipper.js
2026-04-05 15:28:04 +02:00

170 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
const { randomUUID } = require('crypto');
const { registerNode, randomTokenHex } = require('./register-node');
function getArgs() {
return process.argv.slice(2);
}
function getFlagValue(args, flag, fallback) {
const index = args.indexOf(flag);
if (index === -1 || index + 1 >= args.length) {
return fallback;
}
return args[index + 1];
}
function hasFlag(args, flag) {
return args.includes(flag);
}
function jsonOut(value) {
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
}
async function fetchEnvelope(url, options) {
const response = await fetch(url, options);
const payload = await response.json();
if (!response.ok || payload.error) {
const error = new Error(payload && payload.error ? payload.error.message : `Request failed: ${response.status}`);
error.payload = payload;
throw error;
}
return payload;
}
function buildContextHeaders() {
return {
'x-request-id': randomUUID(),
'x-correlation-id': randomUUID(),
};
}
function usage() {
process.stdout.write([
'Skipper CLI',
'',
'Commands:',
' skipper health --url <base_url>',
' skipper deploy apply --url <base_url> --admin-token <token> --tenant-id <tenant_id> [--idempotency-key <key>]',
' skipper node register --node-id <id> [--role <role>] [--region <region>] [--token <token>]',
' skipper node show --url <base_url> --admin-token <token> --node-id <id>',
'',
].join('\n'));
}
async function cmdHealth(args) {
const baseUrl = getFlagValue(args, '--url', process.env.SKIPPER_URL || 'http://127.0.0.1:3000');
const payload = await fetchEnvelope(`${baseUrl.replace(/\/$/, '')}/v1/health`, {
headers: buildContextHeaders(),
});
jsonOut(payload);
}
async function cmdDeployApply(args) {
const baseUrl = getFlagValue(args, '--url', process.env.SKIPPER_URL || 'http://127.0.0.1:3000');
const adminToken = getFlagValue(args, '--admin-token', process.env.ADMIN_TOKEN);
const tenantId = getFlagValue(args, '--tenant-id', process.env.TENANT_ID);
const idempotencyKey = getFlagValue(args, '--idempotency-key', process.env.IDEMPOTENCY_KEY || randomUUID());
if (!adminToken) {
throw new Error('Missing admin token. Use --admin-token or ADMIN_TOKEN.');
}
if (!tenantId) {
throw new Error('Missing tenant id. Use --tenant-id or TENANT_ID.');
}
const payload = await fetchEnvelope(`${baseUrl.replace(/\/$/, '')}/v1/deployments/${tenantId}/apply`, {
method: 'POST',
headers: {
...buildContextHeaders(),
'x-admin-token': adminToken,
'x-idempotency-key': idempotencyKey,
},
});
jsonOut(payload);
}
async function cmdNodeRegister(args) {
const nodeId = getFlagValue(args, '--node-id', process.env.NODE_ID);
const role = getFlagValue(args, '--role', process.env.NODE_ROLE || 'worker');
const region = getFlagValue(args, '--region', process.env.NODE_REGION || 'default');
const token = getFlagValue(args, '--token', process.env.NODE_TOKEN || randomTokenHex(32));
if (!nodeId) {
throw new Error('Missing node id. Use --node-id or NODE_ID.');
}
jsonOut(await registerNode({ nodeId, role, region, token }));
}
async function cmdNodeShow(args) {
const baseUrl = getFlagValue(args, '--url', process.env.SKIPPER_URL || 'http://127.0.0.1:3000');
const adminToken = getFlagValue(args, '--admin-token', process.env.ADMIN_TOKEN);
const nodeId = getFlagValue(args, '--node-id', process.env.NODE_ID);
if (!adminToken) {
throw new Error('Missing admin token. Use --admin-token or ADMIN_TOKEN.');
}
if (!nodeId) {
throw new Error('Missing node id. Use --node-id or NODE_ID.');
}
const payload = await fetchEnvelope(`${baseUrl.replace(/\/$/, '')}/v1/resources/node/${nodeId}`, {
headers: {
...buildContextHeaders(),
'x-admin-token': adminToken,
},
});
jsonOut(payload);
}
async function main() {
const args = getArgs();
if (args.length === 0 || hasFlag(args, '--help') || hasFlag(args, '-h')) {
usage();
return;
}
const [group, command, subcommand] = args;
if (group === 'health') {
await cmdHealth(args.slice(1));
return;
}
if (group === 'deploy' && command === 'apply') {
await cmdDeployApply(args.slice(2));
return;
}
if (group === 'node' && command === 'register') {
await cmdNodeRegister(args.slice(2));
return;
}
if (group === 'node' && command === 'show') {
await cmdNodeShow(args.slice(2));
return;
}
throw new Error(`Unknown command: ${args.join(' ')}`);
}
main().catch((error) => {
process.stderr.write(`${error.message}\n`);
if (error.payload) {
process.stderr.write(`${JSON.stringify(error.payload, null, 2)}\n`);
}
process.exit(1);
});