#!/usr/bin/env node import { Either, getRequiredEnvVars, getStdout, type IEither, LogTraceable, LogMetricTraceable, Metric, prependWith, TraceUtil, } from '@emprespresso/pengueno'; import type { AnsiblePlaybookJob } from '@emprespresso/ci_model'; import { Bitwarden, BitwardenKey, getPathOnHost, type SecureNote } from '@emprespresso/ci_worker'; import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; import { rmSync } from 'fs'; const eitherJob = getRequiredEnvVars(['path', 'playbooks']).mapRight( (baseArgs) => { type: 'ansible_playbook.js', arguments: baseArgs, }, ); const eitherVault = Bitwarden.getConfigFromEnvironment().mapRight((config) => new Bitwarden(config)); const playbookMetric = Metric.fromName('ansiblePlaybook.playbook'); const _logJob = LogTraceable.of(eitherJob).flatMap(TraceUtil.withTrace('ansible_playbook')); await LogMetricTraceable.ofLogTraceable(_logJob) .flatMap(TraceUtil.withMetricTrace(playbookMetric)) .peek((tEitherJob) => tEitherJob.trace.trace('starting ansible playbook job! (⑅˘꒳˘)')) .map((tEitherJob) => tEitherJob.get().flatMapAsync((job) => eitherVault.flatMapAsync(async (vault) => { const eitherKey = await vault.unlock(tEitherJob); tEitherJob.trace.trace('unlocked vault :3'); return eitherKey.mapRight((key) => ({ job, key, vault })); }), ), ) .map( async ( tEitherJobVault, ): Promise< IEither< Error, { secretFiles: { ssh_key: string; ansible_secrets: string }; key: BitwardenKey; vault: Bitwarden; job: AnsiblePlaybookJob; } > > => ['ssh_key', 'ansible_secrets'].reduce( async (_result, secret) => { const result = await _result; return result.joinRightAsync( result.flatMapAsync(({ key, vault, job }) => vault .fetchSecret(tEitherJobVault, key, secret) .then((eSecret) => eSecret.flatMapAsync(({ notes }) => saveToTempFile(job, notes))), ), (file, prev) => ({ ...prev, secretFiles: { ...prev.secretFiles, [secret]: file } }), ); }, tEitherJobVault.get().then((e) => e.mapRight((r) => ({ ...r, secretFiles: {} }))), ), ) .map(async (tEitherJobAndSecrets) => { const eitherJobAndSecrets = await tEitherJobAndSecrets.get(); return eitherJobAndSecrets.flatMapAsync(async ({ job, secretFiles, vault, key }) => { const [src, sshKey, ansibleSecrets] = ( await Promise.all( [join(process.cwd(), job.arguments.path), secretFiles.ssh_key, secretFiles.ansible_secrets].map( (x) => getPathOnHost(x), ), ) ).map((x) => x.right().get()); (await vault.lock(tEitherJobAndSecrets, key)).right().get(); const volumes = [ `${src}:/ansible`, `${sshKey}:/root/.ssh/id_ed25519`, `${ansibleSecrets}:/ansible/secrets.yml`, ]; const playbookCmd = `ansible-playbook -e @secrets.yml ${job.arguments.playbooks}`; const deployCmd = [ 'docker', 'run', ...prependWith(volumes, '-v'), '--workdir=/ansible', 'willhallonline/ansible:latest', ...playbookCmd.split(' '), ]; tEitherJobAndSecrets.trace.trace(`running ansible magic~ (◕ᴗ◕✿) ${deployCmd}`); return tEitherJobAndSecrets .move(deployCmd) .map((c) => getStdout(c, { streamTraceable: ['stdout', 'stderr'] }).then((e) => { Object.values(secretFiles).forEach((f) => rmSync(f)); return e; }), ) .get(); }); }) .map(async (tEitherJob) => { const eitherJob = await tEitherJob.get(); return eitherJob.fold( (e) => Promise.reject(e), () => Promise.resolve(0), ); }) .get(); async function saveToTempFile(job: AnsiblePlaybookJob, text: string): Promise> { const dir = join(process.cwd(), '.secrets', crypto.randomUUID()); const file = join(dir, 'secret'); return Either.fromFailableAsync(() => mkdir(dir, { recursive: true }).then(async () => { await writeFile(file, text, { encoding: 'utf-8' }); return file; }), ); }