Skip to content

Commit

Permalink
Bsky short link service (#4542)
Browse files Browse the repository at this point in the history
* bskylink: scaffold service w/ initial config and schema

* bskylink: implement link creation and redirects

* bskylink: tidy

* bskylink: tests

* bskylink: tidy, add error handler

* bskylink: add dockerfile

* bskylink: add build

* bskylink: fix some express plumbing

* bskyweb: proxy fallthrough routes to link service redirects

* bskyweb: build w/ link proxy

* Add AASA to bskylink (#4588)

---------

Co-authored-by: Hailey <[email protected]>
  • Loading branch information
devinivy and haileyok authored Jun 21, 2024
1 parent ba21fdd commit 55812b0
Show file tree
Hide file tree
Showing 29 changed files with 2,118 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/build-and-push-bskyweb-aws.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- divy/bskylink

env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/build-and-push-link-aws.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: build-and-push-link-aws
on:
push:
branches:
- divy/bskylink

env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
IMAGE_NAME: bskylink

jobs:
link-container-aws:
if: github.repository == 'bluesky-social/social-app'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1

- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
file: ./Dockerfile.bskylink
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
41 changes: 41 additions & 0 deletions Dockerfile.bskylink
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
FROM node:20.11-alpine3.18 as build

# Move files into the image and install
WORKDIR /app

COPY ./bskylink/package.json ./
COPY ./bskylink/yarn.lock ./
RUN yarn install --frozen-lockfile

COPY ./bskylink ./

# build then prune dev deps
RUN yarn build
RUN yarn install --production --ignore-scripts --prefer-offline

# Uses assets from build stage to reduce build size
FROM node:20.11-alpine3.18

RUN apk add --update dumb-init

# Avoid zombie processes, handle signal forwarding
ENTRYPOINT ["dumb-init", "--"]

WORKDIR /app
COPY --from=build /app /app
RUN mkdir /app/data && chown node /app/data

VOLUME /app/data
EXPOSE 3000
ENV LINK_PORT=3000
ENV NODE_ENV=production
# potential perf issues w/ io_uring on this version of node
ENV UV_USE_IO_URING=0

# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
USER node
CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"]

LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
LABEL org.opencontainers.image.description="Bsky Link Service"
LABEL org.opencontainers.image.licenses=UNLICENSED
26 changes: 26 additions & 0 deletions bskylink/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "bskylink",
"version": "0.0.0",
"type": "module",
"main": "index.ts",
"scripts": {
"test": "./tests/infra/with-test-db.sh node --loader ts-node/esm --test ./tests/index.ts",
"build": "tsc"
},
"dependencies": {
"@atproto/common": "^0.4.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.19.2",
"http-terminator": "^3.2.0",
"kysely": "^0.27.3",
"pg": "^8.12.0",
"pino": "^9.2.0",
"uint8arrays": "^5.1.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/pg": "^8.11.6",
"typescript": "^5.4.5"
}
}
24 changes: 24 additions & 0 deletions bskylink/src/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js'

async function main() {
const env = readEnv()
const cfg = envToCfg(env)
if (cfg.db.migrationUrl) {
const migrateDb = Database.postgres({
url: cfg.db.migrationUrl,
schema: cfg.db.schema,
})
await migrateDb.migrateToLatestOrThrow()
await migrateDb.close()
}
const link = await LinkService.create(cfg)
await link.start()
httpLogger.info('link service is running')
process.on('SIGTERM', async () => {
httpLogger.info('link service is stopping')
await link.destroy()
httpLogger.info('link service is stopped')
})
}

main()
82 changes: 82 additions & 0 deletions bskylink/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {envInt, envList, envStr} from '@atproto/common'

export type Config = {
service: ServiceConfig
db: DbConfig
}

export type ServiceConfig = {
port: number
version?: string
hostnames: string[]
appHostname: string
}

export type DbConfig = {
url: string
migrationUrl?: string
pool: DbPoolConfig
schema?: string
}

export type DbPoolConfig = {
size: number
maxUses: number
idleTimeoutMs: number
}

export type Environment = {
port?: number
version?: string
hostnames: string[]
appHostname?: string
dbPostgresUrl?: string
dbPostgresMigrationUrl?: string
dbPostgresSchema?: string
dbPostgresPoolSize?: number
dbPostgresPoolMaxUses?: number
dbPostgresPoolIdleTimeoutMs?: number
}

export const readEnv = (): Environment => {
return {
port: envInt('LINK_PORT'),
version: envStr('LINK_VERSION'),
hostnames: envList('LINK_HOSTNAMES'),
appHostname: envStr('LINK_APP_HOSTNAME'),
dbPostgresUrl: envStr('LINK_DB_POSTGRES_URL'),
dbPostgresMigrationUrl: envStr('LINK_DB_POSTGRES_MIGRATION_URL'),
dbPostgresSchema: envStr('LINK_DB_POSTGRES_SCHEMA'),
dbPostgresPoolSize: envInt('LINK_DB_POSTGRES_POOL_SIZE'),
dbPostgresPoolMaxUses: envInt('LINK_DB_POSTGRES_POOL_MAX_USES'),
dbPostgresPoolIdleTimeoutMs: envInt(
'LINK_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS',
),
}
}

export const envToCfg = (env: Environment): Config => {
const serviceCfg: ServiceConfig = {
port: env.port ?? 3000,
version: env.version,
hostnames: env.hostnames,
appHostname: env.appHostname || 'bsky.app',
}
if (!env.dbPostgresUrl) {
throw new Error('Must configure postgres url (LINK_DB_POSTGRES_URL)')
}
const dbCfg: DbConfig = {
url: env.dbPostgresUrl,
migrationUrl: env.dbPostgresMigrationUrl,
schema: env.dbPostgresSchema,
pool: {
idleTimeoutMs: env.dbPostgresPoolIdleTimeoutMs ?? 10000,
maxUses: env.dbPostgresPoolMaxUses ?? Infinity,
size: env.dbPostgresPoolSize ?? 10,
},
}
return {
service: serviceCfg,
db: dbCfg,
}
}
33 changes: 33 additions & 0 deletions bskylink/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {Config} from './config.js'
import Database from './db/index.js'

export type AppContextOptions = {
cfg: Config
db: Database
}

export class AppContext {
cfg: Config
db: Database
abortController = new AbortController()

constructor(private opts: AppContextOptions) {
this.cfg = this.opts.cfg
this.db = this.opts.db
}

static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) {
const db = Database.postgres({
url: cfg.db.url,
schema: cfg.db.schema,
poolSize: cfg.db.pool.size,
poolMaxUses: cfg.db.pool.maxUses,
poolIdleTimeoutMs: cfg.db.pool.idleTimeoutMs,
})
return new AppContext({
cfg,
db,
...overrides,
})
}
}
Loading

0 comments on commit 55812b0

Please sign in to comment.