feat(ql3): admit plugin secret action jobs

This commit is contained in:
whyour
2026-08-14 10:22:14 +08:00
parent 75dbe7a4c4
commit 7f5339e047
27 changed files with 2019 additions and 10 deletions
@@ -15,6 +15,9 @@ import {
isPostgresTlsDnsServername,
loadPostgresCertificateAuthorityFile,
loadPostgresConnectionEnvironment,
PostgresApprovedActionExecutionRepository,
PostgresPluginPackageSecretBindingApprovalPlanReader,
PostgresPluginPackageSecretBindingTransitionApprovalPlanReader,
type PostgresConnectionOptions,
type PostgresPoolOptions,
type PostgresSchemaReadinessReport,
@@ -48,6 +51,12 @@ import { ProjectedPluginPackageSecretExistenceInspector } from '../secret-bindin
import {
runClusterPluginPackagePublisherRevocation,
} from '../publisher/pluginPackagePublisherRevocation';
import {
createClusterPluginPackageKubernetesSecretActionController,
type ClusterPluginPackageKubernetesSecretActionControllerResource,
type PluginPackageKubernetesSecretActionControllerSummary,
} from './pluginPackageKubernetesSecretActionController';
import type { PluginPackageKubernetesSecretActionJobOptions } from './pluginPackageKubernetesSecretActionJob';
export type ClusterPluginPackageExecutorProcessEnvironment = Readonly<
Record<string, string | undefined>
@@ -66,6 +75,11 @@ export type ClusterPluginPackageExecutorProcessConfig =
revocationMaxPages: number;
dispatchId: string | null;
secretProjectionRoot: string | null;
kubernetesSecretActions: Readonly<{
enabled: true;
limit: number;
job: Readonly<PluginPackageKubernetesSecretActionJobOptions>;
}> | null;
database: Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
@@ -78,6 +92,7 @@ export interface ClusterPluginPackageExecutorBatchResult {
readonly secretBindingApprovals: Readonly<ClusterPluginPackageSecretBindingApprovalSummary>;
readonly secretBindingTransitionApprovals: Readonly<ClusterPluginPackageSecretBindingTransitionApprovalSummary>;
readonly dispatch: Readonly<ApprovedActionDispatchBatchSummary>;
readonly secretActionJobs: Readonly<PluginPackageKubernetesSecretActionControllerSummary>;
}
export type ClusterPluginPackageExecutorProcessResult =
@@ -116,6 +131,13 @@ export interface RunClusterPluginPackageExecutorProcessOptions {
pool: PostgresPool,
) => Promise<PostgresSchemaReadinessReport>;
readonly now?: () => number;
readonly createSecretActionController?: (
options: Parameters<
typeof createClusterPluginPackageKubernetesSecretActionController
>[0],
) => Promise<
Readonly<ClusterPluginPackageKubernetesSecretActionControllerResource>
>;
}
export class ClusterPluginPackageExecutorProcessConfigError extends TypeError {
@@ -131,6 +153,11 @@ export class ClusterPluginPackageExecutorProcessConfigError extends TypeError {
const SAFE_OWNER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SAFE_DISPATCH_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DNS_LABEL = /^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const DNS_NAME = /^(?=.{1,253}$)[a-z0-9](?:[-a-z0-9.]*[a-z0-9])?$/;
const SECRET_KEY = /^[A-Za-z0-9._-]{1,253}$/;
const IMAGE_DIGEST =
/^[a-z0-9](?:[a-z0-9._:/-]{0,510}[a-z0-9])?@sha256:[0-9a-f]{64}$/;
function enabledValue(
environment: ClusterPluginPackageExecutorProcessEnvironment,
@@ -307,6 +334,179 @@ function secretProjectionRoot(
return value;
}
function requiredEnvironment(
environment: ClusterPluginPackageExecutorProcessEnvironment,
name: string,
maximumLength: number,
): string {
const value = boundedValue(environment, name, maximumLength);
if (value === undefined) {
throw new ClusterPluginPackageExecutorProcessConfigError(
`${name} is required when the Kubernetes Secret action controller is enabled`,
);
}
return value;
}
function kubernetesSecretActions(
environment: ClusterPluginPackageExecutorProcessEnvironment,
): Readonly<{
enabled: true;
limit: number;
job: Readonly<PluginPackageKubernetesSecretActionJobOptions>;
}> | null {
const enabled = environment.QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_ENABLED;
if (enabled === undefined || enabled === '' || enabled === 'false') return null;
if (enabled !== 'true') {
throw new ClusterPluginPackageExecutorProcessConfigError(
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_ENABLED must be true or false',
);
}
const namespace =
boundedValue(environment, 'QL3_PLUGIN_PACKAGE_SECRET_ACTION_NAMESPACE', 63) ??
'qinglong3-system';
const serviceAccountName =
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_SERVICE_ACCOUNT',
63,
) ?? 'ql3-plugin-package-secret-action';
const sourceSecretName =
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_SOURCE_SECRET',
63,
) ?? 'ql3-cluster-plugin-package-values';
const image = requiredEnvironment(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_IMAGE',
640,
);
const postgresCaSecretName = requiredEnvironment(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_SECRET',
63,
);
const postgresCaKey =
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_CA_KEY',
253,
) ?? 'ca.crt';
const servername = requiredEnvironment(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_SERVERNAME',
253,
);
const urlSecretName = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_SECRET',
63,
);
const urlSecretKey = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_KEY',
253,
);
const fieldNames = [
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_AUTH_SECRET',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_HOST',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PORT',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_DATABASE',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_USERNAME_KEY',
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_PASSWORD_KEY',
] as const;
const fields = fieldNames.map((name) => boundedValue(environment, name, 253));
const urlMode = urlSecretName !== undefined || urlSecretKey !== undefined;
const fieldMode = fields.some((value) => value !== undefined);
if (
!DNS_LABEL.test(namespace) ||
!DNS_LABEL.test(serviceAccountName) ||
!DNS_LABEL.test(sourceSecretName) ||
!IMAGE_DIGEST.test(image) ||
!DNS_LABEL.test(postgresCaSecretName) ||
!SECRET_KEY.test(postgresCaKey) ||
!DNS_NAME.test(servername) ||
urlMode === fieldMode
) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'Kubernetes Secret action Job identity is invalid',
);
}
let connection: PluginPackageKubernetesSecretActionJobOptions['postgres']['connection'];
if (urlMode) {
if (
urlSecretName === undefined ||
urlSecretKey === undefined ||
!DNS_LABEL.test(urlSecretName) ||
!SECRET_KEY.test(urlSecretKey)
) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'Kubernetes Secret action PostgreSQL URL reference is invalid',
);
}
connection = Object.freeze({
mode: 'url',
secretName: urlSecretName,
urlKey: urlSecretKey,
});
} else {
if (fields.some((value) => value === undefined)) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'Kubernetes Secret action PostgreSQL field reference is incomplete',
);
}
const [authSecretName, host, portValue, database, usernameKey, passwordKey] =
fields as [string, string, string, string, string, string];
const port = Number(portValue);
if (
!DNS_LABEL.test(authSecretName) ||
!DNS_NAME.test(host) ||
!Number.isSafeInteger(port) ||
port < 1 ||
port > 65_535 ||
!/^[A-Za-z_][A-Za-z0-9_$-]{0,62}$/.test(database) ||
!SECRET_KEY.test(usernameKey) ||
!SECRET_KEY.test(passwordKey)
) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'Kubernetes Secret action PostgreSQL field reference is invalid',
);
}
connection = Object.freeze({
mode: 'fields',
authSecretName,
host,
port,
database,
usernameKey,
passwordKey,
});
}
return Object.freeze({
enabled: true,
limit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_CONTROLLER_LIMIT',
8,
1,
32,
),
job: Object.freeze({
namespace,
serviceAccountName,
sourceSecretName,
image,
postgres: Object.freeze({
connection,
caSecretName: postgresCaSecretName,
caKey: postgresCaKey,
servername,
}),
}),
});
}
export function loadClusterPluginPackageExecutorProcessConfig(
environment: ClusterPluginPackageExecutorProcessEnvironment,
): ClusterPluginPackageExecutorProcessConfig {
@@ -382,6 +582,7 @@ export function loadClusterPluginPackageExecutorProcessConfig(
),
dispatchId,
secretProjectionRoot: secretProjectionRoot(environment),
kubernetesSecretActions: kubernetesSecretActions(environment),
database: databaseConfig(environment),
});
}
@@ -424,6 +625,18 @@ function emptyApprovalSummary(): Readonly<{
});
}
function emptySecretActionJobSummary(): Readonly<PluginPackageKubernetesSecretActionControllerSummary> {
return Object.freeze({
scanned: 0,
created: 0,
existing: 0,
active: 0,
recoveryRequired: 0,
unavailable: 0,
truncated: false,
});
}
export async function runClusterPluginPackageExecutorProcess(
options: RunClusterPluginPackageExecutorProcessOptions,
): Promise<ClusterPluginPackageExecutorProcessResult> {
@@ -446,6 +659,8 @@ export async function runClusterPluginPackageExecutorProcess(
typeof options.createDispatcher !== 'function') ||
(options.assertReady !== undefined &&
typeof options.assertReady !== 'function') ||
(options.createSecretActionController !== undefined &&
typeof options.createSecretActionController !== 'function') ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new TypeError('Plugin Package executor process options are invalid');
@@ -464,6 +679,9 @@ export async function runClusterPluginPackageExecutorProcess(
});
const database = await openDatabase();
let failure: unknown;
let secretActionControllerResource:
| Readonly<ClusterPluginPackageKubernetesSecretActionControllerResource>
| undefined;
try {
const evidence = await (
options.assertReady ?? assertPostgresPackageExecutorSchemaReady
@@ -531,6 +749,7 @@ export async function runClusterPluginPackageExecutorProcess(
secretBindingApprovals: emptyApprovalSummary(),
secretBindingTransitionApprovals: emptyApprovalSummary(),
dispatch,
secretActionJobs: emptySecretActionJobSummary(),
}),
]),
});
@@ -559,6 +778,32 @@ export async function runClusterPluginPackageExecutorProcess(
limit: config.approvalBatchSize,
...(options.now ? { now: options.now } : {}),
});
let secretActionJobs = emptySecretActionJobSummary();
if (config.kubernetesSecretActions !== null) {
if (!secretActionControllerResource) {
const createController =
options.createSecretActionController ??
createClusterPluginPackageKubernetesSecretActionController;
secretActionControllerResource = await createController({
executions: new PostgresApprovedActionExecutionRepository(
database.pool,
),
bindingPlans:
new PostgresPluginPackageSecretBindingApprovalPlanReader(
database.pool,
),
transitionPlans:
new PostgresPluginPackageSecretBindingTransitionApprovalPlanReader(
database.pool,
),
job: config.kubernetesSecretActions.job,
...(options.now ? { now: options.now } : {}),
});
}
secretActionJobs = await secretActionControllerResource.controller.reconcile({
limit: config.kubernetesSecretActions.limit,
});
}
const dispatch = await dispatcher.dispatchBatch({
limit: config.dispatchBatchSize,
});
@@ -568,9 +813,10 @@ export async function runClusterPluginPackageExecutorProcess(
secretBindingApprovals,
secretBindingTransitionApprovals,
dispatch,
secretActionJobs,
});
batches.push(batch);
if (isIdleBatch(batch)) break;
if (secretActionJobs.scanned > 0 || isIdleBatch(batch)) break;
}
return Object.freeze({
status: 'completed',
@@ -581,16 +827,33 @@ export async function runClusterPluginPackageExecutorProcess(
failure = error;
throw error;
} finally {
let disposeError: unknown;
try {
secretActionControllerResource?.dispose();
} catch (error) {
disposeError = error;
}
let closeError: unknown;
try {
await database.close();
} catch (closeError) {
} catch (error) {
closeError = error;
}
const cleanupErrors = [disposeError, closeError].filter(
(error) => error !== undefined,
);
if (cleanupErrors.length > 0) {
if (failure !== undefined) {
throw new AggregateError(
[failure, closeError],
'Plugin Package executor process failed and PostgreSQL did not close',
[failure, ...cleanupErrors],
'Plugin Package executor process and resource cleanup failed',
);
}
throw closeError;
if (cleanupErrors.length === 1) throw cleanupErrors[0];
throw new AggregateError(
cleanupErrors,
'Plugin Package executor resource cleanup failed',
);
}
}
}
@@ -0,0 +1,403 @@
import type { ApprovedActionExecutionSnapshot } from '@qinglong/runtime-core/approved-action-execution';
import {
PLUGIN_PACKAGE_SECRET_BINDING_ACTION_TYPE,
type PluginPackageSecretBindingApprovalPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-approval-plan';
import {
PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_ACTION_TYPE,
type PluginPackageSecretBindingTransitionApprovalPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-approval-plan';
import {
createPluginPackageKubernetesSecretActionJob,
type PluginPackageKubernetesSecretActionJobOptions,
} from './pluginPackageKubernetesSecretActionJob';
const FIELD_MANAGER = 'qinglong-plugin-package-secret-action-controller';
const MAX_PAGE_SIZE = 32;
type SecretActionApprovalPlan =
| PluginPackageSecretBindingApprovalPlan
| PluginPackageSecretBindingTransitionApprovalPlan;
type JsonObject = Record<string, unknown>;
export interface PluginPackageSecretActionApprovalPlanReader<T> {
findByActionRef(actionRef: string): Promise<Readonly<T> | null>;
}
export interface PluginPackageKubernetesSecretActionExecutionReader {
listReconciliableExecutions(query: Readonly<{
nowMs: number;
limit: number;
actionTypes: readonly string[];
}>): Promise<Readonly<{
executions: readonly Readonly<ApprovedActionExecutionSnapshot>[];
truncated: boolean;
}>>;
}
export interface PluginPackageKubernetesSecretActionJobResource {
readonly metadata?: Readonly<{
name?: string;
namespace?: string;
labels?: Readonly<Record<string, string>>;
annotations?: Readonly<Record<string, string>>;
}>;
readonly spec?: Readonly<Record<string, unknown>>;
readonly status?: Readonly<{
conditions?: readonly Readonly<{
type?: string;
status?: string;
}>[];
}>;
}
export interface PluginPackageKubernetesSecretActionJobApi {
createNamespacedJob(
request: Readonly<{
namespace: string;
body: Readonly<Record<string, unknown>>;
fieldManager: typeof FIELD_MANAGER;
fieldValidation: 'Strict';
}>,
): Promise<PluginPackageKubernetesSecretActionJobResource>;
readNamespacedJob(
request: Readonly<{
name: string;
namespace: string;
}>,
): Promise<PluginPackageKubernetesSecretActionJobResource>;
}
export interface PluginPackageKubernetesSecretActionControllerOptions {
readonly executions: PluginPackageKubernetesSecretActionExecutionReader;
readonly bindingPlans: PluginPackageSecretActionApprovalPlanReader<PluginPackageSecretBindingApprovalPlan>;
readonly transitionPlans: PluginPackageSecretActionApprovalPlanReader<PluginPackageSecretBindingTransitionApprovalPlan>;
readonly jobs: PluginPackageKubernetesSecretActionJobApi;
readonly job: Readonly<PluginPackageKubernetesSecretActionJobOptions>;
readonly now?: () => number;
}
export interface PluginPackageKubernetesSecretActionControllerSummary {
readonly scanned: number;
readonly created: number;
readonly existing: number;
readonly active: number;
readonly recoveryRequired: number;
readonly unavailable: number;
readonly truncated: boolean;
}
export interface ClusterPluginPackageKubernetesSecretActionControllerResource {
readonly controller: PluginPackageKubernetesSecretActionController;
dispose(): void;
}
export class PluginPackageKubernetesSecretActionControllerConflictError extends Error {
readonly code = 'PLUGIN_PACKAGE_KUBERNETES_SECRET_ACTION_CONTROLLER_CONFLICT';
constructor() {
super('Kubernetes Secret action Job conflicts with the durable dispatch');
this.name = 'PluginPackageKubernetesSecretActionControllerConflictError';
}
}
export class PluginPackageKubernetesSecretActionControllerUnavailableError extends Error {
readonly code =
'PLUGIN_PACKAGE_KUBERNETES_SECRET_ACTION_CONTROLLER_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Kubernetes Secret action Job authority is unavailable', options);
this.name =
'PluginPackageKubernetesSecretActionControllerUnavailableError';
}
}
function apiStatus(error: unknown): number | null {
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 object(value: unknown): JsonObject | null {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as JsonObject)
: null;
}
function expectedSubset(expected: unknown, observed: unknown): boolean {
if (Array.isArray(expected)) {
return (
Array.isArray(observed) &&
expected.length === observed.length &&
expected.every((value, index) => expectedSubset(value, observed[index]))
);
}
const expectedObject = object(expected);
if (expectedObject) {
const observedObject = object(observed);
return (
observedObject !== null &&
Object.entries(expectedObject).every(([key, value]) =>
expectedSubset(value, observedObject[key]),
)
);
}
return Object.is(expected, observed);
}
function terminalStatus(
job: Readonly<PluginPackageKubernetesSecretActionJobResource>,
): 'active' | 'complete' | 'failed' {
const conditions = job.status?.conditions ?? [];
const complete = conditions.some(
(condition) => condition.type === 'Complete' && condition.status === 'True',
);
const failed = conditions.some(
(condition) => condition.type === 'Failed' && condition.status === 'True',
);
if (complete && failed) {
throw new PluginPackageKubernetesSecretActionControllerConflictError();
}
if (complete) return 'complete';
if (failed) return 'failed';
return 'active';
}
function assertObservedJob(
expected: Readonly<Record<string, unknown>>,
observed: Readonly<PluginPackageKubernetesSecretActionJobResource>,
): void {
const expectedMetadata = object(expected.metadata)!;
const expectedSpec = object(expected.spec)!;
const observedMetadata = observed.metadata;
const observedPodSpec = object(
object(object(observed.spec)?.template)?.spec,
);
if (
observedMetadata?.name !== expectedMetadata.name ||
observedMetadata?.namespace !== expectedMetadata.namespace ||
!expectedSubset(expectedMetadata.labels, observedMetadata?.labels) ||
!expectedSubset(expectedMetadata.annotations, observedMetadata?.annotations) ||
!expectedSubset(expectedSpec, observed.spec) ||
observedPodSpec === null ||
observedPodSpec.hostNetwork === true ||
observedPodSpec.hostPID === true ||
observedPodSpec.hostIPC === true ||
(Array.isArray(observedPodSpec.initContainers) &&
observedPodSpec.initContainers.length > 0) ||
(Array.isArray(observedPodSpec.ephemeralContainers) &&
observedPodSpec.ephemeralContainers.length > 0)
) {
throw new PluginPackageKubernetesSecretActionControllerConflictError();
}
}
export class PluginPackageKubernetesSecretActionController {
readonly #now: () => number;
constructor(
private readonly options: PluginPackageKubernetesSecretActionControllerOptions,
) {
if (
!options ||
typeof options !== 'object' ||
!options.executions ||
typeof options.executions.listReconciliableExecutions !== 'function' ||
!options.bindingPlans ||
typeof options.bindingPlans.findByActionRef !== 'function' ||
!options.transitionPlans ||
typeof options.transitionPlans.findByActionRef !== 'function' ||
!options.jobs ||
typeof options.jobs.createNamespacedJob !== 'function' ||
typeof options.jobs.readNamespacedJob !== 'function' ||
!options.job ||
typeof options.job !== 'object' ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new TypeError('Kubernetes Secret action controller options are invalid');
}
this.#now = options.now ?? Date.now;
}
async reconcile(
input: Readonly<{ limit?: number }> = {},
): Promise<Readonly<PluginPackageKubernetesSecretActionControllerSummary>> {
if (
!input ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).some((key) => key !== 'limit')
) {
throw new TypeError('Kubernetes Secret action reconciliation is invalid');
}
const limit = input.limit ?? 8;
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_SIZE) {
throw new RangeError('Kubernetes Secret action reconciliation limit is invalid');
}
const nowMs = this.#now();
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
throw new RangeError('Kubernetes Secret action controller clock is invalid');
}
let page;
try {
page = await this.options.executions.listReconciliableExecutions({
nowMs,
limit,
actionTypes: Object.freeze([
PLUGIN_PACKAGE_SECRET_BINDING_ACTION_TYPE,
PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_ACTION_TYPE,
]),
});
} catch (error) {
throw new PluginPackageKubernetesSecretActionControllerUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
const summary = {
scanned: page.executions.length,
created: 0,
existing: 0,
active: 0,
recoveryRequired: 0,
unavailable: 0,
truncated: page.truncated,
};
for (const snapshot of page.executions) {
try {
const result = await this.#reconcileOne(snapshot, nowMs);
summary[result] += 1;
} catch (error) {
if (
error instanceof
PluginPackageKubernetesSecretActionControllerConflictError
) {
throw error;
}
summary.unavailable += 1;
}
}
return Object.freeze({ ...summary });
}
async #reconcileOne(
snapshot: Readonly<ApprovedActionExecutionSnapshot>,
nowMs: number,
): Promise<'created' | 'existing' | 'active' | 'recoveryRequired'> {
const plan = await this.#plan(snapshot);
if (!plan) {
throw new PluginPackageKubernetesSecretActionControllerUnavailableError();
}
const desired = createPluginPackageKubernetesSecretActionJob({
dispatch: snapshot.dispatch,
approvalPlan: plan,
options: this.options.job,
});
const metadata = object(desired.metadata)!;
const name = metadata.name as string;
const namespace = metadata.namespace as string;
let observed: PluginPackageKubernetesSecretActionJobResource;
let disposition: 'created' | 'existing' | 'active' = 'active';
try {
observed = await this.options.jobs.readNamespacedJob({ name, namespace });
} catch (error) {
if (apiStatus(error) !== 404) throw error;
if (
snapshot.execution.status === 'executing' ||
nowMs > plan.expiresAtMs
) {
return 'recoveryRequired';
}
try {
observed = await this.options.jobs.createNamespacedJob({
namespace,
body: desired,
fieldManager: FIELD_MANAGER,
fieldValidation: 'Strict',
});
disposition = 'created';
} catch (createError) {
// CREATE may have succeeded even when its response was lost. Converge
// every ambiguous failure through an exact-name GET before surfacing it.
try {
observed = await this.options.jobs.readNamespacedJob({
name,
namespace,
});
} catch (readAfterCreateError) {
if (apiStatus(readAfterCreateError) === 404) throw createError;
throw readAfterCreateError;
}
disposition = 'existing';
}
}
assertObservedJob(desired, observed);
const terminal = terminalStatus(observed);
if (terminal !== 'active') return 'recoveryRequired';
return disposition;
}
#plan(
snapshot: Readonly<ApprovedActionExecutionSnapshot>,
): Promise<Readonly<SecretActionApprovalPlan> | null> {
const { actionType, actionRef } = snapshot.dispatch.action;
if (actionType === PLUGIN_PACKAGE_SECRET_BINDING_ACTION_TYPE) {
return this.options.bindingPlans.findByActionRef(actionRef);
}
if (actionType === PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_ACTION_TYPE) {
return this.options.transitionPlans.findByActionRef(actionRef);
}
return Promise.resolve(null);
}
}
type KubernetesModule = typeof import('@kubernetes/client-node', {
with: { 'resolution-mode': 'import' }
});
export async function createClusterPluginPackageKubernetesSecretActionController(
options: Omit<
PluginPackageKubernetesSecretActionControllerOptions,
'jobs'
>,
): Promise<Readonly<ClusterPluginPackageKubernetesSecretActionControllerResource>> {
const kubernetes = (await import('@kubernetes/client-node')) as KubernetesModule;
const config = new kubernetes.KubeConfig();
config.loadFromCluster();
const jobs = config.makeApiClient(
kubernetes.BatchV1Api,
) as unknown as PluginPackageKubernetesSecretActionJobApi;
let active = true;
return Object.freeze({
controller: new PluginPackageKubernetesSecretActionController({
...options,
jobs,
}),
dispose() {
if (!active) return;
active = false;
for (const user of config.getUsers()) {
const mutable = user as {
token?: string;
certData?: string;
keyData?: string;
};
mutable.token = '';
mutable.certData = '';
mutable.keyData = '';
}
config.setCurrentContext('disposed');
},
});
}