Files
qinglong/scripts/ql3-plugin-package-kubernetes-live-actor.cjs
T

655 lines
20 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const k8s = require('@kubernetes/client-node');
const {
PluginPackageActivationConflictError,
PluginPackageActivationUnavailableError,
PLUGIN_PACKAGE_ACTIVATION_INTENT_SCHEMA,
} = require('@qinglong/runtime-core/plugin-package-activation');
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const {
createPluginPackageSecretBindingTarget,
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
const {
createPluginPackageSecretBindingTransitionPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan');
const {
createPluginPackageSecretBindingFromTransitionPlan,
createPluginPackageSecretBindingTransitionReceipt,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt');
const {
secretProjectionFileName,
} = require('@qinglong/runtime-core/secret-projection');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const {
PluginPackageKubernetesActivationPublisher,
pluginPackageKubernetesProjectedSecretWorkloadVolume,
} = require('@qinglong/cluster-admin/plugin-package-kubernetes-activation');
const TOKEN_FILE = '/var/run/secrets/kubernetes.io/serviceaccount/token';
const RESULT_SCHEMA = 'qinglong/plugin-package-kubernetes-live-actor-result@v1';
const NAMESPACE = process.env.QL3_LIVE_NAMESPACE;
const ACTOR = process.env.QL3_LIVE_ACTOR;
const PEER = ACTOR === 'a' ? 'b' : ACTOR === 'b' ? 'a' : null;
const INITIAL_LOCK_DIGEST = 'a'.repeat(64);
const CANDIDATES = Object.freeze({
a: Object.freeze({
installationId: 'install-live-a',
lockDigest: '1'.repeat(64),
stageReceiptDigest: '2'.repeat(64),
stageEvidenceDigest: '3'.repeat(64),
contentDigest: '4'.repeat(64),
intentDigest: '5'.repeat(64),
}),
b: Object.freeze({
installationId: 'install-live-b',
lockDigest: '6'.repeat(64),
stageReceiptDigest: '7'.repeat(64),
stageEvidenceDigest: '8'.repeat(64),
contentDigest: '9'.repeat(64),
intentDigest: '0'.repeat(64),
}),
c: Object.freeze({
installationId: 'install-live-revoke',
lockDigest: 'f'.repeat(64),
stageReceiptDigest: 'a'.repeat(64),
stageEvidenceDigest: 'b'.repeat(64),
contentDigest: 'c'.repeat(64),
intentDigest: 'd'.repeat(64),
}),
});
function fail(message) {
throw new Error(message);
}
function apiStatus(error) {
if (!error || typeof error !== 'object') return null;
if ('code' in error && typeof error.code === 'number') return error.code;
if (
'response' in error &&
error.response &&
typeof error.response === 'object' &&
'statusCode' in error.response &&
typeof error.response.statusCode === 'number'
) {
return error.response.statusCode;
}
return null;
}
function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function waitFor(description, inspect) {
const deadline = Date.now() + 60_000;
let lastFact = 'not observed';
while (Date.now() < deadline) {
try {
const result = await inspect();
if (result?.ready) return result.value;
if (result?.fact) lastFact = result.fact;
} catch (error) {
lastFact = error instanceof Error ? error.message : String(error);
}
await sleep(100);
}
fail(`${description} timed out: ${lastFact}`);
}
function activationIntent(overrides = {}) {
const lockDigest = overrides.lockDigest ?? INITIAL_LOCK_DIGEST;
const installationId = overrides.installationId ?? 'install-live-initial';
const targetGeneration = overrides.targetGeneration ?? 1;
const previousActiveLockDigest = overrides.previousActiveLockDigest ?? null;
const contentDigest = overrides.contentDigest ?? 'd'.repeat(64);
return Object.freeze({
schema: PLUGIN_PACKAGE_ACTIVATION_INTENT_SCHEMA,
installationId,
projectId: 'default',
packageName: 'live-cas-package',
lockDigest,
targetGeneration,
previousActiveLockDigest,
stageRef: `cluster-stage:${lockDigest}`,
stageReceiptDigest: overrides.stageReceiptDigest ?? 'b'.repeat(64),
stageEvidenceDigest: overrides.stageEvidenceDigest ?? 'c'.repeat(64),
contentDigest,
resourceGeneration: createPluginPackageResourceGeneration({
installationId,
projectId: 'default',
packageName: 'live-cas-package',
lockDigest,
generation: targetGeneration,
previousActiveLockDigest,
contentDigest,
contents: {
tasks: ['tasks/live.yaml'],
workflows: [],
prompts: [],
tools: [],
},
}),
intentDigest: overrides.intentDigest ?? 'e'.repeat(64),
});
}
function manifest(version, secrets) {
return Object.freeze({
apiVersion: 'qinglong.io/v1alpha1',
kind: 'Package',
metadata: Object.freeze({
name: 'live-cas-package',
displayName: 'Live CAS Package',
version,
description: 'Real Kubernetes Secret projection CAS gate',
license: 'Apache-2.0',
}),
spec: Object.freeze({
compatibility: Object.freeze({
qinglong: '>=3.0.0-0 <4.0.0',
architectures: Object.freeze(['arm64']),
deploymentProfiles: Object.freeze(['cluster-control']),
}),
runtimes: Object.freeze([]),
resources: Object.freeze({
memory: Object.freeze({ recommended: '16Mi' }),
disk: Object.freeze({ install: '4Mi', working: '16Mi' }),
}),
permissions: Object.freeze({
network: Object.freeze({ allowedHosts: Object.freeze([]) }),
secrets: Object.freeze(secrets),
tools: Object.freeze(secrets.length === 0 ? [] : ['secret.use']),
}),
contents: Object.freeze({
tasks: Object.freeze(['tasks/live.yaml']),
workflows: Object.freeze([]),
prompts: Object.freeze([]),
tools: Object.freeze([]),
}),
}),
});
}
function transitionEvidence(initial, candidate, actor = ACTOR) {
const secretRef = createSecretRef({
projectId: 'default',
name: `live-token-${actor}`,
version: 2,
});
const previousManifest = manifest('1.0.0', []);
const nextManifest = manifest('2.0.0', [
Object.freeze({ name: 'TOKEN', required: true }),
]);
const plan = createPluginPackageSecretBindingTransitionPlan({
previousTarget: createPluginPackageSecretBindingTarget(
initial.resourceGeneration,
previousManifest,
),
previousBinding: null,
previousAttemptGeneration: 1,
nextGeneration: candidate.resourceGeneration,
nextManifest,
assignments: [Object.freeze({ name: 'TOKEN', secretRef })],
plannedAtMs: 100,
});
const binding = createPluginPackageSecretBindingFromTransitionPlan(
plan,
'approved-action-execution',
candidate.stageEvidenceDigest,
200,
);
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: Object.freeze({
kind: 'approved-action-execution',
evidenceDigest: candidate.stageEvidenceDigest,
}),
binding,
committedAtMs: 200,
});
return Object.freeze({ secretRef, binding, receipt });
}
function revokeEvidence(activeCandidate, activeTransition, candidate) {
const plan = createPluginPackageSecretBindingTransitionPlan({
previousTarget: createPluginPackageSecretBindingTarget(
activeCandidate.resourceGeneration,
manifest('2.0.0', [
Object.freeze({ name: 'TOKEN', required: true }),
]),
),
previousBinding: activeTransition.binding,
previousAttemptGeneration: 2,
nextGeneration: candidate.resourceGeneration,
nextManifest: manifest('3.0.0', []),
assignments: [],
plannedAtMs: 300,
});
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: Object.freeze({
kind: 'approved-action-execution',
evidenceDigest: candidate.stageEvidenceDigest,
}),
binding: null,
committedAtMs: 400,
});
return Object.freeze({ binding: null, receipt });
}
function activeTargetName() {
return (
'ql3p-' +
require('node:crypto')
.createHash('sha256')
.update(
Buffer.from('qinglong/plugin-package-kubernetes-target@v1\0', 'utf8'),
)
.update(
require('node:crypto')
.createHash('sha256')
.update('qinglong/plugin-package-kubernetes-cluster@v1\0', 'utf8')
.update('ql3-plugin-package-live-cluster', 'utf8')
.digest('hex'),
'utf8',
)
.update('\0', 'utf8')
.update(NAMESPACE, 'utf8')
.update('\0default\0live-cas-package', 'utf8')
.digest('hex')
.slice(0, 52)
);
}
function exactEvidence(intent) {
return Object.freeze({
lockDigest: intent.lockDigest,
stageRef: intent.stageRef,
stageReceiptDigest: intent.stageReceiptDigest,
stageEvidenceDigest: intent.stageEvidenceDigest,
contentDigest: intent.contentDigest,
});
}
async function expectForbidden(operation) {
try {
await operation();
} catch (error) {
assert.equal(apiStatus(error), 403);
return 403;
}
fail('Kubernetes RBAC unexpectedly allowed a forbidden request');
}
function readyConfigMap(name) {
return Object.freeze({
apiVersion: 'v1',
kind: 'ConfigMap',
immutable: false,
metadata: Object.freeze({
name,
namespace: NAMESPACE,
labels: Object.freeze({
'app.kubernetes.io/managed-by': 'qinglong3-live-gate',
'qinglong.io/live-gate-role': 'cas-barrier',
}),
}),
data: Object.freeze({ actor: ACTOR }),
});
}
async function main() {
if (!/^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/.test(NAMESPACE ?? '')) {
fail('QL3_LIVE_NAMESPACE is invalid');
}
if (ACTOR !== 'a' && ACTOR !== 'b' && ACTOR !== 'c') {
fail('QL3_LIVE_ACTOR must be a, b or c');
}
const token = fs.readFileSync(TOKEN_FILE);
assert.ok(token.length >= 32 && token.length <= 16 * 1024);
token.fill(0);
const kubeConfig = new k8s.KubeConfig();
kubeConfig.loadFromCluster();
const rawApi = kubeConfig.makeApiClient(k8s.CoreV1Api);
let createCalls = 0;
let replaceCalls = 0;
let injectedCreateResponseLoss = ACTOR === 'a';
let replaceResourceVersion = null;
const activationApi = {
readNamespacedConfigMap(request) {
return rawApi.readNamespacedConfigMap(request);
},
async createNamespacedConfigMap(request) {
createCalls += 1;
const created = await rawApi.createNamespacedConfigMap(request);
if (injectedCreateResponseLoss) {
injectedCreateResponseLoss = false;
throw new Error(
'injected response loss after Kubernetes API-confirmed create',
);
}
return created;
},
async replaceNamespacedConfigMap(request) {
replaceCalls += 1;
replaceResourceVersion = request.body.metadata.resourceVersion;
assert.match(replaceResourceVersion ?? '', /^[1-9][0-9]*$/);
if (PEER !== null) {
const ownReadyName = `ql3-live-cas-ready-${ACTOR}`;
const peerReadyName = `ql3-live-cas-ready-${PEER}`;
await rawApi.createNamespacedConfigMap({
namespace: NAMESPACE,
body: readyConfigMap(ownReadyName),
fieldManager: 'qinglong-plugin-package-live-gate',
fieldValidation: 'Strict',
});
await waitFor(
`peer CAS barrier ConfigMap/${peerReadyName}`,
async () => {
try {
const peer = await rawApi.readNamespacedConfigMap({
namespace: NAMESPACE,
name: peerReadyName,
});
return {
ready: peer.data?.actor === PEER,
fact: `peer actor=${String(peer.data?.actor)}`,
};
} catch (error) {
if (apiStatus(error) === 404) return { ready: false };
throw error;
}
},
);
}
return rawApi.replaceNamespacedConfigMap(request);
},
};
let nowCalls = 0;
const createPublisher = (secretProjection) =>
new PluginPackageKubernetesActivationPublisher(
activationApi,
{ verify: async (intent) => exactEvidence(intent) },
{
clusterIdentity: 'ql3-plugin-package-live-cluster',
namespace: NAMESPACE,
now() {
nowCalls += 1;
return (ACTOR === 'a' ? 1_000 : 2_000) + nowCalls;
},
...(secretProjection === undefined ? {} : { secretProjection }),
},
);
if (ACTOR === 'c') {
const initial = activationIntent();
const activeConfigMap = await rawApi.readNamespacedConfigMap({
namespace: NAMESPACE,
name: activeTargetName(),
});
const activePointer = JSON.parse(activeConfigMap.data?.['active.json']);
const winnerActor = ['a', 'b'].find(
(actor) => CANDIDATES[actor].lockDigest === activePointer.intent.lockDigest,
);
assert.ok(winnerActor);
const activeCandidate = activationIntent({
...CANDIDATES[winnerActor],
targetGeneration: 2,
previousActiveLockDigest: INITIAL_LOCK_DIGEST,
});
const activeTransition = transitionEvidence(
initial,
activeCandidate,
winnerActor,
);
assert.equal(
activePointer.secretProjection.bindingDigest,
activeTransition.binding.bindingDigest,
);
const candidate = activationIntent({
...CANDIDATES.c,
targetGeneration: 3,
previousActiveLockDigest: activeCandidate.lockDigest,
});
const transition = revokeEvidence(
activeCandidate,
activeTransition,
candidate,
);
const publisher = createPublisher({
sourceSecretName: 'ql3-cluster-plugin-package-values',
bindings: { find: async () => null },
transitions: { find: async () => transition.receipt },
});
const receipt = await publisher.publish(candidate);
assert.equal(replaceCalls, 1);
const active = await publisher.findActiveDeployment(
'default',
'live-cas-package',
);
assert.ok(active);
assert.equal(active.resourceGeneration.lockDigest, candidate.lockDigest);
assert.equal(active.secretProjection.items.length, 0);
assert.equal(
pluginPackageKubernetesProjectedSecretWorkloadVolume(
active.secretProjection,
),
null,
);
const rbac = Object.freeze({
listConfigMaps: await expectForbidden(() =>
rawApi.listNamespacedConfigMap({ namespace: NAMESPACE }),
),
deleteConfigMap: await expectForbidden(() =>
rawApi.deleteNamespacedConfigMap({
namespace: NAMESPACE,
name: activeTargetName(),
}),
),
readSecret: await expectForbidden(() =>
rawApi.readNamespacedSecret({
namespace: NAMESPACE,
name: 'forbidden-secret',
}),
),
crossNamespaceRead: await expectForbidden(() =>
rawApi.readNamespacedConfigMap({
namespace: 'default',
name: 'kube-root-ca.crt',
}),
),
});
const result = JSON.stringify({
schema: RESULT_SCHEMA,
actor: ACTOR,
mode: 'revoke',
serviceAccountTokenMounted: true,
responseLoss: null,
cas: {
status: 'fulfilled',
receipt,
attemptedResourceVersion: replaceResourceVersion,
replaceCalls,
},
final: {
resourceVersion: activeConfigMap.metadata.resourceVersion,
lockDigest: candidate.lockDigest,
generation: receipt.generation,
pointerSchema: 'qinglong/plugin-package-kubernetes-active-pointer@v3',
projectionDigest: active.secretProjection.projectionDigest,
transitionReceiptDigest:
active.secretProjection.transitionReceiptDigest,
projectionItemCount: active.secretProjection.items.length,
projectedWorkloadVolume: false,
},
rbac,
});
fs.writeFileSync('/dev/termination-log', result, {
encoding: 'utf8',
flag: 'w',
});
process.stdout.write(`${result}\n`);
return;
}
const initial = activationIntent();
const initialPublisher = createPublisher();
let responseLoss = null;
if (ACTOR === 'a') {
await assert.rejects(
initialPublisher.publish(initial),
PluginPackageActivationUnavailableError,
);
const observation = await initialPublisher.inspect(initial);
assert.equal(observation.status, 'published');
const replay = await initialPublisher.publish(initial);
assert.deepEqual(replay, observation.receipt);
assert.equal(createCalls, 1);
assert.equal(nowCalls, 1);
responseLoss = Object.freeze({
injectedAfterApiConfirmedCreate: true,
firstCallFailedClosed: true,
durableInspectPublished: true,
replayReturnedExactReceipt: true,
createCalls,
nowCalls,
});
} else {
await waitFor('initial active pointer', async () => {
const observation = await initialPublisher.inspect(initial);
return {
ready: observation.status === 'published',
fact: observation.status,
};
});
assert.equal(createCalls, 0);
}
const candidate = activationIntent({
...CANDIDATES[ACTOR],
targetGeneration: 2,
previousActiveLockDigest: INITIAL_LOCK_DIGEST,
});
const transition = transitionEvidence(initial, candidate);
const publisher = createPublisher({
sourceSecretName: 'ql3-cluster-plugin-package-values',
bindings: { find: async () => transition.binding },
transitions: { find: async () => transition.receipt },
});
let outcome;
try {
const receipt = await publisher.publish(candidate);
outcome = Object.freeze({
status: 'fulfilled',
receipt,
});
} catch (error) {
assert.ok(error instanceof PluginPackageActivationConflictError);
outcome = Object.freeze({ status: 'conflict' });
}
assert.equal(replaceCalls, 1);
const targetName = activeTargetName();
const finalConfigMap = await rawApi.readNamespacedConfigMap({
namespace: NAMESPACE,
name: targetName,
});
const finalPointer = JSON.parse(finalConfigMap.data['active.json']);
assert.equal(finalPointer.schema.endsWith('@v3'), true);
assert.equal(finalPointer.secretProjection.defaultMode, 0o440);
assert.match(
finalPointer.secretProjection.transitionReceiptDigest,
/^[0-9a-f]{64}$/,
);
const projectionPath = secretProjectionFileName(transition.secretRef);
const winnerUsesThisActor =
finalPointer.intent.lockDigest === candidate.lockDigest;
if (winnerUsesThisActor) {
assert.deepEqual(finalPointer.secretProjection.items, [
{ key: projectionPath, path: projectionPath },
]);
}
const workloadVolume = pluginPackageKubernetesProjectedSecretWorkloadVolume(
finalPointer.secretProjection,
);
assert.equal(workloadVolume.volume.secret.optional, false);
assert.equal(workloadVolume.volume.secret.defaultMode, 0o440);
const rbac = Object.freeze({
listConfigMaps: await expectForbidden(() =>
rawApi.listNamespacedConfigMap({ namespace: NAMESPACE }),
),
deleteConfigMap: await expectForbidden(() =>
rawApi.deleteNamespacedConfigMap({
namespace: NAMESPACE,
name: targetName,
}),
),
readSecret: await expectForbidden(() =>
rawApi.readNamespacedSecret({
namespace: NAMESPACE,
name: 'forbidden-secret',
}),
),
crossNamespaceRead: await expectForbidden(() =>
rawApi.readNamespacedConfigMap({
namespace: 'default',
name: 'kube-root-ca.crt',
}),
),
});
const result = JSON.stringify({
schema: RESULT_SCHEMA,
actor: ACTOR,
mode: 'rotate',
serviceAccountTokenMounted: true,
responseLoss,
cas: {
...outcome,
attemptedResourceVersion: replaceResourceVersion,
replaceCalls,
},
final: {
resourceVersion: finalConfigMap.metadata.resourceVersion,
lockDigest: finalPointer.intent.lockDigest,
generation: finalPointer.receipt.generation,
pointerSchema: finalPointer.schema,
projectionDigest: finalPointer.secretProjection.projectionDigest,
transitionReceiptDigest:
finalPointer.secretProjection.transitionReceiptDigest,
projectionItemCount: finalPointer.secretProjection.items.length,
projectedWorkloadVolume: workloadVolume !== null,
},
rbac,
});
fs.writeFileSync('/dev/termination-log', result, {
encoding: 'utf8',
flag: 'w',
});
process.stdout.write(`${result}\n`);
}
main().catch((error) => {
process.stderr.write(
`${JSON.stringify({
schema: RESULT_SCHEMA,
actor: ACTOR ?? null,
error: error instanceof Error ? error.message : String(error),
})}\n`,
);
process.exitCode = 1;
});