console/deployment/utils/remote-artifact-as-service.ts
Kamil Kisiela dc3bc1ec87 Hello
2022-05-18 09:26:57 +02:00

212 lines
5.6 KiB
TypeScript

import * as kx from '@pulumi/kubernetesx';
import * as k8s from '@pulumi/kubernetes';
import * as azure from '@pulumi/azure';
import * as pulumi from '@pulumi/pulumi';
import { PodBuilder, normalizeEnv } from './pod-builder';
import { PackageInfo } from './pack';
import { isDefined } from './helpers';
const DEFAULT_IMAGE = 'node:16.13.2-alpine3.15';
export class RemoteArtifactAsServiceDeployment {
constructor(
protected name: string,
protected options: {
storageContainer: azure.storage.Container;
env?: kx.types.Container['env'];
packageInfo: PackageInfo;
port?: number;
image?: string;
livenessProbe?: string;
readinessProbe?: string;
memoryLimit?: string;
cpuLimit?: string;
bin?: string;
/**
* Enables /metrics endpoint on port 10254
*/
exposesMetrics?: boolean;
replicas?: number;
},
protected dependencies?: Array<pulumi.Resource | undefined | null>,
protected parent?: pulumi.Resource | null
) {}
deployAsJob() {
const artifactUrl = this.makeArtifactUrl();
const { pb } = this.createPod(artifactUrl, true);
const job = new kx.Job(
this.name,
{
spec: pb.asJobSpec(),
},
{ dependsOn: this.dependencies?.filter(isDefined) }
);
return { job };
}
createPod(artifactUrl: pulumi.Output<string>, asJob: boolean) {
const port = this.options.port || 3000;
const additionalEnv: any[] = normalizeEnv(this.options.env);
let livenessProbe: k8s.types.input.core.v1.Probe | undefined = undefined;
let readinessProbe: k8s.types.input.core.v1.Probe | undefined = undefined;
if (this.options.livenessProbe) {
livenessProbe = {
initialDelaySeconds: 3,
periodSeconds: 20,
failureThreshold: 10,
timeoutSeconds: 5,
httpGet: {
path: this.options.livenessProbe,
port,
},
};
}
if (this.options.readinessProbe) {
readinessProbe = {
initialDelaySeconds: 5,
periodSeconds: 20,
failureThreshold: 5,
timeoutSeconds: 5,
httpGet: {
path: this.options.readinessProbe,
port,
},
};
}
const image = this.options.image || DEFAULT_IMAGE;
const appVolume = {
mountPath: '/app',
name: 'app',
};
const volumeMounts = [appVolume];
if (this.options.exposesMetrics) {
additionalEnv.push({ name: 'METRICS_ENABLED', value: 'true' });
}
const pb = new PodBuilder({
restartPolicy: asJob ? 'Never' : 'Always',
volumes: [
{
name: appVolume.name,
emptyDir: {},
},
],
initContainers: [
{
name: `${this.name}-init`,
image,
workingDir: appVolume.mountPath,
volumeMounts,
command:
this.options.packageInfo.runtime === 'node'
? ['/bin/sh', '-c', artifactUrl.apply((v) => `yarn add ${v}`)]
: this.options.packageInfo.runtime === 'rust'
? ['/bin/sh', '-c', artifactUrl.apply((v) => `wget ${v}`)]
: ['echo missing script!'],
},
],
containers: [
{
livenessProbe,
readinessProbe,
env: [
{ name: 'PORT', value: String(port) },
{
name: 'POD_NAME',
valueFrom: {
fieldRef: {
fieldPath: 'metadata.name',
},
},
},
].concat(additionalEnv),
name: this.name,
image,
workingDir: appVolume.mountPath,
volumeMounts: [appVolume],
command:
this.options.packageInfo.runtime === 'node'
? ['yarn', this.options.bin || this.options.packageInfo.bin]
: this.options.packageInfo.runtime === 'rust'
? [this.options.packageInfo.bin]
: [],
ports: {
http: port,
...(this.options.exposesMetrics
? {
metrics: 10254,
}
: {}),
},
},
],
});
return { pb };
}
private makeArtifactUrl() {
const azureStaticFile = new azure.storage.Blob(`${this.name}-artifact`, {
storageAccountName: this.options.storageContainer.storageAccountName,
storageContainerName: this.options.storageContainer.name,
type: 'Block',
source: new pulumi.asset.FileAsset(this.options.packageInfo.file),
});
return azureStaticFile.url;
}
deploy() {
const artifactUrl = this.makeArtifactUrl();
const { pb } = this.createPod(artifactUrl, false);
const metadata: k8s.types.input.meta.v1.ObjectMeta = {
annotations: {},
};
if (this.options.exposesMetrics) {
metadata.annotations = {
'prometheus.io/port': '10254',
'prometheus.io/path': '/metrics',
'prometheus.io/scrape': 'true',
};
}
const deployment = new kx.Deployment(
this.name,
{
spec: pb.asExtendedDeploymentSpec(
{
replicas: this.options.replicas ?? 1,
strategy: {
type: 'RollingUpdate',
rollingUpdate: {
maxSurge: this.options.replicas ?? 1,
maxUnavailable: 0,
},
},
},
{
annotations: metadata.annotations,
}
),
},
{
dependsOn: this.dependencies?.filter(isDefined),
parent: this.parent ?? undefined,
}
);
const service = deployment.createService({});
return { deployment, service };
}
}