/ 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  }