From f48570e594b5142d551da705a04910e4940eab03 Mon Sep 17 00:00:00 2001 From: tjjfvi Date: Mon, 20 Mar 2023 11:23:38 -0400 Subject: [PATCH] capi.dev --- main.ts | 2 +- server/capi.dev/delegatee.ts | 26 +++++++++ server/capi.dev/delegator.ts | 97 +++++++++++++++++++++++++++++++++ server/factories.ts | 2 +- server/{local.ts => handler.ts} | 24 +++++--- server/mod.ts | 1 + util/cache/base.ts | 7 --- util/cache/fs.ts | 15 ----- util/cache/memory.ts | 3 - util/cache/s3.ts | 8 +-- 10 files changed, 143 insertions(+), 42 deletions(-) create mode 100644 server/capi.dev/delegatee.ts create mode 100644 server/capi.dev/delegator.ts rename server/{local.ts => handler.ts} (74%) diff --git a/main.ts b/main.ts index 91ea63533..c495e2d0b 100644 --- a/main.ts +++ b/main.ts @@ -7,7 +7,7 @@ import { WssProvider, ZombienetProvider, } from "./providers/frame/mod.ts" -import { handler } from "./server/local.ts" +import { handler } from "./server/handler.ts" import { Env } from "./server/mod.ts" import { FsCache } from "./util/cache/mod.ts" diff --git a/server/capi.dev/delegatee.ts b/server/capi.dev/delegatee.ts new file mode 100644 index 000000000..048501be8 --- /dev/null +++ b/server/capi.dev/delegatee.ts @@ -0,0 +1,26 @@ +import { serve } from "../../deps/std/http.ts" +import { WssProvider } from "../../providers/frame/wss.ts" +import { S3Cache } from "../../util/cache/mod.ts" +import { handler } from "../handler.ts" +import { Env } from "../mod.ts" + +// this is only used by the dev providers, which should target the local server +const href = "http://localhost:4646/" + +const controller = new AbortController() +const { signal } = controller + +const cache = new S3Cache(Deno.env.get("DENO_DEPLOYMENT_ID")!, { + accessKeyID: Deno.env.get("S3_ACCESS_KEY")!, + secretKey: Deno.env.get("S3_SECRET_KEY")!, + region: Deno.env.get("S3_REGION")!, + bucket: Deno.env.get("S3_BUCKET")!, +}, signal) + +const env = new Env(href, cache, signal, (env) => ({ + frame: { + wss: new WssProvider(env), + }, +})) + +serve(handler(env)) diff --git a/server/capi.dev/delegator.ts b/server/capi.dev/delegator.ts new file mode 100644 index 000000000..0f40b4d85 --- /dev/null +++ b/server/capi.dev/delegator.ts @@ -0,0 +1,97 @@ +import { serve } from "../../deps/std/http.ts" +import { TimedMemo } from "../../util/memo.ts" +import { f, handleErrors } from "../mod.ts" + +serve(handleErrors(handler)) + +const ttl = 60_000 +const shaAbbrevLength = 8 + +const rTagVersion = /^v(\d+\.\d+\.\d+(?:-.+)?)$/ +const rPrVersion = /^pr:(\d+)$/ +const rVersionedUrl = /^(?:\/@(.+?))?(\/.*)?$/ + +const delegateeProjectId = "70eddd08-c9b0-4cb3-b100-8c6facf52f1e" +const githubApiBase = "https://api.github.com/repos/paritytech/capi/" + +const { signal } = new AbortController() + +async function handler(request: Request) { + const url = new URL(request.url) + const [, version, path = "/"] = rVersionedUrl.exec(url.pathname)! + if (!version) return f.redirect(`/@${await defaultVersion()}${path}`) + const sha = await getSha(version) + let normalizedVersion + if (rTagVersion.test(version)) { + normalizedVersion = version.replace(rTagVersion, "v$1") + } else { + normalizedVersion = sha.slice(0, shaAbbrevLength) + } + if (version !== normalizedVersion) { + return f.redirect(`/@${normalizedVersion}${path}`) + } + const deploymentUrl = await getDeployment(sha) + return await fetch(new URL(path, deploymentUrl), { + method: request.method, + headers: request.headers, + body: request.body, + }) +} + +const defaultVersionMemo = new TimedMemo(ttl, signal) +async function defaultVersion() { + return await defaultVersionMemo.run(undefined, async () => { + const release = await github(`releases/latest`).catch(async () => + (await github(`releases`))[0]! + ) + return release.tag_name + }) +} + +const shaMemo = new TimedMemo(ttl, signal) +async function getSha(version: string): Promise { + const ref = version + .replace(rPrVersion, "pull/$1/head") + .replace(rTagVersion, "v$1") + .replace(/:/g, "/") + return await shaMemo.run( + ref, + async () => (await github(`commits/${ref}`)).sha, + ) +} + +const deploymentMemo = new TimedMemo(ttl, signal) +async function getDeployment(sha: string) { + return await deploymentMemo.run(sha, async () => { + const deployments = await github(`deployments?sha=${sha}`) + const deployment = deployments.find((x) => x.payload.project_id === delegateeProjectId) + if (!deployment) throw f.notFound() + const statuses = await github(deployment.statuses_url) + const url = statuses.map((x) => x.environment_url).find((x) => x) + if (!url) throw f.notFound() + return url + }) +} + +async function github(url: string): Promise { + const response = await fetch(new URL(url, githubApiBase)) + if (!response.ok) throw new Error(`${url}: invalid response`) + return await response.json() +} + +interface GithubDeployment { + statuses_url: string + payload: { project_id: string } +} + +interface GithubStatus { + environment_url?: string +} + +interface GithubRelease { + tag_name: string +} + +interface GithubCommit { + sha: string +} diff --git a/server/factories.ts b/server/factories.ts index 1f7435eda..3a1015806 100644 --- a/server/factories.ts +++ b/server/factories.ts @@ -1,4 +1,4 @@ -import * as shiki from "npm:shiki" +import * as shiki from "https://esm.sh/shiki@0.11.1?bundle" import { escapeHtml } from "../deps/escape.ts" import { Status } from "../deps/std/http.ts" import { CacheBase } from "../util/cache/base.ts" diff --git a/server/local.ts b/server/handler.ts similarity index 74% rename from server/local.ts rename to server/handler.ts index 882cc4fad..f1549c41a 100644 --- a/server/local.ts +++ b/server/handler.ts @@ -4,7 +4,7 @@ import * as f from "./factories.ts" import { parsePathInfo } from "./PathInfo.ts" export function handler(env: Env): Handler { - return async (request) => { + return handleErrors(async (request) => { const url = new URL(request.url) const { pathname } = url if (pathname === "/") return new Response("capi dev server active") @@ -19,13 +19,7 @@ export function handler(env: Env): Handler { } const provider = env.providers[generatorId]?.[providerId] if (provider) { - try { - return await provider.handle(request, pathInfo) - } catch (e) { - if (e instanceof Response) return e.clone() - console.error(e) - return f.serverError(Deno.inspect(e)) - } + return await provider.handle(request, pathInfo) } } for (const dir of staticDirs) { @@ -40,7 +34,19 @@ export function handler(env: Env): Handler { } catch (_e) {} } return f.notFound() - } + }) } const staticDirs = ["../", "./static/"].map((p) => import.meta.resolve(p)) + +export function handleErrors(handler: (request: Request) => Promise) { + return async (request: Request) => { + try { + return await handler(request) + } catch (e) { + if (e instanceof Response) return e.clone() + console.error(e) + return f.serverError(Deno.inspect(e)) + } + } +} diff --git a/server/mod.ts b/server/mod.ts index 8a9ffcaea..44fec260d 100644 --- a/server/mod.ts +++ b/server/mod.ts @@ -1,4 +1,5 @@ export * from "./Env.ts" export * as f from "./factories.ts" +export * from "./handler.ts" export * from "./PathInfo.ts" export * from "./Provider.ts" diff --git a/util/cache/base.ts b/util/cache/base.ts index 84a2f7dbd..e9ba949f9 100644 --- a/util/cache/base.ts +++ b/util/cache/base.ts @@ -33,11 +33,4 @@ export abstract class CacheBase { return value }, ttl) } - - abstract _list(prefix: string): Promise - - listMemo = new WeakMemo() - list(prefix: string) { - return this.listMemo.run(prefix, () => this._list(prefix)) - } } diff --git a/util/cache/fs.ts b/util/cache/fs.ts index 9c0562276..c57eabf93 100644 --- a/util/cache/fs.ts +++ b/util/cache/fs.ts @@ -19,19 +19,4 @@ export class FsCache extends CacheBase { return content } } - - async _list(prefix: string): Promise { - try { - const result = [] - for await (const entry of Deno.readDir(path.join(this.location, prefix))) { - result.push(entry.name) - } - return result - } catch (e) { - if (e instanceof Deno.errors.NotFound) { - return [] - } - throw e - } - } } diff --git a/util/cache/memory.ts b/util/cache/memory.ts index 9b0bfa4cf..e35b8b0c1 100644 --- a/util/cache/memory.ts +++ b/util/cache/memory.ts @@ -6,7 +6,4 @@ export class InMemoryCache extends CacheBase { _getRaw(key: string, init: () => Promise): Promise { return Promise.resolve(this.memo.run(key, init)) } - _list(): Promise { - throw new Error("unimplemented") - } } diff --git a/util/cache/s3.ts b/util/cache/s3.ts index d7baeb3fe..e4f6d8cf4 100644 --- a/util/cache/s3.ts +++ b/util/cache/s3.ts @@ -4,12 +4,13 @@ import { CacheBase } from "./base.ts" export class S3Cache extends CacheBase { bucket - constructor(config: S3BucketConfig, signal: AbortSignal) { + constructor(readonly prefix: string, config: S3BucketConfig, signal: AbortSignal) { super(signal) this.bucket = new S3Bucket(config) } async _getRaw(key: string, init: () => Promise) { + key = this.prefix + key const result = await this.bucket.getObject(key) if (result) { return new Uint8Array(await new Response(result.body).arrayBuffer()) @@ -18,9 +19,4 @@ export class S3Cache extends CacheBase { await this.bucket.putObject(key, value) return value } - - async _list(prefix: string): Promise { - const result = await this.bucket.listObjects({ prefix }) - return result?.contents?.map((object) => object.key!.slice(prefix.length)) ?? [] - } }