Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

feat: capi.dev #764

Merged
merged 1 commit into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
26 changes: 26 additions & 0 deletions server/capi.dev/delegatee.ts
Original file line number Diff line number Diff line change
@@ -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))
97 changes: 97 additions & 0 deletions server/capi.dev/delegator.ts
Original file line number Diff line number Diff line change
@@ -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
harrysolovay marked this conversation as resolved.
Show resolved Hide resolved
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<void, string>(ttl, signal)
async function defaultVersion() {
return await defaultVersionMemo.run(undefined, async () => {
const release = await github<GithubRelease>(`releases/latest`).catch(async () =>
(await github<GithubRelease[]>(`releases`))[0]!
)
return release.tag_name
})
}

const shaMemo = new TimedMemo<string, string>(ttl, signal)
async function getSha(version: string): Promise<string> {
const ref = version
.replace(rPrVersion, "pull/$1/head")
.replace(rTagVersion, "v$1")
.replace(/:/g, "/")
return await shaMemo.run(
ref,
async () => (await github<GithubCommit>(`commits/${ref}`)).sha,
)
}

const deploymentMemo = new TimedMemo<string, string>(ttl, signal)
async function getDeployment(sha: string) {
return await deploymentMemo.run(sha, async () => {
const deployments = await github<GithubDeployment[]>(`deployments?sha=${sha}`)
const deployment = deployments.find((x) => x.payload.project_id === delegateeProjectId)
if (!deployment) throw f.notFound()
const statuses = await github<GithubStatus[]>(deployment.statuses_url)
const url = statuses.map((x) => x.environment_url).find((x) => x)
if (!url) throw f.notFound()
return url
})
}

async function github<T>(url: string): Promise<T> {
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
}
2 changes: 1 addition & 1 deletion server/factories.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
24 changes: 15 additions & 9 deletions server/local.ts → server/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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) {
Expand All @@ -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<Response>) {
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))
}
}
}
1 change: 1 addition & 0 deletions server/mod.ts
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 0 additions & 7 deletions util/cache/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,4 @@ export abstract class CacheBase {
return value
}, ttl)
}

abstract _list(prefix: string): Promise<string[]>

listMemo = new WeakMemo<string, string[]>()
list(prefix: string) {
return this.listMemo.run(prefix, () => this._list(prefix))
}
}
15 changes: 0 additions & 15 deletions util/cache/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,4 @@ export class FsCache extends CacheBase {
return content
}
}

async _list(prefix: string): Promise<string[]> {
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
}
}
}
3 changes: 0 additions & 3 deletions util/cache/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,4 @@ export class InMemoryCache extends CacheBase {
_getRaw(key: string, init: () => Promise<Uint8Array>): Promise<Uint8Array> {
return Promise.resolve(this.memo.run(key, init))
}
_list(): Promise<string[]> {
throw new Error("unimplemented")
}
}
8 changes: 2 additions & 6 deletions util/cache/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8Array>) {
key = this.prefix + key
const result = await this.bucket.getObject(key)
if (result) {
return new Uint8Array(await new Response(result.body).arrayBuffer())
Expand All @@ -18,9 +19,4 @@ export class S3Cache extends CacheBase {
await this.bucket.putObject(key, value)
return value
}

async _list(prefix: string): Promise<string[]> {
const result = await this.bucket.listObjects({ prefix })
return result?.contents?.map((object) => object.key!.slice(prefix.length)) ?? []
}
}