feat: diff crawl docker tool (#4310)

* feat: diff docker tool (wip)

* feat: diff docker tool - download + extract from github

* feat: diff docker tool - pull + create + shutdown containers

* feat: diff docker tool - start app containers + execute prepare script

* fix: many fixes + run target instance server

* feat: diff docker tool - run crawl + options

* fix: diff docker tool - various fixes

* feat: diff docker tool - add tag + commit fetch options

* feat: diff docker tool - handle log save + more options

* feat: diff docker tool - F10 quit option + better exec display + checklist fixes

* fix: diff docker tool - remove latest release option
This commit is contained in:
Nicolas Giard 2022-08-18 13:28:47 -04:00 committed by GitHub
parent 1869d375c4
commit 822a572589
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 3592 additions and 0 deletions

7
dev/diff/.editorconfig Normal file
View file

@ -0,0 +1,7 @@
[*]
indent_size = 2
indent_style = space
charset = utf-8
trim_trailing_whitespace = false
end_of_line = lf
insert_final_newline = true

1
dev/diff/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/node_modules

1
dev/diff/README.md Normal file
View file

@ -0,0 +1 @@
# diff

7
dev/diff/cleanup.sh Normal file
View file

@ -0,0 +1,7 @@
# Force remove docker resources created by this tool
# in case it doesn't clean up properly
docker rm dt-diff-app-source dt-diff-app-target dt-diff-db-source dt-diff-db-target -f
docker network rm dt-diff-net
echo "Docker resources cleaned successfully."

861
dev/diff/cli.js Normal file
View file

@ -0,0 +1,861 @@
#!/usr/bin/env node
import Docker from 'dockerode'
import chalk from 'chalk'
import path from 'path'
import os from 'os'
import fs from 'fs-extra'
import got from 'got'
import { pipeline } from 'stream/promises'
import { PassThrough } from 'stream'
import prettyBytes from 'pretty-bytes'
import extract from 'extract-zip'
import tar from 'tar'
import { kebabCase } from 'lodash-es'
import { Listr } from 'listr2'
import { performance } from 'perf_hooks'
import { Duration } from 'luxon'
import keypress from 'keypress'
let dock = null
let diffOutput = []
const diffStack = []
const config = {
options: [],
source: process.cwd(),
target: null,
tmpDir: null,
savePath: null,
shouldCleanup: false
}
const containers = {
net: null,
dbSource: null,
dbTarget: null,
appSource: null,
appTarget: null
}
const sha1reg = /^[0-9a-f]{5,40}$/
/**
* Prompt the user for a path
*
* @param {Task} task Listr task instance
* @param {String} msg Prompt message
* @param {Boolean} mustExist Whether the path must already exist
* @returns path
*/
async function promptForPath (task, msg, mustExist = true, initial) {
return task.prompt([
{
type: 'input',
name: 'path',
message: msg,
initial,
async validate (input) {
if (!input) {
return 'You must provide a valid path!'
}
const proposedPath = path.resolve('.', input)
if (proposedPath.includes(config.source)) {
return 'Path must be different than the current datatracker project path!'
} else if (mustExist && !(await fs.pathExists(proposedPath))) {
return 'Path is invalid or doesn\'t exist!'
} else {
return true
}
}
}
])
}
/**
* Download and Extract a zip archive
*
* @param {Task} task Listr task instance
* @param {Object} param1 Options
*/
async function downloadExtractZip (task, { msg, url, ext = 'zip', branch }) {
const archivePath = path.join(config.target, `archive.${ext}`)
await fs.emptyDir(config.target)
// Download zip
try {
task.title = msg
const downloadBranchStream = got.stream(url)
downloadBranchStream.on('downloadProgress', progress => {
task.output = `${prettyBytes(progress.transferred)} downloaded.`
})
await pipeline(
downloadBranchStream,
fs.createWriteStream(archivePath)
)
task.title = `Downloaded ${ext} archive successfully.`
} catch (err) {
throw new Error(`Failed to download ${ext} archive from GitHub. ${err.message}`)
}
// Extract zip
try {
task.title = `Extracting ${ext} archive contents...`
if (ext === 'zip') {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dt-'))
await fs.ensureDir(tmpDir)
await extract(archivePath, {
dir: tmpDir,
onEntry (entry) {
task.output = entry.fileName
}
})
task.title = 'Moving extracted files to final location...'
task.output = config.target
await fs.move(path.join(tmpDir, kebabCase(`datatracker-${branch}`)), config.target, { overwrite: true })
await fs.remove(tmpDir)
} else if (ext === 'tgz') {
await tar.x({
strip: 1,
file: archivePath,
cwd: config.target,
filter (path) {
task.output = path
return true
}
})
}
task.title = `Extracted ${ext} archive successfully.`
await fs.remove(archivePath)
} catch (err) {
throw new Error(`Failed to extract ${ext} archive contents. ${err.message}`)
}
}
/**
* Run a command on a running container
*
* @param {Task} task Listr task instance
* @param {Docker.Container} container Docker container instance
* @param {Array<String>} cmd Command to execute
* @param {Boolean} collectOutput Whether to collect and return the command output
*/
async function executeCommand (task, container, cmd, collectOutput = false, silent = false) {
const logStack = []
const errStack = []
let logFStream = null
return new Promise(async (resolve, reject) => {
// Handle stream output
const logStream = new PassThrough()
logStream.on('data', chunk => {
const logLine = chunk.toString('utf8').trim()
if (logLine && !silent) {
task.output = logLine
if (collectOutput) {
logStack.push(...logLine.split('\n').filter(l => l))
}
}
})
logStream.on('error', chunk => {
task.output = chunk.toString('utf8')
errStack.push(chunk.toString('utf8'))
})
if (collectOutput) {
logFStream = fs.createWriteStream(path.join(config.savePath))
logStream.pipe(logFStream)
}
// Execute command in container
const execChmod = await container.exec({
Cmd: cmd,
AttachStdout: true,
AttachStderr: true
})
const execChmodStream = await execChmod.start()
// Handle stream close
execChmodStream.on('close', () => {
if (collectOutput) {
logFStream.close()
}
if (errStack.length > 0) {
reject(new Error(errStack))
} else {
if (!silent) {
}
task.output = ''
resolve(logStack)
}
})
// Pipe container stream to log stream
container.modem.demuxStream(execChmodStream, logStream, logStream)
})
}
/**
* Run cleanup tasks (e.g. stop/remove docker resources)
*
* @param {Boolean} exitAfter Whether to exit the process at the end
*/
async function cleanup (exitAfter = false) {
try {
const cleanupTasks = new Listr([
// ------------------------
// Stop + Remove Containers
// ------------------------
{
title: 'Stop + remove docker containers',
task: async (ctx, task) => {
task.output = 'Stopping containers...'
try {
await Promise.allSettled([
containers.dbSource && containers.dbSource.stop(),
containers.dbTarget && containers.dbTarget.stop(),
containers.appSource && containers.appSource.stop(),
containers.appTarget && containers.appTarget.stop()
])
} catch (err) { }
task.output = 'Removing containers...'
try {
await Promise.allSettled([
containers.dbSource && containers.dbSource.remove({ v: true }),
containers.dbTarget && containers.dbTarget.remove({ v: true }),
containers.appSource && containers.appSource.remove({ v: true }),
containers.appTarget && containers.appTarget.remove({ v: true })
])
} catch (err) { }
task.output = 'Removing network...'
try {
await containers.net.remove()
} catch (err) {}
}
},
// --------------------
// Restore config files
// --------------------
{
title: 'Restore original source settings file',
task: async (ctx, task) => {
const sourceSettingsPath = path.join(config.source, 'ietf/settings_local.py')
if (await fs.pathExists(`${sourceSettingsPath}.bak`)) {
await fs.move(`${sourceSettingsPath}.bak`, sourceSettingsPath, { overwrite: true })
task.title = 'Restored original source settings file.'
} else {
task.skip('Nothing to restore.')
}
}
},
{
title: 'Restore original target settings file',
task: async (ctx, task) => {
const targetSettingsPath = path.join(config.target, 'ietf/settings_local.py')
if (await fs.pathExists(`${targetSettingsPath}.bak`)) {
await fs.move(`${targetSettingsPath}.bak`, targetSettingsPath, { overwrite: true })
task.title = 'Restored original target settings file.'
} else {
task.skip('Nothing to restore.')
}
}
}
], {
registerSignalListeners: false
})
await cleanupTasks.run()
// Cleanup
if (config.tmpDir) {
await fs.remove(config.tmpDir)
}
} catch (err) {
console.error(chalk.redBright(err.message))
process.exit(1)
}
if (exitAfter) {
process.exit(0)
}
}
async function main () {
console.clear()
console.info('╔════════════════════════════╗')
console.info('║ IETF DATATRACKER DIFF TOOL ║')
console.info('╚════════════════════════════╝\n')
try {
const tasks = new Listr([
// ----------------------------
// Connect to Docker Engine API
// ----------------------------
{
title: 'Connect to Docker Engine API',
task: async (ctx, task) => {
dock = new Docker()
await dock.ping()
task.title = 'Connected to Docker Engine API.'
}
},
// ---------------------------------------------------------------
// Find base path so that it works from both / and /dev/diff paths
// ---------------------------------------------------------------
{
title: 'Find base datatracker instance base path',
task: async (ctx, task) => {
let parentIdx = 0
while(!(await fs.pathExists(path.join(config.source, 'requirements.txt')))) {
config.source = path.resolve(config.source, '..')
parentIdx++
if (parentIdx > 2) {
throw new Error('Start the CLI from a valid datatracker project path.')
}
}
task.title = `Using path ${config.source} for source datatracker instance.`
}
},
// --------------------------------------
// Select comparison datatracker instance
// --------------------------------------
{
title: 'Select diff target',
task: async (ctx, task) => {
ctx.targetMode = await task.prompt({
type: 'select',
message: 'What do you want to compare against?',
choices: [
{ name: 'local', message: 'Local folder path...' },
{ name: 'branch', message: 'Remote GitHub branch...' },
{ name: 'tag', message: 'Remote GitHub tag...' },
{ name: 'commit', message: 'Remote GitHub commit hash...' }
// { name: 'release', message: 'Latest GitHub release', disabled: true }
]
})
task.title = `Selected diff target: ${ctx.targetMode}`
}
},
// ---------------------------------
// Fetch target datatracker instance
// ---------------------------------
{
title: 'Fetch diff target',
task: async (ctx, task) => {
switch (ctx.targetMode) {
// MODE: LOCAL
// ------------------------------------------------
case 'local': {
task.title = 'Waiting for diff target path input'
config.target = await promptForPath(task, 'Enter the local path to the datatracker project to compare against:')
task.title = `Using path ${config.target} for target datatracker instance.`
break
}
// MODE: REMOTE BRANCH
// ------------------------------------------------
case 'branch': {
// Prompt for branch
const branches = []
let branch = 'main'
try {
task.title = 'Fetching available remote branches...'
const branchesResp = await got('https://api.github.com/repos/ietf-tools/datatracker/branches?per_page=100').json()
if (branchesResp?.length < 1) {
throw new Error('No remote branches available.')
}
branches.push(...branchesResp.map(b => b.name))
task.output = `Fetched ${branches.length} remote branches.`
} catch (err) {
throw new Error(`Failed to fetch branches! ${err.message}`)
}
branch = await task.prompt([
{
type: 'select',
message: 'Select the remote branch to compare against:',
choices: branches
}
])
// Prompt for local path where to download branch contents
config.target = await promptForPath(task, 'Enter a local path where the branch contents will be downloaded to:', false)
await fs.ensureDir(config.target)
// Download / Extract branch zip
await downloadExtractZip(task, {
msg: `Downloading ${branch} branch contents...`,
url: `https://github.com/ietf-tools/datatracker/archive/refs/heads/${branch}.tar.gz`,
ext: 'tgz'
})
task.title = `Fetched branch ${branch} to ${config.target}`
break
}
// MODE: REMOTE TAG
// ------------------------------------------------
case 'tag': {
// Prompt for tag
const tag = await task.prompt([
{
type: 'input',
message: 'Enter the remote repository tag to compare against:'
}
])
// Prompt for local path where to download tag contents
config.target = await promptForPath(task, 'Enter a local path where the tag contents will be downloaded to:', false)
await fs.ensureDir(config.target)
// Download / Extract tag tarball
await downloadExtractZip(task, {
msg: `Downloading tag ${tag} contents...`,
url: `https://github.com/ietf-tools/datatracker/archive/refs/tags/${tag}.tar.gz`,
ext: 'tgz'
})
task.title = `Fetched tag ${tag} to ${config.target}`
break
}
// MODE: REMOTE COMMIT
// ------------------------------------------------
case 'commit': {
// Prompt for commit hash
const commit = await task.prompt([
{
type: 'input',
message: 'Enter the FULL commit hash to compare against:',
async validate (input) {
if (!input) {
return 'You must provide a hash!'
} else if (!sha1reg.test(input)) {
return 'Invalid hash!'
}
return true
}
}
])
// Prompt for local path where to download commit contents
config.target = await promptForPath(task, 'Enter a local path where the commit contents will be downloaded to:', false)
await fs.ensureDir(config.target)
// Download / Extract commit tarball
await downloadExtractZip(task, {
msg: `Downloading commit ${commit} contents...`,
url: `https://github.com/ietf-tools/datatracker/archive/${commit}.tar.gz`,
ext: 'tgz'
})
task.title = `Fetched commit ${commit} to ${config.target}`
break
}
// MODE: LATEST RELEASE
// ------------------------------------------------
case 'release': {
task.title = 'Waiting for diff target download location'
// Prompt for local path where to download release
config.target = await promptForPath(task, 'Enter a local path where the latest release will be downloaded to:', false)
await fs.ensureDir(config.target)
// Download / extract latest release
await downloadExtractZip(task, {
msg: 'Downloading latest release...',
url: 'https://github.com/ietf-tools/datatracker/releases/latest/download/release.tar.gz',
ext: 'tgz'
})
task.title = `Fetched latest release to ${config.target}`
break
}
default: {
throw new Error('Invalid selection. Exiting...')
}
}
// Add missing files not present in branch
if (!(await fs.pathExists(path.join(config.target, 'dev/diff')))) {
task.output = `Add missing diff tool files...`
await fs.ensureDir(path.join(config.target, 'dev/diff'))
await fs.copy(path.join(config.source, 'dev/diff/prepare.sh'), path.join(config.target, 'dev/diff/prepare.sh'))
await fs.copy(path.join(config.source, 'dev/diff/settings_local.py'), path.join(config.target, 'dev/diff/settings_local.py'))
}
}
},
// ------------------------
// Prompt for crawl options
// ------------------------
{
title: 'Select additional crawl options',
task: async (ctx, task) => {
const toggleIndicatorFn = (state, choice) => {
return choice.enabled ? chalk.greenBright('✔') : chalk.gray('o')
}
config.options = await task.prompt([
{
type: 'multiselect',
message: 'Select additional options to enable:',
hint: '(use <SPACE> to toggle, <ENTER> to confirm)',
choices: [
{ message: 'Skip HTML Validation', name: '--skip-html-validation', hint: 'Skip HTML Validation', indicator: toggleIndicatorFn },
{ message: 'Fail-fast', name: '--failfast', hint: 'Stop the crawl on the first page failure', indicator: toggleIndicatorFn },
{ message: 'No-Follow', name: '--no-follow', hint: 'Do not follow URLs found in fetched pages, just check the given URLs', indicator: toggleIndicatorFn },
{ message: 'No-Revisit', name: '--no-revisit', hint: 'Don\'t revisit already visited URLs', indicator: toggleIndicatorFn },
{ message: 'Pedantic', name: '--pedantic', hint: 'Stop the crawl on the first error or warning', indicator: toggleIndicatorFn },
{ message: 'Random', name: '--random', hint: 'Crawl URLs randomly', indicator: toggleIndicatorFn },
{ message: 'Validate All', name: '--validate-all', hint: 'Run html 5 validation on all pages, without skipping similar urls', indicator: toggleIndicatorFn },
{ message: 'Verbose', name: '--verbose', hint: 'Be more verbose', indicator: toggleIndicatorFn }
]
}
])
if (config.options.length > 0) {
task.title = `Selected additional crawl options: ${config.options.join(' ')}`
}
}
},
// ---------------------------
// Prompt to save crawl output
// ---------------------------
{
title: 'Save crawl output',
task: async (ctx, task) => {
const saveToDisk = await task.prompt({
type: 'confirm',
message: 'Save the crawl output to file?'
})
if (saveToDisk) {
config.savePath = await promptForPath(task, 'Enter the path where the crawl output will be saved:', false, path.join(os.homedir(), 'Desktop/crawl-out.txt'))
task.title = `Crawl output will be saved to ${config.savePath}`
} else {
config.tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dt-'))
config.savePath = path.join(config.tmpDir, 'out.txt')
task.title = 'Crawl output will not be saved.'
}
}
},
// ----------------------------
// Set datatracker config files
// ----------------------------
{
title: 'Set datatracker config files',
task: async (ctx, task) => {
// Source
const sourceSettingsPath = path.join(config.source, 'ietf/settings_local.py')
if (await fs.pathExists(sourceSettingsPath)) {
await fs.move(sourceSettingsPath, `${sourceSettingsPath}.bak`, { overwrite: true })
}
const cfgSourceRaw = await fs.readFile(path.join(config.source, 'dev/diff/settings_local.py'), 'utf8')
await fs.outputFile(sourceSettingsPath, cfgSourceRaw.replace('__DBHOST__', 'dt-diff-db-source'))
// Target
const targetSettingsPath = path.join(config.target, 'ietf/settings_local.py')
if (await fs.pathExists(targetSettingsPath)) {
await fs.move(targetSettingsPath, `${targetSettingsPath}.bak`, { overwrite: true })
}
const cfgTargetRaw = await fs.readFile(path.join(config.target, 'dev/diff/settings_local.py'), 'utf8')
await fs.outputFile(targetSettingsPath, cfgTargetRaw.replace('__DBHOST__', 'dt-diff-db-target'))
}
},
// ------------------
// Pull latest images
// ------------------
{
title: 'Pull latest docker images',
task: (ctx, task) => task.newListr([
{
title: 'Pulling latest DB docker image...',
task: async (subctx, subtask) => {
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))
})
subtask.title = `Pulled latest DB docker image successfully.`
}
},
{
title: 'Pulling latest Datatracker base docker image...',
task: async (subctx, subtask) => {
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))
})
subtask.title = `Pulled latest Datatracker base docker image successfully.`
}
}
], {
concurrent: true,
rendererOptions: {
collapse: false
}
})
},
// --------------
// Create network
// --------------
{
title: 'Create docker network',
task: async (ctx, task) => {
config.shouldCleanup = true
containers.net = await dock.createNetwork({
Name: 'dt-diff-net',
CheckDuplicate: true
})
task.title = 'Created docker network (dt-diff-net).'
}
},
// ----------------------------
// Create + Start DB containers
// ----------------------------
{
title: 'Create DB docker containers',
task: (ctx, task) => task.newListr([
{
title: 'Creating source DB docker container...',
task: async (subctx, subtask) => {
containers.dbSource = await dock.createContainer({
Image: 'ghcr.io/ietf-tools/datatracker-db:latest',
name: 'dt-diff-db-source',
Hostname: 'dbsource',
HostConfig: {
NetworkMode: 'dt-diff-net'
}
})
await containers.dbSource.start()
subtask.title = `Created source DB docker container (dt-diff-db-source) successfully.`
}
},
{
title: 'Creating target DB docker container...',
task: async (subctx, subtask) => {
containers.dbTarget = await dock.createContainer({
Image: 'ghcr.io/ietf-tools/datatracker-db:latest',
name: 'dt-diff-db-target',
Hostname: 'dbtarget',
HostConfig: {
NetworkMode: 'dt-diff-net'
}
})
await containers.dbTarget.start()
subtask.title = `Created target DB docker container (dt-diff-db-target) successfully.`
}
}
], {
concurrent: true,
rendererOptions: {
collapse: false
}
})
},
// -------------------------------------
// Create + Start Datatracker containers
// -------------------------------------
{
title: 'Create Datatracker docker containers',
task: (ctx, task) => task.newListr([
{
title: 'Creating source Datatracker docker container...',
task: async (subctx, subtask) => {
containers.appSource = await dock.createContainer({
Image: 'ghcr.io/ietf-tools/datatracker-app-base:latest',
name: 'dt-diff-app-source',
Tty: true,
Hostname: 'appsource',
HostConfig: {
Binds: [
`${config.source}:/workspace`
],
NetworkMode: 'dt-diff-net'
}
})
await containers.appSource.start()
subtask.title = `Created source Datatracker docker container (dt-diff-app-source) successfully.`
}
},
{
title: 'Creating target Datatracker docker container...',
task: async (subctx, subtask) => {
containers.appTarget = await dock.createContainer({
Image: 'ghcr.io/ietf-tools/datatracker-app-base:latest',
name: 'dt-diff-app-target',
Tty: true,
Hostname: 'apptarget',
HostConfig: {
Binds: [
`${config.target}:/workspace`
],
NetworkMode: 'dt-diff-net'
}
})
await containers.appTarget.start()
subtask.title = `Created target Datatracker docker container (dt-diff-app-target) successfully.`
}
}
], {
concurrent: true,
rendererOptions: {
collapse: false
}
})
},
// -------------------
// Run prepare scripts
// -------------------
{
title: 'Prepare Datatracker instances',
task: (ctx, task) => task.newListr([
{
title: 'Preparing source Datatracker instance...',
task: async (subctx, subtask) => {
await executeCommand(subtask, containers.appSource, ['bash', '-c', 'chmod +x ./dev/diff/prepare.sh'])
await executeCommand(subtask, containers.appSource, ['bash', './dev/diff/prepare.sh'])
subtask.title = `Preparing source Datatracker instance - Running checks...`
await executeCommand(subtask, containers.appSource, ['bash', '-c', './ietf/manage.py check'])
subtask.title = `Preparing source Datatracker instance - Applying migrations...`
await executeCommand(subtask, containers.appSource, ['bash', '-c', './ietf/manage.py migrate'])
subtask.title = `Source Datatracker instance is now ready.`
}
},
{
title: 'Preparing target Datatracker instance...',
task: async (subctx, subtask) => {
await executeCommand(subtask, containers.appTarget, ['bash', '-c', 'chmod +x ./dev/diff/prepare.sh'])
await executeCommand(subtask, containers.appTarget, ['bash', './dev/diff/prepare.sh'])
subtask.title = `Run target Datatracker instance - Running checks...`
await executeCommand(subtask, containers.appTarget, ['bash', '-c', './ietf/manage.py check'])
subtask.title = `Run target Datatracker instance - Applying migrations...`
await executeCommand(subtask, containers.appTarget, ['bash', '-c', './ietf/manage.py migrate'])
subtask.title = `Run target Datatracker instance - Starting server...`
executeCommand(subtask, containers.appTarget, ['bash', '-c', './ietf/manage.py runserver 0.0.0.0:8000 --settings=settings_local'])
subtask.title = `Run target Datatracker instance - Waiting for server to accept connections...`
await executeCommand(subtask, containers.appTarget, ['bash', '-c', '/usr/local/bin/wait-for localhost:8000 -t 300'])
subtask.title = `Target Datatracker instance is now ready and accepting connections.`
}
}
], {
concurrent: true,
rendererOptions: {
collapse: false
}
})
},
// --------------
// Run crawl tool
// --------------
{
title: 'Run crawl',
task: async (ctx, task) => {
task.title = 'Running crawl... (Press F10 to cancel)'
task.output = 'Starting ./bin/test-crawl... (Results will start appearing soon)'
const startMs = performance.now()
config.options.push('--settings=ietf.settings_testcrawl')
config.options.push('--diff http://dt-diff-app-target:8000/')
// Run crawl
const errStack = []
let execChmodStream = null
let linesScanned = 0
const execPromise = new Promise(async (resolve, reject) => {
// Handle stream output
const logStream = new PassThrough()
logStream.on('data', chunk => {
const logLines = chunk.toString('utf8').trim().split('\n').filter(l => l.trim())
// Check for DIFF mentions
if (logLines.length > 0) {
linesScanned += logLines.length
for (const logLine of logLines) {
if (logLine.includes('DIFF')) {
diffStack.push(logLine)
}
}
const currentDur = Duration.fromMillis(performance.now() - startMs)
task.output = `[${currentDur.toFormat('hh:mm:ss')}] Scanned ${linesScanned} lines. Found ${diffStack.length} DIFF mentions.`
}
})
// Handle error stream
logStream.on('error', chunk => {
task.output = chunk.toString('utf8')
errStack.push(chunk.toString('utf8'))
})
// Pipe to file stream
const logFStream = fs.createWriteStream(path.join(config.savePath))
logStream.pipe(logFStream)
// Execute command in container
const execChmod = await containers.appSource.exec({
Cmd: ['bash', '-c', `./bin/test-crawl ${config.options.join(' ')}`],
AttachStdout: true,
AttachStderr: true
})
execChmodStream = await execChmod.start()
// Handle stream close
execChmodStream.on('close', () => {
logFStream.close()
process.stdin.setRawMode(false)
process.stdin.resume()
if (errStack.length > 0) {
reject(new Error(errStack))
} else {
resolve()
}
})
// Pipe container stream to log stream
containers.appSource.modem.demuxStream(execChmodStream, logStream, logStream)
})
// Handle F10 Exit
keypress(process.stdin)
process.stdin.on('keypress', (ch, key) => {
if (key && key.name === 'f10') {
task.title = 'Run crawl'
execChmodStream.destroy()
task.skip()
}
})
process.stdin.setRawMode(true)
process.stdin.resume()
return execPromise
}
}
])
await tasks.run()
} catch (err) {
console.error(chalk.redBright(err.message))
}
// ------------------------
// Cleanup
// ------------------------
await cleanup()
// ------------------------
// Output results
// ------------------------
console.info('\n=====================')
console.info('RESULTS')
console.info('=====================\n')
if (diffStack.length > 0) {
for (const diffLine of diffStack) {
console.info(`> ${diffLine}`)
}
console.info(chalk.blueBright(`\nFound ${diffStack.length} mention(s) of DIFF.\n`))
} else {
console.info(chalk.blueBright(`No mention of DIFF were found.\n`))
}
process.exit(0)
}
// Handle user interrupt
// process.once('SIGINT', async () => {
// console.info('Trying to shutdown gracefully...')
// if (config.shouldCleanup) {
// setTimeout(() => { process.exit(1) }, 30000).unref()
// try {
// await cleanup()
// } catch (err) {
// console.warn(`Failed to shutdown gracefully: ${err.message}`)
// }
// }
// process.exit(0)
// })
main()

2602
dev/diff/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
dev/diff/package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "diff",
"type": "module",
"dependencies": {
"chalk": "^5.0.1",
"dockerode": "^3.3.3",
"enquirer": "^2.3.6",
"extract-zip": "^2.0.1",
"fs-extra": "^10.1.0",
"got": "^12.3.1",
"keypress": "^0.2.1",
"listr2": "^5.0.2",
"lodash-es": "^4.17.21",
"luxon": "^3.0.1",
"pretty-bytes": "^6.0.0",
"tar": "^6.1.11",
"yargs": "^17.5.1"
},
"engines": {
"node": ">=16"
}
}

14
dev/diff/prepare.sh Normal file
View file

@ -0,0 +1,14 @@
#!/bin/bash
echo "Fixing permissions..."
chmod -R 777 ./
echo "Ensure all requirements.txt packages are installed..."
pip --disable-pip-version-check --no-cache-dir install -r requirements.txt
echo "Compiling native node packages..."
yarn rebuild
echo "Building static assets..."
yarn build
yarn legacy:build
echo "Creating data directories..."
chmod +x ./docker/scripts/app-create-dirs.sh
./docker/scripts/app-create-dirs.sh

View file

@ -0,0 +1,76 @@
# Copyright The IETF Trust 2007-2019, All Rights Reserved
# -*- coding: utf-8 -*-
from ietf.settings import * # pyflakes:ignore
ALLOWED_HOSTS = ['*']
DATABASES = {
'default': {
'HOST': '__DBHOST__',
'PORT': 3306,
'NAME': 'ietf_utf8',
'ENGINE': 'django.db.backends.mysql',
'USER': 'django',
'PASSWORD': 'RkTkDPFnKpko',
'OPTIONS': {
'sql_mode': 'STRICT_TRANS_TABLES',
'init_command': 'SET storage_engine=InnoDB; SET names "utf8"',
},
},
}
DATABASE_TEST_OPTIONS = {
'init_command': 'SET storage_engine=InnoDB',
}
IDSUBMIT_IDNITS_BINARY = "/usr/local/bin/idnits"
IDSUBMIT_REPOSITORY_PATH = "test/id/"
IDSUBMIT_STAGING_PATH = "test/staging/"
INTERNET_DRAFT_ARCHIVE_DIR = "test/archive/"
INTERNET_ALL_DRAFTS_ARCHIVE_DIR = "test/archive/"
RFC_PATH = "test/rfc/"
AGENDA_PATH = '/assets/www6s/proceedings/'
MEETINGHOST_LOGO_PATH = AGENDA_PATH
USING_DEBUG_EMAIL_SERVER=True
EMAIL_HOST='localhost'
EMAIL_PORT=2025
MEDIA_BASE_DIR = '/assets'
MEDIA_ROOT = MEDIA_BASE_DIR + '/media/'
MEDIA_URL = '/media/'
PHOTOS_DIRNAME = 'photo'
PHOTOS_DIR = MEDIA_ROOT + PHOTOS_DIRNAME
SUBMIT_YANG_CATALOG_MODEL_DIR = '/assets/ietf-ftp/yang/catalogmod/'
SUBMIT_YANG_DRAFT_MODEL_DIR = '/assets/ietf-ftp/yang/draftmod/'
SUBMIT_YANG_INVAL_MODEL_DIR = '/assets/ietf-ftp/yang/invalmod/'
SUBMIT_YANG_IANA_MODEL_DIR = '/assets/ietf-ftp/yang/ianamod/'
SUBMIT_YANG_RFC_MODEL_DIR = '/assets/ietf-ftp/yang/rfcmod/'
# Set INTERNAL_IPS for use within Docker. See https://knasmueller.net/fix-djangos-debug-toolbar-not-showing-inside-docker
import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS = [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
# DEV_TEMPLATE_CONTEXT_PROCESSORS = [
# 'ietf.context_processors.sql_debug',
# ]
DOCUMENT_PATH_PATTERN = '/assets/ietf-ftp/{doc.type_id}/'
INTERNET_DRAFT_PATH = '/assets/ietf-ftp/internet-drafts/'
RFC_PATH = '/assets/ietf-ftp/rfc/'
CHARTER_PATH = '/assets/ietf-ftp/charter/'
BOFREQ_PATH = '/assets/ietf-ftp/bofreq/'
CONFLICT_REVIEW_PATH = '/assets/ietf-ftp/conflict-reviews/'
STATUS_CHANGE_PATH = '/assets/ietf-ftp/status-changes/'
INTERNET_DRAFT_ARCHIVE_DIR = '/assets/ietf-ftp/internet-drafts/'
INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/assets/ietf-ftp/internet-drafts/'
NOMCOM_PUBLIC_KEYS_DIR = 'data/nomcom_keys/public_keys/'
SLIDE_STAGING_PATH = 'test/staging/'
DE_GFM_BINARY = '/usr/local/bin/de-gfm'

1
ietf/.gitignore vendored
View file

@ -1,5 +1,6 @@
/*.pyc
/settings_local.py
/settings_local.py.bak
/settings_local_debug.py
/settings_local_sqlitetest.py
/settings_local_vite.py