Skip to content

Commit 5e47df6

Browse files
committed
rebase
2 parents 6ba2add + 0fb6962 commit 5e47df6

23 files changed

+444
-431
lines changed

.dockerignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
dist
3-
.env
3+
.env
4+
DB_MIGRATION_HASH_FILE

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ data/
99
bin/
1010
coverage/
1111
.idea/
12-
migrations/tenants-migration-hash.txt
12+
DB_MIGRATION_HASH_FILE

Dockerfile

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
1-
FROM node:18-alpine
1+
# Base stage for shared environment setup
2+
FROM node:18-alpine as base
23
RUN apk add --no-cache g++ make python3
34
WORKDIR /app
45
COPY package.json package-lock.json ./
5-
RUN npm ci --production
66

7-
FROM node:18-alpine
8-
RUN apk add --no-cache g++ make python3
9-
WORKDIR /app
10-
COPY . .
7+
# Dependencies stage - install and cache all dependencies
8+
FROM base as dependencies
119
RUN npm ci
10+
# Cache the installed node_modules for later stages
11+
RUN cp -R node_modules /node_modules_cache
12+
13+
# Build stage - use cached node_modules for building the application
14+
FROM base as build
15+
COPY --from=dependencies /node_modules_cache ./node_modules
16+
COPY . .
1217
RUN npm run build
1318

14-
FROM node:18-alpine
19+
# Production dependencies stage - use npm cache to install only production dependencies
20+
FROM base as production-deps
21+
COPY --from=dependencies /node_modules_cache ./node_modules
22+
RUN npm ci --production
23+
24+
# Final stage - for the production build
25+
FROM base as final
1526
ARG VERSION
1627
ENV VERSION=$VERSION
17-
WORKDIR /app
1828
COPY migrations migrations
19-
COPY ecosystem.config.js package.json ./
20-
COPY --from=0 /app/node_modules node_modules
21-
COPY --from=1 /app/dist dist
29+
30+
# Copy production node_modules from the production dependencies stage
31+
COPY --from=production-deps /app/node_modules node_modules
32+
# Copy build artifacts from the build stage
33+
COPY --from=build /app/dist dist
34+
COPY ./docker-entrypoint.sh .
35+
36+
RUN node dist/scripts/migration-hash.js
37+
2238
EXPOSE 5000
23-
ENTRYPOINT ["docker-entrypoint.sh"]
24-
CMD ["node", "dist/server.js"]
39+
ENTRYPOINT ["./docker-entrypoint.sh"]
40+
CMD ["node", "dist/server.js"]

docker-entrypoint.sh

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,11 @@
1-
#!/usr/bin/env bash
1+
#!/usr/bin/env sh
22
set -Eeuo pipefail
33

4-
# usage: file_env VAR [DEFAULT]
5-
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
6-
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
7-
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
8-
file_env() {
9-
local var="$1"
10-
local fileVar="${var}_FILE"
11-
local def="${2:-}"
12-
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
13-
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
14-
exit 1
15-
fi
16-
local val="$def"
17-
if [ "${!var:-}" ]; then
18-
val="${!var}"
19-
elif [ "${!fileVar:-}" ]; then
20-
val="$(< "${!fileVar}")"
21-
fi
22-
export "$var"="$val"
23-
unset "$fileVar"
24-
}
254

26-
# load secrets either from environment variables or files
27-
file_env 'ANON_KEY'
28-
file_env 'SERVICE_KEY'
29-
file_env 'PGRST_JWT_SECRET'
30-
file_env 'DATABASE_URL'
31-
file_env 'MULTITENANT_DATABASE_URL'
32-
file_env 'LOGFLARE_API_KEY'
33-
file_env 'LOGFLARE_SOURCE_TOKEN'
5+
# Check if the DB_MIGRATION_HASH_FILE exists and is not empty
6+
if [ -s DB_MIGRATION_HASH_FILE ]; then
7+
export DB_MIGRATION_HASH=$(cat DB_MIGRATION_HASH_FILE)
8+
fi
349

3510
exec "${@}"
3611

ecosystem.config.js

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/admin-app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const build = (opts: FastifyServerOptions = {}, appInstance?: FastifyInstance):
77
app.register(plugins.adminTenantId)
88
app.register(plugins.logTenantId)
99
app.register(plugins.logRequest({ excludeUrls: ['/status', '/metrics', '/health'] }))
10-
app.register(routes.tenant, { prefix: 'tenants' })
10+
app.register(routes.tenants, { prefix: 'tenants' })
11+
app.register(routes.migrations, { prefix: 'migrations' })
1112

1213
let registriesToMerge: Registry[] = []
1314

src/config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ type StorageConfigType = {
3131
dbSuperUser: string
3232
dbSearchPath: string
3333
dbMigrationHash?: string
34-
dbDisableTenantMigrations: boolean
3534
databaseURL: string
3635
databaseSSLRootCert?: string
3736
databasePoolURL?: string
@@ -236,7 +235,6 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
236235
),
237236
dbSuperUser: getOptionalConfigFromEnv('DB_SUPER_USER') || 'postgres',
238237
dbMigrationHash: getOptionalConfigFromEnv('DB_MIGRATION_HASH'),
239-
dbDisableTenantMigrations: getOptionalConfigFromEnv('DB_DISABLE_TENANT_MIGRATIONS') === 'true',
240238

241239
// Database - Connection
242240
dbSearchPath: getOptionalConfigFromEnv('DATABASE_SEARCH_PATH', 'DB_SEARCH_PATH') || '',

src/database/migrate.ts

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { Client, ClientConfig } from 'pg'
22
import { loadMigrationFiles, MigrationError } from 'postgres-migrations'
33
import { getConfig } from '../config'
4-
import { logger } from '../monitoring'
4+
import { logger, logSchema } from '../monitoring'
55
import { BasicPgClient, Migration } from 'postgres-migrations/dist/types'
66
import { validateMigrationHashes } from 'postgres-migrations/dist/validation'
77
import { runMigration } from 'postgres-migrations/dist/run-migration'
88
import SQL from 'sql-template-strings'
99
import { searchPath } from './connection'
10-
import { updateTenantMigrationVersion } from './tenant'
10+
import { listTenantsToMigrate, updateTenantMigrationVersion } from './tenant'
11+
import { knex } from './multitenant-db'
12+
import { RunMigrationsOnTenants } from '../queue'
1113

1214
const {
1315
isMultitenant,
1416
multitenantDatabaseUrl,
17+
pgQueueEnable,
1518
databaseSSLRootCert,
1619
dbAnonRole,
1720
dbAuthenticatedRole,
@@ -34,6 +37,43 @@ const backportMigrations = [
3437
},
3538
]
3639

40+
/**
41+
* Runs migrations for all tenants
42+
* only one instance at the time is allowed to run
43+
*/
44+
export async function runMigrationsOnAllTenants() {
45+
if (pgQueueEnable) {
46+
return
47+
}
48+
const result = await knex.raw(`SELECT pg_try_advisory_lock(?);`, ['-8575985245963000605'])
49+
const lockAcquired = result.rows.shift()?.pg_try_advisory_lock || false
50+
51+
if (!lockAcquired) {
52+
return
53+
}
54+
55+
try {
56+
const tenants = listTenantsToMigrate()
57+
for await (const tenantBatch of tenants) {
58+
await Promise.allSettled(
59+
tenantBatch.map((tenant) => {
60+
return RunMigrationsOnTenants.send({
61+
tenantId: tenant,
62+
singletonKey: tenant,
63+
tenant: {
64+
ref: tenant,
65+
},
66+
})
67+
})
68+
)
69+
}
70+
} finally {
71+
try {
72+
await knex.raw(`SELECT pg_advisory_unlock(?);`, ['-8575985245963000605'])
73+
} catch (e) {}
74+
}
75+
}
76+
3777
/**
3878
* Runs multi-tenant migrations
3979
*/
@@ -46,15 +86,20 @@ export async function runMultitenantMigrations(): Promise<void> {
4686
/**
4787
* Runs migrations on a specific tenant by providing its database DSN
4888
* @param databaseUrl
89+
* @param tenantId
4990
*/
50-
export async function runMigrationsOnTenant(databaseUrl: string): Promise<void> {
91+
export async function runMigrationsOnTenant(databaseUrl: string, tenantId?: string): Promise<void> {
5192
let ssl: ClientConfig['ssl'] | undefined = undefined
5293

5394
if (databaseSSLRootCert) {
5495
ssl = { ca: databaseSSLRootCert }
5596
}
5697

57-
await connectAndMigrate(databaseUrl, './migrations/tenant', ssl)
98+
await connectAndMigrate(databaseUrl, './migrations/tenant', ssl, undefined, tenantId)
99+
100+
if (isMultitenant && tenantId) {
101+
await updateTenantMigrationVersion([tenantId])
102+
}
58103
}
59104

60105
/**
@@ -63,12 +108,14 @@ export async function runMigrationsOnTenant(databaseUrl: string): Promise<void>
63108
* @param migrationsDirectory
64109
* @param ssl
65110
* @param shouldCreateStorageSchema
111+
* @param tenantId
66112
*/
67113
async function connectAndMigrate(
68114
databaseUrl: string | undefined,
69115
migrationsDirectory: string,
70116
ssl?: ClientConfig['ssl'],
71-
shouldCreateStorageSchema?: boolean
117+
shouldCreateStorageSchema?: boolean,
118+
tenantId?: string
72119
) {
73120
const dbConfig: ClientConfig = {
74121
connectionString: databaseUrl,
@@ -78,6 +125,13 @@ async function connectAndMigrate(
78125
}
79126

80127
const client = new Client(dbConfig)
128+
client.on('error', (err) => {
129+
logSchema.error(logger, 'Error on database connection', {
130+
type: 'error',
131+
error: err,
132+
project: tenantId,
133+
})
134+
})
81135
try {
82136
await client.connect()
83137
await migrate({ client }, migrationsDirectory, shouldCreateStorageSchema)
@@ -114,6 +168,8 @@ function runMigrations(migrationsDirectory: string, shouldCreateStorageSchema =
114168
try {
115169
const migrationTableName = 'migrations'
116170

171+
await client.query(`SET search_path TO ${searchPath.join(',')}`)
172+
117173
let appliedMigrations: Migration[] = []
118174
if (await doesTableExist(client, migrationTableName)) {
119175
const { rows } = await client.query(`SELECT * FROM ${migrationTableName} ORDER BY id`)

src/database/tenant.ts

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { knex } from './multitenant-db'
44
import { StorageBackendError } from '../storage'
55
import { JwtPayload } from 'jsonwebtoken'
66
import { PubSubAdapter } from '../pubsub'
7-
import { RunMigrationsEvent } from '../queue/events/run-migrations'
87

98
interface TenantConfig {
109
anonKey?: string
@@ -26,14 +25,7 @@ export interface Features {
2625
}
2726
}
2827

29-
const {
30-
isMultitenant,
31-
dbServiceRole,
32-
serviceKey,
33-
jwtSecret,
34-
dbMigrationHash,
35-
dbDisableTenantMigrations,
36-
} = getConfig()
28+
const { isMultitenant, dbServiceRole, serviceKey, jwtSecret, dbMigrationHash } = getConfig()
3729

3830
const tenantConfigCache = new Map<string, TenantConfig>()
3931

@@ -80,42 +72,6 @@ export async function* listTenantsToMigrate() {
8072
}
8173
}
8274

83-
/**
84-
* Runs migrations for all tenants
85-
*/
86-
export async function runMigrations() {
87-
if (dbDisableTenantMigrations) {
88-
return
89-
}
90-
const result = await knex.raw(`SELECT pg_try_advisory_lock(?);`, ['-8575985245963000605'])
91-
const lockAcquired = result.rows.shift()?.pg_try_advisory_lock || false
92-
93-
if (!lockAcquired) {
94-
return
95-
}
96-
97-
try {
98-
const tenants = listTenantsToMigrate()
99-
for await (const tenantBatch of tenants) {
100-
await Promise.allSettled(
101-
tenantBatch.map((tenant) => {
102-
return RunMigrationsEvent.send({
103-
tenantId: tenant,
104-
singletonKey: tenant,
105-
tenant: {
106-
ref: tenant,
107-
},
108-
})
109-
})
110-
)
111-
}
112-
} finally {
113-
try {
114-
await knex.raw(`SELECT pg_advisory_unlock(?);`, ['-8575985245963000605'])
115-
} catch (e) {}
116-
}
117-
}
118-
11975
export function updateTenantMigrationVersion(tenantIds: string[]) {
12076
return knex
12177
.table('tenants')

src/http/routes/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { default as bucket } from './bucket'
22
export { default as object } from './object'
33
export { default as render } from './render'
4-
export { default as tenant } from './tenant'
54
export { default as multiPart } from './tus'
65
export { default as healthcheck } from './health'
6+
export * from './tenant'

0 commit comments

Comments
 (0)