UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

496 lines (439 loc) 15.6 kB
import { KubeObject, KubeObjectInterface } from '@kinvolk/headlamp-plugin/lib/k8s/cluster'; import { ClusterV1Condition, DeletionTimeoutsV1Beta1, DeletionTimeoutsV1Beta2, KubeReference, MetaV1Condition, ObjectMeta, ReadinessGate, } from './common'; const CLUSTERCLASS_API_GROUP = 'cluster.x-k8s.io'; const CLUSTERCLASS_CRD_NAME = 'clusterclasses.cluster.x-k8s.io'; /** * LocalObjectTemplate is a reference to a local resource template. */ export interface LocalObjectTemplate extends KubeReference { apiVersion: string; kind: string; name: string; /** * Namespace of the referent. * @deprecated v1beta2 removes namespace from object references to be GitOps-friendly. */ namespace?: string; } /** * TemplateRefWrapper unifies how template references are wrapped across API versions. * v1beta2 uses `templateRef`; v1beta1 uses `ref`. */ export interface TemplateRefWrapper { templateRef?: LocalObjectTemplate; // v1beta2 ref?: LocalObjectTemplate; // v1beta1 } export type TemplateOrReference = LocalObjectTemplate | TemplateRefWrapper; export function isTemplateRefWrapper( value: TemplateOrReference | LocalObjectTemplate | null | undefined ): value is TemplateRefWrapper { return !!value && typeof value === 'object' && ('templateRef' in value || 'ref' in value); } export function extractTemplateRef( value: TemplateOrReference | LocalObjectTemplate | null | undefined ): LocalObjectTemplate | undefined { if (!value) return undefined; if (isTemplateRefWrapper(value)) return value.templateRef || value.ref; return value as LocalObjectTemplate; } /** * Unhealthy condition (v1beta1). * Duration is a human-readable string, e.g. "5m". */ export interface UnhealthyConditionV1Beta1 { type: string; status: string; timeout?: string; } /** * Unhealthy condition (v1beta2). * Duration is expressed in seconds — all Duration fields were renamed with the * `Seconds` suffix in v1beta2 per the CAPI API conventions. * @see https://main.cluster-api.sigs.k8s.io/developer/providers/migrations/v1.10-to-v1.11 */ export interface UnhealthyConditionV1Beta2 { type: string; status: string; timeoutSeconds?: number; } /** * Health check configuration for a machine class (v1beta1). */ export interface MachineHealthCheckClassV1Beta1 { nodeStartupTimeout?: string; unhealthyConditions?: UnhealthyConditionV1Beta1[]; maxUnhealthy?: string; unhealthyRange?: string; remediationTemplate?: LocalObjectTemplate; } /** * Health check configuration for a machine class (v1beta2). * * Per CAPI docs this field exists on `controlPlane` and * `workers.machineDeployments[]` only. It is NOT present on * `workers.machinePools[]` in v1beta2. * * @see https://cluster-api.sigs.k8s.io/tasks/experimental-features/cluster-class/write-clusterclass */ export interface HealthCheckClassV1Beta2 { checks?: { nodeStartupTimeoutSeconds?: number; unhealthyNodeConditions?: UnhealthyConditionV1Beta2[]; unhealthyMachineConditions?: UnhealthyConditionV1Beta2[]; }; remediation?: { triggerIf?: { unhealthyLessThanOrEqualTo?: string; unhealthyInRange?: string; }; }; } export interface ControlPlaneClassV1Beta1 extends DeletionTimeoutsV1Beta1 { /** @deprecated use templateRef in v1beta2 */ ref?: LocalObjectTemplate; templateRef?: LocalObjectTemplate; metadata?: ObjectMeta; /** @deprecated wrapped in TemplateRefWrapper in v1beta2 */ machineInfrastructure?: LocalObjectTemplate; machineHealthCheck?: MachineHealthCheckClassV1Beta1; namingStrategy?: { template?: string }; readinessGates?: ReadinessGate[]; } export interface ControlPlaneClassV1Beta2 extends DeletionTimeoutsV1Beta2 { templateRef: LocalObjectTemplate; metadata?: ObjectMeta; machineInfrastructure?: TemplateRefWrapper; healthCheck?: HealthCheckClassV1Beta2; namingStrategy?: { template?: string }; readinessGates?: ReadinessGate[]; } export interface InfrastructureClassV1Beta1 { /** @deprecated use templateRef in v1beta2 */ ref?: LocalObjectTemplate; templateRef?: LocalObjectTemplate; } export interface InfrastructureClassV1Beta2 { templateRef: LocalObjectTemplate; } export type InfrastructureClass = InfrastructureClassV1Beta1 | InfrastructureClassV1Beta2; export type ControlPlaneClass = ControlPlaneClassV1Beta1 | ControlPlaneClassV1Beta2; export interface MachineDeploymentClassV1Beta1 extends DeletionTimeoutsV1Beta1 { class: string; template: { metadata?: ObjectMeta; bootstrap?: TemplateOrReference; infrastructure?: TemplateOrReference; }; machineHealthCheck?: MachineHealthCheckClassV1Beta1; failureDomain?: string; namingStrategy?: { template?: string }; minReadySeconds?: number; readinessGates?: ReadinessGate[]; } export interface MachineDeploymentClassV1Beta2 extends DeletionTimeoutsV1Beta2 { class: string; template?: { metadata?: ObjectMeta }; bootstrap?: TemplateRefWrapper; infrastructure?: TemplateRefWrapper; healthCheck?: HealthCheckClassV1Beta2; failureDomain?: string; namingStrategy?: { template?: string }; minReadySeconds?: number; readinessGates?: ReadinessGate[]; } export interface MachinePoolClassV1Beta1 extends DeletionTimeoutsV1Beta1 { class: string; template: { metadata?: ObjectMeta; bootstrap?: TemplateOrReference; infrastructure?: TemplateOrReference; }; machineHealthCheck?: MachineHealthCheckClassV1Beta1; failureDomains?: string[]; namingStrategy?: { template?: string }; minReadySeconds?: number; } export interface MachinePoolClassV1Beta2 extends DeletionTimeoutsV1Beta2 { class: string; template?: { metadata?: ObjectMeta }; bootstrap?: TemplateRefWrapper; infrastructure?: TemplateRefWrapper; // healthCheck is intentionally absent — CAPI v1beta2 does not define // health checks for workers.machinePools[]. failureDomains?: string[]; namingStrategy?: { template?: string }; minReadySeconds?: number; } export interface WorkersClassV1Beta1 { machineDeployments?: MachineDeploymentClassV1Beta1[]; machinePools?: MachinePoolClassV1Beta1[]; } export interface WorkersClassV1Beta2 { machineDeployments?: MachineDeploymentClassV1Beta2[]; machinePools?: MachinePoolClassV1Beta2[]; } export type MachineDeploymentClass = MachineDeploymentClassV1Beta1 | MachineDeploymentClassV1Beta2; export type MachinePoolClass = MachinePoolClassV1Beta1 | MachinePoolClassV1Beta2; export type WorkerClass = MachineDeploymentClass | MachinePoolClass; export type TemplateSource = | WorkerClass | InfrastructureClassV1Beta1 | InfrastructureClassV1Beta2 | ControlPlaneClassV1Beta1 | ControlPlaneClassV1Beta2; export function getTemplateReference( obj: TemplateSource | TemplateOrReference | null | undefined, key?: string ): LocalObjectTemplate | undefined { if (!obj) return undefined; if (key) { const container = obj as Record<string, unknown>; const template = container.template as Record<string, unknown> | undefined; const val = container[key] || template?.[key]; if (val) return extractTemplateRef(val as TemplateOrReference); } else { return extractTemplateRef(obj as TemplateOrReference); } return undefined; } export function getWorkerBootstrap(row: WorkerClass): LocalObjectTemplate | undefined { return getTemplateReference(row, 'bootstrap'); } export function getWorkerInfrastructure(row: WorkerClass): LocalObjectTemplate | undefined { return getTemplateReference(row, 'infrastructure'); } /** * Extracts normalised health check props from a worker class. * * - v1beta2 MachineDeployment: `healthCheck` (HealthCheckClassV1Beta2) * - v1beta1 MachineDeployment/MachinePool: `machineHealthCheck` (MachineHealthCheckClassV1Beta1) * - v1beta2 MachinePool: neither (not defined in the spec) */ export function getWorkerHealthChecks(row: WorkerClass) { return { healthCheck: 'healthCheck' in row ? (row as MachineDeploymentClassV1Beta2).healthCheck : undefined, machineHealthCheck: 'machineHealthCheck' in row ? (row as MachineDeploymentClassV1Beta1).machineHealthCheck : undefined, }; } /** * Extracts normalised health check props from a control plane class. * * - v1beta2: `healthCheck` (HealthCheckClassV1Beta2) * - v1beta1: `machineHealthCheck` (MachineHealthCheckClassV1Beta1) */ export function getControlPlaneHealthChecks(cp: ControlPlaneClass | null | undefined) { if (!cp) return { healthCheck: undefined, machineHealthCheck: undefined }; return { healthCheck: 'healthCheck' in cp ? (cp as ControlPlaneClassV1Beta2).healthCheck : undefined, machineHealthCheck: 'machineHealthCheck' in cp ? (cp as ControlPlaneClassV1Beta1).machineHealthCheck : undefined, }; } export interface ClusterClassVariableSchema { openAPIV3Schema: { type?: string; default?: unknown; description?: string; [key: string]: unknown; }; } export interface ClusterClassVariable { name: string; required: boolean; schema: ClusterClassVariableSchema; metadata?: { labels?: Record<string, string>; annotations?: Record<string, string>; }; } export interface JSONPatch { op: 'add' | 'replace' | 'remove'; path: string; value?: unknown; valueFrom?: { variable?: string; template?: string }; } export interface PatchDefinition { selector: { apiVersion: string; kind: string; matchResources: { controlPlane?: boolean; infrastructureCluster?: boolean; machineDeploymentClass?: { names?: string[] }; machinePoolClass?: { names?: string[] }; }; }; jsonPatches: JSONPatch[]; } export interface ExternalPatchDefinition { generateExtension?: string; validateExtension?: string; discoverVariableExtension?: string; settings?: Record<string, string>; } export interface ClusterClassPatch { name: string; description?: string; /** @deprecated renamed to enabledIf in v1beta2 */ enableIf?: string; /** CEL expression — v1beta2 rename of enableIf */ enabledIf?: string; definitions?: PatchDefinition[]; external?: ExternalPatchDefinition[]; } export interface ClusterClassStatusVariable { name: string; definitionsConflict: boolean; definitions?: Array<{ from: string; required: boolean; schema: ClusterClassVariableSchema; }>; } /** * ClusterClass status (v1beta1). * * `v1beta2` sub-object is a Phase-1 preview of coming metav1.Condition fields * backported into v1beta1. Its presence does NOT indicate the object is v1beta2. */ export interface ClusterClassStatusV1Beta1 { observedGeneration?: number; variables?: ClusterClassStatusVariable[]; conditions?: ClusterV1Condition[]; v1beta2?: { conditions?: MetaV1Condition[] }; } /** * ClusterClass status (v1beta2). * * The `deprecated` key is the canonical discriminator — identical to how * MachinePool and all other CAPI resources signal v1beta2 status (Phase 2). */ export interface ClusterClassStatusV1Beta2 { observedGeneration?: number; variables?: ClusterClassStatusVariable[]; conditions?: MetaV1Condition[]; /** * Deprecated fields kept for down-conversion. * Presence of this key is the canonical discriminator for v1beta2 objects. */ deprecated?: { v1beta1?: { conditions?: ClusterV1Condition[] } }; } export interface ClusterClassSpecV1Beta1 { availabilityGates?: ReadinessGate[]; infrastructure?: InfrastructureClassV1Beta1; /** @deprecated use infrastructure.namingStrategy in v1beta2 */ infrastructureNamingStrategy?: { template?: string }; controlPlane?: ControlPlaneClassV1Beta1; workers?: WorkersClassV1Beta1; variables?: ClusterClassVariable[]; patches?: ClusterClassPatch[]; } export interface ClusterClassSpecV1Beta2 { availabilityGates?: ReadinessGate[]; infrastructure?: InfrastructureClassV1Beta2; controlPlane?: ControlPlaneClassV1Beta2; workers?: WorkersClassV1Beta2; variables?: ClusterClassVariable[]; patches?: ClusterClassPatch[]; } export interface ClusterApiClusterClassV1Beta1 extends KubeObjectInterface { spec?: ClusterClassSpecV1Beta1; status?: ClusterClassStatusV1Beta1; } export interface ClusterApiClusterClassV1Beta2 extends KubeObjectInterface { spec?: ClusterClassSpecV1Beta2; status?: ClusterClassStatusV1Beta2; } export type ClusterApiClusterClass = ClusterApiClusterClassV1Beta1 | ClusterApiClusterClassV1Beta2; /** * Returns true when the status belongs to a v1beta2 ClusterClass. * * Uses the `deprecated` key sentinel — same pattern applied to every CAPI * resource in the v1beta2 status migration. */ function isV1Beta2ClusterClassStatus( status: ClusterClassStatusV1Beta1 | ClusterClassStatusV1Beta2 ): status is ClusterClassStatusV1Beta2 { return 'deprecated' in status; } /** * Returns normalised conditions for a ClusterClass. * * Resolution order: * 1. v1beta2 → status.conditions (MetaV1Condition[]) * 2. v1beta1 → status.v1beta2.conditions (MetaV1Condition[], Phase-1 preview) * 3. v1beta1 → status.conditions (ClusterV1Condition[]) */ export function getClusterClassConditions( item: ClusterApiClusterClass | null | undefined ): MetaV1Condition[] | ClusterV1Condition[] | undefined { const status = item?.status; if (!status) return undefined; if (isV1Beta2ClusterClassStatus(status)) return status.conditions; if (status.v1beta2?.conditions?.length) return status.v1beta2.conditions; return status.conditions; } export function getClusterClassInfrastructureRef( item: ClusterApiClusterClass | null | undefined ): LocalObjectTemplate | undefined { return getTemplateReference(item?.spec?.infrastructure); } export function getClusterClassControlPlaneRef( item: ClusterApiClusterClass | null | undefined ): LocalObjectTemplate | undefined { return getTemplateReference(item?.spec?.controlPlane); } export function getClusterClassControlPlaneMachineInfraRef( item: ClusterApiClusterClass | null | undefined ): LocalObjectTemplate | undefined { return getTemplateReference(item?.spec?.controlPlane, 'machineInfrastructure'); } /** * ClusterClass KubeObject implementation. * @see https://cluster-api.sigs.k8s.io/tasks/experimental-features/cluster-class/ */ export class ClusterClass extends KubeObject<ClusterApiClusterClass> { static readonly apiName = 'clusterclasses'; static apiVersion = `${CLUSTERCLASS_API_GROUP}/v1beta1`; static readonly isNamespaced = true; static readonly kind = 'ClusterClass'; static readonly crdName = CLUSTERCLASS_CRD_NAME; static get detailsRoute() { return '/cluster-api/clusterclasses/:namespace/:name'; } static withApiVersion(version: string): typeof ClusterClass { const versionedClass = class extends ClusterClass {} as typeof ClusterClass; versionedClass.apiVersion = `${CLUSTERCLASS_API_GROUP}/${version}`; return versionedClass; } get spec(): ClusterClassSpecV1Beta1 | ClusterClassSpecV1Beta2 | undefined { return this.jsonData.spec; } get status(): ClusterClassStatusV1Beta1 | ClusterClassStatusV1Beta2 | undefined { return this.jsonData.status; } get conditions(): MetaV1Condition[] | ClusterV1Condition[] | undefined { return getClusterClassConditions(this.jsonData); } get infrastructureRef(): LocalObjectTemplate | undefined { return getClusterClassInfrastructureRef(this.jsonData); } get controlPlaneRef(): LocalObjectTemplate | undefined { return getClusterClassControlPlaneRef(this.jsonData); } get controlPlaneMachineInfraRef(): LocalObjectTemplate | undefined { return getClusterClassControlPlaneMachineInfraRef(this.jsonData); } }