-
Notifications
You must be signed in to change notification settings - Fork 0
chore(tests): add an end to end test that uses real terraform plan output #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| { | ||
| "name": "infra-diff", | ||
| "image": "mcr.microsoft.com/devcontainers/typescript-node:22", | ||
| "features": { | ||
| "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, | ||
| "ghcr.io/devcontainers-contrib/features/terraform-asdf:2": { | ||
| "version": "1.13.4" | ||
| } | ||
| }, | ||
| "customizations": { | ||
| "vscode": { | ||
| "extensions": [ | ||
| "dbaeumer.vscode-eslint", | ||
| "esbenp.prettier-vscode", | ||
| "biomejs.biome", | ||
| "hashicorp.terraform" | ||
| ], | ||
| "settings": { | ||
| "editor.formatOnSave": true, | ||
| "editor.defaultFormatter": "biomejs.biome" | ||
| } | ||
| } | ||
| }, | ||
| "postCreateCommand": "npm install", | ||
| "forwardPorts": [50000], | ||
| "mounts": [ | ||
| "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" | ||
| ], | ||
| "remoteUser": "node" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 1.13.4 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| version: '3.8' | ||
|
|
||
| services: | ||
| moto: | ||
| image: motoserver/moto:5.0.0 | ||
| container_name: infra-diff-moto-test | ||
| ports: | ||
| - "50000:5000" | ||
| healthcheck: | ||
| test: ["CMD", "curl", "-f", "http://localhost:5000"] | ||
| interval: 5s | ||
| timeout: 3s | ||
| retries: 10 | ||
| start_period: 10s | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,140 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { execSync } from "node:child_process"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { existsSync, rmSync, unlinkSync } from "node:fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as path from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { GenericContainer, type StartedTestContainer } from "testcontainers"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { afterAll, beforeAll, describe, expect, it } from "vitest"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ParsePlanUseCase } from "../src/domain/usecases/ParsePlanUseCase"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ReadPlanFileUseCase } from "../src/domain/usecases/ReadPlanFileUseCase"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { FilesystemAdapter } from "../src/infrastructure/adapters/FilesystemAdapter"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("E2E: Terraform Integration with moto", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const terraformDir = path.join(process.cwd(), "e2e", "terraform"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const planFile = path.join(terraformDir, "plan.bin"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const planJsonFile = path.join(terraformDir, "plan.json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let motoContainer: StartedTestContainer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let motoPort: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeAll(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Start moto server using testcontainers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Starting moto server..."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| motoPort = 50000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| motoContainer = await new GenericContainer("motoserver/moto:5.0.0") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .withExposedPorts({ container: 5000, host: motoPort }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .withStartupTimeout(120000) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Moto server is ready on port ${motoPort}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Moto server is ready on port ${motoPort}`); | |
| console.log(JSON.stringify({ event: "moto_server_ready", port: motoPort })); |
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
| console.log("Initializing Terraform..."); | |
| console.log(JSON.stringify({ message: "Initializing Terraform..." })); |
Check failure on line 31 in e2e/terraform-integration.test.ts
GitHub Actions / validate
e2e/terraform-integration.test.ts > E2E: Terraform Integration with moto
Error: Command failed: terraform init
/bin/sh: 1: terraform: not found
❯ e2e/terraform-integration.test.ts:31:4
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { status: 127, signal: null, output: [ null, '<Buffer(0) ...>', '<Buffer(33) ...>' ], pid: 3192, stdout: '<Buffer(0) ...>', stderr: '<Buffer(33) ...>' }
Check failure on line 31 in e2e/terraform-integration.test.ts
GitHub Actions / test
e2e/terraform-integration.test.ts > E2E: Terraform Integration with moto
Error: Command failed: terraform init
/bin/sh: 1: terraform: not found
❯ e2e/terraform-integration.test.ts:31:4
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { status: 127, signal: null, output: [ null, '<Buffer(0) ...>', '<Buffer(33) ...>' ], pid: 3138, stdout: '<Buffer(0) ...>', stderr: '<Buffer(33) ...>' }
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
| console.log("Generating Terraform plan..."); | |
| console.log(JSON.stringify({ event: "Generating Terraform plan" })); |
Check failure on line 35 in e2e/terraform-integration.test.ts
GitHub Actions / test-terraform-integration
e2e/terraform-integration.test.ts > E2E: Terraform Integration with moto
Error: Command failed: terraform plan -out=/home/runner/work/infra-diff/infra-diff/e2e/terraform/plan.bin
╷
│ Error: No valid credential sources found
│
│ with provider["registry.terraform.io/hashicorp/aws"],
│ on provider.tf line 1, in provider "aws":
│ 1: provider "aws" {
│
│ Please see https://registry.terraform.io/providers/hashicorp/aws
│ for more information about providing credentials.
│
│ Error: failed to refresh cached credentials, no EC2 IMDS role found,
│ operation error ec2imds: GetMetadata, access disabled to EC2 IMDS via
│ client option, or "AWS_EC2_METADATA_DISABLED" environment variable
│
╵
❯ e2e/terraform-integration.test.ts:35:4
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { status: 1, signal: null, output: [ null, '<Buffer(146) ...>', '<Buffer(785) ...>' ], pid: 3140, stdout: '<Buffer(146) ...>', stderr: '<Buffer(785) ...>' }
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium
absolute path
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
The best fix is to avoid constructing shell command strings by interpolating dynamic paths. Instead, use argument arrays to pass parameters directly to the underlying process, thereby avoiding shell interpolation altogether (which can be tricked by paths with spaces or shell metacharacters). Specifically, for the vulnerable terraform plan -out=${planFile} invocation at line 35, move from backtick/interpolated string usage to argument array usage like ["plan", "-out", planFile]. Change the invocation on line 35 to:
execSync("terraform plan -out=${planFile}", { ... });to
execSync("terraform", ["plan", "-out", planFile], { ... });Also, be sure to select the correct method signature: execSync(command, options) runs via the shell, while execFileSync(file, args, options) runs the file directly with argument separation (the secure way). Therefore, switch to execFileSync from the node:child_process module.
In addition, you must import execFileSync at the top if not already imported (which is not done in the shown code), since currently only execSync is imported.
The fix only needs to be applied to line 35 (and the import statement at the top).
-
Copy modified line R1 -
Copy modified line R35
| @@ -1,4 +1,4 @@ | ||
| import { execSync } from "node:child_process"; | ||
| import { execSync, execFileSync } from "node:child_process"; | ||
| import { existsSync, rmSync, unlinkSync } from "node:fs"; | ||
| import * as path from "node:path"; | ||
| import { GenericContainer, type StartedTestContainer } from "testcontainers"; | ||
| @@ -32,7 +32,7 @@ | ||
|
|
||
| // Generate plan | ||
| console.log("Generating Terraform plan..."); | ||
| execSync(`terraform plan -out=${planFile}`, { | ||
| execFileSync("terraform", ["plan", "-out", planFile], { | ||
| cwd: terraformDir, | ||
| stdio: "pipe", | ||
| }); |
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium
absolute path
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
To fix the highlighted issue, we should avoid dynamically constructing a shell command string where path variables could include unsafe shell metacharacters. Instead, we should invoke the underlying command directly using an API that accepts command arguments as an array, with output redirection done programmatically, not via shell syntax. Specifically, for terraform show -json ${planFile} > ${planJsonFile}, we should:
- Use
execFileSyncfromchild_processto execute:- Command:
terraform - Arguments:
["show", "-json", planFile]
- Command:
- Capture the stdout result of the command and write it to
planJsonFileusing node'sfsfunctionality (writeFileSync). - Remove the output redirection (
> ...) from the shell command string and the need for a shell.
Only lines 42-45 are affected in e2e/terraform-integration.test.ts.
No new method definitions are required, but we must ensure that writeFileSync is imported from fs if not already present.
-
Copy modified line R2 -
Copy modified line R42 -
Copy modified line R46
| @@ -1,5 +1,5 @@ | ||
| import { execSync } from "node:child_process"; | ||
| import { existsSync, rmSync, unlinkSync } from "node:fs"; | ||
| import { existsSync, rmSync, unlinkSync, writeFileSync } from "node:fs"; | ||
| import * as path from "node:path"; | ||
| import { GenericContainer, type StartedTestContainer } from "testcontainers"; | ||
| import { afterAll, beforeAll, describe, expect, it } from "vitest"; | ||
| @@ -39,11 +39,11 @@ | ||
|
|
||
| // Convert plan to JSON | ||
| console.log("Converting plan to JSON..."); | ||
| execSync(`terraform show -json ${planFile} > ${planJsonFile}`, { | ||
| const planJsonOutput = execSync("terraform show -json " + planFile, { | ||
| cwd: terraformDir, | ||
| stdio: "pipe", | ||
| shell: "/bin/bash", | ||
| }); | ||
| writeFileSync(planJsonFile, planJsonOutput); | ||
| } catch (error) { | ||
| console.error("Setup failed:", error); | ||
| throw error; |
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.error violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The beforeAll hook contains multiple complex setup steps (container startup, terraform init, plan, and JSON conversion) that violate the principle of single responsibility. Consider extracting these steps into separate, well-named helper functions to improve readability and maintainability.
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
| describe("E2E: Terraform Integration with moto", () => { | |
| const terraformDir = path.join(process.cwd(), "e2e", "terraform"); | |
| const planFile = path.join(terraformDir, "plan.bin"); | |
| const planJsonFile = path.join(terraformDir, "plan.json"); | |
| let motoContainer: StartedTestContainer; | |
| let motoPort: number; | |
| beforeAll(async () => { | |
| // Start moto server using testcontainers | |
| console.log("Starting moto server..."); | |
| try { | |
| motoPort = 50000; | |
| motoContainer = await new GenericContainer("motoserver/moto:5.0.0") | |
| .withExposedPorts({ container: 5000, host: motoPort }) | |
| .withStartupTimeout(120000) | |
| .start(); | |
| console.log(`Moto server is ready on port ${motoPort}`); | |
| // Initialize Terraform | |
| console.log("Initializing Terraform..."); | |
| execSync("terraform init", { cwd: terraformDir, stdio: "pipe" }); | |
| // Generate plan | |
| console.log("Generating Terraform plan..."); | |
| execSync(`terraform plan -out=${planFile}`, { | |
| cwd: terraformDir, | |
| stdio: "pipe", | |
| }); | |
| // Convert plan to JSON | |
| console.log("Converting plan to JSON..."); | |
| execSync(`terraform show -json ${planFile} > ${planJsonFile}`, { | |
| cwd: terraformDir, | |
| stdio: "pipe", | |
| shell: "/bin/bash", | |
| }); | |
| } catch (error) { | |
| console.error("Setup failed:", error); | |
| throw error; | |
| } | |
| }, 120000); // 2 minute timeout for setup |
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
| console.log("Cleaning up..."); | |
| console.log(JSON.stringify({ event: "cleanup", message: "Cleaning up..." })); |
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.error violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
| console.error("Failed to cleanup plan files:", error); | |
| console.log(JSON.stringify({ level: "error", message: "Failed to cleanup plan files", error: error instanceof Error ? error.message : String(error) })); |
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.error violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
| console.error("Failed to cleanup .terraform directory:", error); | |
| console.log(JSON.stringify({ level: "error", message: "Failed to cleanup .terraform directory", error })); |
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.error violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
Copilot
AI
Nov 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using console.error violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| terraform { | ||
| backend "local" {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| provider "aws" { | ||
| region = "us-east-1" | ||
| skip_credentials_validation = true | ||
| skip_metadata_api_check = true | ||
| skip_requesting_account_id = true | ||
|
|
||
| endpoints { | ||
| sts = "http://localhost:50000" | ||
| s3 = "http://localhost:50000" | ||
| sqs = "http://localhost:50000" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| resource "aws_sqs_queue" "queue" { | ||
| name = "test-queue" | ||
| tags = { | ||
| github = "https://github.com/zpratt/infra-diff.git" | ||
| random = 1234 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # End to End with Terraform | ||
|
|
||
| Goal: Demonstrate how Infra Diff can be used in an end-to-end workflow with Terraform and moto to provide a fake of the AWS API. The goal is to produce a real binary terraform plan, convert it to json, and then run infra-diff against that json plan. | ||
|
|
||
| ## Context | ||
|
|
||
| In this workflow, we will use Terraform to create a simple infrastructure setup on AWS. We will use moto to mock the AWS API, allowing us to generate a binary terraform plan without needing access to a real AWS account. This plan will then be converted to JSON format and analyzed using Infra Diff to identify any potential changes or issues. The test itself should be implemented in typescript, using the infra-diff library to run the analysis programmatically, treating the production code as if it is a complete black box. We should run moto in a docker container to ensure a clean and isolated environment for the test. Everything that is created should be runnable locally with a single command and should also be included in our CI pipeline to ensure consistent results across different environments. I've added example terraform fixtures in the e2e/terraform directory to get started. Ensure you examine the e2e/terraform/provider.tf file to see how to configure terraform to point at the moto server. | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Ensure the pipeline uses setup-terraform action to install terraform | ||
| - Ensure we're testing with the latest version of terraform | ||
| - Use moto server in a docker container to mock AWS API | ||
| - When running locally, create a devcontainer environment that we can use to run the tests with a specific version of node and terraform. | ||
| - Write the test in typescript using infra-diff library to analyze the terraform plan json output. | ||
| - Ensure the entire setup can be run with a single command both locally and in CI. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'version' field in docker-compose.yml is deprecated as of Docker Compose v1.27.0+ and is no longer required. Consider removing this line as modern versions of Docker Compose ignore it.