diff options
author | Elizabeth Hunt <me@liz.coffee> | 2025-07-26 21:07:36 -0700 |
---|---|---|
committer | Elizabeth Hunt <me@liz.coffee> | 2025-07-27 00:00:12 -0700 |
commit | df76fa3c266f7f9b22d2bfaf98ad5accebcabd35 (patch) | |
tree | 8aaef9f227e385b8e31bd6c69fc6a8231326f361 | |
parent | 9ee3bf3345b006a745b2ee28fee3613819011796 (diff) | |
download | ci-df76fa3c266f7f9b22d2bfaf98ad5accebcabd35.tar.gz ci-df76fa3c266f7f9b22d2bfaf98ad5accebcabd35.zip |
Fixes type inference for Either.joinRightAsync. Regarding ansible-docker. Will it ever be okay to trust docs?!!
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | u/types/fn/either.ts | 6 | ||||
-rwxr-xr-x | worker/scripts/ansible_playbook.ts | 99 | ||||
-rwxr-xr-x | worker/scripts/fetch_code | 10 | ||||
-rw-r--r-- | worker/secret.ts | 10 |
5 files changed, 74 insertions, 55 deletions
@@ -1,8 +1,8 @@ # @emprespresso/ci (⑅˘꒳˘) -this is my ci server, built on top of [laminar](https://laminar.ohwg.net/docs.html), because while the jenkins logo looks hot and classy, i want something to hack on myself! +this is my ci server, built on top of [laminar](https://laminar.ohwg.net/docs.html), because while the jenkins logo looks hot and classy, i want something to hack on myself -also, to scrap out pengueno :3 +also! to scrap out pengueno :3 ## how to use it? (。•̀ᴗ-)✧ diff --git a/u/types/fn/either.ts b/u/types/fn/either.ts index 5e2dca0..0f65859 100644 --- a/u/types/fn/either.ts +++ b/u/types/fn/either.ts @@ -28,8 +28,8 @@ export interface IEither<E, T> extends Tagged<IEitherTag> { readonly fold: <_T>(leftFolder: Mapper<E, _T>, rightFolder: Mapper<T, _T>) => _T; readonly joinRight: <O, _T>(other: IEither<E, O>, mapper: (a: O, b: T) => _T) => IEither<E, _T>; readonly joinRightAsync: <O, _T>( - other: Supplier<Promise<IEither<E, O>>> | Promise<IEither<E, O>>, - mapper: BiMapper<O, T, _T>, + other: (() => Promise<IEither<E, O>>) | Promise<IEither<E, O>>, + mapper: (a: O, b: T) => _T, ) => Promise<IEither<E, _T>>; } @@ -115,7 +115,7 @@ export class Either<E, T> extends _Tagged implements IEither<E, T> { ) { return this.flatMapAsync(async (t) => { const o = typeof other === 'function' ? other() : other; - return o.then((other) => other.mapRight((o) => mapper(o, t))); + return await o.then((other) => other.mapRight((o) => mapper(o, t))); }); } diff --git a/worker/scripts/ansible_playbook.ts b/worker/scripts/ansible_playbook.ts index 20f85d8..2048d44 100755 --- a/worker/scripts/ansible_playbook.ts +++ b/worker/scripts/ansible_playbook.ts @@ -12,10 +12,10 @@ import { TraceUtil, } from '@emprespresso/pengueno'; import type { AnsiblePlaybookJob } from '@emprespresso/ci_model'; -import { Bitwarden, type SecureNote } from '@emprespresso/ci_worker'; -import { writeFile, mkdtemp } from 'fs/promises'; +import { Bitwarden, BitwardenKey, getPathOnHost, type SecureNote } from '@emprespresso/ci_worker'; +import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; -import { tmpdir } from 'os'; +import { rmSync } from 'fs'; const eitherJob = getRequiredEnvVars(['path', 'playbooks']).mapRight( (baseArgs) => @@ -41,51 +41,69 @@ await LogMetricTraceable.ofLogTraceable(_logJob) }), ), ) - .map(async (tEitherJobVault) => { - const eitherJobVault = await tEitherJobVault.get(); - - const eitherSshKey = await eitherJobVault.flatMapAsync(({ key, vault }) => - vault.fetchSecret<SecureNote>(tEitherJobVault, key, 'ssh_key'), - ); - const eitherSshKeyFile = await eitherSshKey.mapRight(({ notes }) => notes).flatMapAsync(saveToTempFile); - const eitherAnsibleSecrets = await eitherJobVault.flatMapAsync(({ key, vault }) => - vault.fetchSecret<SecureNote>(tEitherJobVault, key, 'ansible_secrets'), - ); - const eitherAnsibleSecretsFile = await eitherAnsibleSecrets - .mapRight(({ notes }) => notes) - .flatMapAsync(saveToTempFile); - - return eitherJobVault.flatMapAsync(async ({ job, vault, key }) => { - const eitherLocked = await vault.lock(tEitherJobVault, key); - return eitherLocked.flatMap((_locked) => - eitherSshKeyFile.flatMap((sshKeyFile) => - eitherAnsibleSecretsFile.mapRight((secretsFile) => ({ - job, - sshKeyFile, - secretsFile, - })), - ), - ); - }); - }) + .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(({ job, sshKeyFile, secretsFile }) => { + return eitherJobAndSecrets.flatMapAsync(async ({ job, secretFiles }) => { + 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()); const volumes = [ - `${job.arguments.path}:/ansible`, - `${sshKeyFile}:/root/id_rsa`, - `${secretsFile}:/ansible/secrets.yml`, + `${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(getStdout).get(); + 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) => { @@ -97,12 +115,13 @@ await LogMetricTraceable.ofLogTraceable(_logJob) }) .get(); -function saveToTempFile(text: string): Promise<IEither<Error, string>> { +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(() => - mkdtemp(join(tmpdir(), 'ci-')).then(async (dir) => { - const filePath = join(dir, 'temp-file'); - await writeFile(filePath, text); - return filePath; + mkdir(dir, { recursive: true }).then(async () => { + await writeFile(file, text, { encoding: 'utf-8' }); + return file; }), ); } diff --git a/worker/scripts/fetch_code b/worker/scripts/fetch_code index 2691832..d711f82 100755 --- a/worker/scripts/fetch_code +++ b/worker/scripts/fetch_code @@ -1,10 +1,10 @@ #!/bin/bash -export LOG_PREFIX="[fetch_code $remote @ $checkout -> $path]" +export LOG_PREFIX="[fetch_code $remoteUrl @ $checkout -> $path]" -if [[ "$remote" == ssh://* ]]; then - host=$(echo "$remote" | sed -E 's#ssh://([^:]+):[0-9]+/.*#\1#') - port=$(echo "$remote" | sed -E 's#ssh://[^:]+:([0-9]+)/.*#\1#') +if [[ "$remoteUrl" == ssh://* ]]; then + host=$(echo "$remoteUrl" | sed -E 's#ssh://([^:]+):[0-9]+/.*#\1#') + port=$(echo "$remoteUrl" | sed -E 's#ssh://[^:]+:([0-9]+)/.*#\1#') log "populating host keyz~ $host:$port" ssh-keyscan -p "$port" "$host" > ./cur_known_hosts @@ -14,7 +14,7 @@ if [[ "$remote" == ssh://* ]]; then fi log "getting the codez~ time to fetch!" -git clone "$remote" "$path" +git clone "$remoteUrl" "$path" if [ ! $? -eq 0 ]; then log "D: oh nyo! couldn't clone the repo" exit 1 diff --git a/worker/secret.ts b/worker/secret.ts index 11daf06..071b539 100644 --- a/worker/secret.ts +++ b/worker/secret.ts @@ -39,12 +39,12 @@ export interface IVault<TClient, TKey, TItemId> { // -- <Vault> -- type TClient = ITraceable<unknown, LogMetricTraceSupplier>; -type TKey = { +export type BitwardenKey = { BW_SESSION: string; BITWARDENCLI_APPDATA_DIR: string; }; type TItemId = string; -export class Bitwarden implements IVault<TClient, TKey, TItemId> { +export class Bitwarden implements IVault<TClient, BitwardenKey, TItemId> { constructor(private readonly config: BitwardenConfig) {} public unlock(client: TClient) { @@ -52,7 +52,7 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> { .move(this.config) .flatMap(TraceUtil.withMetricTrace(Bitwarden.loginMetric)) .map((tConfig) => - Either.fromFailable<Error, { config: BitwardenConfig; key: Pick<TKey, 'BITWARDENCLI_APPDATA_DIR'> }>( + Either.fromFailable<Error, { config: BitwardenConfig; key: Pick<BitwardenKey, 'BITWARDENCLI_APPDATA_DIR'> }>( () => { const sessionPath = path.join(this.config.sessionBaseDirectory, randomUUID()); mkdirSync(sessionPath, { recursive: true }); @@ -78,7 +78,7 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> { .get(); } - public fetchSecret<T extends SecretItem>(client: TClient, key: TKey, item: string): Promise<IEither<Error, T>> { + public fetchSecret<T extends SecretItem>(client: TClient, key: BitwardenKey, item: string): Promise<IEither<Error, T>> { return client .move(key) .flatMap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric)) @@ -104,7 +104,7 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> { .get(); } - public lock(client: TClient, key: TKey) { + public lock(client: TClient, key: BitwardenKey) { return client .move(key) .flatMap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric)) |