@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.
Files changed (60) hide show
  1. package/bin/index.js +7 -0
  2. package/discovery-router/Dockerfile +18 -0
  3. package/discovery-router/README.md +99 -0
  4. package/discovery-router/package.json +13 -0
  5. package/discovery-router/registry.example.json +5 -0
  6. package/discovery-router/server.js +272 -0
  7. package/lib/account.js +120 -0
  8. package/lib/auth-dev-keys.js +12 -0
  9. package/lib/bankid.js +130 -0
  10. package/lib/cli.js +27 -0
  11. package/lib/command/bankid.js +45 -0
  12. package/lib/command/customer.js +108 -0
  13. package/lib/command/db.js +156 -0
  14. package/lib/command/env.js +114 -0
  15. package/lib/command/logs.js +143 -0
  16. package/lib/command/measure.js +81 -0
  17. package/lib/command/service.js +166 -0
  18. package/lib/command/setup.js +92 -0
  19. package/lib/command/shred.js +60 -0
  20. package/lib/compose/README.md +98 -0
  21. package/lib/compose/generate.js +375 -0
  22. package/lib/compose/manifests.js +108 -0
  23. package/lib/db/roles.js +118 -0
  24. package/lib/discovery/client.js +40 -0
  25. package/lib/engine/GUIDE.md +176 -0
  26. package/lib/engine/PROCESS.md +571 -0
  27. package/lib/engine/dbbuild.js +325 -0
  28. package/lib/engine/gen-schema-map.js +479 -0
  29. package/lib/engine/purge.js +137 -0
  30. package/lib/engine/schema-map.json +11016 -0
  31. package/lib/engine/seed.js +1045 -0
  32. package/lib/obc.js +72 -0
  33. package/lib/registry.js +123 -0
  34. package/lib/runtime.js +101 -0
  35. package/lib/service-token.js +40 -0
  36. package/lib/shred/README.md +118 -0
  37. package/lib/shred/audit.js +128 -0
  38. package/lib/shred/faker.js +545 -0
  39. package/lib/shred/index.js +126 -0
  40. package/lib/shred/scripts/base-partner-emails.sql +9 -0
  41. package/lib/shred/scripts/dev-accounts.sql +195 -0
  42. package/lib/shred/scripts/emails.sql +48 -0
  43. package/lib/shred/scripts/institution-browser.sql +3 -0
  44. package/lib/shred/scripts/notification-targets.sql +5 -0
  45. package/lib/shred/scripts/partners.sql +2 -0
  46. package/lib/shred/scripts/passwords.sql +8 -0
  47. package/lib/shred/scripts/personal-numbers.sql +177 -0
  48. package/lib/shred/scripts/phone-numbers.sql +22 -0
  49. package/lib/shred/scripts/salary-spec-reports.sql +5 -0
  50. package/lib/shred/scripts/service-activity-tracker-data.sql +4 -0
  51. package/lib/shred/scripts/service-core-objects.sql +19 -0
  52. package/lib/shred/scripts/service-event-stream.sql +2 -0
  53. package/lib/shred/scripts/service-integrations.sql +4 -0
  54. package/lib/shred/scripts/template.sql +4 -0
  55. package/lib/shred/scripts/x-service-billing.sql +34 -0
  56. package/lib/shred/scripts/xxx-history-tables.sql +25 -0
  57. package/lib/stub.js +8 -0
  58. package/local-postgres/Dockerfile +11 -0
  59. package/package.json +46 -0
  60. 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).