datatracker/dev/deploy-to-container/cli.js
2024-02-28 08:30:51 -06:00

316 lines
11 KiB
JavaScript

#!/usr/bin/env node
import Docker from 'dockerode'
import path from 'path'
import fs from 'fs-extra'
import tar from 'tar'
import yargs from 'yargs/yargs'
import { hideBin } from 'yargs/helpers'
import slugify from 'slugify'
import { nanoid, customAlphabet } from 'nanoid'
import { alphanumeric } from 'nanoid-dictionary'
const nanoidAlphaNum = customAlphabet(alphanumeric, 16)
async function main () {
const basePath = process.cwd()
const releasePath = path.join(basePath, 'release')
const argv = yargs(hideBin(process.argv)).argv
// Parse branch argument
let branch = argv.branch
if (!branch) {
throw new Error('Missing --branch argument!')
}
if (branch.indexOf('/') >= 0) {
branch = branch.split('/').slice(1).join('-')
}
branch = slugify(branch, { lower: true, strict: true })
if (branch.length < 1) {
throw new Error('Branch name is empty!')
}
console.info(`Will use branch name "${branch}"`)
// Parse domain argument
const domain = argv.domain
if (!domain) {
throw new Error('Missing --domain argument!')
}
const hostname = `dt-${branch}.${domain}`
console.info(`Will use hostname "${hostname}"`)
// Connect to Docker Engine API
console.info('Connecting to Docker Engine API...')
const dock = new Docker()
await dock.ping()
console.info('Connected to Docker Engine API.')
// Extract release artifact
console.info('Extracting release artifact...')
if (!(await fs.pathExists(path.join(basePath, 'release.tar.gz')))) {
throw new Error('Missing release.tar.gz file!')
}
await fs.emptyDir(releasePath)
await tar.x({
cwd: releasePath,
file: 'release.tar.gz'
})
console.info('Extracted release artifact successfully.')
// Update the settings_local.py file
console.info('Setting configuration files...')
const mqKey = nanoidAlphaNum()
const settingsPath = path.join(releasePath, 'ietf/settings_local.py')
const cfgRaw = await fs.readFile(path.join(basePath, 'dev/deploy-to-container/settings_local.py'), 'utf8')
await fs.outputFile(settingsPath,
cfgRaw
.replace('__DBHOST__', `dt-db-${branch}`)
.replace('__SECRETKEY__', nanoid(36))
.replace('__MQCONNSTR__', `amqp://datatracker:${mqKey}@dt-mq-${branch}/dt`)
.replace('__HOSTNAME__', hostname)
)
await fs.copy(path.join(basePath, 'docker/scripts/app-create-dirs.sh'), path.join(releasePath, 'app-create-dirs.sh'))
await fs.copy(path.join(basePath, 'dev/deploy-to-container/start.sh'), path.join(releasePath, 'start.sh'))
await fs.copy(path.join(basePath, 'test/data'), path.join(releasePath, 'test/data'))
console.info('Updated configuration files.')
// Pull latest DB image
console.info('Pulling latest DB docker image...')
const dbImagePullStream = await dock.pull('ghcr.io/ietf-tools/datatracker-db:latest')
await new Promise((resolve, reject) => {
dock.modem.followProgress(dbImagePullStream, (err, res) => err ? reject(err) : resolve(res))
})
console.info('Pulled latest DB docker image successfully.')
// Pull latest Datatracker Base image
console.info('Pulling latest Datatracker base docker image...')
const appImagePullStream = await dock.pull('ghcr.io/ietf-tools/datatracker-app-base:latest')
await new Promise((resolve, reject) => {
dock.modem.followProgress(appImagePullStream, (err, res) => err ? reject(err) : resolve(res))
})
console.info('Pulled latest Datatracker base docker image.')
// Pull latest MQ image
console.info('Pulling latest MQ docker image...')
const mqImagePullStream = await dock.pull('ghcr.io/ietf-tools/datatracker-mq:latest')
await new Promise((resolve, reject) => {
dock.modem.followProgress(mqImagePullStream, (err, res) => err ? reject(err) : resolve(res))
})
console.info('Pulled latest MQ docker image.')
// Pull latest Celery image
console.info('Pulling latest Celery docker image...')
const celeryImagePullStream = await dock.pull('ghcr.io/ietf-tools/datatracker-celery:latest')
await new Promise((resolve, reject) => {
dock.modem.followProgress(celeryImagePullStream, (err, res) => err ? reject(err) : resolve(res))
})
console.info('Pulled latest Celery docker image.')
// Terminate existing containers
console.info('Ensuring existing containers with same name are terminated...')
const containers = await dock.listContainers({ all: true })
for (const container of containers) {
if (
container.Names.includes(`/dt-db-${branch}`) ||
container.Names.includes(`/dt-app-${branch}`) ||
container.Names.includes(`/dt-mq-${branch}`) ||
container.Names.includes(`/dt-celery-${branch}`) ||
container.Names.includes(`/dt-beat-${branch}`)
) {
console.info(`Terminating old container ${container.Id}...`)
const oldContainer = dock.getContainer(container.Id)
if (container.State === 'running') {
await oldContainer.stop({ t: 5 })
}
await oldContainer.remove({
force: true,
v: true
})
}
}
console.info('Existing containers with same name have been terminated.')
// Get shared docker network
console.info('Querying shared docker network...')
const networks = await dock.listNetworks()
if (!networks.some(n => n.Name === 'shared')) {
console.info('No shared docker network found, creating a new one...')
await dock.createNetwork({
Name: 'shared',
CheckDuplicate: true
})
console.info('Created shared docker network successfully.')
} else {
console.info('Existing shared docker network found.')
}
// Get assets docker volume
console.info('Querying assets docker volume...')
const assetsVolume = await dock.getVolume('dt-assets')
if (!assetsVolume) {
console.info('No assets docker volume found, creating a new one...')
await dock.createVolume({
Name: 'dt-assets'
})
console.info('Created assets docker volume successfully.')
} else {
console.info('Existing assets docker volume found.')
}
// Get shared test docker volume
console.info('Querying shared test docker volume...')
try {
const testVolume = await dock.getVolume(`dt-test-${branch}`)
console.info('Attempting to delete any existing shared test docker volume...')
await testVolume.remove({ force: true })
} catch (err) {}
console.info('Creating new shared test docker volume...')
await dock.createVolume({
Name: `dt-test-${branch}`
})
console.info('Created shared test docker volume successfully.')
// Create DB container
console.info(`Creating DB docker container... [dt-db-${branch}]`)
const dbContainer = await dock.createContainer({
Image: 'ghcr.io/ietf-tools/datatracker-db:latest',
name: `dt-db-${branch}`,
Hostname: `dt-db-${branch}`,
Labels: {
...argv.nodbrefresh === 'true' && { nodbrefresh: '1' }
},
HostConfig: {
NetworkMode: 'shared',
RestartPolicy: {
Name: 'unless-stopped'
}
}
})
await dbContainer.start()
console.info('Created and started DB docker container successfully.')
// Create MQ container
console.info(`Creating MQ docker container... [dt-mq-${branch}]`)
const mqContainer = await dock.createContainer({
Image: 'ghcr.io/ietf-tools/datatracker-mq:latest',
name: `dt-mq-${branch}`,
Hostname: `dt-mq-${branch}`,
Env: [
`CELERY_PASSWORD=${mqKey}`
],
Labels: {
...argv.nodbrefresh === 'true' && { nodbrefresh: '1' }
},
HostConfig: {
Memory: 4 * (1024 ** 3), // in bytes
NetworkMode: 'shared',
RestartPolicy: {
Name: 'unless-stopped'
}
}
})
await mqContainer.start()
console.info('Created and started MQ docker container successfully.')
// Create Celery containers
console.info(`Creating Celery docker containers... [dt-celery-${branch}, dt-beat-${branch}]`)
const conConfs = [
{ name: 'celery', role: 'worker' },
{ name: 'beat', role: 'beat' }
]
const celeryContainers = {}
for (const conConf of conConfs) {
celeryContainers[conConf.name] = await dock.createContainer({
Image: 'ghcr.io/ietf-tools/datatracker-celery:latest',
name: `dt-${conConf.name}-${branch}`,
Hostname: `dt-${conConf.name}-${branch}`,
Env: [
'CELERY_APP=ietf',
`CELERY_ROLE=${conConf.role}`,
'UPDATE_REQUIREMENTS_FROM=requirements.txt'
],
Labels: {
...argv.nodbrefresh === 'true' && { nodbrefresh: '1' }
},
HostConfig: {
Binds: [
'dt-assets:/assets',
`dt-test-${branch}:/test`
],
Init: true,
NetworkMode: 'shared',
RestartPolicy: {
Name: 'unless-stopped'
}
},
Cmd: ['--loglevel=INFO']
})
}
console.info('Created Celery docker containers successfully.')
// Create Datatracker container
console.info(`Creating Datatracker docker container... [dt-app-${branch}]`)
const appContainer = await dock.createContainer({
Image: 'ghcr.io/ietf-tools/datatracker-app-base:latest',
name: `dt-app-${branch}`,
Hostname: `dt-app-${branch}`,
Env: [
// `LETSENCRYPT_HOST=${hostname}`,
`VIRTUAL_HOST=${hostname}`,
`VIRTUAL_PORT=8000`,
`PGHOST=dt-db-${branch}`
],
Labels: {
appversion: `${argv.appversion}` ?? '0.0.0',
commit: `${argv.commit}` ?? 'unknown',
ghrunid: `${argv.ghrunid}` ?? '0',
hostname,
...argv.nodbrefresh === 'true' && { nodbrefresh: '1' }
},
HostConfig: {
Binds: [
'dt-assets:/assets',
`dt-test-${branch}:/test`
],
NetworkMode: 'shared',
RestartPolicy: {
Name: 'unless-stopped'
}
},
Entrypoint: ['bash', '-c', 'chmod +x ./start.sh && ./start.sh']
})
console.info(`Created Datatracker docker container successfully.`)
// Inject updated release into container
console.info('Building updated release tarball to inject into containers...')
const tgzPath = path.join(basePath, 'import.tgz')
await tar.c({
gzip: true,
file: tgzPath,
cwd: releasePath,
filter (path) {
if (path.includes('.git') || path.includes('node_modules')) { return false }
return true
}
}, ['.'])
console.info('Injecting archive into Datatracker + Celery docker containers...')
await celeryContainers.celery.putArchive(tgzPath, { path: '/workspace' })
await celeryContainers.beat.putArchive(tgzPath, { path: '/workspace' })
await appContainer.putArchive(tgzPath, { path: '/workspace' })
await fs.remove(tgzPath)
console.info(`Imported working files into Datatracker + Celery docker containers successfully.`)
console.info('Starting Celery containers...')
await celeryContainers.celery.start()
await celeryContainers.beat.start()
console.info('Celery containers started successfully.')
console.info('Starting Datatracker container...')
await appContainer.start()
console.info('Datatracker container started successfully.')
process.exit(0)
}
main()