Refactor, init k8s pvc build id separately

pull/273/head
Frostebite 2021-06-20 00:06:44 +01:00
parent 5a7ed829ac
commit d3a99ff1b3
9 changed files with 414 additions and 250 deletions

337
dist/index.js vendored
View File

@ -858,7 +858,7 @@ class AWSBuildEnvironment {
constructor(buildParameters) {
this.stackName = buildParameters.awsStackName;
}
CleanupSharedBuildResources(
cleanupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid,
// eslint-disable-next-line no-unused-vars
@ -869,7 +869,7 @@ class AWSBuildEnvironment {
defaultSecretsArray) {
throw new Error('Method not implemented.');
}
SetupSharedBuildResources(
setupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid,
// eslint-disable-next-line no-unused-vars
@ -1276,11 +1276,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
const k8s = __importStar(__webpack_require__(89679));
const core = __importStar(__webpack_require__(42186));
const client_node_1 = __webpack_require__(89679);
const stream_1 = __webpack_require__(92413);
const async_wait_until_1 = __webpack_require__(41299);
const kubernetes_storage_1 = __importDefault(__webpack_require__(38941));
const base64 = __webpack_require__(85848);
const kubernetes_logging_1 = __importDefault(__webpack_require__(37290));
const kubernetes_secret_1 = __importDefault(__webpack_require__(35129));
const kubernetes_utils_1 = __importDefault(__webpack_require__(92040));
class Kubernetes {
constructor(buildParameters) {
this.buildId = '';
@ -1300,84 +1299,44 @@ class Kubernetes {
this.namespace = 'default';
this.buildParameters = buildParameters;
}
CleanupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid,
// eslint-disable-next-line no-unused-vars
buildParameters,
// eslint-disable-next-line no-unused-vars
branchName,
// eslint-disable-next-line no-unused-vars
defaultSecretsArray) {
return __awaiter(this, void 0, void 0, function* () {
yield this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace);
});
}
SetupSharedBuildResources(buildUid, buildParameters,
setupSharedBuildResources(buildUid, buildParameters,
// eslint-disable-next-line no-unused-vars
branchName,
// eslint-disable-next-line no-unused-vars
defaultSecretsArray) {
return __awaiter(this, void 0, void 0, function* () {
const pvcName = `unity-builder-pvc-${buildUid}`;
this.pvcName = pvcName;
yield kubernetes_storage_1.default.createPersistentVolumeClaim(buildParameters, this.pvcName, this.kubeClient, this.namespace);
});
}
runBuildTask(buildId, image, commands, mountdir, workingdir, environment, secrets) {
return __awaiter(this, void 0, void 0, function* () {
try {
this.setUniqueBuildId(buildId);
// setup
yield this.createSecret(secrets);
this.buildId = buildId;
this.secretName = `build-credentials-${buildId}`;
this.jobName = `unity-builder-job-${buildId}`;
yield kubernetes_secret_1.default.createSecret(secrets, this.secretName, this.namespace, this.kubeClient);
yield kubernetes_storage_1.default.createPersistentVolumeClaim(this.buildParameters, this.pvcName, this.kubeClient, this.namespace);
//run
const jobSpec = this.getJobSpec(commands, image, mountdir, workingdir, environment);
//run
core.info('Creating build job');
yield this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec);
core.info('Job created');
yield kubernetes_storage_1.default.watchUntilPVCNotPending(this.kubeClient, this.pvcName, this.namespace);
core.info('PVC Bound');
this.setPodNameAndContainerName(yield this.findPod());
this.setPodNameAndContainerName(yield kubernetes_utils_1.default.findPodFromJob(this.kubeClient, this.jobName, this.namespace));
core.info('Watching pod until running');
yield this.watchUntilPodRunning();
yield kubernetes_utils_1.default.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace);
core.info('Pod running, streaming logs');
yield this.streamLogs();
yield this.cleanup();
yield kubernetes_logging_1.default.streamLogs(this.kubeConfig, this.kubeClient, this.jobName, this.podName, this.containerName, this.namespace, core.info);
yield this.cleanupTaskResources();
}
catch (error) {
core.info('Running job failed');
core.error(JSON.stringify(error, undefined, 4));
yield this.cleanup();
throw error;
}
});
}
setUniqueBuildId(buildId) {
const pvcName = `unity-builder-pvc-${buildId}`;
const secretName = `build-credentials-${buildId}`;
const jobName = `unity-builder-job-${buildId}`;
this.buildId = buildId;
this.pvcName = pvcName;
this.secretName = secretName;
this.jobName = jobName;
}
createSecret(secrets) {
return __awaiter(this, void 0, void 0, function* () {
const secret = new k8s.V1Secret();
secret.apiVersion = 'v1';
secret.kind = 'Secret';
secret.type = 'Opaque';
secret.metadata = {
name: this.secretName,
};
secret.data = {};
for (const buildSecret of secrets) {
secret.data[buildSecret.EnvironmentVariable] = base64.encode(buildSecret.ParameterValue);
secret.data[`${buildSecret.EnvironmentVariable}_NAME`] = base64.encode(buildSecret.ParameterKey);
}
try {
yield this.kubeClient.createNamespacedSecret(this.namespace, secret);
}
catch (error) {
yield this.cleanupTaskResources();
throw error;
}
});
@ -1509,46 +1468,90 @@ class Kubernetes {
};
return job;
}
findPod() {
return __awaiter(this, void 0, void 0, function* () {
const pod = (yield this.kubeClient.listNamespacedPod(this.namespace)).body.items.find((x) => { var _a, _b; return ((_b = (_a = x.metadata) === null || _a === void 0 ? void 0 : _a.labels) === null || _b === void 0 ? void 0 : _b['job-name']) === this.jobName; });
if (pod === undefined) {
throw new Error("pod with job-name label doesn't exist");
}
return pod;
});
}
watchUntilPodRunning() {
return __awaiter(this, void 0, void 0, function* () {
let success = false;
core.info(`Watching ${this.podName} ${this.namespace}`);
yield async_wait_until_1.waitUntil(() => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const phase = (_b = (_a = (yield this.kubeClient.readNamespacedPodStatus(this.podName, this.namespace))) === null || _a === void 0 ? void 0 : _a.body.status) === null || _b === void 0 ? void 0 : _b.phase;
success = phase === 'Running';
if (success || phase !== 'Pending')
return true;
return false;
}), {
timeout: 500000,
intervalBetweenAttempts: 15000,
});
return success;
});
}
setPodNameAndContainerName(pod) {
var _a, _b, _c;
this.podName = ((_a = pod.metadata) === null || _a === void 0 ? void 0 : _a.name) || '';
this.containerName = ((_c = (_b = pod.status) === null || _b === void 0 ? void 0 : _b.containerStatuses) === null || _c === void 0 ? void 0 : _c[0].name) || '';
}
streamLogs() {
cleanupTaskResources() {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Streaming logs from pod: ${this.podName} container: ${this.containerName} namespace: ${this.namespace}`);
core.info('cleaning up');
try {
yield this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace);
yield this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace);
}
catch (error) {
core.info('Failed to cleanup, error:');
core.error(JSON.stringify(error, undefined, 4));
core.info('Abandoning cleanup, build error:');
}
});
}
cleanupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid,
// eslint-disable-next-line no-unused-vars
buildParameters,
// eslint-disable-next-line no-unused-vars
branchName,
// eslint-disable-next-line no-unused-vars
defaultSecretsArray) {
return __awaiter(this, void 0, void 0, function* () {
yield this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace);
});
}
}
exports.default = Kubernetes;
/***/ }),
/***/ 37290:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const client_node_1 = __webpack_require__(89679);
const stream_1 = __webpack_require__(92413);
const core = __importStar(__webpack_require__(42186));
class KubernetesLogging {
static streamLogs(kubeConfig, kubeClient, jobName, podName, containerName, namespace, logCallback) {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Streaming logs from pod: ${podName} container: ${containerName} namespace: ${namespace}`);
const stream = new stream_1.Writable();
let didStreamAnyLogs = false;
stream._write = (chunk, encoding, next) => {
didStreamAnyLogs = true;
core.info(chunk.toString());
logCallback(chunk.toString());
next();
};
const logOptions = {
@ -1557,16 +1560,16 @@ class Kubernetes {
previous: false,
};
try {
const resultError = yield new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { return new client_node_1.Log(this.kubeConfig).log(this.namespace, this.podName, this.containerName, stream, resolve, logOptions); }));
const resultError = yield new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { return new client_node_1.Log(kubeConfig).log(namespace, podName, containerName, stream, resolve, logOptions); }));
if (resultError) {
throw resultError;
}
if (!didStreamAnyLogs) {
throw new Error(JSON.stringify({
message: 'Failed to stream any logs, listing namespace events, check for an error with the container',
events: (yield this.kubeClient.listNamespacedEvent(this.namespace)).body.items
events: (yield kubeClient.listNamespacedEvent(namespace)).body.items
.filter((x) => {
return x.involvedObject.name === this.podName || x.involvedObject.name === this.jobName;
return x.involvedObject.name === podName || x.involvedObject.name === jobName;
})
.map((x) => {
return {
@ -1584,22 +1587,73 @@ class Kubernetes {
core.info('end of log stream');
});
}
cleanup() {
}
exports.default = KubernetesLogging;
/***/ }),
/***/ 35129:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const k8s = __importStar(__webpack_require__(89679));
const base64 = __webpack_require__(85848);
class KubernetesSecret {
static createSecret(secrets, secretName, namespace, kubeClient) {
return __awaiter(this, void 0, void 0, function* () {
core.info('cleaning up');
const secret = new k8s.V1Secret();
secret.apiVersion = 'v1';
secret.kind = 'Secret';
secret.type = 'Opaque';
secret.metadata = {
name: secretName,
};
secret.data = {};
for (const buildSecret of secrets) {
secret.data[buildSecret.EnvironmentVariable] = base64.encode(buildSecret.ParameterValue);
secret.data[`${buildSecret.EnvironmentVariable}_NAME`] = base64.encode(buildSecret.ParameterKey);
}
try {
yield this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace);
yield this.kubeClient.deleteNamespacedSecret(this.secretName, this.namespace);
yield kubeClient.createNamespacedSecret(namespace, secret);
}
catch (error) {
core.info('Failed to cleanup, error:');
core.error(JSON.stringify(error, undefined, 4));
core.info('Abandoning cleanup, build error:');
throw error;
}
});
}
}
exports.default = Kubernetes;
exports.default = KubernetesSecret;
/***/ }),
@ -1692,6 +1746,79 @@ class KubernetesStorage {
exports.default = KubernetesStorage;
/***/ }),
/***/ 92040:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const async_wait_until_1 = __importDefault(__webpack_require__(41299));
const core = __importStar(__webpack_require__(42186));
class KubernetesUtilities {
static findPodFromJob(kubeClient, jobName, namespace) {
return __awaiter(this, void 0, void 0, function* () {
const pod = (yield kubeClient.listNamespacedPod(namespace)).body.items.find((x) => { var _a, _b; return ((_b = (_a = x.metadata) === null || _a === void 0 ? void 0 : _a.labels) === null || _b === void 0 ? void 0 : _b['job-name']) === jobName; });
if (pod === undefined) {
throw new Error("pod with job-name label doesn't exist");
}
return pod;
});
}
static watchUntilPodRunning(kubeClient, podName, namespace) {
return __awaiter(this, void 0, void 0, function* () {
let success = false;
core.info(`Watching ${podName} ${namespace}`);
yield async_wait_until_1.default(() => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const phase = (_b = (_a = (yield kubeClient.readNamespacedPodStatus(podName, namespace))) === null || _a === void 0 ? void 0 : _a.body.status) === null || _b === void 0 ? void 0 : _b.phase;
success = phase === 'Running';
if (success || phase !== 'Pending')
return true;
return false;
}), {
timeout: 500000,
intervalBetweenAttempts: 15000,
});
return success;
});
}
}
exports.default = KubernetesUtilities;
/***/ }),
/***/ 92560:
@ -1809,7 +1936,7 @@ class RemoteBuilder {
this.RemoteBuilderProviderPlatform = new kubernetes_build_platform_1.default(buildParameters);
break;
}
yield this.RemoteBuilderProviderPlatform.SetupSharedBuildResources(buildUid, buildParameters, branchName, defaultSecretsArray);
yield this.RemoteBuilderProviderPlatform.setupSharedBuildResources(buildUid, buildParameters, branchName, defaultSecretsArray);
yield RemoteBuilder.SetupStep(buildUid, buildParameters, branchName, defaultSecretsArray);
yield RemoteBuilder.BuildStep(buildUid, buildParameters, baseImage, defaultSecretsArray);
yield RemoteBuilder.CompressionStep(buildUid, buildParameters, branchName, defaultSecretsArray);
@ -1817,12 +1944,12 @@ class RemoteBuilder {
if (this.SteamDeploy) {
yield RemoteBuilder.DeployToSteam(buildUid, buildParameters, defaultSecretsArray);
}
yield this.RemoteBuilderProviderPlatform.CleanupSharedBuildResources(buildUid, buildParameters, branchName, defaultSecretsArray);
yield this.RemoteBuilderProviderPlatform.cleanupSharedBuildResources(buildUid, buildParameters, branchName, defaultSecretsArray);
}
catch (error) {
core.setFailed(error);
core.error(error);
yield this.RemoteBuilderProviderPlatform.CleanupSharedBuildResources(buildUid, buildParameters, branchName, defaultSecretsArray);
core.error(JSON.stringify(error, undefined, 4));
core.setFailed('Remote Builder failed');
yield this.RemoteBuilderProviderPlatform.cleanupSharedBuildResources(buildUid, buildParameters, branchName, defaultSecretsArray);
}
});
}

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@ class AWSBuildEnvironment implements RemoteBuilderProviderInterface {
constructor(buildParameters: BuildParameters) {
this.stackName = buildParameters.awsStackName;
}
CleanupSharedBuildResources(
cleanupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid: string,
// eslint-disable-next-line no-unused-vars
@ -28,7 +28,7 @@ class AWSBuildEnvironment implements RemoteBuilderProviderInterface {
) {
throw new Error('Method not implemented.');
}
SetupSharedBuildResources(
setupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid: string,
// eslint-disable-next-line no-unused-vars

View File

@ -1,17 +1,16 @@
import * as k8s from '@kubernetes/client-node';
import { BuildParameters } from '..';
import * as core from '@actions/core';
import { KubeConfig, Log } from '@kubernetes/client-node';
import { Writable } from 'stream';
import { RemoteBuilderProviderInterface } from './remote-builder-provider-interface';
import RemoteBuilderSecret from './remote-builder-secret';
import { waitUntil } from 'async-wait-until';
import KubernetesStorage from './kubernetes-storage';
import RemoteBuilderEnvironmentVariable from './remote-builder-environment-variable';
import KubernetesLogging from './kubernetes-logging';
import KubernetesSecret from './kubernetes-secret';
import KubernetesUtilities from './kubernetes-utils';
const base64 = require('base-64');
class Kubernetes implements RemoteBuilderProviderInterface {
private kubeConfig: KubeConfig;
private kubeConfig: k8s.KubeConfig;
private kubeClient: k8s.CoreV1Api;
private kubeClientBatch: k8s.BatchV1Api;
private buildId: string = '';
@ -37,19 +36,7 @@ class Kubernetes implements RemoteBuilderProviderInterface {
this.namespace = 'default';
this.buildParameters = buildParameters;
}
async CleanupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid: 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 }[],
) {
await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace);
}
public async SetupSharedBuildResources(
public async setupSharedBuildResources(
buildUid: string,
buildParameters: BuildParameters,
// eslint-disable-next-line no-unused-vars
@ -57,6 +44,8 @@ class Kubernetes implements RemoteBuilderProviderInterface {
// eslint-disable-next-line no-unused-vars
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
) {
const pvcName = `unity-builder-pvc-${buildUid}`;
this.pvcName = pvcName;
await KubernetesStorage.createPersistentVolumeClaim(buildParameters, this.pvcName, this.kubeClient, this.namespace);
}
@ -70,64 +59,45 @@ class Kubernetes implements RemoteBuilderProviderInterface {
secrets: RemoteBuilderSecret[],
): Promise<void> {
try {
this.setUniqueBuildId(buildId);
// setup
await this.createSecret(secrets);
this.buildId = buildId;
this.secretName = `build-credentials-${buildId}`;
this.jobName = `unity-builder-job-${buildId}`;
await KubernetesSecret.createSecret(secrets, this.secretName, this.namespace, this.kubeClient);
await KubernetesStorage.createPersistentVolumeClaim(
this.buildParameters,
this.pvcName,
this.kubeClient,
this.namespace,
);
const jobSpec = this.getJobSpec(commands, image, mountdir, workingdir, environment);
//run
const jobSpec = this.getJobSpec(commands, image, mountdir, workingdir, environment);
core.info('Creating build job');
await this.kubeClientBatch.createNamespacedJob(this.namespace, jobSpec);
core.info('Job created');
await KubernetesStorage.watchUntilPVCNotPending(this.kubeClient, this.pvcName, this.namespace);
core.info('PVC Bound');
this.setPodNameAndContainerName(await this.findPod());
this.setPodNameAndContainerName(
await KubernetesUtilities.findPodFromJob(this.kubeClient, this.jobName, this.namespace),
);
core.info('Watching pod until running');
await this.watchUntilPodRunning();
await KubernetesUtilities.watchUntilPodRunning(this.kubeClient, this.podName, this.namespace);
core.info('Pod running, streaming logs');
await this.streamLogs();
await this.cleanup();
await KubernetesLogging.streamLogs(
this.kubeConfig,
this.kubeClient,
this.jobName,
this.podName,
this.containerName,
this.namespace,
core.info,
);
await this.cleanupTaskResources();
} catch (error) {
core.info('Running job failed');
core.error(JSON.stringify(error, undefined, 4));
await this.cleanup();
throw error;
}
}
setUniqueBuildId(buildId: string) {
const pvcName = `unity-builder-pvc-${buildId}`;
const secretName = `build-credentials-${buildId}`;
const jobName = `unity-builder-job-${buildId}`;
this.buildId = buildId;
this.pvcName = pvcName;
this.secretName = secretName;
this.jobName = jobName;
}
async createSecret(secrets: RemoteBuilderSecret[]) {
const secret = new k8s.V1Secret();
secret.apiVersion = 'v1';
secret.kind = 'Secret';
secret.type = 'Opaque';
secret.metadata = {
name: this.secretName,
};
secret.data = {};
for (const buildSecret of secrets) {
secret.data[buildSecret.EnvironmentVariable] = base64.encode(buildSecret.ParameterValue);
secret.data[`${buildSecret.EnvironmentVariable}_NAME`] = base64.encode(buildSecret.ParameterKey);
}
try {
await this.kubeClient.createNamespacedSecret(this.namespace, secret);
} catch (error) {
await this.cleanupTaskResources();
throw error;
}
}
@ -269,89 +239,12 @@ class Kubernetes implements RemoteBuilderProviderInterface {
return job;
}
async findPod() {
const pod = (await this.kubeClient.listNamespacedPod(this.namespace)).body.items.find(
(x) => x.metadata?.labels?.['job-name'] === this.jobName,
);
if (pod === undefined) {
throw new Error("pod with job-name label doesn't exist");
}
return pod;
}
async watchUntilPodRunning() {
let success: boolean = false;
core.info(`Watching ${this.podName} ${this.namespace}`);
await waitUntil(
async () => {
const phase = (await this.kubeClient.readNamespacedPodStatus(this.podName, this.namespace))?.body.status?.phase;
success = phase === 'Running';
if (success || phase !== 'Pending') return true;
return false;
},
{
timeout: 500000,
intervalBetweenAttempts: 15000,
},
);
return success;
}
setPodNameAndContainerName(pod: k8s.V1Pod) {
this.podName = pod.metadata?.name || '';
this.containerName = pod.status?.containerStatuses?.[0].name || '';
}
async streamLogs() {
core.info(`Streaming logs from pod: ${this.podName} container: ${this.containerName} namespace: ${this.namespace}`);
const stream = new Writable();
let didStreamAnyLogs: boolean = false;
stream._write = (chunk, encoding, next) => {
didStreamAnyLogs = true;
core.info(chunk.toString());
next();
};
const logOptions = {
follow: true,
pretty: true,
previous: false,
};
try {
const resultError = await new Promise(async (resolve) =>
new Log(this.kubeConfig).log(this.namespace, this.podName, this.containerName, stream, resolve, logOptions),
);
if (resultError) {
throw resultError;
}
if (!didStreamAnyLogs) {
throw new Error(
JSON.stringify(
{
message: 'Failed to stream any logs, listing namespace events, check for an error with the container',
events: (await this.kubeClient.listNamespacedEvent(this.namespace)).body.items
.filter((x) => {
return x.involvedObject.name === this.podName || x.involvedObject.name === this.jobName;
})
.map((x) => {
return {
type: x.involvedObject.kind,
name: x.involvedObject.name,
message: x.message,
};
}),
},
undefined,
4,
),
);
}
} catch (error) {
throw error;
}
core.info('end of log stream');
}
async cleanup() {
async cleanupTaskResources() {
core.info('cleaning up');
try {
await this.kubeClientBatch.deleteNamespacedJob(this.jobName, this.namespace);
@ -362,5 +255,18 @@ class Kubernetes implements RemoteBuilderProviderInterface {
core.info('Abandoning cleanup, build error:');
}
}
async cleanupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid: 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 }[],
) {
await this.kubeClient.deleteNamespacedPersistentVolumeClaim(this.pvcName, this.namespace);
}
}
export default Kubernetes;

View File

@ -0,0 +1,64 @@
import { CoreV1Api, KubeConfig, Log } from '@kubernetes/client-node';
import { Writable } from 'stream';
import * as core from '@actions/core';
class KubernetesLogging {
static async streamLogs(
kubeConfig: KubeConfig,
kubeClient: CoreV1Api,
jobName: string,
podName: string,
containerName: string,
namespace: string,
logCallback: any,
) {
core.info(`Streaming logs from pod: ${podName} container: ${containerName} namespace: ${namespace}`);
const stream = new Writable();
let didStreamAnyLogs: boolean = false;
stream._write = (chunk, encoding, next) => {
didStreamAnyLogs = true;
logCallback(chunk.toString());
next();
};
const logOptions = {
follow: true,
pretty: true,
previous: false,
};
try {
const resultError = await new Promise(async (resolve) =>
new Log(kubeConfig).log(namespace, podName, containerName, stream, resolve, logOptions),
);
if (resultError) {
throw resultError;
}
if (!didStreamAnyLogs) {
throw new Error(
JSON.stringify(
{
message: 'Failed to stream any logs, listing namespace events, check for an error with the container',
events: (await kubeClient.listNamespacedEvent(namespace)).body.items
.filter((x) => {
return x.involvedObject.name === podName || x.involvedObject.name === jobName;
})
.map((x) => {
return {
type: x.involvedObject.kind,
name: x.involvedObject.name,
message: x.message,
};
}),
},
undefined,
4,
),
);
}
} catch (error) {
throw error;
}
core.info('end of log stream');
}
}
export default KubernetesLogging;

View File

@ -0,0 +1,33 @@
import { CoreV1Api } from '@kubernetes/client-node';
import RemoteBuilderSecret from './remote-builder-secret';
import * as k8s from '@kubernetes/client-node';
const base64 = require('base-64');
class KubernetesSecret {
static async createSecret(
secrets: RemoteBuilderSecret[],
secretName: string,
namespace: string,
kubeClient: CoreV1Api,
) {
const secret = new k8s.V1Secret();
secret.apiVersion = 'v1';
secret.kind = 'Secret';
secret.type = 'Opaque';
secret.metadata = {
name: secretName,
};
secret.data = {};
for (const buildSecret of secrets) {
secret.data[buildSecret.EnvironmentVariable] = base64.encode(buildSecret.ParameterValue);
secret.data[`${buildSecret.EnvironmentVariable}_NAME`] = base64.encode(buildSecret.ParameterKey);
}
try {
await kubeClient.createNamespacedSecret(namespace, secret);
} catch (error) {
throw error;
}
}
}
export default KubernetesSecret;

View File

@ -0,0 +1,34 @@
import { CoreV1Api } from '@kubernetes/client-node';
import waitUntil from 'async-wait-until';
import * as core from '@actions/core';
class KubernetesUtilities {
static async findPodFromJob(kubeClient: CoreV1Api, jobName: string, namespace: string) {
const pod = (await kubeClient.listNamespacedPod(namespace)).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;
}
static async watchUntilPodRunning(kubeClient: CoreV1Api, podName: string, namespace: string) {
let success: boolean = false;
core.info(`Watching ${podName} ${namespace}`);
await waitUntil(
async () => {
const phase = (await kubeClient.readNamespacedPodStatus(podName, namespace))?.body.status?.phase;
success = phase === 'Running';
if (success || phase !== 'Pending') return true;
return false;
},
{
timeout: 500000,
intervalBetweenAttempts: 15000,
},
);
return success;
}
}
export default KubernetesUtilities;

View File

@ -3,7 +3,7 @@ import RemoteBuilderEnvironmentVariable from './remote-builder-environment-varia
import RemoteBuilderSecret from './remote-builder-secret';
export interface RemoteBuilderProviderInterface {
CleanupSharedBuildResources(
cleanupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid: string,
// eslint-disable-next-line no-unused-vars
@ -13,7 +13,7 @@ export interface RemoteBuilderProviderInterface {
// eslint-disable-next-line no-unused-vars
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
);
SetupSharedBuildResources(
setupSharedBuildResources(
// eslint-disable-next-line no-unused-vars
buildUid: string,
// eslint-disable-next-line no-unused-vars

View File

@ -48,7 +48,7 @@ class RemoteBuilder {
this.RemoteBuilderProviderPlatform = new Kubernetes(buildParameters);
break;
}
await this.RemoteBuilderProviderPlatform.SetupSharedBuildResources(
await this.RemoteBuilderProviderPlatform.setupSharedBuildResources(
buildUid,
buildParameters,
branchName,
@ -61,16 +61,16 @@ class RemoteBuilder {
if (this.SteamDeploy) {
await RemoteBuilder.DeployToSteam(buildUid, buildParameters, defaultSecretsArray);
}
await this.RemoteBuilderProviderPlatform.CleanupSharedBuildResources(
await this.RemoteBuilderProviderPlatform.cleanupSharedBuildResources(
buildUid,
buildParameters,
branchName,
defaultSecretsArray,
);
} catch (error) {
core.setFailed(error);
core.error(error);
await this.RemoteBuilderProviderPlatform.CleanupSharedBuildResources(
core.error(JSON.stringify(error, undefined, 4));
core.setFailed('Remote Builder failed');
await this.RemoteBuilderProviderPlatform.cleanupSharedBuildResources(
buildUid,
buildParameters,
branchName,