mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-13 17:33:54 -07:00
9d475434d3
* Rename "Cloud Runner" to "Orchestrator" across entire codebase Breaking change: All CloudRunner classes, options, environment variables, and action.yml inputs have been renamed to Orchestrator equivalents. - Renamed src/model/cloud-runner/ directory to src/model/orchestrator/ - Renamed all cloud-runner-* files to orchestrator-* - Renamed all CloudRunner* classes to Orchestrator* (15+ classes) - Renamed all cloudRunner* properties to orchestrator* equivalents - Renamed CLOUD_RUNNER_* env vars to ORCHESTRATOR_* - Updated action.yml [CloudRunner] markers to [Orchestrator] - Updated workflow files and package.json test scripts - Updated all runtime strings (cache paths, log messages, branch refs) - Rebuilt dist/index.js No backward compatibility layer is provided. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove tracked log/temp files and add to .gitignore Remove $LOG_FILE and temp/job-log.txt debug artifacts that should not be in the repository. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
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;
|