9d9bc5f22f
- Fix handleRotate bug ignoring Result from rotateSealedSecret() - Fix dead code branch in useControllerHealth - Replace all `any` types with `unknown` + type guards - Delete unused functions/exports (452 lines removed) - Add 18 new test files covering all hooks, libs, and components - 233 tests passing, zero tsc errors, zero lint issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
205 lines
5.3 KiB
TypeScript
205 lines
5.3 KiB
TypeScript
/**
|
|
* SealedSecret Custom Resource Definition
|
|
*/
|
|
|
|
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
|
|
|
|
const { apiFactoryWithNamespace } = ApiProxy;
|
|
const { KubeObject } = K8s.cluster;
|
|
import { AsyncResult, Err, Ok, tryCatchAsync } from '../types';
|
|
import {
|
|
SealedSecretInterface,
|
|
SealedSecretScope,
|
|
SealedSecretSpec,
|
|
SealedSecretStatus,
|
|
} from '../types';
|
|
|
|
interface CRDVersion {
|
|
name: string;
|
|
storage?: boolean;
|
|
served?: boolean;
|
|
}
|
|
|
|
/**
|
|
* SealedSecret CRD class
|
|
* Represents a Bitnami Sealed Secret resource in the cluster
|
|
*/
|
|
export class SealedSecret extends KubeObject<SealedSecretInterface> {
|
|
/**
|
|
* Default API version (fallback)
|
|
*/
|
|
static readonly DEFAULT_VERSION = 'bitnami.com/v1alpha1';
|
|
|
|
/**
|
|
* Cached detected API version
|
|
*/
|
|
private static detectedVersion: string | null = null;
|
|
|
|
/**
|
|
* API endpoint for SealedSecret resources
|
|
* bitnami.com/v1alpha1/sealedsecrets
|
|
*/
|
|
static apiEndpoint = apiFactoryWithNamespace('bitnami.com', 'v1alpha1', 'sealedsecrets');
|
|
|
|
/**
|
|
* Class name used for Headlamp registration
|
|
*/
|
|
static get className(): string {
|
|
return 'SealedSecret';
|
|
}
|
|
|
|
/**
|
|
* Get the SealedSecret spec
|
|
*/
|
|
get spec(): SealedSecretSpec {
|
|
return this.jsonData.spec;
|
|
}
|
|
|
|
/**
|
|
* Get the SealedSecret status
|
|
*/
|
|
get status(): SealedSecretStatus | undefined {
|
|
return this.jsonData.status;
|
|
}
|
|
|
|
/**
|
|
* Get the scope of this SealedSecret (strict, namespace-wide, or cluster-wide)
|
|
*/
|
|
get scope(): SealedSecretScope {
|
|
const annotations = this.metadata.annotations || {};
|
|
|
|
if (annotations['sealedsecrets.bitnami.com/cluster-wide'] === 'true') {
|
|
return 'cluster-wide';
|
|
}
|
|
if (annotations['sealedsecrets.bitnami.com/namespace-wide'] === 'true') {
|
|
return 'namespace-wide';
|
|
}
|
|
return 'strict';
|
|
}
|
|
|
|
/**
|
|
* Get the count of encrypted keys
|
|
*/
|
|
get encryptedKeysCount(): number {
|
|
return Object.keys(this.spec.encryptedData || {}).length;
|
|
}
|
|
|
|
/**
|
|
* Check if the SealedSecret is synced
|
|
*/
|
|
get isSynced(): boolean {
|
|
const syncCondition = this.status?.conditions?.find(c => c.type === 'Synced');
|
|
return syncCondition?.status === 'True';
|
|
}
|
|
|
|
/**
|
|
* Get the sync status condition
|
|
*/
|
|
get syncCondition() {
|
|
return this.status?.conditions?.find(c => c.type === 'Synced');
|
|
}
|
|
|
|
/**
|
|
* Get the sync status message
|
|
*/
|
|
get syncMessage(): string {
|
|
const condition = this.syncCondition;
|
|
if (!condition) {
|
|
return 'Unknown';
|
|
}
|
|
// Ensure we always return a string, not an object
|
|
const message = condition.message || condition.reason || condition.status;
|
|
return String(message || 'Unknown');
|
|
}
|
|
|
|
/**
|
|
* Detect the API version available in the cluster
|
|
*
|
|
* Queries the SealedSecrets CRD to determine which API version is installed
|
|
* and preferred. Returns the storage version (the version used for persisting
|
|
* objects in etcd).
|
|
*
|
|
* @returns Result containing the API version string (e.g., "bitnami.com/v1alpha1")
|
|
*/
|
|
static async detectApiVersion(): AsyncResult<string, string> {
|
|
// Return cached version if available
|
|
if (this.detectedVersion) {
|
|
return Ok(this.detectedVersion);
|
|
}
|
|
|
|
const result = await tryCatchAsync(async () => {
|
|
// Query the CRD to get available versions using Headlamp's API proxy
|
|
const crd = await ApiProxy.request(
|
|
'/apis/apiextensions.k8s.io/v1/customresourcedefinitions/sealedsecrets.bitnami.com'
|
|
);
|
|
|
|
// Find the storage version (the version used for persistence)
|
|
const storageVersion = crd.spec?.versions?.find((v: CRDVersion) => v.storage === true);
|
|
|
|
if (storageVersion) {
|
|
const version = `${crd.spec.group}/${storageVersion.name}`;
|
|
this.detectedVersion = version;
|
|
return version;
|
|
}
|
|
|
|
// Fallback to first served version if no storage version found
|
|
const servedVersion = crd.spec?.versions?.find((v: CRDVersion) => v.served === true);
|
|
if (servedVersion) {
|
|
const version = `${crd.spec.group}/${servedVersion.name}`;
|
|
this.detectedVersion = version;
|
|
return version;
|
|
}
|
|
|
|
// Ultimate fallback to default
|
|
return this.DEFAULT_VERSION;
|
|
});
|
|
|
|
if (result.ok === false) {
|
|
return Err(result.error.message);
|
|
}
|
|
|
|
return Ok(result.value);
|
|
}
|
|
|
|
/**
|
|
* Get API endpoint with auto-detected version
|
|
*
|
|
* Automatically detects and uses the correct API version from the cluster.
|
|
* Falls back to default version (v1alpha1) if detection fails.
|
|
*
|
|
* @returns API endpoint configured with the detected version
|
|
*/
|
|
static async getApiEndpoint() {
|
|
const versionResult = await this.detectApiVersion();
|
|
|
|
if (versionResult.ok) {
|
|
const [group, version] = versionResult.value.split('/');
|
|
return apiFactoryWithNamespace(group, version, 'sealedsecrets');
|
|
}
|
|
|
|
// Fallback to default endpoint
|
|
return this.apiEndpoint;
|
|
}
|
|
|
|
/**
|
|
* Get the detected API version
|
|
*
|
|
* Returns the cached detected version or null if not yet detected.
|
|
*
|
|
* @returns The detected API version string or null
|
|
*/
|
|
static getDetectedVersion(): string | null {
|
|
return this.detectedVersion;
|
|
}
|
|
|
|
/**
|
|
* Clear the cached API version
|
|
*
|
|
* Forces re-detection on next call to detectApiVersion().
|
|
* Useful for refreshing after CRD updates.
|
|
*/
|
|
static clearVersionCache(): void {
|
|
this.detectedVersion = null;
|
|
}
|
|
}
|