@dooer/dooer-test-env 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +7 -0
- package/discovery-router/Dockerfile +18 -0
- package/discovery-router/README.md +99 -0
- package/discovery-router/package.json +13 -0
- package/discovery-router/registry.example.json +5 -0
- package/discovery-router/server.js +272 -0
- package/lib/account.js +120 -0
- package/lib/auth-dev-keys.js +12 -0
- package/lib/bankid.js +130 -0
- package/lib/cli.js +27 -0
- package/lib/command/bankid.js +45 -0
- package/lib/command/customer.js +108 -0
- package/lib/command/db.js +156 -0
- package/lib/command/env.js +114 -0
- package/lib/command/logs.js +143 -0
- package/lib/command/measure.js +81 -0
- package/lib/command/service.js +166 -0
- package/lib/command/setup.js +92 -0
- package/lib/command/shred.js +60 -0
- package/lib/compose/README.md +98 -0
- package/lib/compose/generate.js +375 -0
- package/lib/compose/manifests.js +108 -0
- package/lib/db/roles.js +118 -0
- package/lib/discovery/client.js +40 -0
- package/lib/engine/GUIDE.md +176 -0
- package/lib/engine/PROCESS.md +571 -0
- package/lib/engine/dbbuild.js +325 -0
- package/lib/engine/gen-schema-map.js +479 -0
- package/lib/engine/purge.js +137 -0
- package/lib/engine/schema-map.json +11016 -0
- package/lib/engine/seed.js +1045 -0
- package/lib/obc.js +72 -0
- package/lib/registry.js +123 -0
- package/lib/runtime.js +101 -0
- package/lib/service-token.js +40 -0
- package/lib/shred/README.md +118 -0
- package/lib/shred/audit.js +128 -0
- package/lib/shred/faker.js +545 -0
- package/lib/shred/index.js +126 -0
- package/lib/shred/scripts/base-partner-emails.sql +9 -0
- package/lib/shred/scripts/dev-accounts.sql +195 -0
- package/lib/shred/scripts/emails.sql +48 -0
- package/lib/shred/scripts/institution-browser.sql +3 -0
- package/lib/shred/scripts/notification-targets.sql +5 -0
- package/lib/shred/scripts/partners.sql +2 -0
- package/lib/shred/scripts/passwords.sql +8 -0
- package/lib/shred/scripts/personal-numbers.sql +177 -0
- package/lib/shred/scripts/phone-numbers.sql +22 -0
- package/lib/shred/scripts/salary-spec-reports.sql +5 -0
- package/lib/shred/scripts/service-activity-tracker-data.sql +4 -0
- package/lib/shred/scripts/service-core-objects.sql +19 -0
- package/lib/shred/scripts/service-event-stream.sql +2 -0
- package/lib/shred/scripts/service-integrations.sql +4 -0
- package/lib/shred/scripts/template.sql +4 -0
- package/lib/shred/scripts/x-service-billing.sql +34 -0
- package/lib/shred/scripts/xxx-history-tables.sql +25 -0
- package/lib/stub.js +8 -0
- package/local-postgres/Dockerfile +11 -0
- package/package.json +46 -0
- package/readme.md +92 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const { spawn, spawnSync } = require('child_process')
|
|
3
|
+
const chalk = require('chalk')
|
|
4
|
+
const rt = require('../runtime')
|
|
5
|
+
|
|
6
|
+
// Fetch one container's logs since `since`, merging stdout+stderr (docker logs uses both). Resolves to the
|
|
7
|
+
// text (never rejects) so one bad container can't sink the whole parallel sweep. A hard timeout guards
|
|
8
|
+
// against `docker logs` blocking indefinitely on a container that is mid-restart (observed with the
|
|
9
|
+
// excluded institution-browser) — we kill it and return whatever we captured.
|
|
10
|
+
function fetchLogs(name, since, timeoutMs = 10000) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const p = spawn('docker', ['logs', '--since', since, name])
|
|
13
|
+
let out = ''
|
|
14
|
+
let done = false
|
|
15
|
+
const finish = () => {
|
|
16
|
+
if (done) return
|
|
17
|
+
done = true
|
|
18
|
+
clearTimeout(timer)
|
|
19
|
+
resolve(out)
|
|
20
|
+
}
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
try {
|
|
23
|
+
p.kill('SIGKILL')
|
|
24
|
+
} catch (_) {
|
|
25
|
+
/* already gone */
|
|
26
|
+
}
|
|
27
|
+
finish()
|
|
28
|
+
}, timeoutMs)
|
|
29
|
+
p.stdout.on('data', (d) => (out += d))
|
|
30
|
+
p.stderr.on('data', (d) => (out += d))
|
|
31
|
+
p.on('close', finish)
|
|
32
|
+
p.on('error', finish)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Search logs across EVERY service in the local env at once — e.g. find a transactionId or an error string
|
|
37
|
+
// wherever it surfaced. `docker logs` is per-container, so we fan out over the compose project's containers
|
|
38
|
+
// and grep each. See ENVIRONMENT-PLAN.md.
|
|
39
|
+
|
|
40
|
+
// The compose project name docker derives from the run dir (leading dot + odd chars stripped, lowercased).
|
|
41
|
+
function projectName() {
|
|
42
|
+
return path
|
|
43
|
+
.basename(rt.RUN_DIR)
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9]/g, '')
|
|
46
|
+
.replace(/^dooertestenv$/, 'dooer-test-env') // basename ".dooer-test-env" → docker project "dooer-test-env"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Containers in this project (running by default; all with --all). Returns [{ name, short }].
|
|
50
|
+
function listContainers({ all }) {
|
|
51
|
+
const args = [
|
|
52
|
+
'ps',
|
|
53
|
+
all ? '-a' : null,
|
|
54
|
+
'--filter',
|
|
55
|
+
`label=com.docker.compose.project=${projectName()}`,
|
|
56
|
+
'--format',
|
|
57
|
+
'{{.Names}}',
|
|
58
|
+
].filter(Boolean)
|
|
59
|
+
const r = spawnSync('docker', args, { encoding: 'utf8' })
|
|
60
|
+
const names = (r.stdout || '')
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map((s) => s.trim())
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
const prefix = `${projectName()}-`
|
|
65
|
+
return names
|
|
66
|
+
.map((name) => ({ name, short: name.replace(prefix, '').replace(/-1$/, '') }))
|
|
67
|
+
.sort((a, b) => a.short.localeCompare(b.short))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
command: 'logs <pattern>',
|
|
72
|
+
aliases: ['search'],
|
|
73
|
+
describe: 'search logs across all local services for a pattern (e.g. a transactionId or error text)',
|
|
74
|
+
builder: (y) =>
|
|
75
|
+
y
|
|
76
|
+
.positional('pattern', { type: 'string', describe: 'JS regular expression to match against each log line' })
|
|
77
|
+
.option('since', {
|
|
78
|
+
type: 'string',
|
|
79
|
+
default: '5m',
|
|
80
|
+
describe: 'how far back to search (docker duration, e.g. 30m, 2h)',
|
|
81
|
+
})
|
|
82
|
+
.option('service', {
|
|
83
|
+
alias: 's',
|
|
84
|
+
type: 'string',
|
|
85
|
+
describe: 'only search services whose name contains this substring',
|
|
86
|
+
})
|
|
87
|
+
.option('ignore-case', { alias: 'i', type: 'boolean', describe: 'case-insensitive match' })
|
|
88
|
+
.option('all', { type: 'boolean', describe: 'include stopped containers too' })
|
|
89
|
+
.option('context', {
|
|
90
|
+
alias: 'C',
|
|
91
|
+
type: 'number',
|
|
92
|
+
default: 0,
|
|
93
|
+
describe: 'lines of context to print around each match',
|
|
94
|
+
}),
|
|
95
|
+
handler: async (argv) => {
|
|
96
|
+
let re
|
|
97
|
+
try {
|
|
98
|
+
re = new RegExp(argv.pattern, argv.ignoreCase ? 'i' : '')
|
|
99
|
+
} catch (e) {
|
|
100
|
+
throw new Error(`invalid pattern: ${e.message}`)
|
|
101
|
+
}
|
|
102
|
+
let containers = listContainers({ all: argv.all })
|
|
103
|
+
if (argv.service) containers = containers.filter((c) => c.short.includes(argv.service))
|
|
104
|
+
if (!containers.length) {
|
|
105
|
+
console.log(chalk.yellow('no matching local containers (is the env up?)'))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fetch every container's logs concurrently, then print in stable (sorted) container order.
|
|
110
|
+
const fetched = await Promise.all(containers.map((c) => fetchLogs(c.name, argv.since).then((out) => ({ c, out }))))
|
|
111
|
+
|
|
112
|
+
let totalMatches = 0
|
|
113
|
+
const hitServices = []
|
|
114
|
+
for (const { c, out } of fetched) {
|
|
115
|
+
const lines = out.split('\n')
|
|
116
|
+
const matchIdx = []
|
|
117
|
+
lines.forEach((line, i) => {
|
|
118
|
+
if (re.test(line)) matchIdx.push(i)
|
|
119
|
+
})
|
|
120
|
+
if (!matchIdx.length) continue
|
|
121
|
+
hitServices.push(c.short)
|
|
122
|
+
totalMatches += matchIdx.length
|
|
123
|
+
const printed = new Set()
|
|
124
|
+
matchIdx.forEach((i) => {
|
|
125
|
+
const lo = Math.max(0, i - argv.context)
|
|
126
|
+
const hi = Math.min(lines.length - 1, i + argv.context)
|
|
127
|
+
for (let j = lo; j <= hi; j++) {
|
|
128
|
+
if (printed.has(j)) continue
|
|
129
|
+
printed.add(j)
|
|
130
|
+
const tag = chalk.cyan(c.short.padEnd(28))
|
|
131
|
+
const text = lines[j]
|
|
132
|
+
console.log(`${tag} ${j === i ? text : chalk.gray(text)}`) // matched line plain, context greyed
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
console.log(
|
|
137
|
+
chalk.bold(
|
|
138
|
+
`\n${totalMatches} match(es) across ${hitServices.length} service(s)` +
|
|
139
|
+
(hitServices.length ? `: ${hitServices.join(', ')}` : '')
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { spawnSync } = require('child_process')
|
|
5
|
+
const chalk = require('chalk')
|
|
6
|
+
const rt = require('../runtime')
|
|
7
|
+
|
|
8
|
+
// Measure resource usage of the running env (CPU/RAM per container + totals + disk) so we can publish
|
|
9
|
+
// concrete dev-machine requirements (ENVIRONMENT-PLAN.md §6). Samples `docker stats` N times and reports
|
|
10
|
+
// the peak per container. Writes a Markdown report.
|
|
11
|
+
|
|
12
|
+
function docker(args) {
|
|
13
|
+
const r = spawnSync('docker', args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 })
|
|
14
|
+
return r.status === 0 ? r.stdout : ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// parse a memory string like "1.23GiB" / "512MiB" / "900KiB" → MiB
|
|
18
|
+
function toMiB(s) {
|
|
19
|
+
const m = /([\d.]+)\s*([KMGT]i?B)/i.exec(s || '')
|
|
20
|
+
if (!m) return 0
|
|
21
|
+
const v = parseFloat(m[1])
|
|
22
|
+
const u = m[2].toUpperCase()
|
|
23
|
+
const mult = { KIB: 1 / 1024, KB: 1 / 1024, MIB: 1, MB: 1, GIB: 1024, GB: 1024, TIB: 1024 * 1024, TB: 1024 * 1024 }
|
|
24
|
+
return v * (mult[u] || 1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
command: 'measure',
|
|
29
|
+
describe: 'sample docker stats for the running env and write a resource report (CPU/RAM/disk)',
|
|
30
|
+
builder: (y) =>
|
|
31
|
+
y
|
|
32
|
+
.option('samples', { type: 'number', default: 5, describe: 'number of stats samples' })
|
|
33
|
+
.option('interval', { type: 'number', default: 5, describe: 'seconds between samples' })
|
|
34
|
+
.option('out', { type: 'string', describe: 'report path (default: ~/.dooer-test-env/resource-report.md)' }),
|
|
35
|
+
handler: async (argv) => {
|
|
36
|
+
const peak = {} // name -> { cpu, memMiB }
|
|
37
|
+
for (let i = 0; i < argv.samples; i++) {
|
|
38
|
+
const raw = docker(['stats', '--no-stream', '--format', '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'])
|
|
39
|
+
raw
|
|
40
|
+
.split('\n')
|
|
41
|
+
.filter((l) => l.includes('dooer-test-env'))
|
|
42
|
+
.forEach((l) => {
|
|
43
|
+
const [name, cpu, mem] = l.split('\t')
|
|
44
|
+
const cpuN = parseFloat(cpu) || 0
|
|
45
|
+
const memN = toMiB((mem || '').split('/')[0])
|
|
46
|
+
if (!peak[name] || cpuN + memN > peak[name].cpu + peak[name].memMiB) peak[name] = { cpu: cpuN, memMiB: memN }
|
|
47
|
+
})
|
|
48
|
+
if (i < argv.samples - 1) await new Promise((r) => setTimeout(r, argv.interval * 1000))
|
|
49
|
+
process.stderr.write(`\rsampled ${i + 1}/${argv.samples}`)
|
|
50
|
+
}
|
|
51
|
+
process.stderr.write('\n')
|
|
52
|
+
|
|
53
|
+
const rows = Object.entries(peak)
|
|
54
|
+
.map(([name, p]) => ({
|
|
55
|
+
name: name.replace(/^dooer-test-env-/, '').replace(/-\d+$/, ''),
|
|
56
|
+
cpu: p.cpu,
|
|
57
|
+
memMiB: p.memMiB,
|
|
58
|
+
}))
|
|
59
|
+
.sort((a, b) => b.memMiB - a.memMiB)
|
|
60
|
+
const totCpu = rows.reduce((s, r) => s + r.cpu, 0)
|
|
61
|
+
const totMem = rows.reduce((s, r) => s + r.memMiB, 0)
|
|
62
|
+
const df = docker(['system', 'df'])
|
|
63
|
+
|
|
64
|
+
const gib = (m) => (m / 1024).toFixed(2)
|
|
65
|
+
const md =
|
|
66
|
+
`# dooer-test-env — resource usage\n\n` +
|
|
67
|
+
`Host: ${os.cpus().length} logical CPUs, ${(os.totalmem() / 1024 ** 3).toFixed(0)} GB RAM. ` +
|
|
68
|
+
`Samples: ${argv.samples} @ ${argv.interval}s (peak per container).\n\n` +
|
|
69
|
+
`**Containers:** ${rows.length} · **Total CPU:** ${totCpu.toFixed(0)}% (of ${os.cpus().length * 100}%) · ` +
|
|
70
|
+
`**Total RAM:** ${gib(totMem)} GiB\n\n` +
|
|
71
|
+
`| container | peak CPU % | peak RAM (MiB) |\n|---|---:|---:|\n` +
|
|
72
|
+
rows.map((r) => `| ${r.name} | ${r.cpu.toFixed(1)} | ${r.memMiB.toFixed(0)} |`).join('\n') +
|
|
73
|
+
`\n\n## docker system df\n\n\`\`\`\n${df}\`\`\`\n`
|
|
74
|
+
|
|
75
|
+
const out = argv.out || path.join(rt.RUN_DIR, 'resource-report.md')
|
|
76
|
+
rt.ensureRunDir()
|
|
77
|
+
fs.writeFileSync(out, md)
|
|
78
|
+
console.log(chalk.green(`\n${rows.length} containers · ${totCpu.toFixed(0)}% CPU · ${gib(totMem)} GiB RAM`))
|
|
79
|
+
console.log(`report: ${out}`)
|
|
80
|
+
},
|
|
81
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const yaml = require('js-yaml')
|
|
5
|
+
const chalk = require('chalk')
|
|
6
|
+
const rt = require('../runtime')
|
|
7
|
+
const discovery = require('../discovery/client')
|
|
8
|
+
const { imageFor } = require('../compose/manifests')
|
|
9
|
+
|
|
10
|
+
// Per-service control in a running env. `local`/`unlocal`/`deploy` update the discovery router so peers
|
|
11
|
+
// pick up the change with no restarts (see ENVIRONMENT-PLAN.md §4/§6). Local host-process ports are
|
|
12
|
+
// CLI-managed. All docker interaction goes through lib/runtime.
|
|
13
|
+
|
|
14
|
+
// read a service's environment from the generated compose file, rewriting container-DNS infra hosts to
|
|
15
|
+
// localhost so the same dev config works for a host process. Returns {} if the compose isn't generated.
|
|
16
|
+
function localEnv(name, port) {
|
|
17
|
+
let env = {}
|
|
18
|
+
try {
|
|
19
|
+
const doc = yaml.safeLoad(fs.readFileSync(rt.COMPOSE_FILE, 'utf8'))
|
|
20
|
+
env = { ...((doc.services[name] && doc.services[name].environment) || {}) }
|
|
21
|
+
} catch (_) {
|
|
22
|
+
/* compose not generated yet — fall back to the minimum below */
|
|
23
|
+
}
|
|
24
|
+
const rw = (v) =>
|
|
25
|
+
String(v)
|
|
26
|
+
.replace(/\bpostgres:/g, 'localhost:')
|
|
27
|
+
.replace(/(^|@)postgres\b/g, `$1localhost`)
|
|
28
|
+
.replace(/\bredis:/g, 'localhost:')
|
|
29
|
+
.replace(/\bdiscovery-router:/g, 'localhost:')
|
|
30
|
+
.replace(/\bminio:/g, 'localhost:')
|
|
31
|
+
.replace(/\bkafka:/g, 'localhost:')
|
|
32
|
+
for (const k of Object.keys(env)) env[k] = rw(env[k])
|
|
33
|
+
// guaranteed minimum wiring (also covers the no-compose fallback)
|
|
34
|
+
env.DOOER_SQL_HOST = 'localhost'
|
|
35
|
+
env.DOOER_SQL_PORT = env.DOOER_SQL_PORT || '5432'
|
|
36
|
+
env.DOOER_SQL_DATABASE = env.DOOER_SQL_DATABASE || 'dooer'
|
|
37
|
+
env.DOOER_IS_ONEPLATFORMER = 'false'
|
|
38
|
+
env.CONSUL_HTTP_ADDR = 'localhost:8500'
|
|
39
|
+
env.DOOER_MICROSERVICE_PORT = String(port)
|
|
40
|
+
env.DOOER_SERVICE_NAME = env.DOOER_SERVICE_NAME || name
|
|
41
|
+
env.DOOER_ENVIRONMENT_NAME = env.DOOER_ENVIRONMENT_NAME || 'local'
|
|
42
|
+
env.NODE_ENV = env.NODE_ENV || 'development'
|
|
43
|
+
return env
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function repoPath(name, given) {
|
|
47
|
+
return given || path.join(os.homedir(), 'dooer', name)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
command: 'service <command>',
|
|
52
|
+
describe: 'control a single service in the running env',
|
|
53
|
+
builder: (y) =>
|
|
54
|
+
y
|
|
55
|
+
.command({
|
|
56
|
+
command: 'start <name>',
|
|
57
|
+
describe: 'start a service container',
|
|
58
|
+
handler: (a) => {
|
|
59
|
+
process.exitCode = rt.dc(['up', '-d', a.name])
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
.command({
|
|
63
|
+
command: 'stop <name>',
|
|
64
|
+
describe: 'stop a service container',
|
|
65
|
+
handler: (a) => {
|
|
66
|
+
process.exitCode = rt.dc(['stop', a.name])
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
.command({
|
|
70
|
+
command: 'restart <name>',
|
|
71
|
+
describe: 'restart a service container',
|
|
72
|
+
handler: (a) => {
|
|
73
|
+
process.exitCode = rt.dc(['restart', a.name])
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
.command({
|
|
77
|
+
command: 'deploy <name> <version>',
|
|
78
|
+
describe: 'switch a service to a published registry version (upgrade/downgrade)',
|
|
79
|
+
handler: (a) => {
|
|
80
|
+
const base = imageFor(a.name, rt.SERVICES_DIR)
|
|
81
|
+
if (!base) throw new Error(`unknown service ${a.name} (no manifest image)`)
|
|
82
|
+
const image = `${base.replace(/:[^:/]+$/, '')}:${a.version}`
|
|
83
|
+
// merge into the override compose file
|
|
84
|
+
let ov = { services: {} }
|
|
85
|
+
if (fs.existsSync(rt.OVERRIDE_FILE))
|
|
86
|
+
ov = yaml.safeLoad(fs.readFileSync(rt.OVERRIDE_FILE, 'utf8')) || { services: {} }
|
|
87
|
+
ov.services = ov.services || {}
|
|
88
|
+
ov.services[a.name] = { image }
|
|
89
|
+
rt.ensureRunDir()
|
|
90
|
+
fs.writeFileSync(rt.OVERRIDE_FILE, yaml.safeDump(ov))
|
|
91
|
+
const st = rt.readState()
|
|
92
|
+
st.deploy[a.name] = image
|
|
93
|
+
rt.writeState(st)
|
|
94
|
+
console.log(chalk.gray(`pinned ${a.name} → ${image}`))
|
|
95
|
+
process.exitCode = rt.dc(['up', '-d', a.name])
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
.command({
|
|
99
|
+
command: 'local <name> [repoPath]',
|
|
100
|
+
describe: 'run a service from locally checked-out code instead of its image',
|
|
101
|
+
builder: (y2) =>
|
|
102
|
+
y2
|
|
103
|
+
.option('port', { type: 'number', describe: 'host port for the local process (default: auto)' })
|
|
104
|
+
.option('script', { type: 'string', default: 'start', describe: 'package.json script to run' }),
|
|
105
|
+
handler: async (a) => {
|
|
106
|
+
const cwd = repoPath(a.name, a.repoPath)
|
|
107
|
+
if (!fs.existsSync(path.join(cwd, 'package.json')))
|
|
108
|
+
throw new Error(`no package.json at ${cwd} (pass the repo path)`)
|
|
109
|
+
const port = a.port || 4000 + Math.floor((Date.now() % 1000) + Math.random() * 100) // CLI-managed
|
|
110
|
+
rt.dc(['stop', a.name]) // free the container so only the local process serves
|
|
111
|
+
const pid = rt.spawnDetached(a.name, 'yarn', [a.script], { cwd, env: localEnv(a.name, port) })
|
|
112
|
+
await discovery.setTarget(rt.ROUTER_URL, a.name, { address: 'host.docker.internal', port })
|
|
113
|
+
const st = rt.readState()
|
|
114
|
+
st.local[a.name] = { pid, port, cwd }
|
|
115
|
+
rt.writeState(st)
|
|
116
|
+
console.log(
|
|
117
|
+
chalk.green(
|
|
118
|
+
`${a.name} now runs from ${cwd} (pid ${pid}, port ${port}); router repointed. Logs: ${path.join(
|
|
119
|
+
rt.RUN_DIR,
|
|
120
|
+
`${a.name}.local.log`
|
|
121
|
+
)}`
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
.command({
|
|
127
|
+
command: 'unlocal <name>',
|
|
128
|
+
describe: 'switch a service back to its published image',
|
|
129
|
+
handler: async (a) => {
|
|
130
|
+
const st = rt.readState()
|
|
131
|
+
const info = st.local[a.name]
|
|
132
|
+
if (info && rt.isAlive(info.pid)) {
|
|
133
|
+
try {
|
|
134
|
+
process.kill(info.pid)
|
|
135
|
+
} catch (_) {
|
|
136
|
+
/* already gone */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
delete st.local[a.name]
|
|
140
|
+
rt.writeState(st)
|
|
141
|
+
await discovery.clearTarget(rt.ROUTER_URL, a.name).catch(() => {})
|
|
142
|
+
rt.dc(['up', '-d', a.name]) // bring the container back
|
|
143
|
+
console.log(chalk.green(`${a.name} back on its container image; router reverted.`))
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
.command({
|
|
147
|
+
command: 'version <name>',
|
|
148
|
+
describe: 'report whether a service runs a published image (+tag) or local code',
|
|
149
|
+
handler: (a) => {
|
|
150
|
+
const st = rt.readState()
|
|
151
|
+
if (st.local[a.name] && rt.isAlive(st.local[a.name].pid)) {
|
|
152
|
+
console.log(
|
|
153
|
+
`${a.name}: LOCAL code — ${st.local[a.name].cwd} (pid ${st.local[a.name].pid}, port ${
|
|
154
|
+
st.local[a.name].port
|
|
155
|
+
})`
|
|
156
|
+
)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const image = (st.deploy && st.deploy[a.name]) || imageFor(a.name, rt.SERVICES_DIR) || '(unknown)'
|
|
160
|
+
console.log(`${a.name}: published image — ${image}`)
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
.demandCommand(1)
|
|
164
|
+
.strict(),
|
|
165
|
+
handler: () => {},
|
|
166
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const { spawnSync } = require('child_process')
|
|
3
|
+
const chalk = require('chalk')
|
|
4
|
+
const rt = require('../runtime')
|
|
5
|
+
const registry = require('../registry')
|
|
6
|
+
const bankid = require('../bankid')
|
|
7
|
+
const { generateCompose } = require('../compose/generate')
|
|
8
|
+
|
|
9
|
+
// `setup` makes a machine ready to run the env, entirely through the CLI — no manual docker/kubectl steps.
|
|
10
|
+
// It checks prerequisites, logs docker into both private registries using the cluster's own pull secrets
|
|
11
|
+
// (prompting only if creds truly can't be found), generates the compose file, and can pre-pull images.
|
|
12
|
+
// See ENVIRONMENT-PLAN.md §6.
|
|
13
|
+
|
|
14
|
+
function ok(b) {
|
|
15
|
+
return b ? chalk.green('✓') : chalk.red('✗')
|
|
16
|
+
}
|
|
17
|
+
function check(cmd, args) {
|
|
18
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8' })
|
|
19
|
+
return r.status === 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
command: 'setup',
|
|
24
|
+
describe: 'make this machine ready: check prerequisites, log in to the registries, generate compose',
|
|
25
|
+
builder: (y) =>
|
|
26
|
+
y
|
|
27
|
+
.option('namespace', {
|
|
28
|
+
type: 'string',
|
|
29
|
+
default: 'dooer-staging',
|
|
30
|
+
describe: 'namespace to read pull secrets from',
|
|
31
|
+
})
|
|
32
|
+
.option('pull', { type: 'boolean', describe: 'also pre-pull all service images (large)' })
|
|
33
|
+
.option('profile', { type: 'string', default: 'full', describe: 'profile to pre-pull when --pull' }),
|
|
34
|
+
handler: async (argv) => {
|
|
35
|
+
console.log(chalk.bold('\ndooer-test-env setup\n'))
|
|
36
|
+
|
|
37
|
+
const dockerOk = registry.have('docker') && check('docker', ['info'])
|
|
38
|
+
console.log(`${ok(dockerOk)} docker (installed + daemon running)`)
|
|
39
|
+
const kubectlOk = registry.have('kubectl')
|
|
40
|
+
console.log(`${ok(kubectlOk)} kubectl installed`)
|
|
41
|
+
// k8s reachable + access: `get ns <ns>` needs the API server (VPN) AND RBAC, so it proves both at once.
|
|
42
|
+
const nsOk = kubectlOk && check('kubectl', ['get', 'ns', argv.namespace, '--request-timeout=10s'])
|
|
43
|
+
console.log(`${ok(nsOk)} k8s cluster reachable + access to namespace ${argv.namespace} (needs VPN)`)
|
|
44
|
+
const secretsOk = nsOk && check('kubectl', ['auth', 'can-i', 'get', 'secrets', '-n', argv.namespace])
|
|
45
|
+
console.log(`${ok(secretsOk)} can read secrets (registry pull secrets + DB creds) in ${argv.namespace}`)
|
|
46
|
+
const dbOk =
|
|
47
|
+
nsOk && check('kubectl', ['get', 'svc', 'dooer-database', '-n', argv.namespace, '--request-timeout=10s'])
|
|
48
|
+
console.log(`${ok(dbOk)} can reach the dooer-database service in ${argv.namespace}`)
|
|
49
|
+
if (!dockerOk) throw new Error('docker is required (install Docker + start the daemon).')
|
|
50
|
+
if (!kubectlOk) throw new Error('kubectl is required.')
|
|
51
|
+
if (!nsOk)
|
|
52
|
+
throw new Error(
|
|
53
|
+
`cannot reach/access namespace ${argv.namespace} — connect the VPN and check your kubectl context/RBAC.`
|
|
54
|
+
)
|
|
55
|
+
if (!secretsOk) throw new Error(`no access to read secrets in ${argv.namespace} — needed for registry + DB creds.`)
|
|
56
|
+
|
|
57
|
+
// registry login (the part that used to be manual)
|
|
58
|
+
console.log('\nLogging in to the private registries…')
|
|
59
|
+
const logins = await registry.loginAll({ namespace: argv.namespace })
|
|
60
|
+
for (const l of logins) console.log(` ${ok(l.ok)} ${l.host} (${l.via})`)
|
|
61
|
+
if (logins.some((l) => !l.ok)) throw new Error('one or more registry logins failed')
|
|
62
|
+
|
|
63
|
+
// generate the compose file
|
|
64
|
+
rt.ensureRunDir()
|
|
65
|
+
const r = generateCompose({ servicesDir: rt.SERVICES_DIR, out: rt.COMPOSE_FILE })
|
|
66
|
+
console.log(`\n${ok(true)} generated ${rt.COMPOSE_FILE} (${(r && r.serviceCount) || '?'} services)`)
|
|
67
|
+
|
|
68
|
+
// BankID certs → keychain, so local login matches staging (production BankID). Idempotent; only pulls
|
|
69
|
+
// if not already stored. Non-fatal — the env still runs (test-mode BankID) without them.
|
|
70
|
+
if (bankid.status().every((s) => !s.present)) {
|
|
71
|
+
try {
|
|
72
|
+
const stored = bankid.pullAndStore({ namespace: argv.namespace })
|
|
73
|
+
console.log(`${ok(true)} BankID: pulled ${stored.length} secret(s) from staging → keychain`)
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.log(`${chalk.yellow('!')} BankID: skipped (${e.message}); local login stays in test mode`)
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.log(`${ok(true)} BankID: certs already in keychain`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (argv.pull) {
|
|
82
|
+
console.log(`\nPre-pulling images (profile: ${argv.profile})… this is large.`)
|
|
83
|
+
const code = rt.dc(['pull'], { profile: argv.profile })
|
|
84
|
+
console.log(`${ok(code === 0)} docker compose pull`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(
|
|
88
|
+
chalk.green('\nsetup complete.') + ' Next: `dooer-test-env up` then `dooer-test-env db pull --execute`.'
|
|
89
|
+
)
|
|
90
|
+
if (!fs.existsSync(rt.COMPOSE_FILE)) process.exitCode = 1
|
|
91
|
+
},
|
|
92
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { Client } = require('pg')
|
|
2
|
+
const chalk = require('chalk')
|
|
3
|
+
const { shred } = require('../shred')
|
|
4
|
+
const { auditPii } = require('../shred/audit')
|
|
5
|
+
|
|
6
|
+
// Anonymize PII in the LOCAL Postgres (the shredder rules). DEV-FACING shred is LOCALHOST-ONLY — it has
|
|
7
|
+
// no option to target a k8s DB (the in-cluster base build is the only context that shreds a non-local
|
|
8
|
+
// scratch DB). See ENVIRONMENT-PLAN.md §5 (Safety rails) / §9.7.
|
|
9
|
+
const LOCAL_HOSTS = ['127.0.0.1', 'localhost', '::1']
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
command: 'shred',
|
|
13
|
+
describe: 'anonymize PII in the LOCAL Postgres (localhost only; emails always anonymized)',
|
|
14
|
+
builder: (y) =>
|
|
15
|
+
y
|
|
16
|
+
.option('host', { type: 'string', default: '127.0.0.1', describe: 'DB host — must be localhost' })
|
|
17
|
+
.option('port', { type: 'number', default: 55432 })
|
|
18
|
+
.option('database', { type: 'string', default: 'dooer' })
|
|
19
|
+
.option('user', { type: 'string', default: 'dooer' })
|
|
20
|
+
.option('password', { type: 'string', default: 'dooer' })
|
|
21
|
+
.option('audit-only', { type: 'boolean', describe: 'only run the PII audit (no changes)' })
|
|
22
|
+
.option('execute', { type: 'boolean', default: false, describe: 'actually write (default: dry-run)' }),
|
|
23
|
+
handler: async (argv) => {
|
|
24
|
+
if (!LOCAL_HOSTS.includes(argv.host)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`shred is localhost-only; refusing host "${argv.host}". (Only the in-cluster base build may shred a scratch DB.)`
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
const client = new Client({
|
|
30
|
+
host: argv.host,
|
|
31
|
+
port: argv.port,
|
|
32
|
+
database: argv.database,
|
|
33
|
+
user: argv.user,
|
|
34
|
+
password: argv.password,
|
|
35
|
+
})
|
|
36
|
+
await client.connect()
|
|
37
|
+
try {
|
|
38
|
+
if (argv.auditOnly) {
|
|
39
|
+
const a = await auditPii(client)
|
|
40
|
+
console.log(`PII audit: scanned ${a.scanned}, covered ${a.covered}, uncovered ${a.uncovered.length}`)
|
|
41
|
+
a.uncovered.forEach((u) => console.log(` UNCOVERED ${u.category}: ${u.schema}.${u.table}.${u.column}`))
|
|
42
|
+
process.exitCode = a.uncovered.length ? 1 : 0
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const res = await shred(client, { execute: argv.execute, verbose: true })
|
|
46
|
+
const a = await auditPii(client)
|
|
47
|
+
console.log(`\nPII audit: scanned ${a.scanned}, covered ${a.covered}, uncovered ${a.uncovered.length}`)
|
|
48
|
+
a.uncovered.forEach((u) =>
|
|
49
|
+
console.log(` ${chalk.red('UNCOVERED')} ${u.category}: ${u.schema}.${u.table}.${u.column}`)
|
|
50
|
+
)
|
|
51
|
+
if (!argv.execute) console.log(chalk.gray('\n(dry-run — nothing written; re-run with --execute)'))
|
|
52
|
+
if (res && res.failed && res.failed.length) {
|
|
53
|
+
console.log(chalk.yellow(`\n${res.failed.length} script(s) had errors (schema drift?):`))
|
|
54
|
+
res.failed.forEach((f) => console.log(` ${f.script}: ${f.error}`))
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
await client.end()
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# `lib/compose` — docker-compose generator
|
|
2
|
+
|
|
3
|
+
Translates the s-e032 (staging) k8s manifests into a docker-compose spec that runs
|
|
4
|
+
the whole Dooer backend locally, wired together. This is the Phase-2 generator from
|
|
5
|
+
`ENVIRONMENT-PLAN.md` §6.
|
|
6
|
+
|
|
7
|
+
## Files
|
|
8
|
+
|
|
9
|
+
- `manifests.js` — reads `service-*.yaml` Deployments. `listServices(dir)` →
|
|
10
|
+
`[{ name, image, env, secretEnv, port }]` (sorted, deterministic);
|
|
11
|
+
`imageFor(name, dir)` → the verbatim image ref for one service.
|
|
12
|
+
- `generate.js` — `generateCompose({ profile, servicesDir, out })` builds the
|
|
13
|
+
compose object, writes YAML to `out`, and returns a summary
|
|
14
|
+
(`{ compose, yaml, out, dooerServiceCount, infraServiceCount, bookingProfileServices }`).
|
|
15
|
+
|
|
16
|
+
## What maps to what
|
|
17
|
+
|
|
18
|
+
Each `service-*.yaml` is a multi-doc manifest (Deployment + Service, sometimes an
|
|
19
|
+
ObjectBucketClaim / CronJob / RBAC). We read the **Deployment's** `node` container:
|
|
20
|
+
|
|
21
|
+
- **image** — taken **verbatim**. The fleet uses **two registries**
|
|
22
|
+
(`143848169226.dkr.ecr.eu-west-1.amazonaws.com` and `docker.roboten.com`); the
|
|
23
|
+
manifest already names the right one, so we never rewrite it.
|
|
24
|
+
- **env** — plain `value` vars are reproduced as-is; `valueFrom` (secret) vars are
|
|
25
|
+
replaced with dev values (see below). Then the local overrides are applied last.
|
|
26
|
+
- Files with **no Deployment** (the e2e-nightly and token CronJobs) are skipped —
|
|
27
|
+
they are not runnable services. That is why the generated file has ~73 dooer
|
|
28
|
+
services from 75 `service-*.yaml` files.
|
|
29
|
+
|
|
30
|
+
### Env overrides (applied to every service)
|
|
31
|
+
|
|
32
|
+
| var | local value | why |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `DOOER_SQL_HOST` | `postgres` | local Postgres container |
|
|
35
|
+
| `DOOER_SQL_PORT` / `DOOER_SQL_DATABASE` | `5432` / `dooer` | |
|
|
36
|
+
| `DOOER_SQL_APPLICATION_USER` / `_PASSWORD` | `dooer` / `dooer` | static-password mode |
|
|
37
|
+
| `DOOER_SQL_MIGRATION_USER` / `_PASSWORD` | `dooer` / `dooer` | `CMD-OPF.sh` runs `db-migrate` on boot |
|
|
38
|
+
| `DOOER_IS_ONEPLATFORMER` | `false` | static-password (local) path |
|
|
39
|
+
| `CONSUL_HTTP_ADDR` | `discovery-router:8500` | local mutable discovery router |
|
|
40
|
+
| `DOOER_CONSUL_HOST` | `discovery-router` | (where present) |
|
|
41
|
+
| `DOOER_HOST_REDIS` | `redis:6379` | plain redis, no sentinel |
|
|
42
|
+
| `DOOER_REDIS_USE_TLS` | `false` | |
|
|
43
|
+
| `DOOER_KAFKA_BOOTSTRAP_BROKER` | `kafka:9092` | local Redpanda broker |
|
|
44
|
+
| `DOOER_STREAM_NAME` | `local-s01` | |
|
|
45
|
+
| `AWS_ENDPOINT_URL_S3` | `http://minio:9000` | local MinIO |
|
|
46
|
+
| `DOOER_MICROSERVICE_PORT` | `3000` | |
|
|
47
|
+
| `NODE_ENV` / `DOOER_ENVIRONMENT_NAME` | `development` / `local` | |
|
|
48
|
+
|
|
49
|
+
**Dropped:** `DOOER_REDIS_SENTINEL_MASTER` (a plain redis container has no sentinel).
|
|
50
|
+
|
|
51
|
+
**Secret (`valueFrom`) vars** become dev values: `*ACCESS_KEY_ID` / `*SECRET_ACCESS_KEY`
|
|
52
|
+
→ the MinIO creds (`minioadmin`/`minioadmin`); SQL passwords → `dooer`;
|
|
53
|
+
`SECRET_DOOER_REDIS_PASSWORD` → empty. Everything else (LLM API keys, the service
|
|
54
|
+
token, …) becomes a compose passthrough `${NAME:-}` so a dev can `export NAME=…`
|
|
55
|
+
without editing the file. **Bucket names are kept from the manifest** (e.g.
|
|
56
|
+
`file-storage-bucket`) — create them in MinIO before use (the `db pull` / copy tool,
|
|
57
|
+
or `mc mb`).
|
|
58
|
+
|
|
59
|
+
`depends_on` is `[postgres, redis, discovery-router]` for every dooer service, per
|
|
60
|
+
the plan. Host ports are **not** published for the dooer services (all listen on
|
|
61
|
+
3000 and would clash); reach them through the discovery router. Infra containers
|
|
62
|
+
publish their ports (5432 / 6379 / 9092 / 9000 / 9001 / 8500).
|
|
63
|
+
|
|
64
|
+
## Infra image choices
|
|
65
|
+
|
|
66
|
+
| container | image | note |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| postgres | `postgres:14` | matches the Zalando PG14 in `database-cluster.yaml` |
|
|
69
|
+
| redis | `redis:7-alpine` | single node, no auth/sentinel locally |
|
|
70
|
+
| **kafka** | `docker.redpanda.com/redpandadata/redpanda:v24.1.7` | **single-node Redpanda** — Kafka-API compatible, **no ZooKeeper**, far lighter on a laptop than Strimzi Kafka+ZK. Advertises as `kafka:9092`. |
|
|
71
|
+
| **minio** | `minio/minio:latest` | S3-compatible; `minioadmin`/`minioadmin`; console on 9001 |
|
|
72
|
+
| discovery-router | `build: ../discovery-router` | repo-local image (owned by another agent); port 8500 |
|
|
73
|
+
|
|
74
|
+
## Profiles — how the "all" vs "booking" selection works
|
|
75
|
+
|
|
76
|
+
Docker Compose profiles are **additive**: a service with no `profiles:` always
|
|
77
|
+
starts, and activating a profile only *adds* services — it cannot *exclude* them.
|
|
78
|
+
So a single file cannot make a bare `docker compose up` start **all** services
|
|
79
|
+
while a `booking` profile starts only a **subset**. We therefore:
|
|
80
|
+
|
|
81
|
+
- put **all** dooer services in the `full` profile,
|
|
82
|
+
- **also** put the 10 booking services in the `booking` profile,
|
|
83
|
+
- leave the **infra** containers with no profile (always up).
|
|
84
|
+
|
|
85
|
+
The CLI's `up` (default) activates `full` (`docker compose --profile full up` /
|
|
86
|
+
`COMPOSE_PROFILES=full`) → **everything**. `up --profile booking`
|
|
87
|
+
(`docker compose --profile booking up`) → the 10 booking services + infra. This is
|
|
88
|
+
exactly the ENVIRONMENT-PLAN model (`up [--profile <name>]`, default = everything).
|
|
89
|
+
|
|
90
|
+
`generateCompose({ profile })` also accepts a `profile` to *filter which services
|
|
91
|
+
are written* into a smaller file, but the profile keys in the full file are the
|
|
92
|
+
runtime mechanism.
|
|
93
|
+
|
|
94
|
+
## Dependency
|
|
95
|
+
|
|
96
|
+
`generate.js` / `manifests.js` `require('js-yaml')`. It is currently only a
|
|
97
|
+
**transitive** dependency — **add `js-yaml` to `package.json` `dependencies`**
|
|
98
|
+
(tested against `js-yaml@3.15.2`, whose `safeLoadAll` / `safeDump` API this uses).
|