/ globalSetup.ts
globalSetup.ts
1 import { 2 GenericContainer, 3 Wait, 4 getContainerRuntimeClient, 5 } from "testcontainers" 6 import { ContainerInfo } from "dockerode" 7 import * as path from "path" 8 import * as lockfile from "proper-lockfile" 9 import { execSync } from "child_process" 10 11 interface DockerContext { 12 Name: string 13 Description: string 14 DockerEndpoint: string 15 ContextType: string 16 Error: string 17 } 18 19 function getCurrentDockerContext(): DockerContext { 20 const out = execSync("docker context ls --format json") 21 for (const line of out.toString().split("\n")) { 22 const parsed = JSON.parse(line) 23 if (parsed.Current) { 24 return parsed as DockerContext 25 } 26 } 27 throw new Error("No current Docker context") 28 } 29 30 async function getBudibaseContainers() { 31 const client = await getContainerRuntimeClient() 32 const containers = await client.container.list() 33 return containers.filter( 34 container => 35 container.Labels["com.budibase"] === "true" && 36 container.Labels["org.testcontainers"] === "true" 37 ) 38 } 39 40 async function killContainers(containers: ContainerInfo[]) { 41 const client = await getContainerRuntimeClient() 42 for (const container of containers) { 43 const c = client.container.getById(container.Id) 44 await c.kill() 45 await c.remove() 46 } 47 } 48 49 export default async function setup() { 50 process.env.TESTCONTAINERS_RYUK_DISABLED = "true" 51 52 // For whatever reason, testcontainers doesn't always use the correct current 53 // docker context. This bit of code forces the issue by finding the current 54 // context and setting it as the DOCKER_HOST environment 55 if (!process.env.DOCKER_HOST) { 56 const dockerContext = getCurrentDockerContext() 57 process.env.DOCKER_HOST = dockerContext.DockerEndpoint 58 } 59 60 const lockPath = path.resolve(__dirname, "globalSetup.ts") 61 // If you run multiple tests at the same time, it's possible for the CouchDB 62 // shared container to get started multiple times despite having an 63 // identical reuse hash. To avoid that, we do a filesystem-based lock so 64 // that only one globalSetup.ts is running at a time. 65 lockfile.lockSync(lockPath) 66 67 // Remove any containers that are older than 24 hours. This is to prevent 68 // containers getting full volumes or accruing any other problems from being 69 // left up for very long periods of time. 70 const threshold = new Date(Date.now() - 1000 * 60 * 60 * 24) 71 const containers = (await getBudibaseContainers()).filter(container => { 72 const created = new Date(container.Created * 1000) 73 return created < threshold 74 }) 75 76 await killContainers(containers) 77 78 try { 79 const couchdb = new GenericContainer("budibase/database:2.1.0") 80 .withName("couchdb_testcontainer") 81 .withExposedPorts(5984, 4984) 82 .withEnvironment({ 83 COUCHDB_PASSWORD: "budibase", 84 COUCHDB_USER: "budibase", 85 DATA_DIR: "/data", 86 }) 87 .withCopyContentToContainer([ 88 { 89 content: ` 90 [log] 91 level = warn 92 93 [httpd] 94 socket_options = [{nodelay, true}] 95 96 [couchdb] 97 single_node = true 98 99 [cluster] 100 n = 1 101 q = 1 102 `, 103 target: "/opt/couchdb/etc/local.d/test-couchdb.ini", 104 }, 105 ]) 106 .withLabels({ "com.budibase": "true" }) 107 .withTmpFs({ "/data": "rw" }) 108 .withReuse() 109 .withWaitStrategy( 110 Wait.forSuccessfulCommand( 111 "curl http://budibase:budibase@localhost:5984/_up" 112 ).withStartupTimeout(20000) 113 ) 114 115 const minio = new GenericContainer("minio/minio") 116 .withName("minio_testcontainer") 117 .withExposedPorts(9000) 118 .withCommand(["server", "/data"]) 119 .withTmpFs({ "/data": "rw" }) 120 .withEnvironment({ 121 MINIO_ACCESS_KEY: "budibase", 122 MINIO_SECRET_KEY: "budibase", 123 }) 124 .withLabels({ "com.budibase": "true" }) 125 .withReuse() 126 .withWaitStrategy( 127 Wait.forHttp("/minio/health/ready", 9000).withStartupTimeout(10000) 128 ) 129 130 await Promise.all([couchdb.start(), minio.start()]) 131 } finally { 132 lockfile.unlockSync(lockPath) 133 } 134 }