create-interview-cockpit 0.23.2 → 0.24.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-interview-cockpit",
3
- "version": "0.23.2",
3
+ "version": "0.24.0",
4
4
  "description": "Scaffold a personal AI-powered interview prep cockpit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,12 @@
1
1
  import { useState } from "react";
2
2
  import { useStore } from "../store";
3
- import { DOCKER_DEEP_DIVE_LAB, parseInfraLabWorkspace } from "../infraLab";
3
+ import {
4
+ AZURE_NETWORK_ACL_BLANK_TEMPLATE,
5
+ AZURE_NETWORK_ACL_DOCKER_LAB,
6
+ DOCKER_DEEP_DIVE_LAB,
7
+ POSTGRES_DOCKER_PROVIDER_LAB,
8
+ parseInfraLabWorkspace,
9
+ } from "../infraLab";
4
10
  import {
5
11
  DEFAULT_GHA_LAB,
6
12
  parseGhaLabWorkspace,
@@ -641,6 +647,24 @@ export default function LabsPanel() {
641
647
  "Dockerfile + Compose + Node API + Redis, with command-line practice",
642
648
  onClick: () => openInfraLab(DOCKER_DEEP_DIVE_LAB),
643
649
  },
650
+ {
651
+ label: "Postgres DB in Docker",
652
+ description:
653
+ "Terraform Docker provider deploys a local PostgreSQL database container",
654
+ onClick: () => openInfraLab(POSTGRES_DOCKER_PROVIDER_LAB),
655
+ },
656
+ {
657
+ label: "Azure ACL Failure (Docker)",
658
+ description:
659
+ "Reproduce stale Azure subnet allowlists locally with Terraform + Docker mocks",
660
+ onClick: () => openInfraLab(AZURE_NETWORK_ACL_DOCKER_LAB),
661
+ },
662
+ {
663
+ label: "Azure ACL Blank Template",
664
+ description:
665
+ "Same Azure-style Terraform/Docker file layout, but every file starts empty",
666
+ onClick: () => openInfraLab(AZURE_NETWORK_ACL_BLANK_TEMPLATE),
667
+ },
644
668
  {
645
669
  label: "Enterprise BFF Docker Stack",
646
670
  description:
@@ -370,6 +370,662 @@ setInterval(async () => {
370
370
  },
371
371
  };
372
372
 
373
+ export const POSTGRES_DOCKER_PROVIDER_LAB: InfraLabWorkspace = {
374
+ version: 1,
375
+ label: "Postgres Docker Provider Lab",
376
+ provider: "docker",
377
+ executionMode: "docker",
378
+ activeFile: "README.md",
379
+ files: {
380
+ "README.md": `# Postgres Docker Provider Lab
381
+
382
+ This lab is a clearer Terraform + Docker mental model:
383
+
384
+ 1. Terraform uses the Docker provider.
385
+ 2. Terraform pulls the Postgres Docker image.
386
+ 3. Terraform creates a Docker volume for database files.
387
+ 4. Terraform creates a running Postgres container during \`terraform apply\`.
388
+ 5. Terraform removes the container during \`terraform destroy\`.
389
+
390
+ In this lab, Docker is the local infrastructure platform and Postgres is the deployed infrastructure.
391
+
392
+ ## Prerequisites
393
+
394
+ - Docker Desktop / Docker Engine must already be running.
395
+ - The first \`terraform init\` downloads the Docker Terraform provider.
396
+
397
+ ## Run it
398
+
399
+ Use the Console tab one command at a time:
400
+
401
+ 1. \`terraform init\`
402
+ 2. \`terraform plan -out=tfplan\`
403
+ 3. \`terraform apply -auto-approve\`
404
+ 4. \`terraform output\`
405
+ 5. \`docker ps\`
406
+ 6. \`docker exec terraform-postgres-db pg_isready -U appuser -d appdb\`
407
+ 7. \`docker exec -e PGPASSWORD=local-dev-password terraform-postgres-db psql -U appuser -d appdb -c "select current_database(), current_user;"\`
408
+
409
+ ## Create a table
410
+
411
+ After apply succeeds, try these one at a time:
412
+
413
+ 1. \`docker exec -e PGPASSWORD=local-dev-password terraform-postgres-db psql -U appuser -d appdb -c "create table if not exists notes (id serial primary key, body text not null);"\`
414
+ 2. \`docker exec -e PGPASSWORD=local-dev-password terraform-postgres-db psql -U appuser -d appdb -c "insert into notes (body) values ('hello from terraform-managed postgres');"\`
415
+ 3. \`docker exec -e PGPASSWORD=local-dev-password terraform-postgres-db psql -U appuser -d appdb -c "select * from notes;"\`
416
+
417
+ ## Change something
418
+
419
+ Edit \`variables.tf\`, for example change \`host_port\` from \`5433\` to \`5434\`, then run:
420
+
421
+ 1. \`terraform plan -out=tfplan\`
422
+ 2. \`terraform apply -auto-approve\`
423
+ 3. \`terraform output\`
424
+
425
+ ## Clean up
426
+
427
+ Run:
428
+
429
+ \`terraform destroy -auto-approve\`
430
+
431
+ The named Docker volume is also Terraform-managed, so destroying the lab removes the database data volume too.
432
+
433
+ ## Mental model
434
+
435
+ This is IaC [Infrastructure as Code] managing local Docker resources:
436
+
437
+ - \`provider.tf\` tells Terraform how to talk to Docker.
438
+ - \`main.tf\` declares Docker resources: image, volume, container.
439
+ - \`terraform apply\` makes Docker match that desired state.
440
+ - \`outputs.tf\` prints useful connection details.
441
+
442
+ So this is not "Docker deployed to Docker". It is Terraform deploying a Postgres database container into local Docker.
443
+ `,
444
+ "provider.tf": `terraform {
445
+ required_version = ">= 1.5.0"
446
+
447
+ required_providers {
448
+ docker = {
449
+ source = "kreuzwerker/docker"
450
+ version = "~> 3.0"
451
+ }
452
+ }
453
+ }
454
+
455
+ provider "docker" {}
456
+ `,
457
+ "variables.tf": `variable "container_name" {
458
+ description = "Name for the local Postgres Docker container."
459
+ type = string
460
+ default = "terraform-postgres-db"
461
+ }
462
+
463
+ variable "host_port" {
464
+ description = "Host port published to your machine for Postgres connections."
465
+ type = number
466
+ default = 5433
467
+ }
468
+
469
+ variable "postgres_version" {
470
+ description = "Postgres Docker image version."
471
+ type = string
472
+ default = "16-alpine"
473
+ }
474
+
475
+ variable "database_name" {
476
+ description = "Database created by the Postgres image on first startup."
477
+ type = string
478
+ default = "appdb"
479
+ }
480
+
481
+ variable "database_user" {
482
+ description = "Database user created by the Postgres image on first startup."
483
+ type = string
484
+ default = "appuser"
485
+ }
486
+
487
+ variable "database_password" {
488
+ description = "Local development password for the database user."
489
+ type = string
490
+ default = "local-dev-password"
491
+ sensitive = true
492
+ }
493
+ `,
494
+ "main.tf": `resource "docker_image" "postgres" {
495
+ name = "postgres:\${var.postgres_version}"
496
+ keep_locally = true
497
+ }
498
+
499
+ resource "docker_volume" "postgres_data" {
500
+ name = "\${var.container_name}-data"
501
+ }
502
+
503
+ resource "docker_container" "postgres" {
504
+ name = var.container_name
505
+ image = docker_image.postgres.image_id
506
+ restart = "unless-stopped"
507
+
508
+ env = [
509
+ "POSTGRES_DB=\${var.database_name}",
510
+ "POSTGRES_USER=\${var.database_user}",
511
+ "POSTGRES_PASSWORD=\${var.database_password}",
512
+ "PGDATA=/var/lib/postgresql/data/pgdata"
513
+ ]
514
+
515
+ ports {
516
+ internal = 5432
517
+ external = var.host_port
518
+ }
519
+
520
+ volumes {
521
+ volume_name = docker_volume.postgres_data.name
522
+ container_path = "/var/lib/postgresql/data"
523
+ }
524
+
525
+ healthcheck {
526
+ test = ["CMD-SHELL", "pg_isready -U \${var.database_user} -d \${var.database_name}"]
527
+ interval = "5s"
528
+ timeout = "3s"
529
+ start_period = "10s"
530
+ retries = 10
531
+ }
532
+ }
533
+ `,
534
+ "outputs.tf": `output "container_name" {
535
+ value = docker_container.postgres.name
536
+ }
537
+
538
+ output "host" {
539
+ value = "localhost"
540
+ }
541
+
542
+ output "port" {
543
+ value = var.host_port
544
+ }
545
+
546
+ output "database_name" {
547
+ value = var.database_name
548
+ }
549
+
550
+ output "database_user" {
551
+ value = var.database_user
552
+ }
553
+
554
+ output "docker_exec_psql" {
555
+ value = "docker exec -e PGPASSWORD=local-dev-password \${docker_container.postgres.name} psql -U \${var.database_user} -d \${var.database_name} -c 'select current_database(), current_user;'"
556
+ }
557
+
558
+ output "connection_string" {
559
+ value = "postgresql://\${var.database_user}:\${var.database_password}@localhost:\${var.host_port}/\${var.database_name}"
560
+ sensitive = true
561
+ }
562
+ `,
563
+ },
564
+ };
565
+
566
+ const AZURE_NETWORK_ACL_FILE_NAMES = [
567
+ "README.md",
568
+ "provider.tf",
569
+ "variables.tf",
570
+ "main.tf",
571
+ "outputs.tf",
572
+ "tfvars/dev.wu3.tfvars",
573
+ "modules/keyvault/main.tf",
574
+ "modules/storage-account/main.tf",
575
+ "modules/eventhub/main.tf",
576
+ "apps/azure-resource-mock/Dockerfile",
577
+ "apps/azure-resource-mock/server.js",
578
+ ];
579
+
580
+ export const AZURE_NETWORK_ACL_DOCKER_LAB: InfraLabWorkspace = {
581
+ version: 1,
582
+ label: "Azure Network ACL Failure Lab",
583
+ provider: "docker",
584
+ executionMode: "docker",
585
+ activeFile: "README.md",
586
+ files: {
587
+ "README.md": `# Azure Network ACL Failure Lab — Local Docker Edition
588
+
589
+ This lab recreates a common Azure Terraform failure without requiring an Azure subscription.
590
+
591
+ Terraform still has the same shape as the real system:
592
+
593
+ 1. **tfvars** provides environment-specific values.
594
+ 2. **variables.tf** declares the inputs.
595
+ 3. **main.tf** wires those inputs into modules.
596
+ 4. Each module applies network lock-down rules from the same \`virtual_networks_allowed\` list.
597
+ 5. Docker containers stand in for Azure Key Vault, Storage Account, and Event Hub.
598
+
599
+ The lab starts broken on purpose. The DEV deployment subscription is:
600
+
601
+ - \`d05d3a9b-68eb-4dea-9f1c-208dc705ce2d\`
602
+
603
+ But the subnet allowlist in \`tfvars/dev.wu3.tfvars\` points at the old platform networking subscription:
604
+
605
+ - \`e2cbae49-43fe-49c9-b9e5-fdba61ccb575\`
606
+
607
+ That reproduces the real bug chain:
608
+
609
+ > New DEV resources are being deployed, but the security whitelist still points at old DEV networking.
610
+
611
+ ## Run the failure
612
+
613
+ Use the Console tab one command at a time:
614
+
615
+ 1. \`terraform init\`
616
+ 2. \`terraform plan -var-file=tfvars/dev.wu3.tfvars -out=tfplan\`
617
+
618
+ Terraform should fail before it starts the mock containers. That is intentional.
619
+
620
+ ## Trace the value path
621
+
622
+ Follow this order:
623
+
624
+ 1. Open \`tfvars/dev.wu3.tfvars\` and find \`virtual_networks_allowed\`.
625
+ 2. Open \`variables.tf\` and find \`variable "virtual_networks_allowed"\`.
626
+ 3. Open \`main.tf\` and see that value passed into:
627
+ - \`module "keyvault"\`
628
+ - \`module "storage_account"\`
629
+ - \`module "eventhub"\`
630
+ 4. Open each module and find where the value becomes network rules:
631
+ - \`modules/keyvault/main.tf\` → \`network_acls\`
632
+ - \`modules/storage-account/main.tf\` → \`network_rules\`
633
+ - \`modules/eventhub/main.tf\` → \`network_rulesets\`
634
+
635
+ ## Fix it
636
+
637
+ In \`tfvars/dev.wu3.tfvars\`, replace the old subnet subscription/resource group with the current DEV networking values:
638
+
639
+ - subscription: \`d05d3a9b-68eb-4dea-9f1c-208dc705ce2d\`
640
+ - resource group: \`plfdev02shrwu3-networking\`
641
+
642
+ The fixed subnet IDs should look like:
643
+
644
+ \`/subscriptions/d05d3a9b-68eb-4dea-9f1c-208dc705ce2d/resourceGroups/plfdev02shrwu3-networking/providers/Microsoft.Network/virtualNetworks/plfdev02-vnet/subnets/app\`
645
+
646
+ Then rerun:
647
+
648
+ 1. \`terraform plan -var-file=tfvars/dev.wu3.tfvars -out=tfplan\`
649
+ 2. \`terraform apply -auto-approve -var-file=tfvars/dev.wu3.tfvars\`
650
+ 3. \`terraform output\`
651
+ 4. \`curl -s http://localhost:4311/health\`
652
+ 5. \`curl -s http://localhost:4312/health\`
653
+ 6. \`curl -s http://localhost:4313/health\`
654
+
655
+ ## Clean up
656
+
657
+ Run:
658
+
659
+ \`terraform destroy -auto-approve -var-file=tfvars/dev.wu3.tfvars\`
660
+
661
+ If you want to inspect Docker directly:
662
+
663
+ - \`docker ps -a\`
664
+ - \`docker logs azure-acl-lab-keyvault\`
665
+ - \`docker logs azure-acl-lab-storage\`
666
+ - \`docker logs azure-acl-lab-eventhub\`
667
+
668
+ ## Interview summary
669
+
670
+ The bug is not that Key Vault, Storage, or Event Hub cannot be created. The bug is that all three modules consume the same stale \`virtual_networks_allowed\` value, so all three try to apply firewall rules for subnets Azure cannot resolve anymore.
671
+ `,
672
+ "provider.tf": `terraform {
673
+ required_version = ">= 1.5.0"
674
+ }
675
+ `,
676
+ "variables.tf": `variable "name_prefix" {
677
+ description = "Prefix used for local Docker containers and image names."
678
+ type = string
679
+ default = "azure-acl-lab"
680
+ }
681
+
682
+ variable "location" {
683
+ description = "Azure region label used by the mock resources."
684
+ type = string
685
+ default = "westus3"
686
+ }
687
+
688
+ variable "az_subscription_id" {
689
+ description = "Current DEV subscription receiving the deployment."
690
+ type = string
691
+ default = "d05d3a9b-68eb-4dea-9f1c-208dc705ce2d"
692
+ }
693
+
694
+ variable "expected_network_resource_group" {
695
+ description = "Networking resource group that should own DEV subnets now."
696
+ type = string
697
+ default = "plfdev02shrwu3-networking"
698
+ }
699
+
700
+ variable "virtual_networks_allowed" {
701
+ description = "Subnet IDs trusted by Key Vault, Storage, and Event Hub firewalls."
702
+ type = list(string)
703
+
704
+ # Broken on purpose. The tfvars file mirrors the same old subnet IDs.
705
+ default = [
706
+ "/subscriptions/e2cbae49-43fe-49c9-b9e5-fdba61ccb575/resourceGroups/plfdev01shrwu3-networking/providers/Microsoft.Network/virtualNetworks/plfdev01-vnet/subnets/app",
707
+ "/subscriptions/e2cbae49-43fe-49c9-b9e5-fdba61ccb575/resourceGroups/plfdev01shrwu3-networking/providers/Microsoft.Network/virtualNetworks/plfdev01-vnet/subnets/private-endpoints"
708
+ ]
709
+ }
710
+ `,
711
+ "main.tf": `locals {
712
+ mock_image = "\${var.name_prefix}/azure-resource-mock:local"
713
+ }
714
+
715
+ # This one Docker image stands in for Azure platform services. The modules below
716
+ # run the same image with different RESOURCE_KIND values so you can inspect
717
+ # Key Vault, Storage Account, and Event Hub independently.
718
+ resource "terraform_data" "mock_image" {
719
+ triggers_replace = {
720
+ dockerfile = filesha1("\${path.module}/apps/azure-resource-mock/Dockerfile")
721
+ server = filesha1("\${path.module}/apps/azure-resource-mock/server.js")
722
+ }
723
+
724
+ provisioner "local-exec" {
725
+ command = "docker build -t \${local.mock_image} apps/azure-resource-mock"
726
+ }
727
+ }
728
+
729
+ module "keyvault" {
730
+ source = "./modules/keyvault"
731
+
732
+ resource_name = "\${var.name_prefix}-kv"
733
+ container_name = "\${var.name_prefix}-keyvault"
734
+ host_port = 4311
735
+ image_name = local.mock_image
736
+ current_subscription_id = var.az_subscription_id
737
+ expected_network_subscription_id = var.az_subscription_id
738
+ expected_network_resource_group = var.expected_network_resource_group
739
+ virtual_networks_allowed = var.virtual_networks_allowed
740
+
741
+ depends_on = [terraform_data.mock_image]
742
+ }
743
+
744
+ module "storage_account" {
745
+ source = "./modules/storage-account"
746
+
747
+ resource_name = replace("\${var.name_prefix}storage", "-", "")
748
+ container_name = "\${var.name_prefix}-storage"
749
+ host_port = 4312
750
+ image_name = local.mock_image
751
+ current_subscription_id = var.az_subscription_id
752
+ expected_network_subscription_id = var.az_subscription_id
753
+ expected_network_resource_group = var.expected_network_resource_group
754
+ virtual_networks_allowed = var.virtual_networks_allowed
755
+
756
+ depends_on = [terraform_data.mock_image]
757
+ }
758
+
759
+ module "eventhub" {
760
+ source = "./modules/eventhub"
761
+
762
+ resource_name = "\${var.name_prefix}-eh"
763
+ container_name = "\${var.name_prefix}-eventhub"
764
+ host_port = 4313
765
+ image_name = local.mock_image
766
+ current_subscription_id = var.az_subscription_id
767
+ expected_network_subscription_id = var.az_subscription_id
768
+ expected_network_resource_group = var.expected_network_resource_group
769
+ virtual_networks_allowed = var.virtual_networks_allowed
770
+
771
+ depends_on = [terraform_data.mock_image]
772
+ }
773
+ `,
774
+ "outputs.tf": `output "keyvault_url" {
775
+ value = module.keyvault.url
776
+ }
777
+
778
+ output "storage_account_url" {
779
+ value = module.storage_account.url
780
+ }
781
+
782
+ output "eventhub_url" {
783
+ value = module.eventhub.url
784
+ }
785
+
786
+ output "debug_chain" {
787
+ value = {
788
+ current_subscription = var.az_subscription_id
789
+ expected_network_rg = var.expected_network_resource_group
790
+ virtual_networks_allowed = var.virtual_networks_allowed
791
+ shared_failure_explainer = "All three modules consume virtual_networks_allowed, so one stale subnet list breaks every resource firewall."
792
+ }
793
+ }
794
+ `,
795
+ "tfvars/dev.wu3.tfvars": `# DEV deploys into the new subscription.
796
+ az_subscription_id = "d05d3a9b-68eb-4dea-9f1c-208dc705ce2d"
797
+ location = "westus3"
798
+
799
+ # DEV networking moved. The modules expect subnet IDs from this resource group.
800
+ expected_network_resource_group = "plfdev02shrwu3-networking"
801
+
802
+ # Broken on purpose: these still point at the old platform networking subscription
803
+ # and old networking resource group.
804
+ virtual_networks_allowed = [
805
+ "/subscriptions/e2cbae49-43fe-49c9-b9e5-fdba61ccb575/resourceGroups/plfdev01shrwu3-networking/providers/Microsoft.Network/virtualNetworks/plfdev01-vnet/subnets/app",
806
+ "/subscriptions/e2cbae49-43fe-49c9-b9e5-fdba61ccb575/resourceGroups/plfdev01shrwu3-networking/providers/Microsoft.Network/virtualNetworks/plfdev01-vnet/subnets/private-endpoints"
807
+ ]
808
+ `,
809
+ "modules/keyvault/main.tf": `variable "resource_name" { type = string }
810
+ variable "container_name" { type = string }
811
+ variable "host_port" { type = number }
812
+ variable "image_name" { type = string }
813
+ variable "current_subscription_id" { type = string }
814
+ variable "expected_network_subscription_id" { type = string }
815
+ variable "expected_network_resource_group" { type = string }
816
+ variable "virtual_networks_allowed" { type = list(string) }
817
+
818
+ # Mirrors azurerm_key_vault.network_acls.virtual_network_subnet_ids.
819
+ resource "terraform_data" "network_acls" {
820
+ input = var.virtual_networks_allowed
821
+
822
+ lifecycle {
823
+ precondition {
824
+ condition = alltrue([
825
+ for subnet_id in var.virtual_networks_allowed :
826
+ can(regex("^/subscriptions/\${var.expected_network_subscription_id}/resourceGroups/\${var.expected_network_resource_group}/providers/Microsoft.Network/virtualNetworks/.+/subnets/.+$", subnet_id))
827
+ ])
828
+ error_message = "Key Vault network_acls rejected virtual_networks_allowed. Expected subnet IDs from subscription \${var.expected_network_subscription_id} and resource group \${var.expected_network_resource_group}."
829
+ }
830
+ }
831
+ }
832
+
833
+ resource "terraform_data" "keyvault_mock" {
834
+ input = {
835
+ container_name = var.container_name
836
+ url = "http://localhost:\${var.host_port}"
837
+ }
838
+
839
+ provisioner "local-exec" {
840
+ command = "docker rm -f \${var.container_name} >/dev/null 2>&1 || true && docker run -d --name \${var.container_name} -p \${var.host_port}:8080 -e RESOURCE_KIND=KeyVault -e RESOURCE_NAME=\${var.resource_name} -e CURRENT_SUBSCRIPTION_ID=\${var.current_subscription_id} -e ALLOWED_SUBNET_IDS='\${join(",", var.virtual_networks_allowed)}' \${var.image_name}"
841
+ }
842
+
843
+ provisioner "local-exec" {
844
+ when = destroy
845
+ command = "docker rm -f \${self.input.container_name} >/dev/null 2>&1 || true"
846
+ }
847
+
848
+ depends_on = [terraform_data.network_acls]
849
+ }
850
+
851
+ output "url" {
852
+ value = terraform_data.keyvault_mock.input.url
853
+ }
854
+ `,
855
+ "modules/storage-account/main.tf": `variable "resource_name" { type = string }
856
+ variable "container_name" { type = string }
857
+ variable "host_port" { type = number }
858
+ variable "image_name" { type = string }
859
+ variable "current_subscription_id" { type = string }
860
+ variable "expected_network_subscription_id" { type = string }
861
+ variable "expected_network_resource_group" { type = string }
862
+ variable "virtual_networks_allowed" { type = list(string) }
863
+
864
+ # Mirrors azurerm_storage_account.network_rules.virtual_network_subnet_ids.
865
+ resource "terraform_data" "network_rules" {
866
+ input = var.virtual_networks_allowed
867
+
868
+ lifecycle {
869
+ precondition {
870
+ condition = alltrue([
871
+ for subnet_id in var.virtual_networks_allowed :
872
+ can(regex("^/subscriptions/\${var.expected_network_subscription_id}/resourceGroups/\${var.expected_network_resource_group}/providers/Microsoft.Network/virtualNetworks/.+/subnets/.+$", subnet_id))
873
+ ])
874
+ error_message = "Storage Account network_rules rejected virtual_networks_allowed. Expected subnet IDs from subscription \${var.expected_network_subscription_id} and resource group \${var.expected_network_resource_group}."
875
+ }
876
+ }
877
+ }
878
+
879
+ resource "terraform_data" "storage_mock" {
880
+ input = {
881
+ container_name = var.container_name
882
+ url = "http://localhost:\${var.host_port}"
883
+ }
884
+
885
+ provisioner "local-exec" {
886
+ command = "docker rm -f \${var.container_name} >/dev/null 2>&1 || true && docker run -d --name \${var.container_name} -p \${var.host_port}:8080 -e RESOURCE_KIND=StorageAccount -e RESOURCE_NAME=\${var.resource_name} -e CURRENT_SUBSCRIPTION_ID=\${var.current_subscription_id} -e ALLOWED_SUBNET_IDS='\${join(",", var.virtual_networks_allowed)}' \${var.image_name}"
887
+ }
888
+
889
+ provisioner "local-exec" {
890
+ when = destroy
891
+ command = "docker rm -f \${self.input.container_name} >/dev/null 2>&1 || true"
892
+ }
893
+
894
+ depends_on = [terraform_data.network_rules]
895
+ }
896
+
897
+ output "url" {
898
+ value = terraform_data.storage_mock.input.url
899
+ }
900
+ `,
901
+ "modules/eventhub/main.tf": `variable "resource_name" { type = string }
902
+ variable "container_name" { type = string }
903
+ variable "host_port" { type = number }
904
+ variable "image_name" { type = string }
905
+ variable "current_subscription_id" { type = string }
906
+ variable "expected_network_subscription_id" { type = string }
907
+ variable "expected_network_resource_group" { type = string }
908
+ variable "virtual_networks_allowed" { type = list(string) }
909
+
910
+ locals {
911
+ virtual_network_rule = [
912
+ for subnet_id in var.virtual_networks_allowed : {
913
+ subnet_id = subnet_id
914
+ ignore_missing_virtual_network_service_endpoint = false
915
+ }
916
+ ]
917
+ }
918
+
919
+ # Mirrors azurerm_eventhub_namespace.network_rulesets.virtual_network_rule.
920
+ resource "terraform_data" "network_rulesets" {
921
+ input = local.virtual_network_rule
922
+
923
+ lifecycle {
924
+ precondition {
925
+ condition = alltrue([
926
+ for rule in local.virtual_network_rule :
927
+ can(regex("^/subscriptions/\${var.expected_network_subscription_id}/resourceGroups/\${var.expected_network_resource_group}/providers/Microsoft.Network/virtualNetworks/.+/subnets/.+$", rule.subnet_id))
928
+ ])
929
+ error_message = "Event Hub network_rulesets rejected virtual_networks_allowed. Expected subnet IDs from subscription \${var.expected_network_subscription_id} and resource group \${var.expected_network_resource_group}."
930
+ }
931
+ }
932
+ }
933
+
934
+ resource "terraform_data" "eventhub_mock" {
935
+ input = {
936
+ container_name = var.container_name
937
+ url = "http://localhost:\${var.host_port}"
938
+ }
939
+
940
+ provisioner "local-exec" {
941
+ command = "docker rm -f \${var.container_name} >/dev/null 2>&1 || true && docker run -d --name \${var.container_name} -p \${var.host_port}:8080 -e RESOURCE_KIND=EventHub -e RESOURCE_NAME=\${var.resource_name} -e CURRENT_SUBSCRIPTION_ID=\${var.current_subscription_id} -e ALLOWED_SUBNET_IDS='\${join(",", var.virtual_networks_allowed)}' \${var.image_name}"
942
+ }
943
+
944
+ provisioner "local-exec" {
945
+ when = destroy
946
+ command = "docker rm -f \${self.input.container_name} >/dev/null 2>&1 || true"
947
+ }
948
+
949
+ depends_on = [terraform_data.network_rulesets]
950
+ }
951
+
952
+ output "url" {
953
+ value = terraform_data.eventhub_mock.input.url
954
+ }
955
+ `,
956
+ "apps/azure-resource-mock/Dockerfile": `FROM node:20-alpine
957
+ WORKDIR /app
958
+ COPY server.js ./server.js
959
+ EXPOSE 8080
960
+ CMD ["node", "server.js"]
961
+ `,
962
+ "apps/azure-resource-mock/server.js": `import http from "node:http";
963
+
964
+ const port = Number(process.env.PORT || 8080);
965
+ const resourceKind = process.env.RESOURCE_KIND || "AzureResource";
966
+ const resourceName = process.env.RESOURCE_NAME || "local-resource";
967
+ const subscriptionId = process.env.CURRENT_SUBSCRIPTION_ID || "unknown";
968
+ const allowedSubnets = (process.env.ALLOWED_SUBNET_IDS || "")
969
+ .split(",")
970
+ .map((value) => value.trim())
971
+ .filter(Boolean);
972
+
973
+ function json(res, statusCode, body) {
974
+ res.writeHead(statusCode, { "content-type": "application/json" });
975
+ res.end(JSON.stringify(body, null, 2));
976
+ }
977
+
978
+ function payload(path) {
979
+ return {
980
+ ok: true,
981
+ path,
982
+ resourceKind,
983
+ resourceName,
984
+ subscriptionId,
985
+ networkLockdown: {
986
+ defaultAction: "Deny",
987
+ allowedSubnets,
988
+ teachingPoint:
989
+ "In Azure this list is used by network_acls, network_rules, or network_rulesets. If any subnet ID is stale, the lock-down update fails."
990
+ }
991
+ };
992
+ }
993
+
994
+ const server = http.createServer((req, res) => {
995
+ const path = new URL(req.url || "/", "http://localhost").pathname;
996
+ if (path === "/" || path === "/health") return json(res, 200, payload(path));
997
+ if (path === "/secret") return json(res, 200, { ...payload(path), secretName: "demo-connection-string" });
998
+ if (path === "/blob") return json(res, 200, { ...payload(path), container: "solution-designs" });
999
+ if (path === "/events") return json(res, 200, { ...payload(path), eventHub: "solution-builder-events" });
1000
+ return json(res, 404, { ok: false, error: "Not found", path });
1001
+ });
1002
+
1003
+ server.listen(port, "0.0.0.0", () => {
1004
+ console.log(resourceKind + " mock listening on 0.0.0.0:" + port);
1005
+ console.log("Allowed subnets:", allowedSubnets);
1006
+ });
1007
+ `,
1008
+ },
1009
+ };
1010
+
1011
+ export const AZURE_NETWORK_ACL_BLANK_TEMPLATE: InfraLabWorkspace = {
1012
+ version: 1,
1013
+ label: "Azure Network ACL Blank Template",
1014
+ provider: "docker",
1015
+ executionMode: "docker",
1016
+ activeFile: "README.md",
1017
+ files: Object.fromEntries(
1018
+ AZURE_NETWORK_ACL_FILE_NAMES.map((fileName) => [fileName, ""]),
1019
+ ),
1020
+ };
1021
+
1022
+ function hasWorkspaceFile(
1023
+ files: Record<string, string>,
1024
+ fileName: string,
1025
+ ): boolean {
1026
+ return Object.prototype.hasOwnProperty.call(files, fileName);
1027
+ }
1028
+
373
1029
  function refreshLegacyEnterpriseAuthFiles(
374
1030
  source: InfraLabWorkspace,
375
1031
  files: Record<string, string>,
@@ -404,7 +1060,7 @@ export function cloneInfraLabWorkspace(
404
1060
  ? { ...source.files }
405
1061
  : { ...DEFAULT_INFRA_FILES };
406
1062
  const files = refreshLegacyEnterpriseAuthFiles(source, sourceFiles);
407
- const activeFile = files[source.activeFile]
1063
+ const activeFile = hasWorkspaceFile(files, source.activeFile)
408
1064
  ? source.activeFile
409
1065
  : (Object.keys(files)[0] ?? "main.tf");
410
1066
 
@@ -438,14 +1094,22 @@ export function getInfraLabFileOrder(workspace: InfraLabWorkspace): string[] {
438
1094
  "provider.tf",
439
1095
  "variables.tf",
440
1096
  "terraform.tfvars",
1097
+ "tfvars/dev.wu3.tfvars",
441
1098
  "outputs.tf",
442
1099
  "locals.tf",
1100
+ "modules/keyvault/main.tf",
1101
+ "modules/storage-account/main.tf",
1102
+ "modules/eventhub/main.tf",
1103
+ "apps/azure-resource-mock/Dockerfile",
1104
+ "apps/azure-resource-mock/server.js",
443
1105
  ];
444
1106
  const extras = Object.keys(workspace.files)
445
1107
  .filter((name) => !preferred.includes(name))
446
1108
  .sort();
447
1109
 
448
- return preferred.filter((name) => workspace.files[name]).concat(extras);
1110
+ return preferred
1111
+ .filter((name) => hasWorkspaceFile(workspace.files, name))
1112
+ .concat(extras);
449
1113
  }
450
1114
 
451
1115
  export function serializeInfraLabWorkspace(
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.23.0"
2
+ "version": "0.23.1"
3
3
  }