diff --git a/Makefile b/Makefile index 861ea7c..58f8882 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,19 @@ TERRAFORM_VERSION:=$(shell cat example/.terraform-version) +define generate-example + docker run \ + --interactive \ + --tty \ + --volume $(shell pwd):/src \ + --workdir /src/example \ + --entrypoint /bin/sh \ + hashicorp/terraform:$(1) \ + -c \ + "terraform init && \ + terraform plan -out tfplan && \ + terraform show -json tfplan > tfplan.json" +endef + .PHONY: help help: ## prints help (only for tasks with comment) @grep -E '^[a-zA-Z._-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @@ -13,28 +27,14 @@ build: ## build the binary install: build ## build and install to /usr/local/bin/ cp $(EXECUTABLE_NAME) /usr/local/bin/$(EXECUTABLE_NAME) -test: lint - go test ./... +test: lint ## go test + go test ./... -count=1 i: install ## build and install to /usr/local/bin/ -lint: +lint: ## lint source code golangci-lint run --timeout 10m -v -define generate-example - docker run \ - --interactive \ - --tty \ - --volume $(shell pwd):/src \ - --workdir /src/example \ - --entrypoint /bin/sh \ - hashicorp/terraform:$(1) \ - -c \ - "terraform init && \ - terraform plan -out tfplan && \ - terraform show -json tfplan > tfplan.json" -endef - -example: +example: ## generate example Terraform plan $(call generate-example,$(TERRAFORM_VERSION)) .PHONY: example diff --git a/README.md b/README.md index 9402cfe..b141845 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,16 @@ Usage of tf-summarize [args] [tf-plan.json|tfplan] -draw [Optional, used only with -tree or -separate-tree] draw trees instead of plain tree + -html + [Optional] print changes in html format + -json + [Optional] print changes in json format + -md + [Optional, used only with table view] output table as markdown -out string [Optional] write output to file -separate-tree - [Optional] print changes in tree format for each add/delete/change/recreate changes + [Optional] print changes in tree format for add/delete/change/recreate changes -tree [Optional] print changes in tree format -v print version diff --git a/example/.terraform.lock.hcl b/example/.terraform.lock.hcl index b28c736..485328c 100644 --- a/example/.terraform.lock.hcl +++ b/example/.terraform.lock.hcl @@ -4,6 +4,7 @@ provider "registry.terraform.io/hashicorp/github" { version = "4.23.0" hashes = [ + "h1:C8mQRZrHenfru/LKN+qqwAI6aEIGbu9iWaWVmGFwcy4=", "h1:eylJFdBYJIxdyyElE1mV7cMcv2HjUYzv+pFZjg/aRLU=", "zh:003f67dcb506ea50b34acce92f575cd04560a21c57bb63de1c9b3874dda10337", "zh:1b9e77fb728e3d2c8d25d04ac613e7587714c63c54532ac96787b4d351b164de", @@ -25,6 +26,7 @@ provider "registry.terraform.io/integrations/github" { version = "4.23.0" constraints = "4.23.0" hashes = [ + "h1:C8mQRZrHenfru/LKN+qqwAI6aEIGbu9iWaWVmGFwcy4=", "h1:eylJFdBYJIxdyyElE1mV7cMcv2HjUYzv+pFZjg/aRLU=", "zh:003f67dcb506ea50b34acce92f575cd04560a21c57bb63de1c9b3874dda10337", "zh:1b9e77fb728e3d2c8d25d04ac613e7587714c63c54532ac96787b4d351b164de", diff --git a/example/github/main.tf b/example/github/main.tf index d19c6ce..1a83d41 100644 --- a/example/github/main.tf +++ b/example/github/main.tf @@ -20,4 +20,8 @@ resource "github_branch" "main" { resource "github_branch" "development" { repository = github_repository.repository.name branch = "development" -} \ No newline at end of file +} + +output "repository_name" { + value = github_repository.repository.name +} diff --git a/example/main.tf b/example/main.tf index bd26518..84c8e72 100644 --- a/example/main.tf +++ b/example/main.tf @@ -58,3 +58,7 @@ resource "github_repository" "terraform_plan_summary" { has_wiki = true vulnerability_alerts = false } + +output "terraform_plan_summary_repository_name" { + value = github_repository.terraform_plan_summary.name +} diff --git a/example/tfplan.json b/example/tfplan.json index a151ff1..c937d9d 100644 --- a/example/tfplan.json +++ b/example/tfplan.json @@ -1 +1 @@ -{"format_version":"1.0","terraform_version":"1.1.4","planned_values":{"root_module":{"resources":[{"address":"github_repository.terraform_plan_summary","mode":"managed","type":"github_repository","name":"terraform_plan_summary","provider_name":"registry.terraform.io/integrations/github","schema_version":0,"values":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"terraform-plan-summary","pages":[],"template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":false},"sensitive_values":{"branches":[],"pages":[],"template":[],"topics":[false,false]}}],"child_modules":[{"resources":[{"address":"module.github[\"demo-repository\"].github_branch.development","mode":"managed","type":"github_branch","name":"development","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"branch":"development","repository":"demo-repository","source_branch":"main"},"sensitive_values":{}},{"address":"module.github[\"demo-repository\"].github_branch.main","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"branch":"main","repository":"demo-repository","source_branch":"main"},"sensitive_values":{}},{"address":"module.github[\"demo-repository\"].github_repository.repository","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"description of the repo","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"demo-repository","pages":[],"template":[],"topics":["tags"],"visibility":"public","vulnerability_alerts":false},"sensitive_values":{"branches":[],"pages":[],"template":[],"topics":[false]}}],"address":"module.github[\"demo-repository\"]"},{"resources":[{"address":"module.github[\"terraform-plan-summary\"].github_branch.demo","mode":"managed","type":"github_branch","name":"demo","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"sensitive_values":false},{"address":"module.github[\"terraform-plan-summary\"].github_branch.development","mode":"managed","type":"github_branch","name":"development","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"branch":"development","repository":"terraform-plan-summary","source_branch":"main"},"sensitive_values":{}},{"address":"module.github[\"terraform-plan-summary\"].github_branch.main","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"branch":"main","repository":"terraform-plan-summary","source_branch":"main"},"sensitive_values":{}},{"address":"module.github[\"terraform-plan-summary\"].github_repository.repository","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"terraform-plan-summary","pages":[],"template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":false},"sensitive_values":{"branches":[],"pages":[],"template":[],"topics":[false,false]}}],"address":"module.github[\"terraform-plan-summary\"]"}]}},"resource_drift":[{"address":"module.github[\"terraform-plan-summary\"].github_branch.demo","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"demo","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["delete"],"before":{"branch":"demo","etag":"W/\"2c138304ec0f031d3ffd19d45dae737be855a74a8d7e48f4fae4fc9213a6dbf6\"","id":"terraform-plan-summary:demo","ref":"refs/heads/demo","repository":"terraform-plan-summary","sha":"2e3d2c0b4513373c72b96b29592b62581e271af9","source_branch":"main","source_sha":null},"after":null,"after_unknown":{},"before_sensitive":{},"after_sensitive":false}},{"address":"module.github[\"terraform-plan-summary\"].github_branch.main","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["delete"],"before":{"branch":"main","etag":"W/\"4d429c1f4818d4314dcd07dd1e034b2bab0014a46bf9088635e37fd6decc64b8\"","id":"terraform-plan-summary:main","ref":"refs/heads/main","repository":"terraform-plan-summary","sha":"ad0cb343cc26f08985e5517bd2da8f0483beaa4b","source_branch":"main","source_sha":null},"after":null,"after_unknown":{},"before_sensitive":{},"after_sensitive":false}},{"address":"module.github[\"terraform-plan-summary\"].github_repository.repository","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["delete"],"before":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":false,"branches":[{"name":"main","protected":false},{"name":"trail","protected":false}],"default_branch":"main","delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","etag":"W/\"7536d6c2714d2e49890a3d1ebed0a31e89b6412f973e0d631089836e25229f29\"","full_name":"dineshba/terraform-plan-summary","git_clone_url":"git://github.com/dineshba/terraform-plan-summary.git","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":"","html_url":"https://github.com/dineshba/terraform-plan-summary","http_clone_url":"https://github.com/dineshba/terraform-plan-summary.git","id":"terraform-plan-summary","ignore_vulnerability_alerts_during_read":null,"is_template":false,"license_template":null,"name":"terraform-plan-summary","node_id":"R_kgDOGq54_w","pages":[],"private":false,"repo_id":447641855,"ssh_clone_url":"git@github.com:dineshba/terraform-plan-summary.git","svn_url":"https://github.com/dineshba/terraform-plan-summary","template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":true},"after":null,"after_unknown":{},"before_sensitive":{"branches":[{},{}],"pages":[],"template":[],"topics":[false,false]},"after_sensitive":false}}],"resource_changes":[{"address":"github_repository.terraform_plan_summary","mode":"managed","type":"github_repository","name":"terraform_plan_summary","provider_name":"registry.terraform.io/integrations/github","change":{"actions":["create"],"before":null,"after":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"terraform-plan-summary","pages":[],"template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":false},"after_unknown":{"branches":true,"default_branch":true,"etag":true,"full_name":true,"git_clone_url":true,"html_url":true,"http_clone_url":true,"id":true,"node_id":true,"pages":[],"private":true,"repo_id":true,"ssh_clone_url":true,"svn_url":true,"template":[],"topics":[false,false]},"before_sensitive":false,"after_sensitive":{"branches":[],"pages":[],"template":[],"topics":[false,false]}}},{"address":"module.github[\"demo-repository\"].github_branch.development","module_address":"module.github[\"demo-repository\"]","mode":"managed","type":"github_branch","name":"development","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"branch":"development","repository":"demo-repository","source_branch":"main"},"after_unknown":{"etag":true,"id":true,"ref":true,"sha":true,"source_sha":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.github[\"demo-repository\"].github_branch.main","module_address":"module.github[\"demo-repository\"]","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"branch":"main","repository":"demo-repository","source_branch":"main"},"after_unknown":{"etag":true,"id":true,"ref":true,"sha":true,"source_sha":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.github[\"demo-repository\"].github_repository.repository","module_address":"module.github[\"demo-repository\"]","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"description of the repo","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"demo-repository","pages":[],"template":[],"topics":["tags"],"visibility":"public","vulnerability_alerts":false},"after_unknown":{"branches":true,"default_branch":true,"etag":true,"full_name":true,"git_clone_url":true,"html_url":true,"http_clone_url":true,"id":true,"node_id":true,"pages":[],"private":true,"repo_id":true,"ssh_clone_url":true,"svn_url":true,"template":[],"topics":[false]},"before_sensitive":false,"after_sensitive":{"branches":[],"pages":[],"template":[],"topics":[false]}}},{"address":"module.github[\"terraform-plan-summary\"].github_branch.demo","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"demo","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["no-op"],"before":null,"after":null,"after_unknown":{},"before_sensitive":false,"after_sensitive":false},"action_reason":"delete_because_no_resource_config"},{"address":"module.github[\"terraform-plan-summary\"].github_branch.development","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"development","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"branch":"development","repository":"terraform-plan-summary","source_branch":"main"},"after_unknown":{"etag":true,"id":true,"ref":true,"sha":true,"source_sha":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.github[\"terraform-plan-summary\"].github_branch.main","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"branch":"main","repository":"terraform-plan-summary","source_branch":"main"},"after_unknown":{"etag":true,"id":true,"ref":true,"sha":true,"source_sha":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.github[\"terraform-plan-summary\"].github_repository.repository","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"terraform-plan-summary","pages":[],"template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":false},"after_unknown":{"branches":true,"default_branch":true,"etag":true,"full_name":true,"git_clone_url":true,"html_url":true,"http_clone_url":true,"id":true,"node_id":true,"pages":[],"private":true,"repo_id":true,"ssh_clone_url":true,"svn_url":true,"template":[],"topics":[false,false]},"before_sensitive":false,"after_sensitive":{"branches":[],"pages":[],"template":[],"topics":[false,false]}}}],"configuration":{"provider_config":{"github":{"name":"github","version_constraint":"4.23.0","expressions":{"owner":{"constant_value":"dineshba"}}}},"root_module":{"resources":[{"address":"github_repository.terraform_plan_summary","mode":"managed","type":"github_repository","name":"terraform_plan_summary","provider_config_key":"github","expressions":{"description":{"constant_value":"A command-line utility to print the summary of the terraform plan"},"has_downloads":{"constant_value":true},"has_issues":{"constant_value":true},"has_projects":{"constant_value":true},"has_wiki":{"constant_value":true},"name":{"constant_value":"terraform-plan-summary"},"topics":{"constant_value":["summary","terraform"]},"visibility":{"constant_value":"public"},"vulnerability_alerts":{"constant_value":false}},"schema_version":0}],"module_calls":{"github":{"source":"./github","expressions":{"description":{"references":["each.value.description","each.value"]},"name":{"references":["each.key"]},"topics":{"references":["each.value.topics","each.value"]}},"for_each_expression":{"references":["local.repos"]},"module":{"resources":[{"address":"github_branch.development","mode":"managed","type":"github_branch","name":"development","provider_config_key":"github:github","expressions":{"branch":{"constant_value":"development"},"repository":{"references":["github_repository.repository.name","github_repository.repository"]}},"schema_version":0},{"address":"github_branch.main","mode":"managed","type":"github_branch","name":"main","provider_config_key":"github:github","expressions":{"branch":{"constant_value":"main"},"repository":{"references":["github_repository.repository.name","github_repository.repository"]}},"schema_version":0},{"address":"github_repository.repository","mode":"managed","type":"github_repository","name":"repository","provider_config_key":"github:github","expressions":{"description":{"references":["var.description"]},"has_downloads":{"constant_value":true},"has_issues":{"constant_value":true},"has_projects":{"constant_value":true},"has_wiki":{"constant_value":true},"name":{"references":["var.name"]},"topics":{"references":["var.topics"]},"visibility":{"constant_value":"public"},"vulnerability_alerts":{"constant_value":false}},"schema_version":0}],"variables":{"description":{},"name":{},"topics":{}}}}}}}} +{"format_version":"1.0","terraform_version":"1.1.4","planned_values":{"outputs":{"terraform_plan_summary_repository_name":{"sensitive":false,"value":"terraform-plan-summary"}},"root_module":{"resources":[{"address":"github_repository.terraform_plan_summary","mode":"managed","type":"github_repository","name":"terraform_plan_summary","provider_name":"registry.terraform.io/integrations/github","schema_version":0,"values":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"terraform-plan-summary","pages":[],"template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":false},"sensitive_values":{"branches":[],"pages":[],"template":[],"topics":[false,false]}}],"child_modules":[{"resources":[{"address":"module.github[\"demo-repository\"].github_branch.development","mode":"managed","type":"github_branch","name":"development","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"branch":"development","repository":"demo-repository","source_branch":"main"},"sensitive_values":{}},{"address":"module.github[\"demo-repository\"].github_branch.main","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"branch":"main","repository":"demo-repository","source_branch":"main"},"sensitive_values":{}},{"address":"module.github[\"demo-repository\"].github_repository.repository","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"description of the repo","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"demo-repository","pages":[],"template":[],"topics":["tags"],"visibility":"public","vulnerability_alerts":false},"sensitive_values":{"branches":[],"pages":[],"template":[],"topics":[false]}}],"address":"module.github[\"demo-repository\"]"},{"resources":[{"address":"module.github[\"terraform-plan-summary\"].github_branch.demo","mode":"managed","type":"github_branch","name":"demo","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"sensitive_values":false},{"address":"module.github[\"terraform-plan-summary\"].github_branch.development","mode":"managed","type":"github_branch","name":"development","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"branch":"development","repository":"terraform-plan-summary","source_branch":"main"},"sensitive_values":{}},{"address":"module.github[\"terraform-plan-summary\"].github_branch.main","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"branch":"main","repository":"terraform-plan-summary","source_branch":"main"},"sensitive_values":{}},{"address":"module.github[\"terraform-plan-summary\"].github_repository.repository","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","schema_version":0,"values":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"terraform-plan-summary","pages":[],"template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":false},"sensitive_values":{"branches":[],"pages":[],"template":[],"topics":[false,false]}}],"address":"module.github[\"terraform-plan-summary\"]"}]}},"resource_drift":[{"address":"module.github[\"terraform-plan-summary\"].github_branch.demo","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"demo","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["delete"],"before":{"branch":"demo","etag":"W/\"2c138304ec0f031d3ffd19d45dae737be855a74a8d7e48f4fae4fc9213a6dbf6\"","id":"terraform-plan-summary:demo","ref":"refs/heads/demo","repository":"terraform-plan-summary","sha":"2e3d2c0b4513373c72b96b29592b62581e271af9","source_branch":"main","source_sha":null},"after":null,"after_unknown":{},"before_sensitive":{},"after_sensitive":false}},{"address":"module.github[\"terraform-plan-summary\"].github_branch.main","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["delete"],"before":{"branch":"main","etag":"W/\"4d429c1f4818d4314dcd07dd1e034b2bab0014a46bf9088635e37fd6decc64b8\"","id":"terraform-plan-summary:main","ref":"refs/heads/main","repository":"terraform-plan-summary","sha":"ad0cb343cc26f08985e5517bd2da8f0483beaa4b","source_branch":"main","source_sha":null},"after":null,"after_unknown":{},"before_sensitive":{},"after_sensitive":false}},{"address":"module.github[\"terraform-plan-summary\"].github_repository.repository","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["delete"],"before":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":false,"branches":[{"name":"main","protected":false},{"name":"trail","protected":false}],"default_branch":"main","delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","etag":"W/\"7536d6c2714d2e49890a3d1ebed0a31e89b6412f973e0d631089836e25229f29\"","full_name":"dineshba/terraform-plan-summary","git_clone_url":"git://github.com/dineshba/terraform-plan-summary.git","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":"","html_url":"https://github.com/dineshba/terraform-plan-summary","http_clone_url":"https://github.com/dineshba/terraform-plan-summary.git","id":"terraform-plan-summary","ignore_vulnerability_alerts_during_read":null,"is_template":false,"license_template":null,"name":"terraform-plan-summary","node_id":"R_kgDOGq54_w","pages":[],"private":false,"repo_id":447641855,"ssh_clone_url":"git@github.com:dineshba/terraform-plan-summary.git","svn_url":"https://github.com/dineshba/terraform-plan-summary","template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":true},"after":null,"after_unknown":{},"before_sensitive":{"branches":[{},{}],"pages":[],"template":[],"topics":[false,false]},"after_sensitive":false}}],"resource_changes":[{"address":"github_repository.terraform_plan_summary","mode":"managed","type":"github_repository","name":"terraform_plan_summary","provider_name":"registry.terraform.io/integrations/github","change":{"actions":["create"],"before":null,"after":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"terraform-plan-summary","pages":[],"template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":false},"after_unknown":{"branches":true,"default_branch":true,"etag":true,"full_name":true,"git_clone_url":true,"html_url":true,"http_clone_url":true,"id":true,"node_id":true,"pages":[],"private":true,"repo_id":true,"ssh_clone_url":true,"svn_url":true,"template":[],"topics":[false,false]},"before_sensitive":false,"after_sensitive":{"branches":[],"pages":[],"template":[],"topics":[false,false]}}},{"address":"module.github[\"demo-repository\"].github_branch.development","module_address":"module.github[\"demo-repository\"]","mode":"managed","type":"github_branch","name":"development","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"branch":"development","repository":"demo-repository","source_branch":"main"},"after_unknown":{"etag":true,"id":true,"ref":true,"sha":true,"source_sha":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.github[\"demo-repository\"].github_branch.main","module_address":"module.github[\"demo-repository\"]","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"branch":"main","repository":"demo-repository","source_branch":"main"},"after_unknown":{"etag":true,"id":true,"ref":true,"sha":true,"source_sha":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.github[\"demo-repository\"].github_repository.repository","module_address":"module.github[\"demo-repository\"]","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"description of the repo","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"demo-repository","pages":[],"template":[],"topics":["tags"],"visibility":"public","vulnerability_alerts":false},"after_unknown":{"branches":true,"default_branch":true,"etag":true,"full_name":true,"git_clone_url":true,"html_url":true,"http_clone_url":true,"id":true,"node_id":true,"pages":[],"private":true,"repo_id":true,"ssh_clone_url":true,"svn_url":true,"template":[],"topics":[false]},"before_sensitive":false,"after_sensitive":{"branches":[],"pages":[],"template":[],"topics":[false]}}},{"address":"module.github[\"terraform-plan-summary\"].github_branch.demo","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"demo","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["no-op"],"before":null,"after":null,"after_unknown":{},"before_sensitive":false,"after_sensitive":false},"action_reason":"delete_because_no_resource_config"},{"address":"module.github[\"terraform-plan-summary\"].github_branch.development","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"development","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"branch":"development","repository":"terraform-plan-summary","source_branch":"main"},"after_unknown":{"etag":true,"id":true,"ref":true,"sha":true,"source_sha":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.github[\"terraform-plan-summary\"].github_branch.main","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_branch","name":"main","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"branch":"main","repository":"terraform-plan-summary","source_branch":"main"},"after_unknown":{"etag":true,"id":true,"ref":true,"sha":true,"source_sha":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.github[\"terraform-plan-summary\"].github_repository.repository","module_address":"module.github[\"terraform-plan-summary\"]","mode":"managed","type":"github_repository","name":"repository","provider_name":"registry.terraform.io/hashicorp/github","change":{"actions":["create"],"before":null,"after":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"archive_on_destroy":null,"archived":false,"auto_init":null,"delete_branch_on_merge":false,"description":"A command-line utility to print the summary of the terraform plan","gitignore_template":null,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":null,"ignore_vulnerability_alerts_during_read":null,"is_template":null,"license_template":null,"name":"terraform-plan-summary","pages":[],"template":[],"topics":["summary","terraform"],"visibility":"public","vulnerability_alerts":false},"after_unknown":{"branches":true,"default_branch":true,"etag":true,"full_name":true,"git_clone_url":true,"html_url":true,"http_clone_url":true,"id":true,"node_id":true,"pages":[],"private":true,"repo_id":true,"ssh_clone_url":true,"svn_url":true,"template":[],"topics":[false,false]},"before_sensitive":false,"after_sensitive":{"branches":[],"pages":[],"template":[],"topics":[false,false]}}}],"output_changes":{"terraform_plan_summary_repository_name":{"actions":["create"],"before":null,"after":"terraform-plan-summary","after_unknown":false,"before_sensitive":false,"after_sensitive":false}},"prior_state":{"format_version":"1.0","terraform_version":"1.1.4","values":{"outputs":{"terraform_plan_summary_repository_name":{"sensitive":false,"value":"terraform-plan-summary"}},"root_module":{}}},"configuration":{"provider_config":{"github":{"name":"github","version_constraint":"4.23.0","expressions":{"owner":{"constant_value":"dineshba"}}}},"root_module":{"outputs":{"terraform_plan_summary_repository_name":{"expression":{"references":["github_repository.terraform_plan_summary.name","github_repository.terraform_plan_summary"]}}},"resources":[{"address":"github_repository.terraform_plan_summary","mode":"managed","type":"github_repository","name":"terraform_plan_summary","provider_config_key":"github","expressions":{"description":{"constant_value":"A command-line utility to print the summary of the terraform plan"},"has_downloads":{"constant_value":true},"has_issues":{"constant_value":true},"has_projects":{"constant_value":true},"has_wiki":{"constant_value":true},"name":{"constant_value":"terraform-plan-summary"},"topics":{"constant_value":["summary","terraform"]},"visibility":{"constant_value":"public"},"vulnerability_alerts":{"constant_value":false}},"schema_version":0}],"module_calls":{"github":{"source":"./github","expressions":{"description":{"references":["each.value.description","each.value"]},"name":{"references":["each.key"]},"topics":{"references":["each.value.topics","each.value"]}},"for_each_expression":{"references":["local.repos"]},"module":{"outputs":{"repository_name":{"expression":{"references":["github_repository.repository.name","github_repository.repository"]}}},"resources":[{"address":"github_branch.development","mode":"managed","type":"github_branch","name":"development","provider_config_key":"github:github","expressions":{"branch":{"constant_value":"development"},"repository":{"references":["github_repository.repository.name","github_repository.repository"]}},"schema_version":0},{"address":"github_branch.main","mode":"managed","type":"github_branch","name":"main","provider_config_key":"github:github","expressions":{"branch":{"constant_value":"main"},"repository":{"references":["github_repository.repository.name","github_repository.repository"]}},"schema_version":0},{"address":"github_repository.repository","mode":"managed","type":"github_repository","name":"repository","provider_config_key":"github:github","expressions":{"description":{"references":["var.description"]},"has_downloads":{"constant_value":true},"has_issues":{"constant_value":true},"has_projects":{"constant_value":true},"has_wiki":{"constant_value":true},"name":{"references":["var.name"]},"topics":{"references":["var.topics"]},"visibility":{"constant_value":"public"},"vulnerability_alerts":{"constant_value":false}},"schema_version":0}],"variables":{"description":{},"name":{},"topics":{}}}}}}}} diff --git a/main.go b/main.go index 837c137..f517699 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ func main() { printVersion := flag.Bool("v", false, "print version") tree := flag.Bool("tree", false, "[Optional] print changes in tree format") json := flag.Bool("json", false, "[Optional] print changes in json format") + html := flag.Bool("html", false, "[Optional] print changes in html format") separateTree := flag.Bool("separate-tree", false, "[Optional] print changes in tree format for add/delete/change/recreate changes") drawable := flag.Bool("draw", false, "[Optional, used only with -tree or -separate-tree] draw trees instead of plain tree") md := flag.Bool("md", false, "[Optional, used only with table view] output table as markdown") @@ -35,7 +36,7 @@ func main() { } args := flag.Args() - err := validateFlags(*tree, *separateTree, *drawable, *md, args) + err := validateFlags(*tree, *separateTree, *drawable, *md, *json, *html, args) logIfErrorAndExit("invalid input flags: %s\n", err, flag.Usage) newReader, err := reader.CreateReader(args) @@ -52,7 +53,7 @@ func main() { terraformstate.FilterNoOpResources(&terraformState) - newWriter := writer.CreateWriter(*tree, *separateTree, *drawable, *md, *json, terraformState) + newWriter := writer.CreateWriter(*tree, *separateTree, *drawable, *md, *json, *html, terraformState) var outputFile io.Writer = os.Stdout @@ -83,7 +84,7 @@ func logIfErrorAndExit(format string, err error, callback func()) { } } -func validateFlags(tree, separateTree, drawable bool, md bool, args []string) error { +func validateFlags(tree, separateTree, drawable bool, md bool, json bool, html bool, args []string) error { if tree && md { return fmt.Errorf("both -tree and -md should not be provided") } @@ -96,8 +97,23 @@ func validateFlags(tree, separateTree, drawable bool, md bool, args []string) er if !tree && !separateTree && drawable { return fmt.Errorf("drawable should be provided with -tree or -seperate-tree") } + if multipleTrueVals(md, json, html) { + return fmt.Errorf("only one of -md, -json, or -html should be provided") + } if len(args) > 1 { return fmt.Errorf("only one argument is allowed which is filename, but got %v", args) } return nil } + +func multipleTrueVals(vals ...bool) bool { + v := []bool{} + + for _, val := range vals { + if val { + v = append(v, val) + } + } + + return len(v) > 1 +} diff --git a/main_test.go b/main_test.go index 80ec18a..d7c50c9 100644 --- a/main_test.go +++ b/main_test.go @@ -61,13 +61,20 @@ func TestTFSummarize(t *testing.T) { }, { command: fmt.Sprintf("cat example/tfplan.json | ./%s -md", testExecutable), expectedOutput: "basic.txt", + }, { + command: fmt.Sprintf("cat example/tfplan.json | ./%s -html", testExecutable), + expectedOutput: "basic.html", + }, { + command: fmt.Sprintf("cat example/tfplan.json | ./%s -md -html", testExecutable), + expectedError: fmt.Errorf("exit status 1"), + expectedOutput: "multiple_format_flags_error.txt", }} for _, test := range tests { t.Run(fmt.Sprintf("when tf-summarize is passed '%q'", test.command), func(t *testing.T) { - output, err := exec.Command("/bin/sh", "-c", test.command).CombinedOutput() - if err != nil && test.expectedError == nil { - t.Errorf("expected '%s' not to error; got '%v'", test.command, err) + output, cmdErr := exec.Command("/bin/sh", "-c", test.command).CombinedOutput() + if cmdErr != nil && test.expectedError == nil { + t.Errorf("expected '%s' not to error; got '%v'", test.command, cmdErr) } b, err := os.ReadFile(fmt.Sprintf("testdata/%s", test.expectedOutput)) @@ -77,12 +84,12 @@ func TestTFSummarize(t *testing.T) { expected := string(b) - if test.expectedError != nil && err == nil { - t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), err) + if test.expectedError != nil && cmdErr == nil { + t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), cmdErr) } - if test.expectedError != nil && err != nil && test.expectedError.Error() != err.Error() { - t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), err.Error()) + if test.expectedError != nil && cmdErr != nil && test.expectedError.Error() != cmdErr.Error() { + t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), cmdErr.Error()) } if string(output) != expected { diff --git a/testdata/basic.html b/testdata/basic.html new file mode 100644 index 0000000..e842168 --- /dev/null +++ b/testdata/basic.html @@ -0,0 +1,34 @@ + + + + + + + + + +
CHANGERESOURCE
add +
    +
  • github_repository.terraform_plan_summary
  • +
  • module.github["demo-repository"].github_branch.development
  • +
  • module.github["demo-repository"].github_branch.main
  • +
  • module.github["demo-repository"].github_repository.repository
  • +
  • module.github["terraform-plan-summary"].github_branch.development
  • +
  • module.github["terraform-plan-summary"].github_branch.main
  • +
  • module.github["terraform-plan-summary"].github_repository.repository
  • +
+
+ + + + + + + + + +
CHANGEOUTPUT
add +
    +
  • terraform_plan_summary_repository_name
  • +
+
diff --git a/testdata/basic.txt b/testdata/basic.txt index bb145c0..755c332 100644 --- a/testdata/basic.txt +++ b/testdata/basic.txt @@ -7,3 +7,7 @@ | | `module.github["terraform-plan-summary"].github_branch.development` | | | `module.github["terraform-plan-summary"].github_branch.main` | | | `module.github["terraform-plan-summary"].github_repository.repository` | + +| CHANGE | OUTPUT | +|--------|------------------------------------------| +| add | `terraform_plan_summary_repository_name` | diff --git a/testdata/multiple_format_flags_error.txt b/testdata/multiple_format_flags_error.txt new file mode 100644 index 0000000..4556cf1 --- /dev/null +++ b/testdata/multiple_format_flags_error.txt @@ -0,0 +1,19 @@ +invalid input flags: only one of -md, -json, or -html should be provided + +Usage of ./tf-summarize-test [args] [tf-plan.json|tfplan] + + -draw + [Optional, used only with -tree or -separate-tree] draw trees instead of plain tree + -html + [Optional] print changes in html format + -json + [Optional] print changes in json format + -md + [Optional, used only with table view] output table as markdown + -out string + [Optional] write output to file + -separate-tree + [Optional] print changes in tree format for add/delete/change/recreate changes + -tree + [Optional] print changes in tree format + -v print version diff --git a/writer/html.go b/writer/html.go new file mode 100644 index 0000000..66ee3b5 --- /dev/null +++ b/writer/html.go @@ -0,0 +1,50 @@ +package writer + +import ( + "io" + "path" + "text/template" + + "github.com/dineshba/tf-summarize/terraformstate" +) + +// HTMLWriter is a Writer that writes HTML. +type HTMLWriter struct { + ResourceChanges map[string]terraformstate.ResourceChanges + OutputChanges map[string][]string +} + +// Write outputs the HTML summary to the io.Writer it's passed. +func (t HTMLWriter) Write(writer io.Writer) error { + templatesDir := "templates" + rcTmpl := "resourceChanges.html" + tmpl, err := template.New(rcTmpl).ParseFS(templates, path.Join(templatesDir, rcTmpl)) + if err != nil { + return err + } + + err = tmpl.Execute(writer, t) + if err != nil { + return err + } + + if !hasOutputChanges(t.OutputChanges) { + return nil + } + + ocTmpl := "outputChanges.html" + outputTmpl, err := template.New(ocTmpl).ParseFS(templates, path.Join(templatesDir, ocTmpl)) + if err != nil { + return err + } + + return outputTmpl.Execute(writer, t) +} + +// NewHTMLWriter returns a new HTMLWriter with the configuration it's passed. +func NewHTMLWriter(changes map[string]terraformstate.ResourceChanges, outputChanges map[string][]string) Writer { + return HTMLWriter{ + ResourceChanges: changes, + OutputChanges: outputChanges, + } +} diff --git a/writer/table.go b/writer/table.go index 05b0adf..33c2b0d 100644 --- a/writer/table.go +++ b/writer/table.go @@ -44,7 +44,7 @@ func (t TableWriter) Write(writer io.Writer) error { table.Render() // Disable the Output Summary if there are no outputs to display - if len(t.outputChanges["add"]) > 0 || len(t.outputChanges["delete"]) > 0 || len(t.outputChanges["update"]) > 0 { + if hasOutputChanges(t.outputChanges) { tableString = make([][]string, 0, 4) for _, change := range tableOrder { changedOutputs := t.outputChanges[change] diff --git a/writer/templates/outputChanges.html b/writer/templates/outputChanges.html new file mode 100644 index 0000000..cae9b49 --- /dev/null +++ b/writer/templates/outputChanges.html @@ -0,0 +1,14 @@ + + + + + {{ range $change, $outputs := .OutputChanges }}{{ $length := len $outputs }}{{ if gt $length 0 }} + + + + {{ end }}{{ end }} +
CHANGEOUTPUT
{{ $change }} +
    {{ range $i, $o := $outputs }} +
  • {{ $o }}
  • {{ end }} +
+
diff --git a/writer/templates/resourceChanges.html b/writer/templates/resourceChanges.html new file mode 100644 index 0000000..8f4f4ab --- /dev/null +++ b/writer/templates/resourceChanges.html @@ -0,0 +1,14 @@ + + + + + {{ range $change, $resources := .ResourceChanges }}{{ $length := len $resources }}{{ if gt $length 0 }} + + + + {{ end }}{{ end }} +
CHANGERESOURCE
{{ $change }} +
    {{ range $i, $r := $resources }} +
  • {{ $r.Address }}
  • {{ end }} +
+
diff --git a/writer/util.go b/writer/util.go new file mode 100644 index 0000000..fa08542 --- /dev/null +++ b/writer/util.go @@ -0,0 +1,21 @@ +package writer + +import "embed" + +// Embed the templates directory in the compiled binary. +// +//go:embed templates +var templates embed.FS + +func hasOutputChanges(opChanges map[string][]string) bool { + hasChanges := false + + for _, v := range opChanges { + if len(v) > 0 { + hasChanges = true + break + } + } + + return hasChanges +} diff --git a/writer/writer.go b/writer/writer.go index dd9d0a7..383bf13 100644 --- a/writer/writer.go +++ b/writer/writer.go @@ -11,8 +11,7 @@ type Writer interface { Write(writer io.Writer) error } -func CreateWriter(tree, separateTree, drawable, mdEnabled, json bool, plan tfjson.Plan) Writer { - +func CreateWriter(tree, separateTree, drawable, mdEnabled, json, html bool, plan tfjson.Plan) Writer { if tree { return NewTreeWriter(plan.ResourceChanges, drawable) } @@ -22,6 +21,9 @@ func CreateWriter(tree, separateTree, drawable, mdEnabled, json bool, plan tfjso if json { return NewJSONWriter(plan.ResourceChanges) } + if html { + return NewHTMLWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllOutputChanges(plan)) + } return NewTableWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllOutputChanges(plan), mdEnabled) }