|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +import Docker from 'dockerode' |
| 4 | +import path from 'path' |
| 5 | +import fs from 'fs-extra' |
| 6 | +import tar from 'tar' |
| 7 | +import yargs from 'yargs/yargs' |
| 8 | +import { hideBin } from 'yargs/helpers' |
| 9 | +import slugify from 'slugify' |
| 10 | +import { nanoid } from 'nanoid' |
| 11 | + |
| 12 | +async function main () { |
| 13 | + const basePath = process.cwd() |
| 14 | + const releasePath = path.join(basePath, 'release') |
| 15 | + const argv = yargs(hideBin(process.argv)).argv |
| 16 | + |
| 17 | + // Parse branch argument |
| 18 | + let branch = argv.branch |
| 19 | + if (!branch) { |
| 20 | + throw new Error('Missing --branch argument!') |
| 21 | + } |
| 22 | + if (branch.indexOf('/') >= 0) { |
| 23 | + branch = branch.split('/')[1] |
| 24 | + } |
| 25 | + branch = slugify(branch, { lower: true, strict: true }) |
| 26 | + if (branch.length < 1) { |
| 27 | + throw new Error('Branch name is empty!') |
| 28 | + } |
| 29 | + console.info(`Will use branch name "${branch}"`) |
| 30 | + |
| 31 | + // Parse domain argument |
| 32 | + const domain = argv.domain |
| 33 | + if (!domain) { |
| 34 | + throw new Error('Missing --domain argument!') |
| 35 | + } |
| 36 | + const hostname = `dt-${branch}.${domain}` |
| 37 | + console.info(`Will use hostname "${hostname}"`) |
| 38 | + |
| 39 | + // Connect to Docker Engine API |
| 40 | + console.info('Connecting to Docker Engine API...') |
| 41 | + const dock = new Docker() |
| 42 | + await dock.ping() |
| 43 | + console.info('Connected to Docker Engine API.') |
| 44 | + |
| 45 | + // Extract release artifact |
| 46 | + console.info('Extracting release artifact...') |
| 47 | + if (!(await fs.pathExists(path.join(basePath, 'release.tar.gz')))) { |
| 48 | + throw new Error('Missing release.tar.gz file!') |
| 49 | + } |
| 50 | + await fs.emptyDir(releasePath) |
| 51 | + await tar.x({ |
| 52 | + cwd: releasePath, |
| 53 | + file: 'release.tar.gz' |
| 54 | + }) |
| 55 | + console.info('Extracted release artifact successfully.') |
| 56 | + |
| 57 | + // Update the settings_local.py file |
| 58 | + console.info('Setting configuration files...') |
| 59 | + const settingsPath = path.join(releasePath, 'ietf/settings_local.py') |
| 60 | + const cfgRaw = await fs.readFile(path.join(basePath, 'dev/deploy-to-container/settings_local.py'), 'utf8') |
| 61 | + await fs.outputFile(settingsPath, cfgRaw.replace('__DBHOST__', `dt-db-${branch}`).replace('__SECRETKEY__', nanoid(36))) |
| 62 | + await fs.copy(path.join(basePath, 'docker/scripts/app-create-dirs.sh'), path.join(releasePath, 'app-create-dirs.sh')) |
| 63 | + await fs.copy(path.join(basePath, 'dev/deploy-to-container/start.sh'), path.join(releasePath, 'start.sh')) |
| 64 | + console.info('Updated configuration files.') |
| 65 | + |
| 66 | + // Pull latest DB image |
| 67 | + console.info('Pulling latest DB docker image...') |
| 68 | + const dbImagePullStream = await dock.pull('ghcr.io/ietf-tools/datatracker-db:latest') |
| 69 | + await new Promise((resolve, reject) => { |
| 70 | + dock.modem.followProgress(dbImagePullStream, (err, res) => err ? reject(err) : resolve(res)) |
| 71 | + }) |
| 72 | + console.info('Pulled latest DB docker image successfully.') |
| 73 | + |
| 74 | + // Pull latest Datatracker Base image |
| 75 | + console.info('Pulling latest Datatracker base docker image...') |
| 76 | + const appImagePullStream = await dock.pull('ghcr.io/ietf-tools/datatracker-app-base:latest') |
| 77 | + await new Promise((resolve, reject) => { |
| 78 | + dock.modem.followProgress(appImagePullStream, (err, res) => err ? reject(err) : resolve(res)) |
| 79 | + }) |
| 80 | + console.info('Pulled latest Datatracker base docker image.') |
| 81 | + |
| 82 | + // Terminate existing containers |
| 83 | + console.info('Ensuring existing containers with same name are terminated...') |
| 84 | + const containers = await dock.listContainers({ all: true }) |
| 85 | + for (const container of containers) { |
| 86 | + if (container.Names.includes(`/dt-db-${branch}`) || container.Names.includes(`/dt-app-${branch}`)) { |
| 87 | + console.info(`Terminating old container ${container.Id}...`) |
| 88 | + const oldContainer = dock.getContainer(container.Id) |
| 89 | + if (container.State === 'running') { |
| 90 | + await oldContainer.stop({ t: 5 }) |
| 91 | + } |
| 92 | + await oldContainer.remove({ force: true }) |
| 93 | + } |
| 94 | + } |
| 95 | + console.info('Existing containers with same name have been terminated.') |
| 96 | + |
| 97 | + // Create DB container |
| 98 | + console.info(`Creating DB docker container... [dt-db-${branch}]`) |
| 99 | + const dbContainer = await dock.createContainer({ |
| 100 | + Image: 'ghcr.io/ietf-tools/datatracker-db:latest', |
| 101 | + name: `dt-db-${branch}`, |
| 102 | + Hostname: `dt-db-${branch}`, |
| 103 | + HostConfig: { |
| 104 | + NetworkMode: 'bridge', |
| 105 | + RestartPolicy: { |
| 106 | + Name: 'unless-stopped' |
| 107 | + } |
| 108 | + } |
| 109 | + }) |
| 110 | + await dbContainer.start() |
| 111 | + console.info('Created and started DB docker container successfully.') |
| 112 | + |
| 113 | + // Create Datatracker container |
| 114 | + console.info(`Creating Datatracker docker container... [dt-app-${branch}]`) |
| 115 | + const appContainer = await dock.createContainer({ |
| 116 | + Image: 'ghcr.io/ietf-tools/datatracker-app-base:latest', |
| 117 | + name: `dt-app-${branch}`, |
| 118 | + Hostname: `dt-app-${branch}`, |
| 119 | + Env: [ |
| 120 | + `LETSENCRYPT_HOST=${hostname}`, |
| 121 | + `VIRTUAL_HOST=${hostname}`, |
| 122 | + `VIRTUAL_PORT=8000` |
| 123 | + ], |
| 124 | + HostConfig: { |
| 125 | + NetworkMode: 'bridge', |
| 126 | + RestartPolicy: { |
| 127 | + Name: 'unless-stopped' |
| 128 | + } |
| 129 | + }, |
| 130 | + Entrypoint: ['bash', '-c', 'chmod +x ./start.sh && ./start.sh'] |
| 131 | + }) |
| 132 | + console.info(`Created Datatracker docker container successfully.`) |
| 133 | + |
| 134 | + // Inject updated release into container |
| 135 | + console.info('Building updated release tarball to inject into container...') |
| 136 | + const tgzPath = path.join(basePath, 'import.tgz') |
| 137 | + await tar.c({ |
| 138 | + gzip: true, |
| 139 | + file: tgzPath, |
| 140 | + cwd: releasePath, |
| 141 | + filter (path) { |
| 142 | + if (path.includes('.git') || path.includes('node_modules')) { return false } |
| 143 | + return true |
| 144 | + } |
| 145 | + }, ['.']) |
| 146 | + console.info('Injecting archive into Datatracker docker container...') |
| 147 | + await appContainer.putArchive(tgzPath, { |
| 148 | + path: '/workspace' |
| 149 | + }) |
| 150 | + await fs.remove(tgzPath) |
| 151 | + console.info(`Imported working files into Datatracker docker container successfully.`) |
| 152 | + |
| 153 | + console.info('Starting Datatracker container...') |
| 154 | + await appContainer.start() |
| 155 | + console.info('Datatracker container started successfully.') |
| 156 | + |
| 157 | + process.exit(0) |
| 158 | +} |
| 159 | + |
| 160 | +main() |
0 commit comments