Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bsky short link service #4542

Merged
merged 12 commits into from
Jun 21, 2024
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
Loading