Created
April 26, 2021 07:22
-
-
Save roderik/0ce3768f36421be827150adc99fcb7f6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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