Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@roderik
Created April 26, 2021 07:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save roderik/0ce3768f36421be827150adc99fcb7f6 to your computer and use it in GitHub Desktop.
Save roderik/0ce3768f36421be827150adc99fcb7f6 to your computer and use it in GitHub Desktop.
import { LockService } from '@bpaas/lock';
import { StrictConfigService } from '@bpaas/strict-config';
import { Injectable } from '@nestjs/common';
import { OgmaLogger, OgmaService } from '@ogma/nestjs-module';
import { DestroyResult, LocalWorkspace, UpResult } from '@pulumi/pulumi/automation';
import { InlineProgramArgs, LocalWorkspaceOptions } from '@pulumi/pulumi/automation/localWorkspace';
import { resolve } from 'path';
export const returnProgress90 = (progress: number) => (progress > 90 ? 90 : progress);
@Injectable()
export class PulumiService {
constructor(
private readonly configService: StrictConfigService,
@OgmaLogger(PulumiService) private readonly logger: OgmaService,
private readonly lockService: LockService
) {}
public async destroy(
progressFn: (value: number) => Promise<void>,
logFn: (row: string) => Promise<void>,
args: InlineProgramArgs
) {
return await this.runPulumi('DESTROY', progressFn, logFn, args);
}
public async getConfiguredStack(args: InlineProgramArgs, opts?: LocalWorkspaceOptions) {
const stack = await LocalWorkspace.createOrSelectStack(
{
...args,
},
{
...opts,
workDir: resolve(process.cwd()), // set work dir to current directory
}
);
await stack.setAllConfig({
'cloudflare:accountId': {
value: this.configService.get<string>('cloudflareAccountId'),
},
'cloudflare:apiKey': {
value: this.configService.get<string>('cloudflareApiKey'),
secret: true,
},
'cloudflare:email': {
value: this.configService.get<string>('cloudflareEmail'),
},
'gcp:project': {
value: this.configService.get<string>('googleCloudProjectId'),
},
'gcp:zone': { value: 'europe-west1-b' },
'gcp:credentials': {
value: JSON.stringify({
type: 'service_account',
project_id: this.configService.get<string>('googleCloudProjectId'),
private_key_id: this.configService.get<string>('googleCloudPrivateKeyId'),
private_key: this.configService.get<string>('googleCloudPrivateKey').replace(/\\n/g, '\n'),
client_email: this.configService.get<string>('googleCloudClientEmail'),
client_id: this.configService.get<string>('googleCloudClientId'),
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
token_uri: 'https://oauth2.googleapis.com/token',
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
client_x509_cert_url: this.configService.get<string>('googleCloudClientX509CertUrl'),
}),
secret: true,
},
'azure:environment': { value: 'public' },
'azure:location': { value: 'westeurope' },
'azure:clientId': {
value: this.configService.get<string>('azureApplicationId'),
},
'azure:clientSecret': {
value: this.configService.get<string>('azureClientSecret'),
secret: true,
},
'azure:subscriptionId': {
value: this.configService.get<string>('azureSubscriptionId'),
},
'azure:tenantId': {
value: this.configService.get<string>('azureTenant'),
},
'aws:region': { value: 'eu-west-3' },
'aws:accessKey': {
value: this.configService.get<string>('awsAccessKeyId'),
},
'aws:secretKey': {
value: this.configService.get<string>('awsSecretAccessKey'),
secret: true,
},
});
return stack;
}
public async logOutput(out: string, stackName: string, logFn: (row: string) => Promise<void>) {
if (this.configService.get<boolean>('bullDebug')) {
const messages = out
.split(/(\r\n|\n|\r)/gm)
.map((v) => v.trim())
.filter(Boolean);
for (const message of messages) {
this.logger.debug(message, {
correlationId: stackName,
});
}
}
await logFn(out);
}
public async runPulumi(
action: 'UP' | 'DESTROY',
progressFn: (value: number) => Promise<void>,
logFn: (row: string) => Promise<void>,
args: InlineProgramArgs
) {
let progress = 0;
await progressFn(progress++);
await this.logOutput(`Waiting for deploy lock`, args.stackName, logFn); // Pulumi concurrency
await this.lockService.acquireLock('pulumi');
await this.logOutput(`Lock aquired, starting...`, args.stackName, logFn);
const stack = await this.getConfiguredStack(args);
try {
await stack.workspace.installPlugin('cloudflare', 'v3.0.0'); // https://github.com/pulumi/pulumi-cloudflare/tags
await stack.workspace.installPlugin('kubernetes', 'v3.0.0'); // https://github.com/pulumi/pulumi-kubernetes/tags
await stack.workspace.installPlugin('azure', 'v4.0.0'); // https://github.com/pulumi/pulumi-azure/tags
await stack.workspace.installPlugin('aws', 'v4.0.0'); // https://github.com/pulumi/pulumi-aws/tags
await stack.workspace.installPlugin('eks', 'v0.30.0'); // https://github.com/pulumi/pulumi-eks/tags
await stack.workspace.installPlugin('gcp', 'v5.0.0'); // https://github.com/pulumi/pulumi-gcp/tags
progress = 10;
await progressFn(progress++);
await this.logOutput(`Refreshing stack`, args.stackName, logFn);
const refresh = await stack.refresh({
onOutput: async (out) => {
await this.logOutput(out, args.stackName, logFn);
await progressFn(progress++);
},
});
await this.logOutput(`${action === 'DESTROY' ? 'Destroying' : 'Updating'} stack`, args.stackName, logFn);
let result: UpResult | DestroyResult;
if (action === 'DESTROY') {
result = await stack.destroy({
onOutput: async (out) => {
await this.logOutput(out, args.stackName, logFn);
await progressFn(returnProgress90(progress++));
},
});
await progressFn(95);
await this.logOutput(`Removing stack`, args.stackName, logFn);
await stack.workspace.removeStack(stack.name);
} else {
result = await stack.up({
onOutput: async (out) => {
await this.logOutput(out, args.stackName, logFn);
await progressFn(returnProgress90(progress++));
},
});
}
await progressFn(100);
return {
refresh,
[action]: result,
};
} catch (err) {
await this.logOutput(err.message, args.stackName, logFn);
await this.logOutput(err.stack, args.stackName, logFn);
try {
await this.logOutput('Cancelling...', args.stackName, logFn);
await stack.cancel();
} catch (error) {
// Commented out, it is an allowed failure
// await this.logOutput(`Cancelling failed: ${error.stack}`, args.stackName, logFn);
}
try {
await this.logOutput('Cleaning up in progress actions...', args.stackName, logFn);
const exportedStack = await stack.exportStack();
if (exportedStack.deployment.pending_operations?.length > 0) {
exportedStack.deployment.pending_operations = [];
await stack.importStack(exportedStack);
}
} catch (error) {
await this.logOutput(`Cleaning up failed: ${error.stack}`, args.stackName, logFn);
}
throw err;
} finally {
await this.logOutput(`Releasing deploy lock`, args.stackName, logFn);
await this.lockService.releaseLock('pulumi');
await this.logOutput(`Lock released`, args.stackName, logFn);
}
}
public async up(
progressFn: (value: number) => Promise<void>,
logFn: (row: string) => Promise<void>,
args: InlineProgramArgs
) {
return await this.runPulumi('UP', progressFn, logFn, args);
}
}
await this.pulumiService.up(progressFn, logFn, {
...entity.stack, // name etc
program: async () => {
// fetch the cluster and kubeconfig
const clusterStack = new pulumi.StackReference(`settlemint/bpaas-clusters/production`);
const provider = new k8s.Provider(entity.uniqueName, {
kubeconfig: pulumi.unsecret(
clusterStack.requireOutput(`${entity.provider}${entity.region}kubeconfig`.toLowerCase())
),
});
// Create the credentials for this service
new ClusterServiceCredentialsResource(entity.uniqueName, entity, { provider });
// Create the resource
new WhateverResource() // here we pass in our own ComponentResource which encapsulates the complte set of what we want to deploy
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment