import { waitUntil } from 'async-wait-until'; import * as core from '@actions/core'; import * as k8s from '@kubernetes/client-node'; import BuildParameters from '../../../build-parameters'; import OrchestratorLogger from '../../services/core/orchestrator-logger'; import { IncomingMessage } from 'node:http'; import GitHub from '../../../github'; class KubernetesStorage { public static async createPersistentVolumeClaim( buildParameters: BuildParameters, pvcName: string, kubeClient: k8s.CoreV1Api, namespace: string, ) { if (buildParameters.kubeVolume !== ``) { OrchestratorLogger.log(`Kube Volume was input was set ${buildParameters.kubeVolume} overriding ${pvcName}`); pvcName = buildParameters.kubeVolume; return; } const allPvc = (await kubeClient.listNamespacedPersistentVolumeClaim(namespace)).body.items; const pvcList = allPvc.map((x) => x.metadata?.name); OrchestratorLogger.log(`Current PVCs in namespace ${namespace}`); OrchestratorLogger.log(JSON.stringify(pvcList, undefined, 4)); if (pvcList.includes(pvcName)) { OrchestratorLogger.log(`pvc ${pvcName} already exists`); if (GitHub.githubInputEnabled) { core.setOutput('volume', pvcName); } return; } OrchestratorLogger.log(`Creating PVC ${pvcName} (does not exist)`); const result = await KubernetesStorage.createPVC(pvcName, buildParameters, kubeClient, namespace); await KubernetesStorage.handleResult(result, kubeClient, namespace, pvcName); } public static async getPVCPhase(kubeClient: k8s.CoreV1Api, name: string, namespace: string) { try { return (await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body.status?.phase; } catch (error) { core.error('Failed to get PVC phase'); core.error(JSON.stringify(error, undefined, 4)); throw error; } } public static async watchUntilPVCNotPending(kubeClient: k8s.CoreV1Api, name: string, namespace: string) { let checkCount = 0; try { OrchestratorLogger.log(`watch Until PVC Not Pending ${name} ${namespace}`); // Check if storage class uses WaitForFirstConsumer binding mode // If so, skip waiting - PVC will bind when pod is created let shouldSkipWait = false; try { const pvcBody = (await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body; const storageClassName = pvcBody.spec?.storageClassName; if (storageClassName) { const kubeConfig = new k8s.KubeConfig(); kubeConfig.loadFromDefault(); const storageV1Api = kubeConfig.makeApiClient(k8s.StorageV1Api); try { const sc = await storageV1Api.readStorageClass(storageClassName); const volumeBindingMode = sc.body.volumeBindingMode; if (volumeBindingMode === 'WaitForFirstConsumer') { OrchestratorLogger.log( `StorageClass "${storageClassName}" uses WaitForFirstConsumer binding mode. PVC will bind when pod is created. Skipping wait.`, ); shouldSkipWait = true; } } catch (scError) { // If we can't check the storage class, proceed with normal wait OrchestratorLogger.log( `Could not check storage class binding mode: ${scError}. Proceeding with normal wait.`, ); } } } catch (pvcReadError) { // If we can't read PVC, proceed with normal wait OrchestratorLogger.log( `Could not read PVC to check storage class: ${pvcReadError}. Proceeding with normal wait.`, ); } if (shouldSkipWait) { OrchestratorLogger.log(`Skipping PVC wait - will bind when pod is created`); return; } const initialPhase = await this.getPVCPhase(kubeClient, name, namespace); OrchestratorLogger.log(`Initial PVC phase: ${initialPhase}`); // Wait until PVC is NOT Pending (i.e., Bound or Available) await waitUntil( async () => { checkCount++; const phase = await this.getPVCPhase(kubeClient, name, namespace); // Log progress every 4 checks (every ~60 seconds) if (checkCount % 4 === 0) { OrchestratorLogger.log(`PVC ${name} still ${phase} (check ${checkCount})`); // Fetch and log PVC events for diagnostics try { const events = await kubeClient.listNamespacedEvent(namespace); const pvcEvents = events.body.items .filter((x) => x.involvedObject?.kind === 'PersistentVolumeClaim' && x.involvedObject?.name === name) .map((x) => ({ message: x.message || '', reason: x.reason || '', type: x.type || '', count: x.count || 0, })) .slice(-5); // Get last 5 events if (pvcEvents.length > 0) { OrchestratorLogger.log(`PVC Events: ${JSON.stringify(pvcEvents, undefined, 2)}`); // Check if event indicates WaitForFirstConsumer const waitForConsumerEvent = pvcEvents.find( (event) => event.reason === 'WaitForFirstConsumer' || event.message?.includes('waiting for first consumer'), ); if (waitForConsumerEvent) { OrchestratorLogger.log( `PVC is waiting for first consumer. This is normal for WaitForFirstConsumer storage classes. Proceeding without waiting.`, ); return true; // Exit wait loop - PVC will bind when pod is created } } } catch { // Ignore event fetch errors } } return phase !== 'Pending'; }, { timeout: 750000, intervalBetweenAttempts: 15000, }, ); const finalPhase = await this.getPVCPhase(kubeClient, name, namespace); OrchestratorLogger.log(`PVC phase after wait: ${finalPhase}`); if (finalPhase === 'Pending') { throw new Error(`PVC ${name} is still Pending after timeout`); } } catch (error: any) { core.error('Failed to watch PVC'); core.error(error.toString()); try { const pvcBody = (await kubeClient.readNamespacedPersistentVolumeClaim(name, namespace)).body; // Fetch PVC events for detailed diagnostics let pvcEvents: any[] = []; try { const events = await kubeClient.listNamespacedEvent(namespace); pvcEvents = events.body.items .filter((x) => x.involvedObject?.kind === 'PersistentVolumeClaim' && x.involvedObject?.name === name) .map((x) => ({ message: x.message || '', reason: x.reason || '', type: x.type || '', count: x.count || 0, })); } catch { // Ignore event fetch errors } // Check if storage class exists let storageClassInfo = ''; try { const storageClassName = pvcBody.spec?.storageClassName; if (storageClassName) { // Create StorageV1Api from default config const kubeConfig = new k8s.KubeConfig(); kubeConfig.loadFromDefault(); const storageV1Api = kubeConfig.makeApiClient(k8s.StorageV1Api); try { const sc = await storageV1Api.readStorageClass(storageClassName); storageClassInfo = `StorageClass "${storageClassName}" exists. Provisioner: ${ sc.body.provisioner || 'unknown' }`; } catch (scError: any) { storageClassInfo = scError.statusCode === 404 ? `StorageClass "${storageClassName}" does NOT exist! This is likely why the PVC is stuck in Pending.` : `Failed to check StorageClass "${storageClassName}": ${scError.message || scError}`; } } } catch (scCheckError) { // Ignore storage class check errors - not critical for diagnostics storageClassInfo = `Could not check storage class: ${scCheckError}`; } core.error( `PVC Body: ${JSON.stringify( { phase: pvcBody.status?.phase, conditions: pvcBody.status?.conditions, accessModes: pvcBody.spec?.accessModes, storageClassName: pvcBody.spec?.storageClassName, storageRequest: pvcBody.spec?.resources?.requests?.storage, }, undefined, 4, )}`, ); if (storageClassInfo) { core.error(storageClassInfo); } if (pvcEvents.length > 0) { core.error(`PVC Events: ${JSON.stringify(pvcEvents, undefined, 2)}`); } else { core.error('No PVC events found - this may indicate the storage provisioner is not responding'); } } catch { // Ignore PVC read errors } throw error; } } private static async createPVC( pvcName: string, buildParameters: BuildParameters, kubeClient: k8s.CoreV1Api, namespace: string, ) { const pvc = new k8s.V1PersistentVolumeClaim(); pvc.apiVersion = 'v1'; pvc.kind = 'PersistentVolumeClaim'; pvc.metadata = { name: pvcName, }; pvc.spec = { accessModes: ['ReadWriteOnce'], storageClassName: buildParameters.kubeStorageClass === '' ? 'standard' : buildParameters.kubeStorageClass, resources: { requests: { storage: buildParameters.kubeVolumeSize, }, }, }; const result = await kubeClient.createNamespacedPersistentVolumeClaim(namespace, pvc); return result; } private static async handleResult( result: { response: IncomingMessage; body: k8s.V1PersistentVolumeClaim }, kubeClient: k8s.CoreV1Api, namespace: string, pvcName: string, ) { const name = result.body.metadata?.name || ''; OrchestratorLogger.log(`PVC ${name} created`); await this.watchUntilPVCNotPending(kubeClient, name, namespace); OrchestratorLogger.log(`PVC ${name} is ready and not pending`); core.setOutput('volume', pvcName); } } export default KubernetesStorage;