diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 8158644c9..9dfb38aeb 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -23,7 +23,7 @@ export default { name: "knex-env", transformMode: "ssr", async setup() { - const logger = await initLogger(); + const logger = initLogger(); const envConfig = initEnvConfig(logger); const db = initDbConnection({ dbConnectionUri: envConfig.DB_CONNECTION_URI, @@ -119,4 +119,5 @@ export default { } }; } -}; \ No newline at end of file +}; + diff --git a/backend/src/auto-start-migrations.ts b/backend/src/auto-start-migrations.ts new file mode 100644 index 000000000..5177eaf4a --- /dev/null +++ b/backend/src/auto-start-migrations.ts @@ -0,0 +1,79 @@ +import path from "node:path"; + +import dotenv from "dotenv"; +import { Knex } from "knex"; +import { Logger } from "pino"; + +import { PgSqlLock } from "./keystore/keystore"; + +dotenv.config(); + +type TArgs = { + auditLogDb?: Knex; + applicationDb: Knex; + logger: Logger; +}; + +const migrationConfig = { + directory: path.join(__dirname, "./db/migrations"), + extension: "ts", + tableName: "infisical_migrations" +}; + +const migrationStatusCheckErrorHandler = (err: Error) => { + // happens for first time in which the migration table itself is not created yet + // error: select * from "infisical_migrations" - relation "infisical_migrations" does not exist + if (err?.message?.includes("does not exist")) { + return true; + } + throw err; +}; + +export const runMigrations = async ({ applicationDb, auditLogDb, logger }: TArgs) => { + try { + const shouldRunMigration = Boolean( + await applicationDb.migrate.status(migrationConfig).catch(migrationStatusCheckErrorHandler) + ); // db.length - code.length + if (!shouldRunMigration) { + logger.info("No migrations pending: Skipping migration process."); + return; + } + + if (auditLogDb) { + await auditLogDb.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); + logger.info("Running audit log migrations."); + + const didPreviousInstanceRunMigration = !(await auditLogDb.migrate + .status(migrationConfig) + .catch(migrationStatusCheckErrorHandler)); + if (didPreviousInstanceRunMigration) { + logger.info("No audit log migrations pending: Applied by previous instance. Skipping migration process."); + return; + } + + await auditLogDb.migrate.latest(migrationConfig); + logger.info("Finished audit log migrations."); + }); + } + + await applicationDb.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); + logger.info("Running application migrations."); + + const didPreviousInstanceRunMigration = !(await applicationDb.migrate + .status(migrationConfig) + .catch(migrationStatusCheckErrorHandler)); + if (didPreviousInstanceRunMigration) { + logger.info("No application migrations pending: Applied by previous instance. Skipping migration process."); + return; + } + + await applicationDb.migrate.latest(migrationConfig); + logger.info("Finished application migrations."); + }); + } catch (err) { + logger.error(err, "Boot up migration failed"); + process.exit(1); + } +}; diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index d4a2a5b2c..5a8dd3d05 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -49,6 +49,9 @@ export const initDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); @@ -64,6 +67,9 @@ export const initDbConnection = ({ ca: Buffer.from(replicaDbCertificate, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); }); @@ -98,6 +104,9 @@ export const initAuditLogDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); diff --git a/backend/src/db/migrations/20250109104500_webhook-to-kms.ts b/backend/src/db/migrations/20250109104500_webhook-to-kms.ts index 6c4936613..0836e1b48 100644 --- a/backend/src/db/migrations/20250109104500_webhook-to-kms.ts +++ b/backend/src/db/migrations/20250109104500_webhook-to-kms.ts @@ -25,7 +25,7 @@ export async function up(knex: Knex): Promise { }); } - await initLogger(); + initLogger(); const envConfig = getMigrationEnvConfig(); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250109104501_secret-rotation-to-kms.ts b/backend/src/db/migrations/20250109104501_secret-rotation-to-kms.ts index fc88171bc..9d10471ad 100644 --- a/backend/src/db/migrations/20250109104501_secret-rotation-to-kms.ts +++ b/backend/src/db/migrations/20250109104501_secret-rotation-to-kms.ts @@ -22,7 +22,7 @@ export async function up(knex: Knex): Promise { }); } - await initLogger(); + initLogger(); const envConfig = getMigrationEnvConfig(); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250109104502_identity-k8-auth-to-kms.ts b/backend/src/db/migrations/20250109104502_identity-k8-auth-to-kms.ts index 1acc588cb..2aa1b61fd 100644 --- a/backend/src/db/migrations/20250109104502_identity-k8-auth-to-kms.ts +++ b/backend/src/db/migrations/20250109104502_identity-k8-auth-to-kms.ts @@ -53,7 +53,7 @@ const reencryptIdentityK8sAuth = async (knex: Knex) => { }); } - await initLogger(); + initLogger(); const envConfig = getMigrationEnvConfig(); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250109104502_identity-oidc-auth-to-kms.ts b/backend/src/db/migrations/20250109104502_identity-oidc-auth-to-kms.ts index 936d5a41b..1b3a4d50c 100644 --- a/backend/src/db/migrations/20250109104502_identity-oidc-auth-to-kms.ts +++ b/backend/src/db/migrations/20250109104502_identity-oidc-auth-to-kms.ts @@ -33,7 +33,7 @@ const reencryptIdentityOidcAuth = async (knex: Knex) => { }); } - await initLogger(); + initLogger(); const envConfig = getMigrationEnvConfig(); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250109104503_dynamic-secret-root-to-kms.ts b/backend/src/db/migrations/20250109104503_dynamic-secret-root-to-kms.ts index 8d8e34673..1abf132d3 100644 --- a/backend/src/db/migrations/20250109104503_dynamic-secret-root-to-kms.ts +++ b/backend/src/db/migrations/20250109104503_dynamic-secret-root-to-kms.ts @@ -28,7 +28,7 @@ export async function up(knex: Knex): Promise { }); } - await initLogger(); + initLogger(); const envConfig = getMigrationEnvConfig(); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250109104508_directory-config-to-kms.ts b/backend/src/db/migrations/20250109104508_directory-config-to-kms.ts index 9c9c0b5a3..2497bba05 100644 --- a/backend/src/db/migrations/20250109104508_directory-config-to-kms.ts +++ b/backend/src/db/migrations/20250109104508_directory-config-to-kms.ts @@ -26,7 +26,7 @@ const reencryptSamlConfig = async (knex: Knex) => { }); } - await initLogger(); + initLogger(); const envConfig = getMigrationEnvConfig(); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); @@ -181,7 +181,7 @@ const reencryptLdapConfig = async (knex: Knex) => { }); } - await initLogger(); + initLogger(); const envConfig = getMigrationEnvConfig(); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); @@ -330,7 +330,7 @@ const reencryptOidcConfig = async (knex: Knex) => { }); } - await initLogger(); + initLogger(); const envConfig = getMigrationEnvConfig(); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/utils/services.ts b/backend/src/db/migrations/utils/services.ts index 4709248f5..731f703e2 100644 --- a/backend/src/db/migrations/utils/services.ts +++ b/backend/src/db/migrations/utils/services.ts @@ -20,7 +20,6 @@ type TDependencies = { export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => { // eslint-disable-next-line no-param-reassign - db.replicaNode = () => db; const hsmModule = initializeHsmModule(envConfig); hsmModule.initialize(); diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index dbfdfd063..2214a3970 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -2,6 +2,11 @@ import { Redis } from "ioredis"; import { Redlock, Settings } from "@app/lib/red-lock"; +export enum PgSqlLock { + BootUpMigration = 2023, + SuperAdminInit = 2024 +} + export type TKeyStoreFactory = ReturnType; // all the key prefixes used must be set here to avoid conflict diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts index 9676496f7..170a0285f 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -98,7 +98,7 @@ const extractReqId = () => { } }; -export const initLogger = async () => { +export const initLogger = () => { const cfg = loggerConfig.parse(process.env); const targets: pino.TransportMultiOptions["targets"][number][] = [ { diff --git a/backend/src/main.ts b/backend/src/main.ts index 076c1df65..461601fc0 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,14 +2,13 @@ import "./lib/telemetry/instrumentation"; import dotenv from "dotenv"; import { Redis } from "ioredis"; -import path from "path"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { runMigrations } from "./auto-start-migrations"; import { initAuditLogDbConnection, initDbConnection } from "./db"; import { keyStoreFactory } from "./keystore/keystore"; -import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env"; -import { isMigrationMode } from "./lib/fn"; +import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; @@ -19,7 +18,7 @@ import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); const run = async () => { - const logger = await initLogger(); + const logger = initLogger(); const envConfig = initEnvConfig(logger); const db = initDbConnection({ @@ -38,22 +37,7 @@ const run = async () => { }) : undefined; - // Case: App is running in packaged mode (binary), and migration mode is enabled. - // Run the migrations and exit the process after completion. - if (IS_PACKAGED && isMigrationMode()) { - try { - logger.info("Running Postgres migrations.."); - await db.migrate.latest({ - directory: path.join(__dirname, "./db/migrations") - }); - logger.info("Postgres migrations completed"); - } catch (err) { - logger.error(err, "Failed to run migrations"); - process.exit(1); - } - - process.exit(0); - } + await runMigrations({ applicationDb: db, auditLogDb, logger }); const smtp = smtpServiceFactory(formatSmtpConfig()); diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b0fdd9c5c..a8f432d92 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -2,7 +2,7 @@ import bcrypt from "bcrypt"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; @@ -87,17 +87,24 @@ export const superAdminServiceFactory = ({ // reset on initialized await keyStore.deleteItem(ADMIN_CONFIG_KEY); - const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); - if (serverCfg) return; + const serverCfg = await serverCfgDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SuperAdminInit]); + const serverCfgInDB = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID, tx); + if (serverCfgInDB) return serverCfgInDB; - const newCfg = await serverCfgDAL.create({ - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - id: ADMIN_CONFIG_DB_UUID, - initialized: false, - allowSignUp: true, - defaultAuthOrgId: null + const newCfg = await serverCfgDAL.create( + { + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: ADMIN_CONFIG_DB_UUID, + initialized: false, + allowSignUp: true, + defaultAuthOrgId: null + }, + tx + ); + return newCfg; }); - return newCfg; + return serverCfg; }; const updateServerCfg = async ( diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 40d17c1b0..9f467d547 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -56,22 +56,8 @@ services: POSTGRES_USER: infisical POSTGRES_DB: infisical-test - db-migration: - container_name: infisical-db-migration - depends_on: - - db - build: - context: ./backend - dockerfile: Dockerfile.dev - env_file: .env - environment: - - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable - command: npm run migration:latest - volumes: - - ./backend/src:/app/src - backend: - container_name: infisical-dev-api + # container_name: infisical-dev-api build: context: ./backend dockerfile: Dockerfile.dev @@ -80,13 +66,11 @@ services: condition: service_started redis: condition: service_started - db-migration: - condition: service_completed_successfully env_file: - .env ports: - - 4000:4000 - - 9464:9464 # for OTEL collection of Prometheus metrics + - 4000-4010:4000 + # - 9464:9464 # for OTEL collection of Prometheus metrics environment: - NODE_ENV=development - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable @@ -192,7 +176,7 @@ services: depends_on: - openldap profiles: [ldap] - + keycloak: image: quay.io/keycloak/keycloak:26.1.0 restart: always @@ -202,7 +186,7 @@ services: command: start-dev ports: - 8088:8080 - profiles: [ sso ] + profiles: [sso] volumes: postgres-data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 77a1e04ab..d3526d841 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,18 +1,6 @@ version: "3" services: - db-migration: - container_name: infisical-db-migration - depends_on: - db: - condition: service_healthy - image: infisical/infisical:latest-postgres - env_file: .env - command: npm run migration:latest - pull_policy: always - networks: - - infisical - backend: container_name: infisical-backend restart: unless-stopped @@ -21,8 +9,6 @@ services: condition: service_healthy redis: condition: service_started - db-migration: - condition: service_completed_successfully image: infisical/infisical:latest-postgres pull_policy: always env_file: .env @@ -69,4 +55,5 @@ volumes: driver: local networks: - infisical: \ No newline at end of file + infisical: +