summaryrefslogtreecommitdiff
path: root/worker/scripts/ansible_playbook.ts
blob: f7315ab274dda96ba1548ba90a829e84e1f37e4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/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) =>
        <AnsiblePlaybookJob>{
            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<SecureNote>(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: <any>{} }))),
            ),
    )
    .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<IEither<Error, string>> {
    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;
        }),
    );
}