unity-builder/src/model/cloud-runner/k8s/index.ts

198 lines
7.2 KiB
TypeScript
Raw Normal View History

2021-05-23 13:31:02 +00:00
import * as k8s from '@kubernetes/client-node';
2021-09-22 20:05:21 +00:00
import { BuildParameters } from '../..';
2021-05-28 17:54:54 +00:00
import * as core from '@actions/core';
import { CloudRunnerProviderInterface } from '../services/cloud-runner-provider-interface';
import CloudRunnerSecret from '../services/cloud-runner-secret';
2021-06-18 19:52:07 +00:00
import KubernetesStorage from './kubernetes-storage';
import CloudRunnerEnvironmentVariable from '../services/cloud-runner-environment-variable';
2021-12-29 17:25:38 +00:00
import KubernetesTaskRunner from './kubernetes-task-runner';
import KubernetesSecret from './kubernetes-secret';
2021-06-26 04:01:43 +00:00
import waitUntil from 'async-wait-until';
2021-06-26 01:50:03 +00:00
import KubernetesJobSpecFactory from './kubernetes-job-spec-factory';
2021-07-13 00:28:16 +00:00
import KubernetesServiceAccount from './kubernetes-service-account';
import CloudRunnerLogger from '../services/cloud-runner-logger';
2021-12-29 17:25:38 +00:00
import { CoreV1Api } from '@kubernetes/client-node';
2021-08-17 20:09:42 +00:00
class Kubernetes implements CloudRunnerProviderInterface {
private kubeConfig: k8s.KubeConfig;
2021-06-06 19:39:06 +00:00
private kubeClient: k8s.CoreV1Api;
private kubeClientBatch: k8s.BatchV1Api;
private buildGuid: string = '';
2021-06-06 19:39:06 +00:00
private buildParameters: BuildParameters;
2021-06-06 21:22:22 +00:00
private pvcName: string = '';
private secretName: string = '';
private jobName: string = '';
2021-06-06 19:39:06 +00:00
private namespace: string;
2021-06-06 20:14:12 +00:00
private podName: string = '';
private containerName: string = '';
2021-06-26 01:50:03 +00:00
private cleanupCronJobName: string = '';
2021-07-13 00:28:16 +00:00
private serviceAccountName: string = '';
constructor(buildParameters: BuildParameters) {
2021-06-26 01:50:03 +00:00
this.kubeConfig = new k8s.KubeConfig();
this.kubeConfig.loadFromDefault();
this.kubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api);
this.kubeClientBatch = this.kubeConfig.makeApiClient(k8s.BatchV1Api);
2021-09-21 18:27:04 +00:00
CloudRunnerLogger.log('Loaded default Kubernetes configuration for this environment');
2021-05-23 13:31:02 +00:00
2021-06-06 21:22:22 +00:00
this.namespace = 'default';
this.buildParameters = buildParameters;
}
2021-12-25 20:10:12 +00:00
public async setupSharedResources(
buildGuid: string,
buildParameters: BuildParameters,
// eslint-disable-next-line no-unused-vars
branchName: string,
// eslint-disable-next-line no-unused-vars
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
) {
2021-06-26 01:54:37 +00:00
try {
2021-09-08 20:21:15 +00:00
this.pvcName = `unity-builder-pvc-${buildGuid}`;
this.cleanupCronJobName = `unity-builder-cronjob-${buildGuid}`;
this.serviceAccountName = `service-account-${buildGuid}`;
2021-06-26 01:54:37 +00:00
await KubernetesStorage.createPersistentVolumeClaim(
buildParameters,
this.pvcName,
this.kubeClient,
this.namespace,
);
2021-07-13 00:28:16 +00:00
await KubernetesServiceAccount.createServiceAccount(this.serviceAccountName, this.namespace, this.kubeClient);
2021-06-26 01:54:37 +00:00
} catch (error) {
throw error;
}
}
2021-12-25 20:10:12 +00:00
async runTask(
buildGuid: string,
image: string,
2021-12-29 16:28:42 +00:00
commands: string,
mountdir: string,
workingdir: string,
2021-08-17 22:13:46 +00:00
environment: CloudRunnerEnvironmentVariable[],
secrets: CloudRunnerSecret[],
2021-12-30 20:25:28 +00:00
): Promise<string> {
try {
// setup
this.buildGuid = buildGuid;
this.secretName = `build-credentials-${buildGuid}`;
this.jobName = `unity-builder-job-${buildGuid}`;
2021-09-15 03:35:57 +00:00
this.containerName = `main`;
await KubernetesSecret.createSecret(secrets, this.secretName, this.namespace, this.kubeClient);
2021-06-26 01:50:03 +00:00
const jobSpec = KubernetesJobSpecFactory.getJobSpec(
commands,
image,
mountdir,
workingdir,
environment,
2021-12-29 20:49:13 +00:00
secrets,
this.buildGuid,
2021-06-26 01:50:03 +00:00
this.buildParameters,
this.secretName,
this.pvcName,
this.jobName,
k8s,
);
//run
const jobResult = await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec);
2022-01-30 18:55:49 +00:00
CloudRunnerLogger.log(`Creating build job ${JSON.stringify(jobResult.body.metadata, undefined, 4)}`);
2022-01-30 16:00:03 +00:00
2021-12-29 22:39:09 +00:00
await new Promise((promise) => setTimeout(promise, 5000));
2021-09-21 18:27:04 +00:00
CloudRunnerLogger.log('Job created');
2021-12-29 17:25:38 +00:00
this.setPodNameAndContainerName(await Kubernetes.findPodFromJob(this.kubeClient, this.jobName, this.namespace));
2021-09-21 18:27:04 +00:00
CloudRunnerLogger.log('Watching pod until running');
2021-12-30 21:00:38 +00:00
let output = '';
// eslint-disable-next-line no-constant-condition
while (true) {
try {
await KubernetesTaskRunner.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace);
CloudRunnerLogger.log('Pod running, streaming logs');
output = await KubernetesTaskRunner.runTask(
this.kubeConfig,
this.kubeClient,
this.jobName,
this.podName,
'main',
this.namespace,
CloudRunnerLogger.log,
);
break;
} catch (error: any) {
if (error.message.includes(`HTTP`)) {
continue;
} else {
throw error;
}
}
}
await this.cleanupTaskResources();
2021-12-30 20:25:28 +00:00
return output;
} catch (error) {
2021-09-21 18:27:04 +00:00
CloudRunnerLogger.log('Running job failed');
core.error(JSON.stringify(error, undefined, 4));
await this.cleanupTaskResources();
2021-06-06 20:10:01 +00:00
throw error;
}
}
2021-06-06 21:22:22 +00:00
setPodNameAndContainerName(pod: k8s.V1Pod) {
this.podName = pod.metadata?.name || '';
this.containerName = pod.status?.containerStatuses?.[0].name || '';
2021-05-28 17:37:30 +00:00
}
async cleanupTaskResources() {
2021-09-21 18:27:04 +00:00
CloudRunnerLogger.log('cleaning up');
try {
await this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace);
await this.kubeClient.deleteNamespacedPod(this.podName, this.namespace);
await this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace);
2022-01-30 21:11:13 +00:00
await new Promise((promise) => setTimeout(promise, 5000));
} catch (error) {
CloudRunnerLogger.log('Failed to cleanup, error:');
core.error(JSON.stringify(error, undefined, 4));
CloudRunnerLogger.log('Abandoning cleanup, build error:');
throw error;
}
2021-06-26 04:01:43 +00:00
try {
await waitUntil(
2022-01-30 15:41:45 +00:00
async () => {
const jobBody = (await this.kubeClientBatch.readNamespacedJob(this.jobName, this.namespace)).body;
2022-01-30 18:47:54 +00:00
const podBody = (await this.kubeClient.readNamespacedPod(this.podName, this.namespace)).body;
return (jobBody === null || jobBody.status?.active === 0) && podBody === null;
2022-01-30 15:41:45 +00:00
},
2021-06-26 04:01:43 +00:00
{
timeout: 500000,
intervalBetweenAttempts: 15000,
},
);
2022-01-12 23:15:24 +00:00
// eslint-disable-next-line no-empty
} catch {}
}
2021-12-25 20:10:12 +00:00
async cleanupSharedResources(
// eslint-disable-next-line no-unused-vars
buildGuid: string,
// eslint-disable-next-line no-unused-vars
buildParameters: BuildParameters,
// eslint-disable-next-line no-unused-vars
branchName: string,
// eslint-disable-next-line no-unused-vars
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
) {
2022-01-30 18:47:54 +00:00
CloudRunnerLogger.log(`deleting PVC`);
2022-01-31 21:03:44 +00:00
await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace);
2022-01-30 21:09:58 +00:00
CloudRunnerLogger.log(`deleted PVC`);
}
2022-01-31 21:03:44 +00:00
2021-12-29 17:25:38 +00:00
static async findPodFromJob(kubeClient: CoreV1Api, jobName: string, namespace: string) {
const namespacedPods = await kubeClient.listNamespacedPod(namespace);
const pod = namespacedPods.body.items.find((x) => x.metadata?.labels?.['job-name'] === jobName);
if (pod === undefined) {
throw new Error("pod with job-name label doesn't exist");
}
return pod;
}
}
export default Kubernetes;