Rework how we connect to containers.
This commit is contained in:
parent
f43f03a3b4
commit
90cfdd661d
|
@ -136,7 +136,8 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test-server:
|
test-server:
|
||||||
runs-on: budi-tubby-tornado-quad-core-150gb
|
runs-on:
|
||||||
|
group: hosted-runners
|
||||||
env:
|
env:
|
||||||
DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull
|
DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { DatabaseImpl } from "../../../src/db"
|
import { DatabaseImpl } from "../../../src/db"
|
||||||
import { execSync } from "child_process"
|
import { execSync } from "child_process"
|
||||||
|
|
||||||
|
const IPV4_PORT_REGEX = new RegExp(`0\\.0\\.0\\.0:(\\d+)->(\\d+)/tcp`, "g")
|
||||||
|
|
||||||
interface ContainerInfo {
|
interface ContainerInfo {
|
||||||
Command: string
|
Command: string
|
||||||
CreatedAt: string
|
CreatedAt: string
|
||||||
|
@ -19,7 +21,10 @@ interface ContainerInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTestcontainers(): ContainerInfo[] {
|
function getTestcontainers(): ContainerInfo[] {
|
||||||
return execSync("docker ps --format json")
|
// We use --format json to make sure the output is nice and machine-readable,
|
||||||
|
// and we use --no-trunc so that the command returns full container IDs so we
|
||||||
|
// can filter on them correctly.
|
||||||
|
return execSync("docker ps --format json --no-trunc")
|
||||||
.toString()
|
.toString()
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter(x => x.length > 0)
|
.filter(x => x.length > 0)
|
||||||
|
@ -27,32 +32,51 @@ function getTestcontainers(): ContainerInfo[] {
|
||||||
.filter(x => x.Labels.includes("org.testcontainers=true"))
|
.filter(x => x.Labels.includes("org.testcontainers=true"))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainerByImage(image: string) {
|
export function getContainerByImage(image: string) {
|
||||||
return getTestcontainers().find(x => x.Image.startsWith(image))
|
const containers = getTestcontainers().filter(x => x.Image.startsWith(image))
|
||||||
|
if (containers.length > 1) {
|
||||||
|
throw new Error(`Multiple containers found with image: ${image}`)
|
||||||
|
}
|
||||||
|
return containers[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExposedPort(container: ContainerInfo, port: number) {
|
export function getContainerById(id: string) {
|
||||||
const match = container.Ports.match(new RegExp(`0.0.0.0:(\\d+)->${port}/tcp`))
|
return getTestcontainers().find(x => x.ID === id)
|
||||||
if (!match) {
|
}
|
||||||
return undefined
|
|
||||||
|
export interface Port {
|
||||||
|
host: number
|
||||||
|
container: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExposedV4Ports(container: ContainerInfo): Port[] {
|
||||||
|
let ports: Port[] = []
|
||||||
|
for (const match of container.Ports.matchAll(IPV4_PORT_REGEX)) {
|
||||||
|
ports.push({ host: parseInt(match[1]), container: parseInt(match[2]) })
|
||||||
}
|
}
|
||||||
return parseInt(match[1])
|
return ports
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExposedV4Port(container: ContainerInfo, port: number) {
|
||||||
|
return getExposedV4Ports(container).find(x => x.container === port)?.host
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupEnv(...envs: any[]) {
|
export function setupEnv(...envs: any[]) {
|
||||||
|
// We start couchdb in globalSetup.ts, in the root of the monorepo, so it
|
||||||
|
// should be relatively safe to look for it by its image name.
|
||||||
const couch = getContainerByImage("budibase/couchdb")
|
const couch = getContainerByImage("budibase/couchdb")
|
||||||
if (!couch) {
|
if (!couch) {
|
||||||
throw new Error("CouchDB container not found")
|
throw new Error("CouchDB container not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const couchPort = getExposedPort(couch, 5984)
|
const couchPort = getExposedV4Port(couch, 5984)
|
||||||
if (!couchPort) {
|
if (!couchPort) {
|
||||||
throw new Error("CouchDB port not found")
|
throw new Error("CouchDB port not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const configs = [
|
const configs = [
|
||||||
{ key: "COUCH_DB_PORT", value: `${couchPort}` },
|
{ key: "COUCH_DB_PORT", value: `${couchPort}` },
|
||||||
{ key: "COUCH_DB_URL", value: `http://localhost:${couchPort}` },
|
{ key: "COUCH_DB_URL", value: `http://127.0.0.1:${couchPort}` },
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const config of configs.filter(x => !!x.value)) {
|
for (const config of configs.filter(x => !!x.value)) {
|
||||||
|
@ -60,7 +84,4 @@ export function setupEnv(...envs: any[]) {
|
||||||
env._set(config.key, config.value)
|
env._set(config.key, config.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
DatabaseImpl.nano = undefined
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import * as mongodb from "./mongodb"
|
||||||
import * as mysql from "./mysql"
|
import * as mysql from "./mysql"
|
||||||
import * as mssql from "./mssql"
|
import * as mssql from "./mssql"
|
||||||
import * as mariadb from "./mariadb"
|
import * as mariadb from "./mariadb"
|
||||||
|
import { GenericContainer } from "testcontainers"
|
||||||
|
import { testContainerUtils } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
export type DatasourceProvider = () => Promise<Datasource>
|
export type DatasourceProvider = () => Promise<Datasource>
|
||||||
|
|
||||||
|
@ -63,3 +65,26 @@ export async function rawQuery(ds: Datasource, sql: string): Promise<any> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function startContainer(container: GenericContainer) {
|
||||||
|
if (process.env.REUSE_CONTAINERS) {
|
||||||
|
container = container.withReuse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedContainer = await container.start()
|
||||||
|
|
||||||
|
const info = testContainerUtils.getContainerById(startedContainer.getId())
|
||||||
|
if (!info) {
|
||||||
|
throw new Error("Container not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some Docker runtimes, when you expose a port, will bind it to both
|
||||||
|
// 127.0.0.1 and ::1, so ipv4 and ipv6. The port spaces of ipv4 and ipv6
|
||||||
|
// addresses are not shared, and testcontainers will sometimes give you back
|
||||||
|
// the ipv6 port. There's no way to know that this has happened, and if you
|
||||||
|
// try to then connect to `localhost:port` you may attempt to bind to the v4
|
||||||
|
// address which could be unbound or even an entirely different container. For
|
||||||
|
// that reason, we don't use testcontainers' `getExposedPort` function,
|
||||||
|
// preferring instead our own method that guaranteed v4 ports.
|
||||||
|
return testContainerUtils.getExposedV4Ports(info)
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,10 @@ import { Datasource, SourceName } from "@budibase/types"
|
||||||
import { GenericContainer, Wait } from "testcontainers"
|
import { GenericContainer, Wait } from "testcontainers"
|
||||||
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
||||||
import { rawQuery } from "./mysql"
|
import { rawQuery } from "./mysql"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator, testContainerUtils } from "@budibase/backend-core/tests"
|
||||||
|
import { startContainer } from "."
|
||||||
|
|
||||||
|
let ports: Promise<testContainerUtils.Port[]>
|
||||||
|
|
||||||
class MariaDBWaitStrategy extends AbstractWaitStrategy {
|
class MariaDBWaitStrategy extends AbstractWaitStrategy {
|
||||||
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
|
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
|
||||||
|
@ -22,22 +25,22 @@ class MariaDBWaitStrategy extends AbstractWaitStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDatasource(): Promise<Datasource> {
|
export async function getDatasource(): Promise<Datasource> {
|
||||||
let container = new GenericContainer("mariadb:lts")
|
if (!ports) {
|
||||||
|
ports = startContainer(
|
||||||
|
new GenericContainer("mariadb:lts")
|
||||||
.withExposedPorts(3306)
|
.withExposedPorts(3306)
|
||||||
.withEnvironment({ MARIADB_ROOT_PASSWORD: "password" })
|
.withEnvironment({ MARIADB_ROOT_PASSWORD: "password" })
|
||||||
.withWaitStrategy(new MariaDBWaitStrategy())
|
.withWaitStrategy(new MariaDBWaitStrategy())
|
||||||
|
)
|
||||||
if (process.env.REUSE_CONTAINERS) {
|
|
||||||
container = container.withReuse()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startedContainer = await container.start()
|
const port = (await ports).find(x => x.container === 3306)?.host
|
||||||
|
if (!port) {
|
||||||
const host = startedContainer.getHost()
|
throw new Error("MariaDB port not found")
|
||||||
const port = startedContainer.getMappedPort(3306)
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
host,
|
host: "127.0.0.1",
|
||||||
port,
|
port,
|
||||||
user: "root",
|
user: "root",
|
||||||
password: "password",
|
password: "password",
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
|
import { testContainerUtils } from "@budibase/backend-core/tests"
|
||||||
import { Datasource, SourceName } from "@budibase/types"
|
import { Datasource, SourceName } from "@budibase/types"
|
||||||
import { GenericContainer, Wait } from "testcontainers"
|
import { GenericContainer, Wait } from "testcontainers"
|
||||||
|
import { startContainer } from "."
|
||||||
|
|
||||||
|
let ports: Promise<testContainerUtils.Port[]>
|
||||||
|
|
||||||
export async function getDatasource(): Promise<Datasource> {
|
export async function getDatasource(): Promise<Datasource> {
|
||||||
let container = new GenericContainer("mongo:7.0-jammy")
|
if (!ports) {
|
||||||
|
ports = startContainer(
|
||||||
|
new GenericContainer("mongo:7.0-jammy")
|
||||||
.withExposedPorts(27017)
|
.withExposedPorts(27017)
|
||||||
.withEnvironment({
|
.withEnvironment({
|
||||||
MONGO_INITDB_ROOT_USERNAME: "mongo",
|
MONGO_INITDB_ROOT_USERNAME: "mongo",
|
||||||
|
@ -13,22 +19,20 @@ export async function getDatasource(): Promise<Datasource> {
|
||||||
`mongosh --eval "db.version()"`
|
`mongosh --eval "db.version()"`
|
||||||
).withStartupTimeout(10000)
|
).withStartupTimeout(10000)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if (process.env.REUSE_CONTAINERS) {
|
|
||||||
container = container.withReuse()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startedContainer = await container.start()
|
const port = (await ports).find(x => x.container === 27017)
|
||||||
|
if (!port) {
|
||||||
const host = startedContainer.getHost()
|
throw new Error("MongoDB port not found")
|
||||||
const port = startedContainer.getMappedPort(27017)
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "datasource",
|
type: "datasource",
|
||||||
source: SourceName.MONGODB,
|
source: SourceName.MONGODB,
|
||||||
plus: false,
|
plus: false,
|
||||||
config: {
|
config: {
|
||||||
connectionString: `mongodb://mongo:password@${host}:${port}`,
|
connectionString: `mongodb://mongo:password@127.0.0.1:${port.host}`,
|
||||||
db: "mongo",
|
db: "mongo",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import { Datasource, SourceName } from "@budibase/types"
|
import { Datasource, SourceName } from "@budibase/types"
|
||||||
import { GenericContainer, Wait } from "testcontainers"
|
import { GenericContainer, Wait } from "testcontainers"
|
||||||
import mssql from "mssql"
|
import mssql from "mssql"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator, testContainerUtils } from "@budibase/backend-core/tests"
|
||||||
|
import { startContainer } from "."
|
||||||
|
|
||||||
|
let ports: Promise<testContainerUtils.Port[]>
|
||||||
|
|
||||||
export async function getDatasource(): Promise<Datasource> {
|
export async function getDatasource(): Promise<Datasource> {
|
||||||
let container = new GenericContainer(
|
if (!ports) {
|
||||||
"mcr.microsoft.com/mssql/server:2022-latest"
|
ports = startContainer(
|
||||||
)
|
new GenericContainer("mcr.microsoft.com/mssql/server:2022-latest")
|
||||||
.withExposedPorts(1433)
|
.withExposedPorts(1433)
|
||||||
.withEnvironment({
|
.withEnvironment({
|
||||||
ACCEPT_EULA: "Y",
|
ACCEPT_EULA: "Y",
|
||||||
|
@ -22,22 +25,17 @@ export async function getDatasource(): Promise<Datasource> {
|
||||||
"/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'"
|
"/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if (process.env.REUSE_CONTAINERS) {
|
|
||||||
container = container.withReuse()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startedContainer = await container.start()
|
const port = (await ports).find(x => x.container === 1433)?.host
|
||||||
|
|
||||||
const host = startedContainer.getHost()
|
|
||||||
const port = startedContainer.getMappedPort(1433)
|
|
||||||
|
|
||||||
const datasource: Datasource = {
|
const datasource: Datasource = {
|
||||||
type: "datasource_plus",
|
type: "datasource_plus",
|
||||||
source: SourceName.SQL_SERVER,
|
source: SourceName.SQL_SERVER,
|
||||||
plus: true,
|
plus: true,
|
||||||
config: {
|
config: {
|
||||||
server: host,
|
server: "127.0.0.1",
|
||||||
port,
|
port,
|
||||||
user: "sa",
|
user: "sa",
|
||||||
password: "Password_123",
|
password: "Password_123",
|
||||||
|
|
|
@ -2,7 +2,10 @@ import { Datasource, SourceName } from "@budibase/types"
|
||||||
import { GenericContainer, Wait } from "testcontainers"
|
import { GenericContainer, Wait } from "testcontainers"
|
||||||
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
||||||
import mysql from "mysql2/promise"
|
import mysql from "mysql2/promise"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator, testContainerUtils } from "@budibase/backend-core/tests"
|
||||||
|
import { startContainer } from "."
|
||||||
|
|
||||||
|
let ports: Promise<testContainerUtils.Port[]>
|
||||||
|
|
||||||
class MySQLWaitStrategy extends AbstractWaitStrategy {
|
class MySQLWaitStrategy extends AbstractWaitStrategy {
|
||||||
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
|
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
|
||||||
|
@ -25,26 +28,23 @@ class MySQLWaitStrategy extends AbstractWaitStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDatasource(): Promise<Datasource> {
|
export async function getDatasource(): Promise<Datasource> {
|
||||||
let container = new GenericContainer("mysql:8.3")
|
if (!ports) {
|
||||||
|
ports = startContainer(
|
||||||
|
new GenericContainer("mysql:8.3")
|
||||||
.withExposedPorts(3306)
|
.withExposedPorts(3306)
|
||||||
.withEnvironment({ MYSQL_ROOT_PASSWORD: "password" })
|
.withEnvironment({ MYSQL_ROOT_PASSWORD: "password" })
|
||||||
.withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000))
|
.withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000))
|
||||||
|
)
|
||||||
if (process.env.REUSE_CONTAINERS) {
|
|
||||||
container = container.withReuse()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startedContainer = await container.start()
|
const port = (await ports).find(x => x.container === 3306)?.host
|
||||||
|
|
||||||
const host = startedContainer.getHost()
|
|
||||||
const port = startedContainer.getMappedPort(3306)
|
|
||||||
|
|
||||||
const datasource: Datasource = {
|
const datasource: Datasource = {
|
||||||
type: "datasource_plus",
|
type: "datasource_plus",
|
||||||
source: SourceName.MYSQL,
|
source: SourceName.MYSQL,
|
||||||
plus: true,
|
plus: true,
|
||||||
config: {
|
config: {
|
||||||
host,
|
host: "127.0.0.1",
|
||||||
port,
|
port,
|
||||||
user: "root",
|
user: "root",
|
||||||
password: "password",
|
password: "password",
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { Datasource, SourceName } from "@budibase/types"
|
import { Datasource, SourceName } from "@budibase/types"
|
||||||
import { GenericContainer, Wait } from "testcontainers"
|
import { GenericContainer, Wait } from "testcontainers"
|
||||||
import pg from "pg"
|
import pg from "pg"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator, testContainerUtils } from "@budibase/backend-core/tests"
|
||||||
|
import { startContainer } from "."
|
||||||
|
|
||||||
|
let ports: Promise<testContainerUtils.Port[]>
|
||||||
|
|
||||||
export async function getDatasource(): Promise<Datasource> {
|
export async function getDatasource(): Promise<Datasource> {
|
||||||
let container = new GenericContainer("postgres:16.1-bullseye")
|
if (!ports) {
|
||||||
|
ports = startContainer(
|
||||||
|
new GenericContainer("postgres:16.1-bullseye")
|
||||||
.withExposedPorts(5432)
|
.withExposedPorts(5432)
|
||||||
.withEnvironment({ POSTGRES_PASSWORD: "password" })
|
.withEnvironment({ POSTGRES_PASSWORD: "password" })
|
||||||
.withWaitStrategy(
|
.withWaitStrategy(
|
||||||
|
@ -12,22 +17,17 @@ export async function getDatasource(): Promise<Datasource> {
|
||||||
"pg_isready -h localhost -p 5432"
|
"pg_isready -h localhost -p 5432"
|
||||||
).withStartupTimeout(10000)
|
).withStartupTimeout(10000)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if (process.env.REUSE_CONTAINERS) {
|
|
||||||
container = container.withReuse()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startedContainer = await container.start()
|
const port = (await ports).find(x => x.container === 5432)?.host
|
||||||
|
|
||||||
const host = startedContainer.getHost()
|
|
||||||
const port = startedContainer.getMappedPort(5432)
|
|
||||||
|
|
||||||
const datasource: Datasource = {
|
const datasource: Datasource = {
|
||||||
type: "datasource_plus",
|
type: "datasource_plus",
|
||||||
source: SourceName.POSTGRES,
|
source: SourceName.POSTGRES,
|
||||||
plus: true,
|
plus: true,
|
||||||
config: {
|
config: {
|
||||||
host,
|
host: "127.0.0.1",
|
||||||
port,
|
port,
|
||||||
database: "postgres",
|
database: "postgres",
|
||||||
user: "postgres",
|
user: "postgres",
|
||||||
|
|
Loading…
Reference in New Issue