feat(ql3): project plugin secrets into kubernetes

This commit is contained in:
whyour
2026-08-14 00:34:49 +08:00
parent d41c821ce0
commit 9d7431c2cd
13 changed files with 1437 additions and 73 deletions
@@ -18,9 +18,34 @@ import {
normalizePluginPackageActivationReceipt,
type PluginPackageActivationReceipt,
} from '@qinglong/runtime-core/plugin-package-install';
import type { PluginPackageSecretBindingRepository } from '@qinglong/runtime-core/plugin-package-secret-binding';
import type { PluginPackageSecretBindingTransitionReceiptRepository } from '@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt';
const ACTIVE_POINTER_SCHEMA =
import {
createPluginPackageKubernetesSecretProjection,
isPluginPackageKubernetesSecretName,
normalizePluginPackageKubernetesSecretProjection,
pluginPackageKubernetesProjectedSecretWorkloadVolume,
type PluginPackageKubernetesActiveDeployment,
type PluginPackageKubernetesProjectedSecretWorkloadVolume,
type PluginPackageKubernetesSecretProjection,
type PluginPackageKubernetesSecretProjectionAssignment,
type PluginPackageKubernetesSecretProjectionItem,
} from '../secret-binding/pluginPackageKubernetesSecretProjection';
export {
pluginPackageKubernetesProjectedSecretWorkloadVolume,
type PluginPackageKubernetesActiveDeployment,
type PluginPackageKubernetesProjectedSecretWorkloadVolume,
type PluginPackageKubernetesSecretProjection,
type PluginPackageKubernetesSecretProjectionAssignment,
type PluginPackageKubernetesSecretProjectionItem,
};
const ACTIVE_POINTER_SCHEMA_V2 =
'qinglong/plugin-package-kubernetes-active-pointer@v2';
const ACTIVE_POINTER_SCHEMA_V3 =
'qinglong/plugin-package-kubernetes-active-pointer@v3';
const ACTIVE_POINTER_KEY = 'active.json';
const MANAGED_BY_LABEL = 'app.kubernetes.io/managed-by';
const MANAGED_BY_VALUE = 'qinglong3';
@@ -58,6 +83,19 @@ export interface PluginPackageKubernetesActivationPublisherOptions {
readonly namespace: string;
/** Explicit authoritative clock called only for a new publication attempt. */
readonly now: () => number | Promise<number>;
/**
* Optional content-blind source used by the production recovery Job. When
* configured, v3 pointers bind the exact projected Secret keys to the same
* resourceVersion-fenced activation as the Package generation.
*/
readonly secretProjection?: Readonly<{
readonly sourceSecretName: string;
readonly bindings: Pick<PluginPackageSecretBindingRepository, 'find'>;
readonly transitions: Pick<
PluginPackageSecretBindingTransitionReceiptRepository,
'find'
>;
}>;
}
export interface PluginPackageKubernetesConfigMap {
@@ -110,17 +148,25 @@ export interface PluginPackageKubernetesConfigMapApi {
): Promise<PluginPackageKubernetesConfigMap>;
}
interface ActivePointer {
readonly schema: typeof ACTIVE_POINTER_SCHEMA;
interface ActivePointerV2 {
readonly schema: typeof ACTIVE_POINTER_SCHEMA_V2;
readonly clusterIdentityDigest: string;
readonly intent: Readonly<PluginPackageActivationIntent>;
readonly receipt: Readonly<PluginPackageActivationReceipt>;
}
interface StoredPointer extends ActivePointer {
readonly resourceVersion: string;
interface ActivePointerV3 {
readonly schema: typeof ACTIVE_POINTER_SCHEMA_V3;
readonly clusterIdentityDigest: string;
readonly intent: Readonly<PluginPackageActivationIntent>;
readonly receipt: Readonly<PluginPackageActivationReceipt>;
readonly secretProjection: Readonly<PluginPackageKubernetesSecretProjection> | null;
}
type ActivePointer = ActivePointerV2 | ActivePointerV3;
type StoredPointer = ActivePointer & Readonly<{ resourceVersion: string }>;
function apiStatus(error: unknown): number | null {
if (!error || typeof error !== 'object') return null;
if ('code' in error && typeof error.code === 'number') return error.code;
@@ -237,11 +283,29 @@ export class PluginPackageKubernetesActivationPublisher
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).sort().join(',') !==
'clusterIdentity,namespace,now' ||
Object.keys(options).some(
(key) =>
key !== 'clusterIdentity' &&
key !== 'namespace' &&
key !== 'now' &&
key !== 'secretProjection',
) ||
!SAFE_IDENTITY.test(options.clusterIdentity) ||
!DNS_LABEL.test(options.namespace) ||
typeof options.now !== 'function'
typeof options.now !== 'function' ||
(options.secretProjection !== undefined &&
(!options.secretProjection ||
typeof options.secretProjection !== 'object' ||
Array.isArray(options.secretProjection) ||
Object.keys(options.secretProjection).sort().join(',') !==
'bindings,sourceSecretName,transitions' ||
!isPluginPackageKubernetesSecretName(
options.secretProjection.sourceSecretName,
) ||
!options.secretProjection.bindings ||
typeof options.secretProjection.bindings.find !== 'function' ||
!options.secretProjection.transitions ||
typeof options.secretProjection.transitions.find !== 'function'))
) {
throw new TypeError(
'Plugin Package Kubernetes activation options are invalid',
@@ -306,6 +370,38 @@ export class PluginPackageKubernetesActivationPublisher
}
}
async #secretProjection(
intent: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageKubernetesSecretProjection> | null> {
const source = this.options.secretProjection;
if (!source) return null;
try {
const generationDigest = intent.resourceGeneration.generationDigest;
const [binding, transition] = await Promise.all([
source.bindings.find(generationDigest),
source.transitions.find(generationDigest),
]);
if (
binding &&
(binding.target.installationId !== intent.installationId ||
binding.target.projectId !== intent.projectId ||
binding.target.packageName !== intent.packageName ||
binding.target.lockDigest !== intent.lockDigest ||
binding.target.generation !== intent.targetGeneration)
) {
throw new PluginPackageActivationConflictError();
}
return createPluginPackageKubernetesSecretProjection(
source.sourceSecretName,
generationDigest,
binding,
transition,
);
} catch (error) {
return preserveDomainError(error);
}
}
#parsePointer(
configMap: PluginPackageKubernetesConfigMap,
expectedName: string,
@@ -336,37 +432,71 @@ export class PluginPackageKubernetesActivationPublisher
const serialized = data[ACTIVE_POINTER_KEY];
if (
labels[MANAGED_BY_LABEL] !== MANAGED_BY_VALUE ||
labels[ACTIVE_LABEL] !== 'v2' ||
(labels[ACTIVE_LABEL] !== 'v2' && labels[ACTIVE_LABEL] !== 'v3') ||
typeof serialized !== 'string' ||
Buffer.byteLength(serialized, 'utf8') > MAX_ACTIVE_POINTER_BYTES
) {
throw new PluginPackageActivationConflictError();
}
const pointer = dataRecord(JSON.parse(serialized));
exactKeys(pointer, [
'schema',
'clusterIdentityDigest',
'intent',
'receipt',
]);
if (pointer.schema === ACTIVE_POINTER_SCHEMA_V2) {
exactKeys(pointer, [
'schema',
'clusterIdentityDigest',
'intent',
'receipt',
]);
} else if (pointer.schema === ACTIVE_POINTER_SCHEMA_V3) {
exactKeys(pointer, [
'schema',
'clusterIdentityDigest',
'intent',
'receipt',
'secretProjection',
]);
} else {
throw new PluginPackageActivationConflictError();
}
const intent = normalizeIntent(
pointer.intent as PluginPackageActivationIntent,
);
const receipt = normalizePluginPackageActivationReceipt(pointer.receipt);
const normalized: ActivePointer = Object.freeze({
schema: ACTIVE_POINTER_SCHEMA,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
});
const secretProjection =
pointer.schema === ACTIVE_POINTER_SCHEMA_V3
? pointer.secretProjection === null
? null
: normalizePluginPackageKubernetesSecretProjection(
pointer.secretProjection,
)
: undefined;
const normalized: ActivePointer =
pointer.schema === ACTIVE_POINTER_SCHEMA_V3
? Object.freeze({
schema: ACTIVE_POINTER_SCHEMA_V3,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
secretProjection: secretProjection!,
})
: Object.freeze({
schema: ACTIVE_POINTER_SCHEMA_V2,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
});
if (
pointer.schema !== ACTIVE_POINTER_SCHEMA ||
pointer.clusterIdentityDigest !== this.#clusterIdentityDigest ||
this.#name(intent) !== expectedName ||
labels[TARGET_LABEL] !==
Buffer.from(this.#targetDigest(intent), 'hex').toString(
'base64url',
) ||
labels[ACTIVE_LABEL] !==
(pointer.schema === ACTIVE_POINTER_SCHEMA_V3 ? 'v3' : 'v2') ||
(secretProjection !== undefined &&
secretProjection !== null &&
secretProjection.generationDigest !==
intent.resourceGeneration.generationDigest) ||
annotations[INTENT_ANNOTATION] !== intent.intentDigest ||
receipt.intentDigest !== intent.intentDigest ||
receipt.generation !== intent.targetGeneration ||
@@ -409,6 +539,7 @@ export class PluginPackageKubernetesActivationPublisher
intent: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageActivationObservation>> {
await this.#verifyStage(intent);
const expectedProjection = await this.#secretProjection(intent);
const pointer = await this.#optionalPointer(intent);
if (!pointer) {
if (intent.previousActiveLockDigest !== null) {
@@ -416,7 +547,12 @@ export class PluginPackageKubernetesActivationPublisher
}
return Object.freeze({ status: 'not_published' });
}
if (same(pointer.intent, intent)) {
if (
same(pointer.intent, intent) &&
(pointer.schema === ACTIVE_POINTER_SCHEMA_V2
? expectedProjection === null
: same(pointer.secretProjection, expectedProjection))
) {
return Object.freeze({ status: 'published', receipt: pointer.receipt });
}
if (
@@ -433,14 +569,24 @@ export class PluginPackageKubernetesActivationPublisher
intent: Readonly<PluginPackageActivationIntent>,
receipt: Readonly<PluginPackageActivationReceipt>,
current: Readonly<StoredPointer> | null,
secretProjection: Readonly<PluginPackageKubernetesSecretProjection> | null,
): ConfigMapWrite {
const targetDigest = this.#targetDigest(intent);
const pointer: Readonly<ActivePointer> = Object.freeze({
schema: ACTIVE_POINTER_SCHEMA,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
});
const usesSecretProjection = secretProjection !== null;
const pointer: Readonly<ActivePointer> = usesSecretProjection
? Object.freeze({
schema: ACTIVE_POINTER_SCHEMA_V3,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
secretProjection,
})
: Object.freeze({
schema: ACTIVE_POINTER_SCHEMA_V2,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
});
const serialized = `${JSON.stringify(pointer)}\n`;
if (Buffer.byteLength(serialized, 'utf8') > MAX_ACTIVE_POINTER_BYTES) {
throw new PluginPackageActivationUnavailableError();
@@ -455,7 +601,7 @@ export class PluginPackageKubernetesActivationPublisher
...(current ? { resourceVersion: current.resourceVersion } : {}),
labels: Object.freeze({
[MANAGED_BY_LABEL]: MANAGED_BY_VALUE,
[ACTIVE_LABEL]: 'v2',
[ACTIVE_LABEL]: usesSecretProjection ? 'v3' : 'v2',
[TARGET_LABEL]: Buffer.from(targetDigest, 'hex').toString(
'base64url',
),
@@ -500,6 +646,42 @@ export class PluginPackageKubernetesActivationPublisher
}
}
/**
* Returns one parsed active deployment snapshot for a workload renderer.
* It performs no Secret API call and exposes only projection keys, never
* Secret material or reversible Secret references.
*/
async findActiveDeployment(
projectId: string,
packageName: string,
): Promise<Readonly<PluginPackageKubernetesActiveDeployment> | null> {
if (
typeof projectId !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(projectId) ||
typeof packageName !== 'string' ||
!DNS_LABEL.test(packageName)
) {
throw new TypeError(
'Plugin Package active deployment identity is invalid',
);
}
try {
const pointer = await this.#optionalPointer(
Object.freeze({ projectId, packageName }),
);
if (!pointer) return null;
return Object.freeze({
resourceGeneration: pointer.intent.resourceGeneration,
secretProjection:
pointer.schema === ACTIVE_POINTER_SCHEMA_V3
? pointer.secretProjection
: null,
});
} catch (error) {
return preserveDomainError(error);
}
}
async publish(
value: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageActivationReceipt>> {
@@ -508,7 +690,18 @@ export class PluginPackageKubernetesActivationPublisher
const first = await this.#observe(intent);
if (first.status === 'published') return first.receipt;
const current = await this.#optionalPointer(intent);
if (current && same(current.intent, intent)) return current.receipt;
if (current && same(current.intent, intent)) {
const expectedProjection = await this.#secretProjection(intent);
if (
(current.schema === ACTIVE_POINTER_SCHEMA_V2 &&
expectedProjection === null) ||
(current.schema === ACTIVE_POINTER_SCHEMA_V3 &&
same(current.secretProjection, expectedProjection))
) {
return current.receipt;
}
throw new PluginPackageActivationConflictError();
}
if (
(!current && intent.previousActiveLockDigest !== null) ||
(current &&
@@ -529,7 +722,8 @@ export class PluginPackageKubernetesActivationPublisher
contentDigest: intent.contentDigest,
activatedAtMs,
});
const body = this.#body(intent, receipt, current);
const secretProjection = await this.#secretProjection(intent);
const body = this.#body(intent, receipt, current, secretProjection);
try {
if (current) {
await this.api.replaceNamespacedConfigMap({
@@ -41,6 +41,7 @@ import {
PostgresPluginPackageMaterializedRevisionRepository,
PostgresPluginPackageSecretBindingRepository,
PostgresPluginPackageSecretBindingActivationPrerequisite,
PostgresPluginPackageSecretBindingTransitionRepository,
PostgresPluginPackagePublisherProvenanceRepository,
PostgresPluginPackageTaskReconciliationRepository,
PostgresProjectToolDefinitionSnapshotRepository,
@@ -307,6 +308,16 @@ export async function recoverClusterPluginPackages(
clusterIdentity: options.clusterIdentity,
namespace: options.namespace,
now: options.now,
secretProjection: {
sourceSecretName: 'ql3-cluster-plugin-package-values',
bindings: new PostgresPluginPackageSecretBindingRepository(
database.pool,
),
transitions:
new PostgresPluginPackageSecretBindingTransitionRepository(
database.pool,
),
},
},
);
const resourceByteSource =
@@ -0,0 +1,315 @@
import { createHash } from 'node:crypto';
import { PluginPackageActivationConflictError } from '@qinglong/runtime-core/plugin-package-activation';
import type { PluginPackageResourceGeneration } from '@qinglong/runtime-core/plugin-package-resource-generation';
import type { PluginPackageSecretBinding } from '@qinglong/runtime-core/plugin-package-secret-binding';
import type { PluginPackageSecretBindingTransitionReceipt } from '@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt';
import { secretProjectionFileName } from '@qinglong/runtime-core/secret-projection';
export const PLUGIN_PACKAGE_KUBERNETES_SECRET_PROJECTION_SCHEMA =
'qinglong/plugin-package-kubernetes-secret-projection@v1' as const;
export const PLUGIN_PACKAGE_KUBERNETES_SECRET_FILE_MODE = 0o440 as const;
const DIGEST = /^[0-9a-f]{64}$/;
const DNS_LABEL = /^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const ASSIGNMENT_NAME = /^[A-Z_][A-Z0-9_]{0,127}$/;
const PROJECTION_DIGEST_DOMAIN = Buffer.from(
'qinglong/plugin-package-kubernetes-secret-projection-digest@v1\0',
'utf8',
);
export interface PluginPackageKubernetesSecretProjectionItem {
readonly key: string;
readonly path: string;
}
export interface PluginPackageKubernetesSecretProjectionAssignment {
readonly name: string;
readonly required: boolean;
readonly path: string | null;
}
export interface PluginPackageKubernetesSecretProjection {
readonly schema: typeof PLUGIN_PACKAGE_KUBERNETES_SECRET_PROJECTION_SCHEMA;
readonly sourceSecretName: string;
readonly defaultMode: typeof PLUGIN_PACKAGE_KUBERNETES_SECRET_FILE_MODE;
readonly generationDigest: string;
readonly bindingDigest: string | null;
readonly transitionReceiptDigest: string | null;
readonly items: readonly Readonly<PluginPackageKubernetesSecretProjectionItem>[];
readonly assignments: readonly Readonly<PluginPackageKubernetesSecretProjectionAssignment>[];
readonly projectionDigest: string;
}
export interface PluginPackageKubernetesActiveDeployment {
readonly resourceGeneration: Readonly<PluginPackageResourceGeneration>;
readonly secretProjection: Readonly<PluginPackageKubernetesSecretProjection> | null;
}
export interface PluginPackageKubernetesProjectedSecretWorkloadVolume {
readonly volume: Readonly<{
readonly name: 'plugin-package-values';
readonly secret: Readonly<{
readonly secretName: string;
readonly optional: false;
readonly defaultMode: typeof PLUGIN_PACKAGE_KUBERNETES_SECRET_FILE_MODE;
readonly items: readonly Readonly<PluginPackageKubernetesSecretProjectionItem>[];
}>;
}>;
readonly volumeMount: Readonly<{
readonly name: 'plugin-package-values';
readonly mountPath: '/var/run/secrets/qinglong3/plugin-package-values';
readonly readOnly: true;
}>;
}
function conflict(): never {
throw new PluginPackageActivationConflictError();
}
function dataRecord(value: unknown): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
return conflict();
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
descriptor.get !== undefined ||
descriptor.set !== undefined ||
descriptor.enumerable !== true,
)
) {
return conflict();
}
return value as Record<string, unknown>;
}
function exactKeys(value: object, expected: readonly string[]): void {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
actual.length !== canonical.length ||
actual.some((key, index) => key !== canonical[index])
) {
conflict();
}
}
export function isPluginPackageKubernetesSecretName(
value: unknown,
): value is string {
return (
typeof value === 'string' &&
value.length <= 253 &&
value.split('.').every((label) => DNS_LABEL.test(label))
);
}
function digest(
value: Omit<PluginPackageKubernetesSecretProjection, 'projectionDigest'>,
): string {
return createHash('sha256')
.update(PROJECTION_DIGEST_DOMAIN)
.update(JSON.stringify(value), 'utf8')
.digest('hex');
}
function uniqueItems(
assignments: readonly Readonly<PluginPackageKubernetesSecretProjectionAssignment>[],
): readonly Readonly<PluginPackageKubernetesSecretProjectionItem>[] {
const seen = new Set<string>();
return Object.freeze(
assignments.flatMap((assignment) => {
if (assignment.path === null || seen.has(assignment.path)) return [];
seen.add(assignment.path);
return [Object.freeze({ key: assignment.path, path: assignment.path })];
}),
);
}
export function createPluginPackageKubernetesSecretProjection(
sourceSecretName: string,
generationDigest: string,
binding: Readonly<PluginPackageSecretBinding> | null,
transition: Readonly<PluginPackageSecretBindingTransitionReceipt> | null,
): Readonly<PluginPackageKubernetesSecretProjection> | null {
if (
!isPluginPackageKubernetesSecretName(sourceSecretName) ||
!DIGEST.test(generationDigest)
) {
return conflict();
}
if (transition) {
if (
transition.transitionPlan.nextTarget.generationDigest !==
generationDigest ||
transition.bindingDigest !== (binding?.bindingDigest ?? null) ||
JSON.stringify(
transition.transitionPlan.nextBindingPlan?.entries ?? [],
) !== JSON.stringify(binding?.entries ?? [])
) {
return conflict();
}
} else if (
binding !== null &&
binding.target.generationDigest !== generationDigest
) {
return conflict();
}
if (!binding && !transition) return null;
const assignments = Object.freeze(
(binding?.entries ?? []).map((entry) =>
Object.freeze({
name: entry.name,
required: entry.required,
path:
entry.secretRef === null
? null
: secretProjectionFileName(entry.secretRef),
}),
),
);
const unsigned = Object.freeze({
schema: PLUGIN_PACKAGE_KUBERNETES_SECRET_PROJECTION_SCHEMA,
sourceSecretName,
defaultMode: PLUGIN_PACKAGE_KUBERNETES_SECRET_FILE_MODE,
generationDigest,
bindingDigest: binding?.bindingDigest ?? null,
transitionReceiptDigest: transition?.receiptDigest ?? null,
items: uniqueItems(assignments),
assignments,
});
return Object.freeze({ ...unsigned, projectionDigest: digest(unsigned) });
}
export function normalizePluginPackageKubernetesSecretProjection(
value: unknown,
): Readonly<PluginPackageKubernetesSecretProjection> {
const candidate = dataRecord(value);
exactKeys(candidate, [
'schema',
'sourceSecretName',
'defaultMode',
'generationDigest',
'bindingDigest',
'transitionReceiptDigest',
'items',
'assignments',
'projectionDigest',
]);
if (
candidate.schema !== PLUGIN_PACKAGE_KUBERNETES_SECRET_PROJECTION_SCHEMA ||
!isPluginPackageKubernetesSecretName(candidate.sourceSecretName) ||
candidate.defaultMode !== PLUGIN_PACKAGE_KUBERNETES_SECRET_FILE_MODE ||
typeof candidate.generationDigest !== 'string' ||
!DIGEST.test(candidate.generationDigest) ||
(candidate.bindingDigest !== null &&
(typeof candidate.bindingDigest !== 'string' ||
!DIGEST.test(candidate.bindingDigest))) ||
(candidate.transitionReceiptDigest !== null &&
(typeof candidate.transitionReceiptDigest !== 'string' ||
!DIGEST.test(candidate.transitionReceiptDigest))) ||
!Array.isArray(candidate.items) ||
!Array.isArray(candidate.assignments) ||
candidate.items.length > 64 ||
candidate.assignments.length > 64
) {
return conflict();
}
const assignments = Object.freeze(
candidate.assignments.map((value) => {
const assignment = dataRecord(value);
exactKeys(assignment, ['name', 'required', 'path']);
if (
typeof assignment.name !== 'string' ||
!ASSIGNMENT_NAME.test(assignment.name) ||
typeof assignment.required !== 'boolean' ||
(assignment.path !== null &&
(typeof assignment.path !== 'string' ||
!DIGEST.test(assignment.path))) ||
(assignment.required && assignment.path === null)
) {
return conflict();
}
return Object.freeze({
name: assignment.name,
required: assignment.required,
path: assignment.path as string | null,
});
}),
);
const items = Object.freeze(
candidate.items.map((value) => {
const item = dataRecord(value);
exactKeys(item, ['key', 'path']);
if (
typeof item.key !== 'string' ||
typeof item.path !== 'string' ||
!DIGEST.test(item.key) ||
item.path !== item.key
) {
return conflict();
}
return Object.freeze({ key: item.key, path: item.path });
}),
);
if (JSON.stringify(items) !== JSON.stringify(uniqueItems(assignments))) {
return conflict();
}
const unsigned = Object.freeze({
schema: PLUGIN_PACKAGE_KUBERNETES_SECRET_PROJECTION_SCHEMA,
sourceSecretName: candidate.sourceSecretName,
defaultMode: PLUGIN_PACKAGE_KUBERNETES_SECRET_FILE_MODE,
generationDigest: candidate.generationDigest,
bindingDigest: candidate.bindingDigest as string | null,
transitionReceiptDigest: candidate.transitionReceiptDigest as string | null,
items,
assignments,
});
if (
typeof candidate.projectionDigest !== 'string' ||
candidate.projectionDigest !== digest(unsigned)
) {
return conflict();
}
return Object.freeze({
...unsigned,
projectionDigest: candidate.projectionDigest,
});
}
/**
* Pure Pod-spec fragment renderer. An empty/revoked projection deliberately
* returns null: an omitted/empty Secret items mapping can mean "all keys".
*/
export function pluginPackageKubernetesProjectedSecretWorkloadVolume(
value: Readonly<PluginPackageKubernetesSecretProjection> | null,
): Readonly<PluginPackageKubernetesProjectedSecretWorkloadVolume> | null {
if (value === null) return null;
const projection = normalizePluginPackageKubernetesSecretProjection(value);
if (projection.items.length === 0) return null;
return Object.freeze({
volume: Object.freeze({
name: 'plugin-package-values' as const,
secret: Object.freeze({
secretName: projection.sourceSecretName,
optional: false as const,
defaultMode: PLUGIN_PACKAGE_KUBERNETES_SECRET_FILE_MODE,
items: projection.items,
}),
}),
volumeMount: Object.freeze({
name: 'plugin-package-values' as const,
mountPath: '/var/run/secrets/qinglong3/plugin-package-values' as const,
readOnly: true as const,
}),
});
}
@@ -9,6 +9,20 @@ const {
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const {
createPluginPackageSecretBinding,
} = 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 {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
@@ -28,6 +42,7 @@ const {
} = require('@qinglong/runtime-core/plugin-package-recovery');
const {
PluginPackageKubernetesActivationPublisher,
pluginPackageKubernetesProjectedSecretWorkloadVolume,
} = require('../dist/plugin-package/recovery/pluginPackageKubernetesActivation');
function apiError(code) {
@@ -298,11 +313,167 @@ function publisher(api = new FakeConfigMapApi(), overrides = {}) {
nowCalls += 1;
return overrides.now?.() ?? 500 + nowCalls;
},
...(overrides.secretProjection === undefined
? {}
: { secretProjection: overrides.secretProjection }),
},
);
return { api, publisher: value, nowCalls: () => nowCalls };
}
function secretManifest(version, secrets) {
return {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version,
description: 'Kubernetes Secret projection fixture',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['cluster-control'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '16Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets,
tools: secrets.length === 0 ? [] : ['secret.use'],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
}
function projectedTransition(kind) {
const previousManifest = secretManifest('1.0.0', [
{ name: 'TOKEN', required: true },
]);
const previousGeneration = createPluginPackageResourceGeneration({
installationId: 'install-secret-v1',
projectId: 'default',
packageName: 'example-monitor',
lockDigest: '1'.repeat(64),
generation: 1,
previousActiveLockDigest: null,
contentDigest: '2'.repeat(64),
contents: previousManifest.spec.contents,
});
const previousBinding = createPluginPackageSecretBinding({
generation: previousGeneration,
manifest: previousManifest,
assignments: [
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: 'default',
name: 'token',
version: 1,
}),
},
],
authority: {
kind: 'approved-action-execution',
evidenceDigest: '3'.repeat(64),
},
boundAtMs: 10,
});
const previousActivation = intent({
installationId: previousGeneration.installationId,
lockDigest: previousGeneration.lockDigest,
targetGeneration: previousGeneration.generation,
previousActiveLockDigest: null,
contentDigest: previousGeneration.contentDigest,
resourceGeneration: previousGeneration,
intentDigest: kind === 'revoke' ? 'b'.repeat(64) : 'c'.repeat(64),
});
const nextManifest =
kind === 'revoke'
? secretManifest('2.0.0', [])
: secretManifest('2.0.0', [{ name: 'TOKEN', required: true }]);
const nextGeneration = createPluginPackageResourceGeneration({
installationId: `install-secret-${kind}`,
projectId: 'default',
packageName: 'example-monitor',
lockDigest: kind === 'revoke' ? '4'.repeat(64) : '5'.repeat(64),
generation: 2,
previousActiveLockDigest: previousBinding.target.lockDigest,
contentDigest: kind === 'revoke' ? '6'.repeat(64) : '7'.repeat(64),
contents: nextManifest.spec.contents,
});
const secretRef = createSecretRef({
projectId: 'default',
name: 'token',
version: 2,
});
const plan = createPluginPackageSecretBindingTransitionPlan({
previousTarget: previousBinding.target,
previousBinding,
previousAttemptGeneration: 1,
nextGeneration,
nextManifest,
assignments: kind === 'revoke' ? [] : [{ name: 'TOKEN', secretRef }],
plannedAtMs: 20,
});
const binding = createPluginPackageSecretBindingFromTransitionPlan(
plan,
'approved-action-execution',
'8'.repeat(64),
30,
);
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'approved-action-execution',
evidenceDigest: '8'.repeat(64),
},
binding,
committedAtMs: 30,
});
const activation = intent({
installationId: nextGeneration.installationId,
lockDigest: nextGeneration.lockDigest,
targetGeneration: nextGeneration.generation,
previousActiveLockDigest: nextGeneration.previousActiveLockDigest,
contentDigest: nextGeneration.contentDigest,
resourceGeneration: nextGeneration,
intentDigest: kind === 'revoke' ? '9'.repeat(64) : 'a'.repeat(64),
});
return { previousActivation, activation, binding, receipt, secretRef };
}
function projectionSource(value) {
return {
sourceSecretName: 'ql3-cluster-plugin-package-values',
bindings: {
async find(generationDigest) {
assert.equal(
generationDigest,
value.activation.resourceGeneration.generationDigest,
);
return value.binding;
},
},
transitions: {
async find(generationDigest) {
assert.equal(
generationDigest,
value.activation.resourceGeneration.generationDigest,
);
return value.receipt;
},
},
};
}
test('publishes one resourceVersion-fenced ConfigMap and exact replays it', async () => {
const fixture = publisher();
const value = intent();
@@ -391,6 +562,195 @@ test('replaces only the exact previous lock and rejects a stale writer', async (
);
});
test('publishes a content-blind v3 projection for an approved Secret rotation', async () => {
const value = projectedTransition('rotate');
const api = new FakeConfigMapApi();
await publisher(api).publisher.publish(value.previousActivation);
const fixture = publisher(api, {
secretProjection: projectionSource(value),
});
await fixture.publisher.publish(value.activation);
const deployment = await fixture.publisher.findActiveDeployment(
'default',
'example-monitor',
);
assert.equal(
deployment.resourceGeneration.generationDigest,
value.activation.resourceGeneration.generationDigest,
);
assert.deepEqual(deployment.secretProjection.assignments, [
{
name: 'TOKEN',
required: true,
path: secretProjectionFileName(value.secretRef),
},
]);
assert.deepEqual(deployment.secretProjection.items, [
{
key: secretProjectionFileName(value.secretRef),
path: secretProjectionFileName(value.secretRef),
},
]);
assert.equal(deployment.secretProjection.defaultMode, 0o440);
assert.equal(
deployment.secretProjection.bindingDigest,
value.binding.bindingDigest,
);
assert.equal(
deployment.secretProjection.transitionReceiptDigest,
value.receipt.receiptDigest,
);
const [stored] = fixture.api.items.values();
const pointer = JSON.parse(stored.data['active.json']);
assert.equal(pointer.schema.endsWith('@v3'), true);
assert.equal(
stored.metadata.labels['qinglong.io/plugin-package-active'],
'v3',
);
assert.equal(JSON.stringify(pointer).includes(value.secretRef), false);
assert.deepEqual(
pluginPackageKubernetesProjectedSecretWorkloadVolume(
deployment.secretProjection,
),
{
volume: {
name: 'plugin-package-values',
secret: {
secretName: 'ql3-cluster-plugin-package-values',
optional: false,
defaultMode: 0o440,
items: [
{
key: secretProjectionFileName(value.secretRef),
path: secretProjectionFileName(value.secretRef),
},
],
},
},
volumeMount: {
name: 'plugin-package-values',
mountPath: '/var/run/secrets/qinglong3/plugin-package-values',
readOnly: true,
},
},
);
});
test('publishes an explicit empty projection for revoke and rejects projection drift', async () => {
const value = projectedTransition('revoke');
const api = new FakeConfigMapApi();
await publisher(api).publisher.publish(value.previousActivation);
const fixture = publisher(api, {
secretProjection: projectionSource(value),
});
await fixture.publisher.publish(value.activation);
const deployment = await fixture.publisher.findActiveDeployment(
'default',
'example-monitor',
);
assert.deepEqual(deployment.secretProjection.items, []);
assert.deepEqual(deployment.secretProjection.assignments, []);
assert.equal(deployment.secretProjection.bindingDigest, null);
assert.equal(
deployment.secretProjection.transitionReceiptDigest,
value.receipt.receiptDigest,
);
assert.equal(
pluginPackageKubernetesProjectedSecretWorkloadVolume(
deployment.secretProjection,
),
null,
);
const [key, stored] = fixture.api.items.entries().next().value;
const pointer = JSON.parse(stored.data['active.json']);
pointer.secretProjection.projectionDigest = '0'.repeat(64);
fixture.api.items.set(key, {
...stored,
data: { 'active.json': `${JSON.stringify(pointer)}\n` },
});
await assert.rejects(
fixture.publisher.findActiveDeployment('default', 'example-monitor'),
PluginPackageActivationConflictError,
);
});
test('converges a lost v3 replacement response without republishing projection', async () => {
const value = projectedTransition('rotate');
const api = new FakeConfigMapApi();
await publisher(api).publisher.publish(value.previousActivation);
api.loseReplaceResponse = true;
const fixture = publisher(api, {
secretProjection: projectionSource(value),
});
await assert.rejects(
fixture.publisher.publish(value.activation),
PluginPackageActivationUnavailableError,
);
assert.equal(api.replaceCalls, 1);
assert.equal(
(await fixture.publisher.inspect(value.activation)).status,
'published',
);
await fixture.publisher.publish(value.activation);
assert.equal(api.replaceCalls, 1);
assert.equal(fixture.nowCalls(), 1);
});
test('does not switch the active pointer when projection evidence is unavailable', async () => {
const value = projectedTransition('rotate');
const api = new FakeConfigMapApi();
await publisher(api).publisher.publish(value.previousActivation);
const previousPointer = structuredClone([...api.items.values()][0]);
const fixture = publisher(api, {
secretProjection: {
...projectionSource(value),
transitions: {
async find() {
throw new Error('database unavailable');
},
},
},
});
await assert.rejects(
fixture.publisher.publish(value.activation),
PluginPackageActivationUnavailableError,
);
assert.deepEqual([...api.items.values()][0], previousPointer);
assert.equal(fixture.api.replaceCalls, 0);
assert.equal(fixture.nowCalls(), 0);
});
test('keeps a staged upgrade without Secret facts on the compatible v2 pointer', async () => {
const value = projectedTransition('rotate');
const api = new FakeConfigMapApi();
await publisher(api).publisher.publish(value.previousActivation);
const fixture = publisher(api, {
secretProjection: {
...projectionSource(value),
bindings: {
async find() {
return null;
},
},
transitions: {
async find() {
return null;
},
},
},
});
await fixture.publisher.publish(value.activation);
const deployment = await fixture.publisher.findActiveDeployment(
'default',
'example-monitor',
);
assert.equal(deployment.secretProjection, null);
const pointer = JSON.parse([...api.items.values()][0].data['active.json']);
assert.equal(pointer.schema.endsWith('@v2'), true);
assert.equal(Object.hasOwn(pointer, 'secretProjection'), false);
});
test('leaves response loss for recovery inspection without republishing', async () => {
const api = new FakeConfigMapApi();
api.loseCreateResponse = true;
@@ -200,6 +200,12 @@ function runtimePrivileges() {
plugin_package_automation_disposition_events: [false, false, false, false],
plugin_package_automation_publication_heads: [true, false, false, false],
plugin_package_secret_binding_approval_plans: [false, false, false, false],
plugin_package_secret_binding_transition_approval_plans: [
false,
false,
false,
false,
],
plugin_package_secret_bindings: [false, false, false, false],
plugin_package_secret_binding_transition_receipts: [
false,
@@ -114,6 +114,12 @@ function runtimePrivileges() {
plugin_package_automation_disposition_events: [false, false, false, false],
plugin_package_automation_publication_heads: [true, false, false, false],
plugin_package_secret_binding_approval_plans: [false, false, false, false],
plugin_package_secret_binding_transition_approval_plans: [
false,
false,
false,
false,
],
plugin_package_secret_bindings: [false, false, false, false],
plugin_package_secret_binding_transition_receipts: [
false,