mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
655 lines
20 KiB
JavaScript
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;
|
|
});
|