Repository: terraform-docs/terraform-docs
Pull request #651 (2022-11-10 17:24:11)

Ignore inputs with terraform-docs-ignore comment
### Description of your changes

In our monorepo projects, we often have a set of variables that is shared amongst modules, eg. a `tags` variable that is accepted by all modules in the repo. This is typically done by symlinking a `common-vars.tf` file into all modules in the repo. This leads to those documentation these inputs being generated on every modules documentation.
This is not only rather repetitive, it also clutters the documentation by mixing the module specific inputs with the common ones, making it harder to read the module docs.

This PR introduces the possibility to add a comment `# terraform-docs-ignore` to a variable, telling `terraform-docs` to ignore and not render the variable in the documentation. 

Example:
```hcl
# terraform-docs-ignore
variable "tags" {
  type        = map(string)
  default     = {}
  description = "A set of tags added to the resource."
}
```

Fixes: #680

I have:

- [x] Read and followed terraform-docs' [contribution process].
- [x] All tests pass when I run `make test`.

### How has this code been tested

The unit test shows that the variable tagged with `# terraform-docs-ignore` is not present after loading the module.

Makeshift: This is exactly what I needed for our repos, with exactly the same usecase. I'd really like to see this merged.
khos2ow: fixes #680 

Patch:
diff --git a/Dockerfile b/Dockerfile
index 40e23fb..3ca02ad 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,7 @@
 # You may obtain a copy of the License at the LICENSE file in
 # the root directory of this source tree.
 
-FROM docker.io/library/golang:1.22.3-alpine AS builder
+FROM golang:1.22.3-alpine AS builder
 
 RUN apk add --update --no-cache make
 
@@ -21,7 +21,7 @@ RUN make build
 
 ################
 
-FROM docker.io/library/alpine:3.20.0
+FROM alpine:3.20.0
 
 # Mitigate CVE-2023-5363
 RUN apk add --no-cache --upgrade "openssl>=3.1.4-r1"
diff --git a/docs/how-to/generate-terraform-tfvars.md b/docs/how-to/generate-terraform-tfvars.md
index fe0f126..14508c1 100644
--- a/docs/how-to/generate-terraform-tfvars.md
+++ b/docs/how-to/generate-terraform-tfvars.md
@@ -4,7 +4,7 @@ description: "How to generate terraform.tfvars file with terraform-docs"
 menu:
   docs:
     parent: "how-to"
-weight: 207
+weight: 208
 toc: false
 ---
 
diff --git a/docs/how-to/github-action.md b/docs/how-to/github-action.md
index 4413413..9554995 100644
--- a/docs/how-to/github-action.md
+++ b/docs/how-to/github-action.md
@@ -4,7 +4,7 @@ description: "How to use terraform-docs with GitHub Actions"
 menu:
   docs:
     parent: "how-to"
-weight: 208
+weight: 209
 toc: false
 ---
 
diff --git a/docs/how-to/ignore-resources.md b/docs/how-to/ignore-resources.md
new file mode 100644
index 0000000..0367c3d
--- /dev/null
+++ b/docs/how-to/ignore-resources.md
@@ -0,0 +1,74 @@
+---
+title: "Ignore Resources to be Generated"
+description: "How to ignore resources from generated output"
+menu:
+  docs:
+    parent: "how-to"
+weight: 204
+toc: false
+---
+
+Since `v0.18.0`
+
+Any type of resources can be ignored from the generated output by prepending them
+with a comment `terraform-docs-ignore`. Supported type of Terraform resources to
+get ignored are:
+
+- `resource`
+- `data`
+- `module`
+- `variable`
+- `output`
+
+{{< alert type="info" >}}
+If a `resource` or `data` is ignored, their corresponding discovered provider
+will also get ignored from "Providers" section.
+{{< /alert>}}
+
+Take the following example:
+
+```hcl
+##################################################################
+# All of the following will be ignored from the generated output #
+##################################################################
+
+# terraform-docs-ignore
+resource "foo_resource" "foo" {}
+
+# This resource is going to get ignored from generated
+# output by using the following known comment.
+#
+# terraform-docs-ignore
+#
+# The ignore keyword also doesn't have to be the first,
+# last, or the only thing in a leading comment
+resource "bar_resource" "bar" {}
+
+# terraform-docs-ignore
+data "foo_data_resource" "foo" {}
+
+# terraform-docs-ignore
+data "bar_data_resource" "bar" {}
+
+// terraform-docs-ignore
+module "foo" {
+  source  = "foo"
+  version = "x.x.x"
+}
+
+# terraform-docs-ignore
+variable "foo" {
+  default = "foo"
+}
+
+// terraform-docs-ignore
+output "foo" {
+  value = "foo"
+}
+```
+
+{{< alert type="info" >}}
+The ignore keyword (i.e. `terraform-docs-ignore`) doesn't have to be the first,
+last, or only thing in a leading comment. As long as the keyword is present in
+a comment, the following resource will get ignored.
+{{< /alert>}}
diff --git a/docs/how-to/include-examples.md b/docs/how-to/include-examples.md
index fb1965e..557627e 100644
--- a/docs/how-to/include-examples.md
+++ b/docs/how-to/include-examples.md
@@ -4,7 +4,7 @@ description: "How to include example in terraform-docs generated output"
 menu:
   docs:
     parent: "how-to"
-weight: 205
+weight: 206
 toc: false
 ---
 
diff --git a/docs/how-to/insert-output-to-file.md b/docs/how-to/insert-output-to-file.md
index 54f11a0..90da9d4 100644
--- a/docs/how-to/insert-output-to-file.md
+++ b/docs/how-to/insert-output-to-file.md
@@ -4,7 +4,7 @@ description: "How to insert generated terraform-docs output to file"
 menu:
   docs:
     parent: "how-to"
-weight: 204
+weight: 205
 toc: false
 ---
 
diff --git a/docs/how-to/pre-commit-hooks.md b/docs/how-to/pre-commit-hooks.md
index 91f093c..80f64e7 100644
--- a/docs/how-to/pre-commit-hooks.md
+++ b/docs/how-to/pre-commit-hooks.md
@@ -4,7 +4,7 @@ description: "How to use pre-commit hooks with terraform-docs"
 menu:
   docs:
     parent: "how-to"
-weight: 209
+weight: 210
 toc: false
 ---
 
diff --git a/docs/how-to/recursive-submodules.md b/docs/how-to/recursive-submodules.md
index d28ccd0..0751b51 100644
--- a/docs/how-to/recursive-submodules.md
+++ b/docs/how-to/recursive-submodules.md
@@ -4,7 +4,7 @@ description: "How to generate submodules documentation recursively with terrafor
 menu:
   docs:
     parent: "how-to"
-weight: 206
+weight: 207
 toc: false
 ---
 
diff --git a/examples/main.tf b/examples/main.tf
index 8b31f04..a800ffa 100644
--- a/examples/main.tf
+++ b/examples/main.tf
@@ -63,8 +63,20 @@ data "aws_caller_identity" "ident" {
   provider = "aws.ident"
 }
 
+# terraform-docs-ignore
+data "aws_caller_identity" "ignored" {
+  provider = "aws"
+}
+
 resource "null_resource" "foo" {}
 
+# This resource is going to get ignored from generated
+# output by using the following known comment.
+# terraform-docs-ignore
+# And the ignore keyword also doesn't have to be the first,
+# last, or only thing in a leading comment
+resource "null_resource" "ignored" {}
+
 module "bar" {
   source  = "baz"
   version = "4.5.6"
@@ -84,3 +96,9 @@ module "baz" {
 module "foobar" {
   source = "git@github.com:module/path?ref=v7.8.9"
 }
+
+// terraform-docs-ignore
+module "ignored" {
+  source  = "foobaz"
+  version = "7.8.9"
+}
diff --git a/examples/outputs.tf b/examples/outputs.tf
index 5bc3fef..e495382 100644
--- a/examples/outputs.tf
+++ b/examples/outputs.tf
@@ -17,3 +17,8 @@ output "output-0.12" {
   value       = join(",", var.list-3)
   description = "terraform 0.12 only"
 }
+
+// terraform-docs-ignore
+output "ignored" {
+  value = "ignored"
+}
diff --git a/examples/variables.tf b/examples/variables.tf
index b58e27c..bafab8f 100644
--- a/examples/variables.tf
+++ b/examples/variables.tf
@@ -14,6 +14,11 @@ variable "bool-1" {
   default = true
 }
 
+# terraform-docs-ignore
+variable "ignored" {
+  default = ""
+}
+
 variable "string-3" {
   default = ""
 }
diff --git a/go.mod b/go.mod
index 823e4d1..ebc3aaf 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
 	github.com/spf13/viper v1.18.2
 	github.com/stretchr/testify v1.9.0
 	github.com/terraform-docs/terraform-config-inspect v0.0.0-20210728164355-9c1f178932fa
+	golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
 	gopkg.in/yaml.v3 v3.0.1
 	honnef.co/go/tools v0.3.2
 	mvdan.cc/xurls/v2 v2.5.0
@@ -58,7 +59,6 @@ require (
 	github.com/zclconf/go-cty v1.14.4 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/crypto v0.23.0 // indirect
-	golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
 	golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e // indirect
 	golang.org/x/mod v0.17.0 // indirect
 	golang.org/x/net v0.25.0 // indirect
diff --git a/scripts/release/Dockerfile b/scripts/release/Dockerfile
index c004911..ca8eb04 100644
--- a/scripts/release/Dockerfile
+++ b/scripts/release/Dockerfile
@@ -6,7 +6,7 @@
 # You may obtain a copy of the License at the LICENSE file in
 # the root directory of this source tree.
 
-FROM docker.io/library/alpine:3.20.0
+FROM alpine:3.20.0
 
 COPY terraform-docs /usr/local/bin/terraform-docs
 
diff --git a/terraform/load.go b/terraform/load.go
index a8587b4..fea188b 100644
--- a/terraform/load.go
+++ b/terraform/load.go
@@ -187,10 +187,17 @@ func loadInputs(tfmodule *tfconfig.Module, config *print.Config) ([]*Input, []*I
 	var optional = make([]*Input, 0, len(tfmodule.Variables))
 
 	for _, input := range tfmodule.Variables {
+		comments := loadComments(input.Pos.Filename, input.Pos.Line)
+
+		// skip over inputs that are marked as being ignored
+		if strings.Contains(comments, "terraform-docs-ignore") {
+			continue
+		}
+
 		// convert CRLF to LF early on (https://github.com/terraform-docs/terraform-docs/issues/305)
 		inputDescription := strings.ReplaceAll(input.Description, "\r\n", "\n")
 		if inputDescription == "" && config.Settings.ReadComments {
-			inputDescription = loadComments(input.Pos.Filename, input.Pos.Line)
+			inputDescription = comments
 		}
 
 		i := &Input{
@@ -245,13 +252,20 @@ func loadModulecalls(tfmodule *tfconfig.Module, config *print.Config) []*ModuleC
 	var source, version string
 
 	for _, m := range tfmodule.ModuleCalls {
-		source, version = formatSource(m.Source, m.Version)
+		comments := loadComments(m.Pos.Filename, m.Pos.Line)
+
+		// skip over modules that are marked as being ignored
+		if strings.Contains(comments, "terraform-docs-ignore") {
+			continue
+		}
 
 		description := ""
 		if config.Settings.ReadComments {
-			description = loadComments(m.Pos.Filename, m.Pos.Line)
+			description = comments
 		}
 
+		source, version = formatSource(m.Source, m.Version)
+
 		modules = append(modules, &ModuleCall{
 			Name:        m.Name,
 			Source:      source,
@@ -277,10 +291,17 @@ func loadOutputs(tfmodule *tfconfig.Module, config *print.Config) ([]*Output, er
 		}
 	}
 	for _, o := range tfmodule.Outputs {
+		comments := loadComments(o.Pos.Filename, o.Pos.Line)
+
+		// skip over outputs that are marked as being ignored
+		if strings.Contains(comments, "terraform-docs-ignore") {
+			continue
+		}
+
 		// convert CRLF to LF early on (https://github.com/terraform-docs/terraform-docs/issues/584)
 		description := strings.ReplaceAll(o.Description, "\r\n", "\n")
 		if description == "" && config.Settings.ReadComments {
-			description = loadComments(o.Pos.Filename, o.Pos.Line)
+			description = comments
 		}
 
 		output := &Output{
@@ -330,7 +351,11 @@ func loadOutputValues(config *print.Config) (map[string]*output, error) {
 	return terraformOutputs, err
 }
 
-func loadProviders(tfmodule *tfconfig.Module, config *print.Config) []*Provider {
+func loadProviders(tfmodule *tfconfig.Module, config *print.Config) []*Provider { //nolint:gocyclo
+	// NOTE(khos2ow): this function is over our cyclomatic complexity goal.
+	// Be wary when adding branches, and look for functionality that could
+	// be reasonably moved into an injected dependency.
+
 	type provider struct {
 		Name        string   `hcl:"name,label"`
 		Version     string   `hcl:"version"`
@@ -360,6 +385,13 @@ func loadProviders(tfmodule *tfconfig.Module, config *print.Config) []*Provider
 
 	for _, resource := range resources {
 		for _, r := range resource {
+			comments := loadComments(r.Pos.Filename, r.Pos.Line)
+
+			// skip over resources that are marked as being ignored
+			if strings.Contains(comments, "terraform-docs-ignore") {
+				continue
+			}
+
 			var version = ""
 			if l, ok := lock[r.Provider.Name]; ok {
 				version = l.Version
@@ -368,6 +400,10 @@ func loadProviders(tfmodule *tfconfig.Module, config *print.Config) []*Provider
 			}
 
 			key := fmt.Sprintf("%s.%s", r.Provider.Name, r.Provider.Alias)
+			if _, ok := discovered[key]; ok {
+				continue
+			}
+
 			discovered[key] = &Provider{
 				Name:    r.Provider.Name,
 				Alias:   types.String(r.Provider.Alias),
@@ -384,6 +420,7 @@ func loadProviders(tfmodule *tfconfig.Module, config *print.Config) []*Provider
 	for _, provider := range discovered {
 		providers = append(providers, provider)
 	}
+
 	return providers
 }
 
@@ -420,6 +457,13 @@ func loadResources(tfmodule *tfconfig.Module, config *print.Config) []*Resource
 
 	for _, resource := range allResources {
 		for _, r := range resource {
+			comments := loadComments(r.Pos.Filename, r.Pos.Line)
+
+			// skip over resources that are marked as being ignored
+			if strings.Contains(comments, "terraform-docs-ignore") {
+				continue
+			}
+
 			var version string
 			if rv, ok := tfmodule.RequiredProviders[r.Provider.Name]; ok {
 				version = resourceVersion(rv.VersionConstraints)
@@ -437,7 +481,7 @@ func loadResources(tfmodule *tfconfig.Module, config *print.Config) []*Resource
 
 			description := ""
 			if config.Settings.ReadComments {
-				description = loadComments(r.Pos.Filename, r.Pos.Line)
+				description = comments
 			}
 
 			discovered[key] = &Resource{


Test patch:
diff --git a/terraform/load_test.go b/terraform/load_test.go
index 1a5b57c..89a87eb 100644
--- a/terraform/load_test.go
+++ b/terraform/load_test.go
@@ -11,12 +11,14 @@ the root directory of this source tree.
 package terraform
 
 import (
+	"fmt"
 	"os"
 	"path/filepath"
 	"sort"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"golang.org/x/exp/slices"
 
 	"github.com/terraform-docs/terraform-docs/print"
 )
@@ -38,6 +40,7 @@ func TestLoadModuleWithOptions(t *testing.T) {
 	assert.Equal(true, module.HasModuleCalls())
 	assert.Equal(true, module.HasProviders())
 	assert.Equal(true, module.HasRequirements())
+	assert.Equal(true, module.HasResources())
 
 	config.Sections.Header = false
 	config.Sections.Footer = true
@@ -774,6 +777,87 @@ func TestLoadProviders(t *testing.T) {
 	}
 }
 
+func TestLoadRequirements(t *testing.T) {
+	type expected struct {
+		requirements []string
+	}
+	tests := []struct {
+		name     string
+		path     string
+		expected expected
+	}{
+		{
+			name: "load module requirements from path",
+			path: "full-example",
+			expected: expected{
+				requirements: []string{"terraform >= 0.12", "aws >= 2.15.0"},
+			},
+		},
+		{
+			name: "load module requirements from path",
+			path: "no-requirements",
+			expected: expected{
+				requirements: []string{},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+
+			module, _ := loadModule(filepath.Join("testdata", tt.path))
+			requirements := loadRequirements(module)
+
+			assert.Equal(len(tt.expected.requirements), len(requirements))
+
+			for i, r := range tt.expected.requirements {
+				assert.Equal(r, fmt.Sprintf("%s %s", requirements[i].Name, requirements[i].Version))
+			}
+		})
+	}
+}
+
+func TestLoadResources(t *testing.T) {
+	type expected struct {
+		resources []string
+	}
+	tests := []struct {
+		name     string
+		path     string
+		expected expected
+	}{
+		{
+			name: "load module resources from path",
+			path: "full-example",
+			expected: expected{
+				resources: []string{"tls_private_key.baz", "aws_caller_identity.current", "null_resource.foo"},
+			},
+		},
+		{
+			name: "load module resources from path",
+			path: "no-resources",
+			expected: expected{
+				resources: []string{},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+
+			config := print.NewConfig()
+			module, _ := loadModule(filepath.Join("testdata", tt.path))
+			resources := loadResources(module, config)
+
+			assert.Equal(len(tt.expected.resources), len(resources))
+
+			for _, r := range resources {
+				assert.True(slices.Contains(tt.expected.resources, fmt.Sprintf("%s_%s.%s", r.ProviderName, r.Type, r.Name)))
+			}
+		})
+	}
+}
+
 func TestLoadComments(t *testing.T) {
 	tests := []struct {
 		name       string
diff --git a/terraform/testdata/full-example/main.tf b/terraform/testdata/full-example/main.tf
index d9946a7..de5adc3 100644
--- a/terraform/testdata/full-example/main.tf
+++ b/terraform/testdata/full-example/main.tf
@@ -21,8 +21,16 @@ data "aws_caller_identity" "current" {
   provider = "aws"
 }
 
+# terraform-docs-ignore
+data "aws_caller_identity" "ignored" {
+  provider = "aws"
+}
+
 resource "null_resource" "foo" {}
 
+# terraform-docs-ignore
+resource "null_resource" "ignored" {}
+
 module "foo" {
   source  = "bar"
   version = "1.2.3"
@@ -35,3 +43,9 @@ module "foobar" {
 locals {
   arn = provider::aws::arn_parse("arn:aws:iam::444455556666:role/example")
 }
+
+// terraform-docs-ignore
+module "ignored" {
+  source  = "baz"
+  version = "1.2.3"
+}
diff --git a/terraform/testdata/full-example/outputs.tf b/terraform/testdata/full-example/outputs.tf
index 012ffb6..211ae2a 100644
--- a/terraform/testdata/full-example/outputs.tf
+++ b/terraform/testdata/full-example/outputs.tf
@@ -17,3 +17,8 @@ output "B" {
 output "D" {
   value = null
 }
+
+# terraform-docs-ignore
+output "ignored" {
+  value = "e"
+}
diff --git a/terraform/testdata/full-example/variables.tf b/terraform/testdata/full-example/variables.tf
index c504a0f..f395bbd 100644
--- a/terraform/testdata/full-example/variables.tf
+++ b/terraform/testdata/full-example/variables.tf
@@ -28,3 +28,9 @@ variable "G" {
   description = "G description"
   default     = null
 }
+
+# terraform-docs-ignore
+variable "ignored" {
+  description = "H description"
+  default = null
+}
diff --git a/terraform/testdata/no-requirements/main.tf b/terraform/testdata/no-requirements/main.tf
new file mode 100644
index 0000000..e69de29
diff --git a/terraform/testdata/no-resources/main.tf b/terraform/testdata/no-resources/main.tf
new file mode 100644
index 0000000..e69de29
diff --git a/terraform/testdata/read-comments/variables.tf b/terraform/testdata/read-comments/variables.tf
index be42f66..5ad8d3f 100644
--- a/terraform/testdata/read-comments/variables.tf
+++ b/terraform/testdata/read-comments/variables.tf
@@ -7,3 +7,8 @@ variable "B" {
 output "B" {
   value = "b"
 }
+
+// terraform-docs-ignore
+output "ignored" {
+  value = "c"
+}