create-interview-cockpit 0.17.3 → 0.18.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.
@@ -1,4 +1,5 @@
1
1
  import type { InfraLabWorkspace } from "./types";
2
+ import { ENTERPRISE_LOCAL_AUTH_LAB } from "./enterpriseLocalLab";
2
3
 
3
4
  const DEFAULT_INFRA_FILES: Record<string, string> = {
4
5
  "provider.tf": `terraform {
@@ -39,15 +40,370 @@ export const DEFAULT_INFRA_LAB: InfraLabWorkspace = {
39
40
  files: DEFAULT_INFRA_FILES,
40
41
  };
41
42
 
43
+ export const DOCKER_DEEP_DIVE_LAB: InfraLabWorkspace = {
44
+ version: 1,
45
+ label: "Docker Deep Dive Lab",
46
+ provider: "docker",
47
+ executionMode: "docker",
48
+ activeFile: "compose.yaml",
49
+ files: {
50
+ "README.md": `# Docker Deep Dive Lab
51
+
52
+ This lab is a small production-shaped Docker system:
53
+
54
+ - **api** is a Node/Express service built from the local Dockerfile.
55
+ - **redis** is an off-the-shelf image with a named volume.
56
+ - **worker** reuses the same image as the api but runs a different command.
57
+ - **labnet** demonstrates Compose service discovery: the api connects to Redis at \`redis:6379\`.
58
+ - **port 4288** publishes the api from the container to your host browser.
59
+
60
+ ## First run
61
+
62
+ Run these in the Console tab, one command at a time:
63
+
64
+ 1. \`docker version\` — confirm the Docker client and engine can talk.
65
+ 2. \`docker compose up -d --build\` — build the api image and start all services.
66
+ 3. \`docker compose ps\` — compare services, containers, health, and published ports.
67
+ 4. \`curl -s http://localhost:4288/api/ping\` — hit the container through the host port.
68
+ 5. \`docker images\` — find the image Compose built from this Dockerfile.
69
+ 6. \`docker logs docker-deep-dive-api\` — read stdout/stderr from the api container.
70
+ 7. \`docker compose exec -T api node -e "console.log(process.env.REDIS_URL)"\` — run a process inside the container.
71
+ 8. \`docker volume ls\` — notice the Redis data volume.
72
+ 9. \`docker compose down\` — stop/remove containers and the lab network.
73
+ 10. \`docker compose down -v\` — also remove the Redis volume when you want a clean slate.
74
+
75
+ The console intentionally disables shell pipes/operators. Use direct commands instead of chaining.
76
+
77
+ ## Experiments that teach the deep parts
78
+
79
+ - Change \`LAB_MESSAGE\` in compose.yaml, run \`docker compose up -d --build\`, then hit \`/api/ping\`.
80
+ - Change the Dockerfile order and rebuild to see which layers are cached.
81
+ - Add an environment variable to the worker only, then compare \`docker inspect docker-deep-dive-api\` and \`docker inspect docker-deep-dive-worker\`.
82
+ - Change the host port from \`4288:3000\` to another port and observe how host networking differs from container networking.
83
+ - Run \`docker compose down\`, start again, and verify Redis data survives because the named volume remains.
84
+ - Run \`docker compose down -v\`, start again, and verify the Redis data resets.
85
+ `,
86
+ ".dockerignore": `node_modules
87
+ npm-debug.log
88
+ .git
89
+ .env
90
+ coverage
91
+ dist
92
+ `,
93
+ Dockerfile: `# syntax=docker/dockerfile:1.7
94
+
95
+ # Senior/interview insight: copy dependency manifests before source files so
96
+ # Docker can reuse the dependency layer when only application code changes.
97
+ FROM node:20-alpine AS deps
98
+ WORKDIR /app
99
+ COPY package*.json ./
100
+ RUN npm install --omit=dev
101
+
102
+ # Senior/interview insight: keep runtime images small and run as a non-root
103
+ # user so a compromised app has less power inside the container.
104
+ FROM node:20-alpine AS runtime
105
+ ENV NODE_ENV=production
106
+ ENV PORT=3000
107
+ WORKDIR /app
108
+ RUN addgroup -S nodeapp && adduser -S nodeapp -G nodeapp
109
+ COPY --from=deps /app/node_modules ./node_modules
110
+ COPY package*.json ./
111
+ COPY src ./src
112
+ USER nodeapp
113
+ EXPOSE 3000
114
+ HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/api/ping').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
115
+ CMD ["node", "src/server.js"]
116
+ `,
117
+ "compose.yaml": `name: interview-cockpit-docker-lab
118
+
119
+ services:
120
+ api:
121
+ build:
122
+ context: .
123
+ dockerfile: Dockerfile
124
+ image: interview-cockpit/docker-deep-dive-api:local
125
+ container_name: docker-deep-dive-api
126
+ ports:
127
+ - "4288:3000"
128
+ environment:
129
+ PORT: "3000"
130
+ REDIS_URL: redis://redis:6379
131
+ LAB_MESSAGE: "Edit me in compose.yaml, rebuild, then hit /api/ping"
132
+ depends_on:
133
+ redis:
134
+ condition: service_healthy
135
+ healthcheck:
136
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/ping').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
137
+ interval: 10s
138
+ timeout: 3s
139
+ retries: 5
140
+ networks:
141
+ - labnet
142
+
143
+ worker:
144
+ image: interview-cockpit/docker-deep-dive-api:local
145
+ container_name: docker-deep-dive-worker
146
+ command: ["node", "src/worker.js"]
147
+ environment:
148
+ REDIS_URL: redis://redis:6379
149
+ WORKER_NAME: compose-worker
150
+ depends_on:
151
+ redis:
152
+ condition: service_healthy
153
+ api:
154
+ condition: service_started
155
+ networks:
156
+ - labnet
157
+
158
+ redis:
159
+ image: redis:7-alpine
160
+ container_name: docker-deep-dive-redis
161
+ command: ["redis-server", "--appendonly", "yes"]
162
+ volumes:
163
+ - redis-data:/data
164
+ healthcheck:
165
+ test: ["CMD", "redis-cli", "ping"]
166
+ interval: 5s
167
+ timeout: 3s
168
+ retries: 10
169
+ networks:
170
+ - labnet
171
+
172
+ volumes:
173
+ redis-data:
174
+
175
+ networks:
176
+ labnet:
177
+ `,
178
+ "package.json": `{
179
+ "name": "docker-deep-dive-api",
180
+ "version": "1.0.0",
181
+ "private": true,
182
+ "type": "module",
183
+ "scripts": {
184
+ "start": "node src/server.js",
185
+ "worker": "node src/worker.js"
186
+ },
187
+ "dependencies": {
188
+ "@redis/client": "^1.6.1",
189
+ "cors": "^2.8.5",
190
+ "express": "^4.18.3"
191
+ }
192
+ }
193
+ `,
194
+ "src/server.js": `import os from "node:os";
195
+ import express from "express";
196
+ import cors from "cors";
197
+ import { createClient } from "@redis/client";
198
+
199
+ const app = express();
200
+ const port = Number(process.env.PORT || 3000);
201
+ const startedAt = new Date();
202
+ let redis;
203
+ let redisReady = false;
204
+
205
+ app.use(cors());
206
+ app.use(express.json());
207
+
208
+ async function getRedis() {
209
+ if (redisReady) return redis;
210
+ if (!redis) {
211
+ redis = createClient({ url: process.env.REDIS_URL });
212
+ redis.on("error", (error) => {
213
+ redisReady = false;
214
+ console.error("redis error", error.message);
215
+ });
216
+ }
217
+ if (!redis.isOpen) await redis.connect();
218
+ redisReady = true;
219
+ return redis;
220
+ }
221
+
222
+ async function readRedis(key) {
223
+ try {
224
+ const client = await getRedis();
225
+ return { ok: true, value: await client.get(key) };
226
+ } catch (error) {
227
+ return { ok: false, error: error.message };
228
+ }
229
+ }
230
+
231
+ app.get("/", (_req, res) => {
232
+ res.type("html").send(\`<!doctype html>
233
+ <html>
234
+ <head>
235
+ <meta charset="utf-8" />
236
+ <title>Docker Deep Dive API</title>
237
+ <style>
238
+ body { margin: 0; font-family: ui-sans-serif, system-ui; background: #020617; color: #dbeafe; }
239
+ main { max-width: 860px; margin: 0 auto; padding: 40px 24px; }
240
+ button, a { border: 1px solid #155e75; border-radius: 999px; background: #083344; color: #a5f3fc; padding: 10px 14px; text-decoration: none; cursor: pointer; }
241
+ pre { background: #0f172a; border: 1px solid #1e293b; border-radius: 16px; padding: 16px; overflow: auto; }
242
+ .grid { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0; }
243
+ .muted { color: #94a3b8; }
244
+ </style>
245
+ </head>
246
+ <body>
247
+ <main>
248
+ <p class="muted">Container hostname: \${os.hostname()}</p>
249
+ <h1>Docker Deep Dive API</h1>
250
+ <p>This page is served from inside the api container and published to your host on port 4288.</p>
251
+ <div class="grid">
252
+ <button onclick="hit('/api/ping')">GET /api/ping</button>
253
+ <button onclick="hit('/api/redis/increment/browser')">Increment Redis counter</button>
254
+ <button onclick="setCache()">Set cache value</button>
255
+ <button onclick="hit('/api/cache/browser-demo')">Read cache value</button>
256
+ </div>
257
+ <pre id="out">Click a button to call the API.</pre>
258
+ </main>
259
+ <script>
260
+ async function hit(path, init) {
261
+ const res = await fetch(path, init);
262
+ document.querySelector('#out').textContent = JSON.stringify(await res.json(), null, 2);
263
+ }
264
+ function setCache() {
265
+ return hit('/api/cache/browser-demo', {
266
+ method: 'POST',
267
+ headers: { 'content-type': 'application/json' },
268
+ body: JSON.stringify({ value: 'saved from the browser at ' + new Date().toISOString() })
269
+ });
270
+ }
271
+ </script>
272
+ </body>
273
+ </html>\`);
274
+ });
275
+
276
+ app.get("/api/ping", async (_req, res) => {
277
+ let hits = null;
278
+ let redisError = null;
279
+ try {
280
+ const client = await getRedis();
281
+ hits = await client.incr("api:pings");
282
+ } catch (error) {
283
+ redisError = error.message;
284
+ }
285
+
286
+ res.json({
287
+ ok: true,
288
+ message: process.env.LAB_MESSAGE || "hello from inside the container",
289
+ container: {
290
+ hostname: os.hostname(),
291
+ pid: process.pid,
292
+ uptimeSeconds: Math.round(process.uptime()),
293
+ startedAt: startedAt.toISOString(),
294
+ },
295
+ environment: {
296
+ NODE_ENV: process.env.NODE_ENV,
297
+ PORT: process.env.PORT,
298
+ REDIS_URL: process.env.REDIS_URL,
299
+ },
300
+ redis: {
301
+ connected: redisReady,
302
+ pingCount: hits,
303
+ error: redisError,
304
+ },
305
+ });
306
+ });
307
+
308
+ app.get("/api/cache/:key", async (req, res) => {
309
+ const result = await readRedis(\`cache:\${req.params.key}\`);
310
+ res.status(result.ok ? 200 : 503).json(result);
311
+ });
312
+
313
+ app.post("/api/cache/:key", async (req, res) => {
314
+ try {
315
+ const client = await getRedis();
316
+ const value = req.body?.value ?? \`stored at \${new Date().toISOString()}\`;
317
+ await client.set(\`cache:\${req.params.key}\`, String(value));
318
+ res.json({ ok: true, key: req.params.key, value });
319
+ } catch (error) {
320
+ res.status(503).json({ ok: false, error: error.message });
321
+ }
322
+ });
323
+
324
+ app.delete("/api/cache/:key", async (req, res) => {
325
+ try {
326
+ const client = await getRedis();
327
+ const deleted = await client.del(\`cache:\${req.params.key}\`);
328
+ res.json({ ok: true, key: req.params.key, deleted });
329
+ } catch (error) {
330
+ res.status(503).json({ ok: false, error: error.message });
331
+ }
332
+ });
333
+
334
+ app.get("/api/redis/increment/:key", async (req, res) => {
335
+ try {
336
+ const client = await getRedis();
337
+ const value = await client.incr(\`counter:\${req.params.key}\`);
338
+ res.json({ ok: true, key: req.params.key, value });
339
+ } catch (error) {
340
+ res.status(503).json({ ok: false, error: error.message });
341
+ }
342
+ });
343
+
344
+ app.get("/api/headers", (req, res) => {
345
+ res.json({ ok: true, headers: req.headers });
346
+ });
347
+
348
+ app.listen(port, "0.0.0.0", () => {
349
+ console.log(\`api listening on 0.0.0.0:\${port}\`);
350
+ });
351
+ `,
352
+ "src/worker.js": `import { createClient } from "@redis/client";
353
+
354
+ const redisUrl = process.env.REDIS_URL;
355
+ const workerName = process.env.WORKER_NAME || "worker";
356
+ const client = createClient({ url: redisUrl });
357
+
358
+ client.on("error", (error) => {
359
+ console.error(\`[\${workerName}] redis error\`, error.message);
360
+ });
361
+
362
+ await client.connect();
363
+ console.log(\`[\${workerName}] connected to \${redisUrl}\`);
364
+
365
+ setInterval(async () => {
366
+ const ticks = await client.incr("worker:ticks");
367
+ console.log(\`[\${workerName}] tick \${ticks}\`);
368
+ }, 5000);
369
+ `,
370
+ },
371
+ };
372
+
373
+ function refreshLegacyEnterpriseAuthFiles(
374
+ source: InfraLabWorkspace,
375
+ files: Record<string, string>,
376
+ ): Record<string, string> {
377
+ const main = files["main.tf"] ?? "";
378
+ const isLegacyDockerProviderBuild =
379
+ source.label === ENTERPRISE_LOCAL_AUTH_LAB.label &&
380
+ source.provider === "docker" &&
381
+ main.includes('resource "docker_image" "claims_api"') &&
382
+ main.includes('resource "docker_image" "bff"') &&
383
+ main.includes("docker_image.bff.image_id");
384
+
385
+ if (!isLegacyDockerProviderBuild) return files;
386
+
387
+ return {
388
+ ...files,
389
+ "README.md":
390
+ ENTERPRISE_LOCAL_AUTH_LAB.files["README.md"] ?? files["README.md"],
391
+ "variables.tf":
392
+ ENTERPRISE_LOCAL_AUTH_LAB.files["variables.tf"] ?? files["variables.tf"],
393
+ "main.tf": ENTERPRISE_LOCAL_AUTH_LAB.files["main.tf"] ?? files["main.tf"],
394
+ };
395
+ }
396
+
42
397
  export function cloneInfraLabWorkspace(
43
398
  workspace?: InfraLabWorkspace | null,
44
399
  ): InfraLabWorkspace {
45
400
  const source = workspace ?? DEFAULT_INFRA_LAB;
46
401
  // Only seed defaults when the source has no files of its own
47
- const files =
402
+ const sourceFiles =
48
403
  source.files && Object.keys(source.files).length > 0
49
404
  ? { ...source.files }
50
405
  : { ...DEFAULT_INFRA_FILES };
406
+ const files = refreshLegacyEnterpriseAuthFiles(source, sourceFiles);
51
407
  const activeFile = files[source.activeFile]
52
408
  ? source.activeFile
53
409
  : (Object.keys(files)[0] ?? "main.tf");
@@ -55,9 +411,13 @@ export function cloneInfraLabWorkspace(
55
411
  return {
56
412
  version: 1,
57
413
  label: source.label || DEFAULT_INFRA_LAB.label,
58
- provider: "aws",
414
+ provider: source.provider === "docker" ? "docker" : "aws",
59
415
  executionMode:
60
- source.executionMode === "plan-only" ? "plan-only" : "localstack",
416
+ source.executionMode === "docker"
417
+ ? "docker"
418
+ : source.executionMode === "plan-only"
419
+ ? "plan-only"
420
+ : "localstack",
61
421
  activeFile,
62
422
  files,
63
423
  };
@@ -65,13 +425,21 @@ export function cloneInfraLabWorkspace(
65
425
 
66
426
  export function getInfraLabFileOrder(workspace: InfraLabWorkspace): string[] {
67
427
  const preferred = [
428
+ "README.md",
429
+ "compose.yaml",
430
+ "docker-compose.yml",
431
+ "docker-compose.yaml",
432
+ "Dockerfile",
433
+ ".dockerignore",
434
+ "package.json",
435
+ "src/server.js",
436
+ "src/worker.js",
68
437
  "main.tf",
69
438
  "provider.tf",
70
439
  "variables.tf",
71
440
  "terraform.tfvars",
72
441
  "outputs.tf",
73
442
  "locals.tf",
74
- "README.md",
75
443
  ];
76
444
  const extras = Object.keys(workspace.files)
77
445
  .filter((name) => !preferred.includes(name))
@@ -109,9 +477,13 @@ export function parseInfraLabWorkspace(raw: string): InfraLabWorkspace | null {
109
477
  typeof parsed.label === "string" && parsed.label.trim()
110
478
  ? parsed.label.trim()
111
479
  : DEFAULT_INFRA_LAB.label,
112
- provider: "aws",
480
+ provider: parsed.provider === "docker" ? "docker" : "aws",
113
481
  executionMode:
114
- parsed.executionMode === "plan-only" ? "plan-only" : "localstack",
482
+ parsed.executionMode === "docker"
483
+ ? "docker"
484
+ : parsed.executionMode === "plan-only"
485
+ ? "plan-only"
486
+ : "localstack",
115
487
  activeFile:
116
488
  typeof parsed.activeFile === "string"
117
489
  ? parsed.activeFile