kubricate
Version:
A TypeScript framework for building reusable, type-safe Kubernetes infrastructure — without the YAML mess.
210 lines (190 loc) • 7.34 kB
text/typescript
import type { BaseProvider, SecretInjectionStrategy } from '@kubricate/core';
import type { BaseStack } from '../stack/BaseStack.js';
import type { FallbackIfNever } from '../types.js';
/**
* Extract only the strategy types allowed for this provider
*/
type ExtractAllowedKinds<Kinds extends SecretInjectionStrategy['kind'] = SecretInjectionStrategy['kind']> = Extract<
SecretInjectionStrategy,
{ kind: Kinds }
>;
/**
* SecretInjectionBuilder provides a fluent API to define how a secret should be injected into a resource.
*
* @example
* injector.secrets('MY_SECRET')
* .inject({ kind: 'env', containerIndex: 0 })
* .intoResource('my-deployment'); // Optional
*/
export class SecretInjectionBuilder<Kinds extends SecretInjectionStrategy['kind'] = SecretInjectionStrategy['kind']> {
private strategy?: SecretInjectionStrategy;
private resourceIdOverride?: string;
/**
* The injected name override (used when `.forName(...)` is called).
*
* This will appear in the final manifest, such as an env var name or volume mount name.
* If not provided, the original secretName will be used.
*/
private targetName?: string;
constructor(
private readonly stack: BaseStack,
private readonly secretName: string,
private readonly provider: BaseProvider,
private readonly ctx: { defaultResourceId?: string; secretManagerId: number; providerId: string }
) {}
/**
* Override the name to be injected into the target manifest.
*
* This is useful when the name used inside the resource (e.g., env var name)
* should differ from the registered secret name in the SecretManager.
*
* If not provided, the original secret name will be used.
*
* Example:
* .secrets('MY_SECRET').forName('API_KEY').inject({ kind: 'env' });
*
* Output:
* - name: API_KEY
* valueFrom:
* secretKeyRef:
* name: secret-application
* key: MY_SECRET
*
* @param name The name to use in the final manifest (e.g., environment variable name).
*/
forName(name: string): this {
this.targetName = name;
return this;
}
/**
* Define how this secret should be injected into the Kubernetes resource.
*
* 👉 You can call `.inject(strategy)` with a specific strategy, or use `.inject()` with no arguments
* if the provider only supports **one** strategy kind (e.g. `'env'`).
*
* This method is **type-safe** and enforces allowed `kind` values per provider via TypeScript inference.
*
* @example
* // Explicit strategy:
* injector.secrets('APP_SECRET').inject('env', { containerIndex: 0 });
*
* // Implicit (default strategy):
* injector.secrets('APP_SECRET').inject(); // uses first provider-supported default
*/
inject(): this;
inject(
kind?: ExtractAllowedKinds<Kinds>['kind'],
strategyOptions?: Omit<FallbackIfNever<ExtractAllowedKinds<Kinds>, SecretInjectionStrategy>, 'kind'>
): this;
inject(
kind?: ExtractAllowedKinds<Kinds>['kind'],
strategyOptions?: Omit<FallbackIfNever<ExtractAllowedKinds<Kinds>, SecretInjectionStrategy>, 'kind'>
): this {
if (kind === undefined) {
// no arguments provided
if (this.provider.supportedStrategies.length !== 1) {
throw new Error(
`[SecretInjectionBuilder] inject() requires a strategy because provider supports multiple strategies: ${this.provider.supportedStrategies.join(', ')}`
);
}
const defaultKind = this.provider.supportedStrategies[0];
this.strategy = this.resolveDefaultStrategy(defaultKind);
} else {
this.strategy = {
kind,
...strategyOptions,
} as SecretInjectionStrategy;
}
return this;
}
/**
* Resolves a default injection strategy based on the `kind` supported by the provider.
* This allows `.inject()` to be used without arguments when the provider supports exactly one kind.
*
* Each kind has its own defaults:
* - `env` → `{ kind: 'env', containerIndex: 0 }`
* - `imagePullSecret` → `{ kind: 'imagePullSecret' }`
* - `annotation` → `{ kind: 'annotation' }`
*
* If the kind is unsupported for defaulting, an error is thrown.
*/
resolveDefaultStrategy(kind: SecretInjectionStrategy['kind']): SecretInjectionStrategy {
let strategy: SecretInjectionStrategy;
if (kind === 'env') {
strategy = { kind: 'env', containerIndex: 0 };
} else if (kind === 'imagePullSecret') {
strategy = { kind: 'imagePullSecret' };
} else if (kind === 'annotation') {
strategy = { kind: 'annotation' };
} else {
throw new Error(`[SecretInjectionBuilder] inject() with no args is not implemented for kind="${kind}" yet`);
}
return strategy;
}
/**
* Explicitly define the resource ID that defined in the composer e.g. 'my-deployment', 'my-job' to inject into.
*/
intoResource(resourceId: string): this {
this.resourceIdOverride = resourceId;
return this;
}
/**
* Resolve and register the final injection into the stack.
* Should be called by SecretsInjectionContext after the injection chain ends.
*/
resolveInjection(): void {
if (!this.strategy) {
throw new Error(`No injection strategy defined for secret: ${this.secretName}`);
}
// resolve resourceId
const resourceId = this.resolveResourceId();
// Get the target path for this injection
const path = this.provider.getTargetPath(this.strategy);
// Register the injection into the stack
this.stack.registerSecretInjection({
provider: this.provider,
providerId: this.ctx.providerId,
resourceId,
path,
meta: {
secretName: this.secretName,
targetName: this.targetName ?? this.secretName,
},
});
}
/**
* Resolve which resource ID to inject into.
* Priority: .intoResource(...) > setDefaultResourceId(...) > infer from provider.targetKind
*/
private resolveResourceId(): string {
// Determine resourceId from override
if (this.resourceIdOverride) return this.resourceIdOverride;
// Determine resourceId from default
if (this.ctx.defaultResourceId) return this.ctx.defaultResourceId;
// Auto-resolve resourceId using targetKind
// This is the default behavior if no resourceId is provided
const kind = this.provider.targetKind;
const composer = this.stack.getComposer();
if (!composer) {
throw new Error(
`[SecretInjectionBuilder] No resource composer found in stack. ` +
`Make sure .from(...) is called before using .useSecrets(...)`
);
}
const helperMessage =
`Please specify a resourceId explicitly \n` +
` → Use .intoResource(...) to specify a resource ID explicitly,\n` +
` → or call setDefaultResourceId(...) in SecretsInjectionContext.`;
const resourceId = composer.findResourceIdsByKind(kind);
if (resourceId.length === 0) {
throw new Error(
`[SecretInjectionBuilder] Could not resolve resourceId from provider.targetKind="${kind}".\n` + helperMessage
);
} else if (resourceId.length > 1) {
throw new Error(
`[SecretInjectionBuilder] Multiple resourceIds found for provider.targetKind="${kind}".\n` + helperMessage
);
}
return resourceId[0];
}
}