From 373d9ec700c0097a22cf665a8e33cf48998d1dc2 Mon Sep 17 00:00:00 2001 From: Elizabeth Date: Mon, 2 Jun 2025 11:14:52 -0700 Subject: Minor things --- .ci/ci.ts | 2 +- .zed/settings.json | 4 +- Dockerfile | 4 +- deno.json | 3 +- hooks/Dockerfile | 8 -- hooks/deno.json | 4 - hooks/main.ts | 12 --- hooks/mod.ts | 2 - hooks/server/ci.ts | 56 ----------- hooks/server/health.ts | 28 ------ hooks/server/job/activity.ts | 110 ---------------------- hooks/server/job/mod.ts | 2 - hooks/server/job/queuer.ts | 88 ----------------- hooks/server/mod.ts | 3 - mod.ts | 31 ++++++ model/deno.json | 2 +- model/pipeline.ts | 2 +- server/Dockerfile | 8 ++ server/ci.ts | 56 +++++++++++ server/deno.json | 4 + server/health.ts | 28 ++++++ server/job.ts | 193 ++++++++++++++++++++++++++++++++++++++ server/mod.ts | 16 ++++ u/deno.json | 3 +- u/fn/either.ts | 2 +- u/process/argv.ts | 65 +++++++++++++ u/process/mod.ts | 1 + u/server/activity/fourohfour.ts | 12 +-- u/server/request.ts | 9 +- worker/Dockerfile | 10 +- worker/deno.json | 2 +- worker/executor.ts | 99 +++++++++++++++++++ worker/executor/job.ts | 42 --------- worker/executor/mod.ts | 2 - worker/executor/pipeline.ts | 58 ------------ worker/jobs/ci_pipeline.run | 8 +- worker/mod.ts | 4 +- worker/scripts/ansible_playbook | 4 +- worker/scripts/build_docker_image | 10 +- worker/secret.ts | 193 ++++++++++++++++++++++++++++++++++++++ worker/secret/bitwarden.ts | 167 --------------------------------- worker/secret/ivault.ts | 24 ----- worker/secret/mod.ts | 2 - 43 files changed, 733 insertions(+), 650 deletions(-) delete mode 100644 hooks/Dockerfile delete mode 100644 hooks/deno.json delete mode 100755 hooks/main.ts delete mode 100755 hooks/mod.ts delete mode 100644 hooks/server/ci.ts delete mode 100644 hooks/server/health.ts delete mode 100644 hooks/server/job/activity.ts delete mode 100644 hooks/server/job/mod.ts delete mode 100644 hooks/server/job/queuer.ts delete mode 100644 hooks/server/mod.ts create mode 100755 mod.ts create mode 100644 server/Dockerfile create mode 100644 server/ci.ts create mode 100644 server/deno.json create mode 100644 server/health.ts create mode 100644 server/job.ts create mode 100644 server/mod.ts create mode 100644 u/process/argv.ts create mode 100644 worker/executor.ts delete mode 100644 worker/executor/job.ts delete mode 100644 worker/executor/mod.ts delete mode 100644 worker/executor/pipeline.ts create mode 100644 worker/secret.ts delete mode 100644 worker/secret/bitwarden.ts delete mode 100644 worker/secret/ivault.ts delete mode 100644 worker/secret/mod.ts diff --git a/.ci/ci.ts b/.ci/ci.ts index 0b8e124..d6f0bfa 100644 --- a/.ci/ci.ts +++ b/.ci/ci.ts @@ -5,7 +5,7 @@ import { BuildDockerImageJob, DefaultGitHookPipelineBuilder, FetchCodeJob, -} from "@emprespresso/ci-model"; +} from "@emprespresso/ci_model"; const REGISTRY = "oci.liz.coffee"; const NAMESPACE = "emprespresso"; diff --git a/.zed/settings.json b/.zed/settings.json index 5d2dc7a..cf1756f 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -10,11 +10,11 @@ }, "languages": { "TypeScript": { - "language_servers": ["deno"], + "language_servers": ["deno", "!typescript-language-server"], "formatter": "prettier" }, "TSX": { - "language_servers": ["deno"], + "language_servers": ["deno", "!typescript-language-server"], "formatter": "prettier" } } diff --git a/Dockerfile b/Dockerfile index 9c1ba66..6aa29f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # -- -- -FROM debian:stable-slim AS laminar-bin +FROM debian:stable-slim AS laminar_bin ENV DEBIAN_FRONTEND=noninteractive RUN useradd --system --home-dir /var/lib/laminar --no-user-group --groups users --uid 100 laminar RUN rm -rf /etc/cron.d/e2scrub_all @@ -26,7 +26,7 @@ RUN cmake -B /opt/laminar/build -S /opt/laminar/src -G Ninja \ # -- -- # -- -- -FROM denoland/deno:debian AS ci-base +FROM denoland/deno:debian AS ci_base RUN apt-get update -yqq && apt-get install -yqq libcapnp-0.9.2 \ libsqlite3-0 zlib1g curl bash diff --git a/deno.json b/deno.json index 50ebd48..adeae7b 100644 --- a/deno.json +++ b/deno.json @@ -1,3 +1,4 @@ { - "workspace": ["./model", "./u", "./worker", "./hooks"] + "package": "@emprespresso/ci", + "workspace": ["./*"] } diff --git a/hooks/Dockerfile b/hooks/Dockerfile deleted file mode 100644 index d1a6cb7..0000000 --- a/hooks/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -# -- -- -FROM oci.liz.coffee/emprespresso/ci-base:release AS hooks - -HEALTHCHECK --interval=10s --retries=3 --start-period=3s \ - CMD [ "curl --fail http://localhost:9000/health" ] - -CMD [ "/app/hooks/main.ts" ] -# -- -- diff --git a/hooks/deno.json b/hooks/deno.json deleted file mode 100644 index cdaf63f..0000000 --- a/hooks/deno.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@emprespresso/ci-hooks", - "exports": "./mod.ts" -} diff --git a/hooks/main.ts b/hooks/main.ts deleted file mode 100755 index 21a3f3f..0000000 --- a/hooks/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run - -import { CiHookServer } from "./server/mod.ts"; - -const server = new CiHookServer(); - -const SERVER_CONFIG = { - host: "0.0.0.0", - port: 9000, -}; - -Deno.serve(SERVER_CONFIG, (request: Request) => server.serve(request)); diff --git a/hooks/mod.ts b/hooks/mod.ts deleted file mode 100755 index cc15112..0000000 --- a/hooks/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./server/mod.ts"; -export * from "./main.ts"; diff --git a/hooks/server/ci.ts b/hooks/server/ci.ts deleted file mode 100644 index 4f0d7ba..0000000 --- a/hooks/server/ci.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - FourOhFourActivityImpl, - getRequiredEnv, - HealthCheckActivityImpl, - type HealthChecker, - type IFourOhFourActivity, - type IHealthCheckActivity, - type ITraceable, - PenguenoRequest, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; -import type { Job } from "@emprespresso/ci-model"; -import { - healthCheck as _healthCheck, - type IJobHookActivity, - type IJobQueuer, - JobHookActivityImpl, - LaminarJobQueuer, -} from "@emprespresso/ci-hooks"; - -export class CiHookServer { - constructor( - healthCheck: HealthChecker = _healthCheck, - jobQueuer: IJobQueuer> = new LaminarJobQueuer( - getRequiredEnv("LAMINAR_URL").fold((err, val) => - err ? "https://ci.liz.coffee" : val, - ), - ), - private readonly healthCheckActivity: IHealthCheckActivity = new HealthCheckActivityImpl( - healthCheck, - ), - private readonly jobHookActivity: IJobHookActivity = new JobHookActivityImpl( - jobQueuer, - ), - private readonly fourOhFourActivity: IFourOhFourActivity = new FourOhFourActivityImpl(), - ) {} - - private route(req: ITraceable) { - const url = new URL(req.get().url); - if (url.pathname === "/health") { - return this.healthCheckActivity.checkHealth(req); - } - if (url.pathname === "/job") { - return this.jobHookActivity.processHook(req); - } - return this.fourOhFourActivity.fourOhFour(req); - } - - public serve(req: Request): Promise { - return PenguenoRequest.from(req) - .bimap(TraceUtil.withClassTrace(this)) - .map(this.route) - .get(); - } -} diff --git a/hooks/server/health.ts b/hooks/server/health.ts deleted file mode 100644 index 1acc074..0000000 --- a/hooks/server/health.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - getRequiredEnv, - getStdout, - type HealthChecker, - type HealthCheckInput, - HealthCheckOutput, - type IEither, - type ITraceable, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; - -export const healthCheck: HealthChecker = ( - input: ITraceable, -): Promise> => - input - .bimap(TraceUtil.withFunctionTrace(healthCheck)) - .move(getRequiredEnv("LAMINAR_HOST")) - // ensure LAMINAR_HOST is propagated to getStdout for other procedures - .map((e) => e.get().moveRight(["laminarc", "show-jobs"])) - .map((i) => - i - .get() - .mapRight(i.move.apply) - .flatMapAsync(getStdout.apply) - .then((gotJobs) => gotJobs.moveRight(HealthCheckOutput.YAASSSLAYQUEEN)), - ) - .get(); diff --git a/hooks/server/job/activity.ts b/hooks/server/job/activity.ts deleted file mode 100644 index 173cedf..0000000 --- a/hooks/server/job/activity.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - Either, - ErrorSource, - type IActivity, - type IEither, - type ITraceable, - jsonModel, - JsonResponse, - LogLevel, - Metric, - PenguenoError, - type PenguenoRequest, - type ServerTrace, - TraceUtil, - validateExecutionEntries, -} from "@emprespresso/pengueno"; -import { isJob, type Job } from "@emprespresso/ci-model"; -import type { IJobQueuer } from "@emprespresso/ci-hooks"; - -const wellFormedJobMetric = Metric.fromName("Job.WellFormed"); - -const jobJsonTransformer = ( - j: ITraceable, -): IEither => - j - .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) - .map((tJson) => { - if (!isJob(tJson) || !validateExecutionEntries(tJson)) { - const err = "seems like a pwetty mawfomed job \\(-.-)/"; - tJson.trace.addTrace(LogLevel.WARN).trace(err); - return Either.left(new PenguenoError(err, 400)); - } - return Either.right(tJson); - }) - .peek((tJob) => - tJob.trace.trace( - tJob - .get() - .fold((err) => - err ? wellFormedJobMetric.failure : wellFormedJobMetric.success, - ), - ), - ) - .get(); - -export interface IJobHookActivity { - processHook: IActivity; -} - -const jobHookRequestMetric = Metric.fromName("JobHook.process"); -export class JobHookActivityImpl implements IJobHookActivity { - constructor( - private readonly queuer: IJobQueuer>, - ) {} - - private trace(r: ITraceable) { - return r - .bimap(TraceUtil.withClassTrace(this)) - .bimap(TraceUtil.withMetricTrace(jobHookRequestMetric)); - } - - public processHook(r: ITraceable) { - return this.trace(r) - .map(jsonModel(jobJsonTransformer)) - .map(async (tEitherJobJson) => { - const eitherJob = await tEitherJobJson.get(); - return eitherJob.flatMapAsync(async (job) => { - const eitherQueued = await tEitherJobJson - .move(job) - .map(this.queuer.queue) - .get(); - return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500)); - }); - }) - .peek( - TraceUtil.promiseify((tJob) => - tJob - .get() - .fold( - (err: PenguenoError | undefined, _val: string | undefined) => { - if (!err) { - tJob.trace.trace(jobHookRequestMetric.success); - tJob.trace.trace( - `all queued up and weady to go :D !! ${_val}`, - ); - return; - } - tJob.trace.trace( - err.source === ErrorSource.SYSTEM - ? jobHookRequestMetric.failure - : jobHookRequestMetric.warn, - ); - tJob.trace.addTrace(err.source).trace(`${err}`); - }, - ), - ), - ) - .map( - TraceUtil.promiseify( - (tEitherQueuedJob) => - new JsonResponse(r, tEitherQueuedJob.get(), { - status: tEitherQueuedJob - .get() - .fold(({ status }, _val) => (_val ? 200 : status)), - }), - ), - ) - .get(); - } -} diff --git a/hooks/server/job/mod.ts b/hooks/server/job/mod.ts deleted file mode 100644 index 6b4ae85..0000000 --- a/hooks/server/job/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./activity.ts"; -export * from "./queuer.ts"; diff --git a/hooks/server/job/queuer.ts b/hooks/server/job/queuer.ts deleted file mode 100644 index d30de22..0000000 --- a/hooks/server/job/queuer.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { - getStdout, - type IEither, - type ITraceable, - LogLevel, - type Mapper, - memoize, - Metric, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; -import type { Job } from "@emprespresso/ci-model"; - -type QueuePosition = string; -export class QueueError extends Error {} -export interface IJobQueuer { - queue: Mapper>>; -} - -export class LaminarJobQueuer - implements IJobQueuer> -{ - constructor(private readonly queuePositionPrefix: string) {} - - private static GetJobTypeTrace = (jobType: string) => - `LaminarJobQueue.Queue.${jobType}`; - private static JobTypeMetrics = memoize((jobType: string) => - Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)), - ); - - public queue(j: ITraceable) { - const { type: jobType } = j.get(); - const trace = LaminarJobQueuer.GetJobTypeTrace(jobType); - const metric = LaminarJobQueuer.JobTypeMetrics(trace); - - return j - .bimap(TraceUtil.withTrace(trace)) - .bimap(TraceUtil.withMetricTrace(metric)) - .map((j) => { - const { type: jobType, arguments: args } = j.get(); - const laminarCommand = [ - "laminarc", - "queue", - jobType, - ...Object.entries(args).map(([key, val]) => `"${key}"="${val}"`), - ]; - return laminarCommand; - }) - .peek((c) => - c.trace.trace( - `im so excited to see how this queue job will end!! (>ᴗ<): ${c - .get() - .toString()}`, - ), - ) - .map(getStdout) - .peek( - TraceUtil.promiseify((q) => - q.trace.trace( - q - .get() - .fold((err, _val) => (err ? metric.failure : metric.success)), - ), - ), - ) - .map( - TraceUtil.promiseify((q) => - q - .get() - .mapRight((stdout) => { - q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${stdout}`); - const [jobName, jobId] = stdout.split(":"); - const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; - - q.trace.trace( - `all queued up and weady to go~ (˘ω˘) => ${jobUrl}`, - ); - return jobUrl; - }) - .mapLeft((err) => { - q.trace.addTrace(LogLevel.ERROR).trace(err.toString()); - return err; - }), - ), - ) - .get(); - } -} diff --git a/hooks/server/mod.ts b/hooks/server/mod.ts deleted file mode 100644 index 0a520f9..0000000 --- a/hooks/server/mod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./ci.ts"; -export * from "./health.ts"; -export * from "./job/mod.ts"; diff --git a/mod.ts b/mod.ts new file mode 100755 index 0000000..81a2bc2 --- /dev/null +++ b/mod.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env -S deno run --allow-env --allow-net + +import { argv } from "@emprespresso/pengueno"; +import { runServer } from "@emprespresso/ci_server"; + +const main = (_argv = Deno.args) => { + const defaults = { + "--port": "9000", + "--host": "0.0.0.0", + }; + const _args = argv(["--run-server", "--port", "--host"], defaults, _argv); + const args = _args.fold((err, args) => { + if (!args || err) throw err; + return { + server_mode: "--run-server" in args, + port: parseInt(args["--port"]), + host: args["--host"], + }; + }); + + const promises: Array> = []; + if (args.server_mode) { + promises.push(runServer(args.port, args.host)); + } + + return Promise.all(promises); +}; + +if (import.meta.main) { + await main(); +} diff --git a/model/deno.json b/model/deno.json index afd9f22..5f5dacf 100644 --- a/model/deno.json +++ b/model/deno.json @@ -1,5 +1,5 @@ { - "name": "@emprespresso/ci-model", + "name": "@emprespresso/ci_model", "version": "0.1.0", "exports": "./mod.ts" } diff --git a/model/pipeline.ts b/model/pipeline.ts index d7b6435..edc8337 100644 --- a/model/pipeline.ts +++ b/model/pipeline.ts @@ -1,5 +1,5 @@ import { Either, type IEither, isObject } from "@emprespresso/pengueno"; -import { type FetchCodeJob, isJob, type Job } from "@emprespresso/ci-model"; +import { type FetchCodeJob, isJob, type Job } from "@emprespresso/ci_model"; export interface PipelineStage { readonly parallelJobs: Array; diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..c66e1b1 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,8 @@ +# -- -- +FROM oci.liz.coffee/emprespresso/ci_base:release AS server + +HEALTHCHECK --interval=10s --retries=3 --start-period=3s \ + CMD [ "curl --fail http://localhost:9000/health" ] + +CMD [ "/app/mod.ts --server --port 9000" ] +# -- -- diff --git a/server/ci.ts b/server/ci.ts new file mode 100644 index 0000000..e1a9ca7 --- /dev/null +++ b/server/ci.ts @@ -0,0 +1,56 @@ +import { + FourOhFourActivityImpl, + getRequiredEnv, + HealthCheckActivityImpl, + type HealthChecker, + type IFourOhFourActivity, + type IHealthCheckActivity, + type ITraceable, + PenguenoRequest, + type ServerTrace, + TraceUtil, +} from "@emprespresso/pengueno"; +import type { Job } from "@emprespresso/ci_model"; +import { + healthCheck as _healthCheck, + type IJobHookActivity, + type IJobQueuer, + JobHookActivityImpl, + LaminarJobQueuer, +} from "@emprespresso/ci_server"; + +export class CiHookServer { + constructor( + healthCheck: HealthChecker = _healthCheck, + jobQueuer: IJobQueuer> = new LaminarJobQueuer( + getRequiredEnv("LAMINAR_URL").fold((err, val) => + err ? "https://ci.liz.coffee" : val!, + ), + ), + private readonly healthCheckActivity: IHealthCheckActivity = new HealthCheckActivityImpl( + healthCheck, + ), + private readonly jobHookActivity: IJobHookActivity = new JobHookActivityImpl( + jobQueuer, + ), + private readonly fourOhFourActivity: IFourOhFourActivity = new FourOhFourActivityImpl(), + ) {} + + private route(req: ITraceable) { + const url = new URL(req.get().url); + if (url.pathname === "/health") { + return this.healthCheckActivity.checkHealth(req); + } + if (url.pathname === "/job") { + return this.jobHookActivity.processHook(req); + } + return this.fourOhFourActivity.fourOhFour(req); + } + + public serve(req: Request): Promise { + return PenguenoRequest.from(req) + .bimap(TraceUtil.withClassTrace(this)) + .map((req) => this.route(req)) + .get(); + } +} diff --git a/server/deno.json b/server/deno.json new file mode 100644 index 0000000..c86c9a7 --- /dev/null +++ b/server/deno.json @@ -0,0 +1,4 @@ +{ + "name": "@emprespresso/ci_server", + "exports": "./mod.ts" +} diff --git a/server/health.ts b/server/health.ts new file mode 100644 index 0000000..1acc074 --- /dev/null +++ b/server/health.ts @@ -0,0 +1,28 @@ +import { + getRequiredEnv, + getStdout, + type HealthChecker, + type HealthCheckInput, + HealthCheckOutput, + type IEither, + type ITraceable, + type ServerTrace, + TraceUtil, +} from "@emprespresso/pengueno"; + +export const healthCheck: HealthChecker = ( + input: ITraceable, +): Promise> => + input + .bimap(TraceUtil.withFunctionTrace(healthCheck)) + .move(getRequiredEnv("LAMINAR_HOST")) + // ensure LAMINAR_HOST is propagated to getStdout for other procedures + .map((e) => e.get().moveRight(["laminarc", "show-jobs"])) + .map((i) => + i + .get() + .mapRight(i.move.apply) + .flatMapAsync(getStdout.apply) + .then((gotJobs) => gotJobs.moveRight(HealthCheckOutput.YAASSSLAYQUEEN)), + ) + .get(); diff --git a/server/job.ts b/server/job.ts new file mode 100644 index 0000000..62582d6 --- /dev/null +++ b/server/job.ts @@ -0,0 +1,193 @@ +import { + getStdout, + type Mapper, + memoize, + Either, + ErrorSource, + type IActivity, + type IEither, + type ITraceable, + jsonModel, + JsonResponse, + LogLevel, + Metric, + PenguenoError, + type PenguenoRequest, + type ServerTrace, + TraceUtil, + validateExecutionEntries, +} from "@emprespresso/pengueno"; +import { isJob, type Job } from "@emprespresso/ci_model"; + +// -- -- +const wellFormedJobMetric = Metric.fromName("Job.WellFormed"); + +const jobJsonTransformer = ( + j: ITraceable, +): IEither => + j + .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) + .map((tJson) => { + if (!isJob(tJson) || !validateExecutionEntries(tJson)) { + const err = "seems like a pwetty mawfomed job \\(-.-)/"; + tJson.trace.addTrace(LogLevel.WARN).trace(err); + return Either.left(new PenguenoError(err, 400)); + } + return Either.right(tJson); + }) + .peek((tJob) => + tJob.trace.trace( + tJob + .get() + .fold((err) => + err ? wellFormedJobMetric.failure : wellFormedJobMetric.success, + ), + ), + ) + .get(); + +export interface IJobHookActivity { + processHook: IActivity; +} + +const jobHookRequestMetric = Metric.fromName("JobHook.process"); +export class JobHookActivityImpl implements IJobHookActivity { + constructor( + private readonly queuer: IJobQueuer>, + ) {} + + private trace(r: ITraceable) { + return r + .bimap(TraceUtil.withClassTrace(this)) + .bimap(TraceUtil.withMetricTrace(jobHookRequestMetric)); + } + + public processHook(r: ITraceable) { + return this.trace(r) + .map(jsonModel(jobJsonTransformer)) + .map(async (tEitherJobJson) => { + const eitherJob = await tEitherJobJson.get(); + return eitherJob.flatMapAsync(async (job) => { + const eitherQueued = await tEitherJobJson + .move(job) + .map(this.queuer.queue) + .get(); + return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500)); + }); + }) + .peek( + TraceUtil.promiseify((tJob) => + tJob + .get() + .fold( + (err: PenguenoError | undefined, _val: string | undefined) => { + if (!err) { + tJob.trace.trace(jobHookRequestMetric.success); + tJob.trace.trace( + `all queued up and weady to go :D !! ${_val}`, + ); + return; + } + tJob.trace.trace( + err.source === ErrorSource.SYSTEM + ? jobHookRequestMetric.failure + : jobHookRequestMetric.warn, + ); + tJob.trace.addTrace(err.source).trace(`${err}`); + }, + ), + ), + ) + .map( + TraceUtil.promiseify( + (tEitherQueuedJob) => + new JsonResponse(r, tEitherQueuedJob.get(), { + status: tEitherQueuedJob + .get() + .fold((err, _val) => (_val ? 200 : err?.status ?? 500)), + }), + ), + ) + .get(); + } +} + +// -- -- + +// -- -- +type QueuePosition = string; +export class QueueError extends Error {} +export interface IJobQueuer { + queue: Mapper>>; +} + +export class LaminarJobQueuer + implements IJobQueuer> +{ + constructor(private readonly queuePositionPrefix: string) {} + + private static GetJobTypeTrace = (jobType: string) => + `LaminarJobQueue.Queue.${jobType}`; + private static JobTypeMetrics = memoize((jobType: string) => + Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)), + ); + + public queue(j: ITraceable) { + const { type: jobType } = j.get(); + const trace = LaminarJobQueuer.GetJobTypeTrace(jobType); + const metric = LaminarJobQueuer.JobTypeMetrics(trace); + + return j + .bimap(TraceUtil.withTrace(trace)) + .bimap(TraceUtil.withMetricTrace(metric)) + .map((j) => { + const { type: jobType, arguments: args } = j.get(); + const laminarCommand = [ + "laminarc", + "queue", + jobType, + ...Object.entries(args).map(([key, val]) => `"${key}"="${val}"`), + ]; + return laminarCommand; + }) + .peek((c) => + c.trace.trace( + `im so excited to see how this queue job will end!! (>ᴗ<): ${c + .get() + .toString()}`, + ), + ) + .map(getStdout) + .peek( + TraceUtil.promiseify((q) => + q.trace.trace( + q + .get() + .fold((err, _val) => (err ? metric.failure : metric.success)), + ), + ), + ) + .map( + TraceUtil.promiseify((q) => + q + .get() + .mapRight((stdout) => { + q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${stdout}`); + const [jobName, jobId] = stdout.split(":"); + const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; + + q.trace.trace( + `all queued up and weady to go~ (˘ω˘) => ${jobUrl}`, + ); + return jobUrl; + }) + .mapLeft((err) => { + q.trace.addTrace(LogLevel.ERROR).trace(err.toString()); + return err; + }), + ), + ) + .get(); + } +} +// -- -- diff --git a/server/mod.ts b/server/mod.ts new file mode 100644 index 0000000..9dc57aa --- /dev/null +++ b/server/mod.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run + +export * from "./ci.ts"; +export * from "./health.ts"; +export * from "./job.ts"; + +import { CiHookServer } from "./mod.ts"; +const server = new CiHookServer(); + +export const runServer = (port: number, host: string) => { + const serverConfig = { + host, + port, + }; + return Deno.serve(serverConfig, (req) => server.serve(req)).finished; +}; diff --git a/u/deno.json b/u/deno.json index 26b08bf..b277873 100644 --- a/u/deno.json +++ b/u/deno.json @@ -1,6 +1,5 @@ { "name": "@emprespresso/pengueno", "version": "0.1.0", - "exports": "./mod.ts", - "workspace": ["./*"] + "exports": "./mod.ts" } diff --git a/u/fn/either.ts b/u/fn/either.ts index 8b233bf..b228af2 100644 --- a/u/fn/either.ts +++ b/u/fn/either.ts @@ -10,7 +10,7 @@ export interface IEither { errBranch: Mapper, okBranch: Mapper, ) => IEither; - fold: (folder: BiMapper) => Tt; + fold: (folder: (err: E | undefined, val: T | undefined) => Tt) => Tt; //BiMapper) => Tt;; moveRight: (t: Tt) => IEither; mapRight: (mapper: Mapper) => IEither; mapLeft: (mapper: Mapper) => IEither; diff --git a/u/process/argv.ts b/u/process/argv.ts new file mode 100644 index 0000000..657c9a7 --- /dev/null +++ b/u/process/argv.ts @@ -0,0 +1,65 @@ +import { Either, type IEither } from "@emprespresso/pengueno"; + +export const isArgKey = (k: string): k is K => + k.startsWith("--"); + +export const getArg = ( + arg: K, + args: Array, +): IEither => { + const result = args.findIndex((_arg) => _arg.startsWith(arg)); + if (result < 0) return Either.left(new Error("no argument found for " + arg)); + const [resultArg, next]: Array = [ + args[result], + args.at(result + 1), + ]; + if (resultArg && resultArg.includes("=")) { + return Either.right(resultArg.split("=")[1] as V); + } + if (typeof next === "undefined") + return Either.left(new Error("no value specified for " + arg)); + if (isArgKey(next)) + return Either.left( + new Error("next value for arg " + arg + " is another arg " + next), + ); + return Either.right(next as V); +}; + +type ObjectFromList, V = string> = { + [K in T extends ReadonlyArray ? U : never]: V; +}; +export const argv = ( + args: ReadonlyArray, + defaultArgs?: Partial>, + argv = Deno.args, +) => { + return args + .map((arg) => [arg, getArg(arg, argv)] as [K, IEither]) + .map(([arg, specified]): [K, IEither] => [ + arg, + specified.fold((e, val) => { + const hasDefaultVal = e && defaultArgs && arg in defaultArgs; + if (hasDefaultVal) { + return Either.right(defaultArgs[arg]!); + } else if (!val || e) { + return Either.left(e ?? new Error("unknown")); + } + return Either.right(val); + }), + ]) + .reduce( + ( + acc: IEither>, + x: [K, IEither], + ): IEither> => { + const [arg, eitherVal] = x; + return acc.flatMap((args) => { + return eitherVal.mapRight((envValue) => ({ + ...args, + [arg]: envValue, + })); + }); + }, + Either.right({} as ObjectFromList), + ); +}; diff --git a/u/process/mod.ts b/u/process/mod.ts index 3f02d46..211e9a7 100644 --- a/u/process/mod.ts +++ b/u/process/mod.ts @@ -1,3 +1,4 @@ export * from "./env.ts"; export * from "./run.ts"; export * from "./validate_identifier.ts"; +export * from "./argv.ts"; diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts index c09aef6..ed8c7eb 100644 --- a/u/server/activity/fourohfour.ts +++ b/u/server/activity/fourohfour.ts @@ -7,14 +7,10 @@ import { } from "@emprespresso/pengueno"; const messages = [ - "(≧ω≦)ゞ Oopsie! This endpoint has gone a-404-dable!", - "。゚(。ノωヽ。)゚。 Meow-t found! Your API call ran away!", - "404-bidden! But like...in a cute way (・`ω´・) !", - "(=①ω①=) This endpoint is hiss-terically missing!", - "┐(´∀`)┌ Whoopsie fluff! No API here!", - "(つ≧▽≦)つ Your data went on a paw-sible vacation!", - "(ꈍᴗꈍ) Uwu~ not found, but found our hearts instead!", - "ヽ(;▽;)ノ Eep! This route has ghosted you~", + "D: Meow-t found! Your API call ran away!", + "404-bidden! But like...in a cute way >:3 !", + ":o Your data went on a paw-sible vacation!", + "uwu~ not found, but found our hearts instead!", ]; const randomFourOhFour = () => messages[Math.random() * messages.length]; diff --git a/u/server/request.ts b/u/server/request.ts index 480ee69..c857f88 100644 --- a/u/server/request.ts +++ b/u/server/request.ts @@ -2,11 +2,10 @@ import { LogMetricTraceable } from "@emprespresso/pengueno"; const greetings = [ "hewwo :D", - "hiya cutie (✿◠‿◠)", - "boop! ૮・ᴥ・ა", - "sending virtual hugs! (づ。◕‿‿◕。)づ", - "stay pawsitive ₍^..^₎⟆", - "⋆。‧˚❆🐧❆˚‧。⋆", + "hiya cutie", + "boop!", + "sending virtual hugs!", + "stay pawsitive", ]; const penguenoGreeting = () => greetings[Math.floor(Math.random() * greetings.length)]; diff --git a/worker/Dockerfile b/worker/Dockerfile index d87d67f..24193be 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -1,4 +1,4 @@ -# -- -- +# -- -- FROM debian:stable-slim AS worker-dependencies # Define versions as build arguments to improve caching @@ -11,10 +11,10 @@ RUN unzip /bw-linux.zip -d / \ RUN curl -L "https://get.docker.com/builds/$(uname -s)/$(uname -m)/docker-latest.tgz" > /docker.tgz RUN tar -xvzf /docker.tgz -# -- -- +# -- -- -# -- -- -FROM oci.liz.coffee/emprespresso/ci-base:release AS worker +# -- -- +FROM oci.liz.coffee/emprespresso/ci.base:release AS worker RUN apt-get update && apt-get install -yqq git jq RUN groupadd docker @@ -36,4 +36,4 @@ HEALTHCHECK --interval=10s --retries=3 --start-period=3s \ CMD [ "/usr/bin/laminarc show-jobs" ] CMD [ "/usr/sbin/laminard" ] -# -- -- +# -- -- diff --git a/worker/deno.json b/worker/deno.json index 90f50c9..c908330 100644 --- a/worker/deno.json +++ b/worker/deno.json @@ -1,4 +1,4 @@ { - "name": "@emprespresso/ci-worker", + "name": "@emprespresso/ci_worker", "exports": "./mod.ts" } diff --git a/worker/executor.ts b/worker/executor.ts new file mode 100644 index 0000000..faa40a6 --- /dev/null +++ b/worker/executor.ts @@ -0,0 +1,99 @@ +import { + getStdout, + type ITraceable, + LogLevel, + type LogMetricTraceSupplier, + memoize, + Metric, + TraceUtil, + validateExecutionEntries, + Either, + type IEither, +} from "@emprespresso/pengueno"; +import type { Job, JobArgT, Pipeline } from "@emprespresso/ci_model"; + +// -- -- +const jobTypeMetric = memoize((type: string) => Metric.fromName(`run.${type}`)); +export const executeJob = (tJob: ITraceable) => + tJob + .bimap(TraceUtil.withMetricTrace(jobTypeMetric(tJob.get().type))) + .peek((tJob) => + tJob.trace.trace(`let's do this little job ok!! ${tJob.get()}`), + ) + .map((tJob) => + validateExecutionEntries(tJob.get().arguments) + .mapLeft((badEntries) => { + tJob.trace.addTrace(LogLevel.ERROR).trace(badEntries.toString()); + return new Error("invalid job arguments"); + }) + .flatMapAsync((args) => + getStdout(tJob.move(tJob.get().type), { env: args }), + ), + ) + .peek( + TraceUtil.promiseify((q) => + q.trace.trace( + q + .get() + .fold( + (err, _val) => + jobTypeMetric(tJob.get().type)[err ? "failure" : "success"], + ), + ), + ), + ) + .get(); +// -- -- + +// -- -- +const pipelinesMetric = Metric.fromName("pipelines"); +export const executePipeline = ( + tPipeline: ITraceable, + baseEnv?: JobArgT, +): Promise> => + tPipeline + .bimap(TraceUtil.withFunctionTrace(executePipeline)) + .bimap(TraceUtil.withMetricTrace(pipelinesMetric)) + .map(async (tJobs): Promise> => { + for (const [i, serialStage] of tJobs.get().serialJobs.entries()) { + tJobs.trace.trace( + `executing stage ${i}. do your best little stage :>\n${serialStage}`, + ); + const jobResults = await Promise.all( + serialStage.parallelJobs.map((job) => + tJobs + .bimap((_) => [job, `stage ${i}`]) + .map( + (tJob) => + { + ...tJob.get(), + arguments: { + ...baseEnv, + ...tJob.get().arguments, + }, + }, + ) + .map(executeJob) + .peek( + TraceUtil.promiseify((tEitherJobOutput) => + tEitherJobOutput + .get() + .mapRight((stdout) => + tEitherJobOutput.trace.addTrace("STDOUT").trace(stdout), + ), + ), + ) + .get(), + ), + ); + const failures = jobResults.filter((e) => e.fold((err) => !!err)); + if (failures.length > 0) { + tJobs.trace.trace(pipelinesMetric.failure); + return Either.left(new Error(failures.toString())); + } + } + tJobs.trace.trace(pipelinesMetric.success); + return Either.right(undefined); + }) + .get(); +// -- -- diff --git a/worker/executor/job.ts b/worker/executor/job.ts deleted file mode 100644 index ca7feed..0000000 --- a/worker/executor/job.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - getStdout, - type ITraceable, - LogLevel, - type LogMetricTraceSupplier, - memoize, - Metric, - TraceUtil, - validateExecutionEntries, -} from "@emprespresso/pengueno"; -import type { Job } from "@emprespresso/ci-model"; - -const jobTypeMetric = memoize((type: string) => Metric.fromName(`run.${type}`)); -export const executeJob = (tJob: ITraceable) => - tJob - .bimap(TraceUtil.withMetricTrace(jobTypeMetric(tJob.get().type))) - .peek((tJob) => - tJob.trace.trace(`let's do this little job ok!! ${tJob.get()}`), - ) - .map((tJob) => - validateExecutionEntries(tJob.get().arguments) - .mapLeft((badEntries) => { - tJob.trace.addTrace(LogLevel.ERROR).trace(badEntries.toString()); - return new Error("invalid job arguments"); - }) - .flatMapAsync((args) => - getStdout(tJob.move(tJob.get().type), { env: args }), - ), - ) - .peek( - TraceUtil.promiseify((q) => - q.trace.trace( - q - .get() - .fold( - (err, _val) => - jobTypeMetric(tJob.get().type)[err ? "failure" : "success"], - ), - ), - ), - ) - .get(); diff --git a/worker/executor/mod.ts b/worker/executor/mod.ts deleted file mode 100644 index 944ab7d..0000000 --- a/worker/executor/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./job.ts"; -export * from "./pipeline.ts"; diff --git a/worker/executor/pipeline.ts b/worker/executor/pipeline.ts deleted file mode 100644 index c8423b1..0000000 --- a/worker/executor/pipeline.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - Either, - type IEither, - type ITraceable, - type LogMetricTraceSupplier, - Metric, - TraceUtil, -} from "@emprespresso/pengueno"; -import type { Job, JobArgT, Pipeline } from "@emprespresso/ci-model"; -import { executeJob } from "./mod.ts"; - -const pipelinesMetric = Metric.fromName("pipelines"); -export const executePipeline = ( - tPipeline: ITraceable, - baseEnv?: JobArgT, -): Promise> => - tPipeline - .bimap(TraceUtil.withFunctionTrace(executePipeline)) - .bimap(TraceUtil.withMetricTrace(pipelinesMetric)) - .map(async (tJobs): Promise> => { - for (const [i, serialStage] of tJobs.get().serialJobs.entries()) { - tJobs.trace.trace( - `executing stage ${i}. do your best little stage :>\n${serialStage}`, - ); - const jobResults = await Promise.all( - serialStage.parallelJobs.map((job) => - tJobs - .bimap((_) => [job, `stage ${i}`]) - .map( - (tJob) => - { - ...tJob.get(), - arguments: { ...baseEnv, ...tJob.get().arguments }, - }, - ) - .map(executeJob) - .peek( - TraceUtil.promiseify((tEitherJobOutput) => - tEitherJobOutput - .get() - .mapRight((stdout) => - tEitherJobOutput.trace.addTrace("STDOUT").trace(stdout), - ), - ), - ) - .get(), - ), - ); - const failures = jobResults.filter((e) => e.fold((err) => !!err)); - if (failures.length > 0) { - tJobs.trace.trace(pipelinesMetric.failure); - return Either.left(new Error(failures.toString())); - } - } - tJobs.trace.trace(pipelinesMetric.success); - return Either.right(undefined); - }) - .get(); diff --git a/worker/jobs/ci_pipeline.run b/worker/jobs/ci_pipeline.run index 337bd53..434850c 100644 --- a/worker/jobs/ci_pipeline.run +++ b/worker/jobs/ci_pipeline.run @@ -1,3 +1,5 @@ +#!/usr/bin/env -S deno run --allow-all + import { type Command, Either, @@ -13,8 +15,8 @@ import { type CheckoutCiJob, type FetchCodeJob, PipelineImpl, -} from "@emprespresso/ci-model"; -import { executeJob, executePipeline } from "@emprespresso/ci-worker"; +} from "@emprespresso/ci_model"; +import { executeJob, executePipeline } from "@emprespresso/ci_worker"; const run = Date.now().toString(); const eitherJob = getRequiredEnvVars(["remote", "refname", "rev"]) @@ -122,7 +124,7 @@ await LogMetricTraceable.from(eitherJob).bimap(TraceUtil.withTrace(trace)) .get() .then((e) => e.flatMap(() => eitherJob).fold((err, val) => { - if (err) throw err; + if (!val || err) throw err; return Deno.remove(getWorkingDirectoryForCiJob(val), { recursive: true }); }) ); diff --git a/worker/mod.ts b/worker/mod.ts index affcb2c..97980a8 100644 --- a/worker/mod.ts +++ b/worker/mod.ts @@ -1,2 +1,2 @@ -export * from "./secret/mod.ts"; -export * from "./executor/mod.ts"; +export * from "./secret.ts"; +export * from "./executor.ts"; diff --git a/worker/scripts/ansible_playbook b/worker/scripts/ansible_playbook index e9e967c..f2cd4b9 100755 --- a/worker/scripts/ansible_playbook +++ b/worker/scripts/ansible_playbook @@ -10,8 +10,8 @@ import { prependWith, TraceUtil, } from "@emprespresso/pengueno"; -import type { AnsiblePlaybookJob } from "@emprespresso/ci-model"; -import { Bitwarden, type SecureNote } from "@emprespresso/ci-worker"; +import type { AnsiblePlaybookJob } from "@emprespresso/ci_model"; +import { Bitwarden, type SecureNote } from "@emprespresso/ci_worker"; const eitherJob = getRequiredEnvVars([ "path", diff --git a/worker/scripts/build_docker_image b/worker/scripts/build_docker_image index 1dd0c3d..dc0e961 100755 --- a/worker/scripts/build_docker_image +++ b/worker/scripts/build_docker_image @@ -11,8 +11,8 @@ import { import type { BuildDockerImageJob, BuildDockerImageJobProps, -} from "@emprespresso/ci-model"; -import { Bitwarden, type LoginItem } from "@emprespresso/ci-worker"; +} from "@emprespresso/ci_model"; +import { Bitwarden, type LoginItem } from "@emprespresso/ci_worker"; const eitherJob = getRequiredEnvVars([ "registry", @@ -38,7 +38,7 @@ await LogMetricTraceable.from(eitherJob) .bimap( (tEitherJob) => { const trace = "build_docker_image." + - tEitherJob.get().fold((_, v) => v?.buildTarget ?? ""); + tEitherJob.get().fold((_, v) => v?.arguments.buildTarget ?? ""); return [tEitherJob.get(), trace]; }, ) @@ -116,13 +116,13 @@ await LogMetricTraceable.from(eitherJob) tEitherWithBuiltImage.trace.trace( buildImageMetric[err ? "failure" : "success"], ); - if (err) { + if (!val || err) { tEitherWithBuiltImage.trace.addTrace(LogLevel.ERROR).trace( `oh nyoo we couldn't buiwd the img :(( ${err}`, ); return; } - tEitherWithBuiltImage.trace.addTrace("buildOutput").trace(val); + tEitherWithBuiltImage.trace.addTrace("buildOutput").trace(val.buildOutput); }); }) .map(async (tEitherWithBuiltImage) => { diff --git a/worker/secret.ts b/worker/secret.ts new file mode 100644 index 0000000..e0a4c5d --- /dev/null +++ b/worker/secret.ts @@ -0,0 +1,193 @@ +import { + Either, + getRequiredEnvVars, + getStdout, + type IEither, + type ITraceable, + type LogMetricTraceSupplier, + Metric, + TraceUtil, +} from "@emprespresso/pengueno"; + +// -- -- +export interface LoginItem { + login: { + username: string; + password: string; + }; +} + +export interface SecureNote { + notes: string; +} + +export type SecretItem = LoginItem | SecureNote; +export interface IVault { + unlock: (client: TClient) => Promise>; + lock: (client: TClient, key: TKey) => Promise>; + + fetchSecret: ( + client: TClient, + key: TKey, + item: TItemId, + ) => Promise>; +} +// -- -- + +// -- -- +type TClient = ITraceable; +type TKey = string; +type TItemId = string; +export class Bitwarden implements IVault { + constructor(private readonly config: BitwardenConfig) {} + + public unlock(client: TClient) { + return client + .move(this.config) + .bimap(TraceUtil.withMetricTrace(Bitwarden.loginMetric)) + .flatMap((tConfig) => + tConfig.move(`bw config server ${tConfig.get().server}`).map(getStdout), + ) + .map(async (tEitherWithConfig) => { + const eitherWithConfig = await tEitherWithConfig.get(); + tEitherWithConfig.trace.trace("logging in~ ^.^"); + return eitherWithConfig.flatMapAsync((_) => + tEitherWithConfig + .move("bw login --apikey --quiet") + .map(getStdout) + .get(), + ); + }) + .peek(async (tEitherWithAuthd) => { + const eitherWithAuthd = await tEitherWithAuthd.get(); + return tEitherWithAuthd.trace.trace( + eitherWithAuthd.fold( + (err, _val) => Bitwarden.loginMetric[err ? "failure" : "success"], + ), + ); + }) + .map(async (tEitherWithAuthd) => { + const eitherWithAuthd = await tEitherWithAuthd.get(); + tEitherWithAuthd.trace.trace("unlocking the secret vault~ (◕ᴗ◕✿)"); + return eitherWithAuthd.flatMapAsync((_) => + tEitherWithAuthd + .move("bw unlock --passwordenv BW_PASSWORD --raw") + .map(getStdout) + .get(), + ); + }) + .peek(async (tEitherWithSession) => { + const eitherWithAuthd = await tEitherWithSession.get(); + return tEitherWithSession.trace.trace( + eitherWithAuthd.fold( + (err, _val) => + Bitwarden.unlockVaultMetric[err ? "failure" : "success"], + ), + ); + }) + .get(); + } + + public fetchSecret( + client: TClient, + key: string, + item: string, + ): Promise> { + return client + .move(key) + .bimap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric)) + .peek((tSession) => + tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`), + ) + .flatMap((tSession) => + tSession + .move("bw list items") + .map((listCmd) => + getStdout(listCmd, { env: { BW_SESSION: tSession.get() } }), + ), + ) + .map( + TraceUtil.promiseify((tEitherItemsJson) => + tEitherItemsJson + .get() + .flatMap( + (itemsJson): IEither> => + Either.fromFailable(() => JSON.parse(itemsJson)), + ) + .flatMap((itemsList): IEither => { + const secret = itemsList.find(({ name }) => name === item); + if (!secret) { + return Either.left( + new Error(`couldn't find the item ${item} (。•́︿•̀。)`), + ); + } + return Either.right(secret); + }), + ), + ) + .peek(async (tEitherWithSecret) => { + const eitherWithSecret = await tEitherWithSecret.get(); + return tEitherWithSecret.trace.trace( + eitherWithSecret.fold( + (err, _val) => + Bitwarden.fetchSecretMetric[err ? "failure" : "success"], + ), + ); + }) + .get(); + } + + public lock(client: TClient, key: TKey) { + return client + .move(key) + .bimap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric)) + .peek((tSession) => + tSession.trace.trace(`taking care of locking the vault :3`), + ) + .flatMap((tSession) => + tSession + .move("bw lock") + .map((lockCmd) => + getStdout(lockCmd, { env: { BW_SESSION: tSession.get() } }), + ), + ) + .peek(async (tEitherWithLocked) => { + const eitherWithLocked = await tEitherWithLocked.get(); + return eitherWithLocked.fold((err, _val) => { + tEitherWithLocked.trace.trace( + Bitwarden.lockVaultMetric[err ? "failure" : "success"], + ); + if (err) return; + tEitherWithLocked.trace.trace( + "all locked up and secure now~ (。•̀ᴗ-)✧", + ); + }); + }) + .get(); + } + + public static getConfigFromEnvironment(): IEither { + return getRequiredEnvVars([ + "BW_SERVER", + "BW_CLIENTSECRET", + "BW_CLIENTID", + "BW_PASSWORD", + ]).mapRight(({ BW_SERVER, BW_CLIENTSECRET, BW_CLIENTID }) => ({ + clientId: BW_CLIENTID, + secret: BW_CLIENTSECRET, + server: BW_SERVER, + })); + } + + private static loginMetric = Metric.fromName("Bitwarden.login"); + private static unlockVaultMetric = Metric.fromName("Bitwarden.unlockVault"); + private static fetchSecretMetric = Metric.fromName("Bitwarden.fetchSecret"); + private static lockVaultMetric = Metric.fromName("Bitwarden.lock"); +} + +export interface BitwardenConfig { + server: string; + secret: string; + clientId: string; +} +// -- -- diff --git a/worker/secret/bitwarden.ts b/worker/secret/bitwarden.ts deleted file mode 100644 index 7145f49..0000000 --- a/worker/secret/bitwarden.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - Either, - getRequiredEnvVars, - getStdout, - type IEither, - type ITraceable, - type LogMetricTraceSupplier, - Metric, - TraceUtil, -} from "@emprespresso/pengueno"; -import type { IVault, SecretItem } from "./mod.ts"; - -type TClient = ITraceable; -type TKey = string; -type TItemId = string; -export class Bitwarden implements IVault { - constructor(private readonly config: BitwardenConfig) {} - - public unlock(client: TClient) { - return client - .move(this.config) - .bimap(TraceUtil.withMetricTrace(Bitwarden.loginMetric)) - .flatMap((tConfig) => - tConfig.move(`bw config server ${tConfig.get().server}`).map(getStdout), - ) - .map(async (tEitherWithConfig) => { - const eitherWithConfig = await tEitherWithConfig.get(); - tEitherWithConfig.trace.trace("logging in~ ^.^"); - return eitherWithConfig.flatMapAsync((_) => - tEitherWithConfig - .move("bw login --apikey --quiet") - .map(getStdout) - .get(), - ); - }) - .peek(async (tEitherWithAuthd) => { - const eitherWithAuthd = await tEitherWithAuthd.get(); - return tEitherWithAuthd.trace.trace( - eitherWithAuthd.fold( - (err, _val) => Bitwarden.loginMetric[err ? "failure" : "success"], - ), - ); - }) - .map(async (tEitherWithAuthd) => { - const eitherWithAuthd = await tEitherWithAuthd.get(); - tEitherWithAuthd.trace.trace("unlocking the secret vault~ (◕ᴗ◕✿)"); - return eitherWithAuthd.flatMapAsync((_) => - tEitherWithAuthd - .move("bw unlock --passwordenv BW_PASSWORD --raw") - .map(getStdout) - .get(), - ); - }) - .peek(async (tEitherWithSession) => { - const eitherWithAuthd = await tEitherWithSession.get(); - return tEitherWithSession.trace.trace( - eitherWithAuthd.fold( - (err, _val) => - Bitwarden.unlockVaultMetric[err ? "failure" : "success"], - ), - ); - }) - .get(); - } - - public fetchSecret( - client: TClient, - key: string, - item: string, - ): Promise> { - return client - .move(key) - .bimap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric)) - .peek((tSession) => - tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`), - ) - .flatMap((tSession) => - tSession - .move("bw list items") - .map((listCmd) => - getStdout(listCmd, { env: { BW_SESSION: tSession.get() } }), - ), - ) - .map( - TraceUtil.promiseify((tEitherItemsJson) => - tEitherItemsJson - .get() - .flatMap( - (itemsJson): IEither> => - Either.fromFailable(() => JSON.parse(itemsJson)), - ) - .flatMap((itemsList): IEither => { - const secret = itemsList.find(({ name }) => name === item); - if (!secret) { - return Either.left( - new Error(`couldn't find the item ${item} (。•́︿•̀。)`), - ); - } - return Either.right(secret); - }), - ), - ) - .peek(async (tEitherWithSecret) => { - const eitherWithSecret = await tEitherWithSecret.get(); - return tEitherWithSecret.trace.trace( - eitherWithSecret.fold( - (err, _val) => - Bitwarden.fetchSecretMetric[err ? "failure" : "success"], - ), - ); - }) - .get(); - } - - public lock(client: TClient, key: TKey) { - return client - .move(key) - .bimap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric)) - .peek((tSession) => - tSession.trace.trace(`taking care of locking the vault :3`), - ) - .flatMap((tSession) => - tSession - .move("bw lock") - .map((lockCmd) => - getStdout(lockCmd, { env: { BW_SESSION: tSession.get() } }), - ), - ) - .peek(async (tEitherWithLocked) => { - const eitherWithLocked = await tEitherWithLocked.get(); - return eitherWithLocked.fold((err, _val) => { - tEitherWithLocked.trace.trace( - Bitwarden.lockVaultMetric[err ? "failure" : "success"], - ); - if (err) return; - tEitherWithLocked.trace.trace( - "all locked up and secure now~ (。•̀ᴗ-)✧", - ); - }); - }) - .get(); - } - - public static getConfigFromEnvironment(): IEither { - return getRequiredEnvVars([ - "BW_SERVER", - "BW_CLIENTSECRET", - "BW_CLIENTID", - "BW_PASSWORD", - ]).mapRight(({ BW_SERVER, BW_CLIENTSECRET, BW_CLIENTID }) => ({ - clientId: BW_CLIENTID, - secret: BW_CLIENTSECRET, - server: BW_SERVER, - })); - } - - private static loginMetric = Metric.fromName("Bitwarden.login"); - private static unlockVaultMetric = Metric.fromName("Bitwarden.unlockVault"); - private static fetchSecretMetric = Metric.fromName("Bitwarden.fetchSecret"); - private static lockVaultMetric = Metric.fromName("Bitwarden.lock"); -} - -export interface BitwardenConfig { - server: string; - secret: string; - clientId: string; -} diff --git a/worker/secret/ivault.ts b/worker/secret/ivault.ts deleted file mode 100644 index e101e56..0000000 --- a/worker/secret/ivault.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IEither } from "@emprespresso/pengueno"; - -export interface LoginItem { - login: { - username: string; - password: string; - }; -} - -export interface SecureNote { - notes: string; -} - -export type SecretItem = LoginItem | SecureNote; -export interface IVault { - unlock: (client: TClient) => Promise>; - lock: (client: TClient, key: TKey) => Promise>; - - fetchSecret: ( - client: TClient, - key: TKey, - item: TItemId, - ) => Promise>; -} diff --git a/worker/secret/mod.ts b/worker/secret/mod.ts deleted file mode 100644 index 70a1ea9..0000000 --- a/worker/secret/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ivault.ts"; -export * from "./bitwarden.ts"; -- cgit v1.2.3-70-g09d2