Move template package from internal to public

Signed-off-by: Khosrow Moossavi <khos2ow@gmail.com>
This commit is contained in:
Khosrow Moossavi
2021-09-28 14:05:59 -04:00
parent b3ff51475c
commit ca8f8333d4
74 changed files with 296 additions and 257 deletions

52
terraform/doc.go Normal file
View File

@@ -0,0 +1,52 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
// Package terraform is the representation of a Terraform Module.
//
// It contains:
//
// • Header: Module header found in shape of multi line '*.tf' comments or an entire file
//
// • Footer: Module footer found in shape of multi line '*.tf' comments or an entire file
//
// • Inputs: List of input 'variables' extracted from the Terraform module .tf files
//
// • ModuleCalls: List of 'modules' extracted from the Terraform module .tf files
//
// • Outputs: List of 'outputs' extracted from Terraform module .tf files
//
// • Providers: List of 'providers' extracted from resources used in Terraform module
//
// • Requirements: List of 'requirements' extracted from the Terraform module .tf files
//
// • Resources: List of 'resources' extracted from the Terraform module .tf files
//
// Usage
//
// options := &terraform.Options{
// Path: "./examples",
// ShowHeader: true,
// HeaderFromFile: "main.tf",
// ShowFooter: true,
// FooterFromFile: "footer.md",
// SortBy: &terraform.SortBy{
// Name: true,
// },
// ReadComments: true,
// }
//
// tfmodule, err := terraform.LoadWithOptions(options)
// if err != nil {
// log.Fatal(err)
// }
//
// ...
//
package terraform

112
terraform/input.go Normal file
View File

@@ -0,0 +1,112 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"
terraformsdk "github.com/terraform-docs/plugin-sdk/terraform"
"github.com/terraform-docs/terraform-docs/internal/types"
)
// Input represents a Terraform input.
type Input struct {
Name string `json:"name" toml:"name" xml:"name" yaml:"name"`
Type types.String `json:"type" toml:"type" xml:"type" yaml:"type"`
Description types.String `json:"description" toml:"description" xml:"description" yaml:"description"`
Default types.Value `json:"default" toml:"default" xml:"default" yaml:"default"`
Required bool `json:"required" toml:"required" xml:"required" yaml:"required"`
Position Position `json:"-" toml:"-" xml:"-" yaml:"-"`
}
// GetValue returns JSON representation of the 'Default' value, which is an 'interface'.
// If 'Default' is a primitive type, the primitive value of 'Default' will be returned
// and not the JSON formatted of it.
func (i *Input) GetValue() string {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetIndent("", " ")
encoder.SetEscapeHTML(false)
err := encoder.Encode(i.Default)
if err != nil {
panic(err)
}
value := strings.TrimSpace(buf.String())
if value == `null` {
if i.Required {
return ""
}
return `null` // explicit 'null' value
}
return value // everything else
}
// HasDefault indicates if a Terraform variable has a default value set.
func (i *Input) HasDefault() bool {
return i.Default.HasDefault() || !i.Required
}
func sortInputsByName(x []*Input) {
sort.Slice(x, func(i, j int) bool {
return x[i].Name < x[j].Name
})
}
func sortInputsByRequired(x []*Input) {
sort.Slice(x, func(i, j int) bool {
if x[i].HasDefault() == x[j].HasDefault() {
return x[i].Name < x[j].Name
}
return !x[i].HasDefault() && x[j].HasDefault()
})
}
func sortInputsByPosition(x []*Input) {
sort.Slice(x, func(i, j int) bool {
if x[i].Position.Filename == x[j].Position.Filename {
return x[i].Position.Line < x[j].Position.Line
}
return x[i].Position.Filename < x[j].Position.Filename
})
}
func sortInputsByType(x []*Input) {
sort.Slice(x, func(i, j int) bool {
if x[i].Type == x[j].Type {
return x[i].Name < x[j].Name
}
return x[i].Type < x[j].Type
})
}
type inputs []*Input
func (ii inputs) convert() []*terraformsdk.Input {
list := []*terraformsdk.Input{}
for _, i := range ii {
list = append(list, &terraformsdk.Input{
Name: i.Name,
Type: fmt.Sprintf("%v", i.Type.Raw()),
Description: fmt.Sprintf("%v", i.Description.Raw()),
Default: i.Default.Raw(),
Required: i.Required,
Position: terraformsdk.Position{
Filename: i.Position.Filename,
Line: i.Position.Line,
},
})
}
return list
}

300
terraform/input_test.go Normal file
View File

@@ -0,0 +1,300 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/terraform-docs/terraform-docs/internal/types"
)
func TestInputValue(t *testing.T) {
inputName := "input"
inputType := types.String("type")
inputDescr := types.String("description")
inputPos := Position{Filename: "foo.tf", Line: 13}
tests := []struct {
name string
input Input
expectValue string
expectDefault bool
expectRequired bool
}{
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(nil),
Required: true,
Position: inputPos,
},
expectValue: "",
expectDefault: false,
expectRequired: true,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(nil),
Required: false,
Position: inputPos,
},
expectValue: "null",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(true),
Required: false,
Position: inputPos,
},
expectValue: "true",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(false),
Required: false,
Position: inputPos,
},
expectValue: "false",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(""),
Required: false,
Position: inputPos,
},
expectValue: "\"\"",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf("foo"),
Required: false,
Position: inputPos,
},
expectValue: "\"foo\"",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(42),
Required: false,
Position: inputPos,
},
expectValue: "42",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(13.75),
Required: false,
Position: inputPos,
},
expectValue: "13.75",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(types.List{"a", "b", "c"}.Underlying()),
Required: false,
Position: inputPos,
},
expectValue: "[\n \"a\",\n \"b\",\n \"c\"\n]",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(types.List{}.Underlying()),
Required: false,
Position: inputPos,
},
expectValue: "[]",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(types.Map{"a": 1, "b": 2, "c": 3}.Underlying()),
Required: false,
Position: inputPos,
},
expectValue: "{\n \"a\": 1,\n \"b\": 2,\n \"c\": 3\n}",
expectDefault: true,
expectRequired: false,
},
{
name: "input Value and HasDefault",
input: Input{
Name: inputName,
Type: inputType,
Description: inputDescr,
Default: types.ValueOf(types.Map{}.Underlying()),
Required: false,
Position: inputPos,
},
expectValue: "{}",
expectDefault: true,
expectRequired: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.expectValue, tt.input.GetValue())
assert.Equal(tt.expectDefault, tt.input.HasDefault())
})
}
}
func TestInputsSorted(t *testing.T) {
inputs := sampleInputs()
tests := map[string]struct {
sortType func([]*Input)
expected []string
}{
"ByName": {
sortType: sortInputsByName,
expected: []string{"a", "b", "c", "d", "e", "f"},
},
"ByRequired": {
sortType: sortInputsByRequired,
expected: []string{"b", "d", "a", "c", "e", "f"},
},
"ByPosition": {
sortType: sortInputsByPosition,
expected: []string{"a", "d", "e", "b", "c", "f"},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
tt.sortType(inputs)
actual := make([]string, len(inputs))
for k, i := range inputs {
actual[k] = i.Name
}
assert.Equal(tt.expected, actual)
})
}
}
func sampleInputs() []*Input {
return []*Input{
{
Name: "e",
Type: types.String(""),
Description: types.String("description of e"),
Default: types.ValueOf(true),
Required: false,
Position: Position{Filename: "foo/variables.tf", Line: 35},
},
{
Name: "a",
Type: types.String("string"),
Description: types.String(""),
Default: types.ValueOf("a"),
Required: false,
Position: Position{Filename: "foo/variables.tf", Line: 10},
},
{
Name: "d",
Type: types.String("string"),
Description: types.String("description for d"),
Default: types.ValueOf(nil),
Required: true,
Position: Position{Filename: "foo/variables.tf", Line: 23},
},
{
Name: "b",
Type: types.String("number"),
Description: types.String("description of b"),
Default: types.ValueOf(nil),
Required: true,
Position: Position{Filename: "foo/variables.tf", Line: 42},
},
{
Name: "c",
Type: types.String("list"),
Description: types.String("description of c"),
Default: types.ValueOf("c"),
Required: false,
Position: Position{Filename: "foo/variables.tf", Line: 51},
},
{
Name: "f",
Type: types.String("string"),
Description: types.String("description of f"),
Default: types.ValueOf(nil),
Required: false,
Position: Position{Filename: "foo/variables.tf", Line: 59},
},
}
}

540
terraform/load.go Normal file
View File

@@ -0,0 +1,540 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/terraform-docs/terraform-config-inspect/tfconfig"
"github.com/terraform-docs/terraform-docs/internal/reader"
"github.com/terraform-docs/terraform-docs/internal/types"
)
// LoadWithOptions returns new instance of Module with all the inputs and
// outputs discovered from provided 'path' containing Terraform config
func LoadWithOptions(options *Options) (*Module, error) {
tfmodule, err := loadModule(options.Path)
if err != nil {
return nil, err
}
module, err := loadModuleItems(tfmodule, options)
if err != nil {
return nil, err
}
sortItems(module, options.SortBy)
return module, nil
}
func loadModule(path string) (*tfconfig.Module, error) {
module, diag := tfconfig.LoadModule(path)
if diag != nil && diag.HasErrors() {
return nil, diag
}
return module, nil
}
func loadModuleItems(tfmodule *tfconfig.Module, options *Options) (*Module, error) {
header, err := loadHeader(options)
if err != nil {
return nil, err
}
footer, err := loadFooter(options)
if err != nil {
return nil, err
}
inputs, required, optional := loadInputs(tfmodule, options)
modulecalls := loadModulecalls(tfmodule)
outputs, err := loadOutputs(tfmodule, options)
if err != nil {
return nil, err
}
providers := loadProviders(tfmodule, options)
requirements := loadRequirements(tfmodule)
resources := loadResources(tfmodule)
return &Module{
Header: header,
Footer: footer,
Inputs: inputs,
ModuleCalls: modulecalls,
Outputs: outputs,
Providers: providers,
Requirements: requirements,
Resources: resources,
RequiredInputs: required,
OptionalInputs: optional,
}, nil
}
func getFileFormat(filename string) string {
if filename == "" {
return ""
}
last := strings.LastIndex(filename, ".")
if last == -1 {
return ""
}
return filename[last:]
}
func isFileFormatSupported(filename string, section string) (bool, error) {
if section == "" {
return false, errors.New("section is missing")
}
if filename == "" {
return false, fmt.Errorf("--%s-from value is missing", section)
}
switch getFileFormat(filename) {
case ".adoc", ".md", ".tf", ".txt":
return true, nil
}
return false, fmt.Errorf("only .adoc, .md, .tf, and .txt formats are supported to read %s from", section)
}
func loadHeader(options *Options) (string, error) {
if !options.ShowHeader {
return "", nil
}
return loadSection(options, options.HeaderFromFile, "header")
}
func loadFooter(options *Options) (string, error) {
if !options.ShowFooter {
return "", nil
}
return loadSection(options, options.FooterFromFile, "footer")
}
func loadSection(options *Options, file string, section string) (string, error) { //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.
if section == "" {
return "", errors.New("section is missing")
}
filename := filepath.Join(options.Path, file)
if ok, err := isFileFormatSupported(file, section); !ok {
return "", err
}
if info, err := os.Stat(filename); os.IsNotExist(err) || info.IsDir() {
if section == "header" && file == "main.tf" {
return "", nil // absorb the error to not break workflow for default value of header and missing 'main.tf'
}
return "", err // user explicitly asked for a file which doesn't exist
}
if getFileFormat(file) != ".tf" {
content, err := ioutil.ReadFile(filepath.Clean(filename))
if err != nil {
return "", err
}
return string(content), nil
}
lines := reader.Lines{
FileName: filename,
LineNum: -1,
Condition: func(line string) bool {
line = strings.TrimSpace(line)
return strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") || strings.HasPrefix(line, "*/")
},
Parser: func(line string) (string, bool) {
tmp := strings.TrimSpace(line)
if strings.HasPrefix(tmp, "/*") || strings.HasPrefix(tmp, "*/") {
return "", false
}
if tmp == "*" {
return "", true
}
line = strings.TrimLeft(line, " ")
line = strings.TrimRight(line, "\r\n")
line = strings.TrimPrefix(line, "* ")
return line, true
},
}
sectionText, err := lines.Extract()
if err != nil {
return "", err
}
return strings.Join(sectionText, "\n"), nil
}
func loadInputs(tfmodule *tfconfig.Module, options *Options) ([]*Input, []*Input, []*Input) {
var inputs = make([]*Input, 0, len(tfmodule.Variables))
var required = make([]*Input, 0, len(tfmodule.Variables))
var optional = make([]*Input, 0, len(tfmodule.Variables))
for _, input := range tfmodule.Variables {
// 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 == "" && options.ReadComments {
inputDescription = loadComments(input.Pos.Filename, input.Pos.Line)
}
i := &Input{
Name: input.Name,
Type: types.TypeOf(input.Type, input.Default),
Description: types.String(inputDescription),
Default: types.ValueOf(input.Default),
Required: input.Required,
Position: Position{
Filename: input.Pos.Filename,
Line: input.Pos.Line,
},
}
inputs = append(inputs, i)
if i.HasDefault() {
optional = append(optional, i)
} else {
required = append(required, i)
}
}
return inputs, required, optional
}
func formatSource(s, v string) (source, version string) {
substr := "?ref="
if v != "" {
return s, v
}
pos := strings.LastIndex(s, substr)
if pos == -1 {
return s, version
}
adjustedPos := pos + len(substr)
if adjustedPos >= len(s) {
return s, version
}
source = s[0:pos]
version = s[adjustedPos:]
return source, version
}
func loadModulecalls(tfmodule *tfconfig.Module) []*ModuleCall {
var modules = make([]*ModuleCall, 0)
var source, version string
for _, m := range tfmodule.ModuleCalls {
source, version = formatSource(m.Source, m.Version)
modules = append(modules, &ModuleCall{
Name: m.Name,
Source: source,
Version: version,
Position: Position{
Filename: m.Pos.Filename,
Line: m.Pos.Line,
},
})
}
return modules
}
func loadOutputs(tfmodule *tfconfig.Module, options *Options) ([]*Output, error) {
outputs := make([]*Output, 0, len(tfmodule.Outputs))
values := make(map[string]*output)
if options.OutputValues {
var err error
values, err = loadOutputValues(options)
if err != nil {
return nil, err
}
}
for _, o := range tfmodule.Outputs {
description := o.Description
if description == "" && options.ReadComments {
description = loadComments(o.Pos.Filename, o.Pos.Line)
}
output := &Output{
Name: o.Name,
Description: types.String(description),
Position: Position{
Filename: o.Pos.Filename,
Line: o.Pos.Line,
},
ShowValue: options.OutputValues,
}
if options.OutputValues {
output.Sensitive = values[output.Name].Sensitive
if values[output.Name].Sensitive {
output.Value = types.ValueOf(`<sensitive>`)
} else {
output.Value = types.ValueOf(values[output.Name].Value)
}
}
outputs = append(outputs, output)
}
return outputs, nil
}
func loadOutputValues(options *Options) (map[string]*output, error) {
var out []byte
var err error
if options.OutputValuesPath == "" {
cmd := exec.Command("terraform", "output", "-json")
cmd.Dir = options.Path
if out, err = cmd.Output(); err != nil {
return nil, fmt.Errorf("caught error while reading the terraform outputs: %w", err)
}
} else if out, err = ioutil.ReadFile(options.OutputValuesPath); err != nil {
return nil, fmt.Errorf("caught error while reading the terraform outputs file at %s: %w", options.OutputValuesPath, err)
}
var terraformOutputs map[string]*output
err = json.Unmarshal(out, &terraformOutputs)
if err != nil {
return nil, err
}
return terraformOutputs, err
}
func loadProviders(tfmodule *tfconfig.Module, options *Options) []*Provider {
type provider struct {
Name string `hcl:"name,label"`
Version string `hcl:"version"`
Constraints *string `hcl:"constraints"`
Hashes []string `hcl:"hashes"`
}
type lockfile struct {
Provider []provider `hcl:"provider,block"`
}
lock := make(map[string]provider)
if options.UseLockFile {
var lf lockfile
filename := filepath.Join(options.Path, ".terraform.lock.hcl")
if err := hclsimple.DecodeFile(filename, nil, &lf); err == nil {
for i := range lf.Provider {
segments := strings.Split(lf.Provider[i].Name, "/")
name := segments[len(segments)-1]
lock[name] = lf.Provider[i]
}
}
}
resources := []map[string]*tfconfig.Resource{tfmodule.ManagedResources, tfmodule.DataResources}
discovered := make(map[string]*Provider)
for _, resource := range resources {
for _, r := range resource {
var version = ""
if l, ok := lock[r.Provider.Name]; ok {
version = l.Version
} else if rv, ok := tfmodule.RequiredProviders[r.Provider.Name]; ok && len(rv.VersionConstraints) > 0 {
version = strings.Join(rv.VersionConstraints, " ")
}
key := fmt.Sprintf("%s.%s", r.Provider.Name, r.Provider.Alias)
discovered[key] = &Provider{
Name: r.Provider.Name,
Alias: types.String(r.Provider.Alias),
Version: types.String(version),
Position: Position{
Filename: r.Pos.Filename,
Line: r.Pos.Line,
},
}
}
}
providers := make([]*Provider, 0, len(discovered))
for _, provider := range discovered {
providers = append(providers, provider)
}
return providers
}
func loadRequirements(tfmodule *tfconfig.Module) []*Requirement {
var requirements = make([]*Requirement, 0)
for _, core := range tfmodule.RequiredCore {
requirements = append(requirements, &Requirement{
Name: "terraform",
Version: types.String(core),
})
}
names := make([]string, 0, len(tfmodule.RequiredProviders))
for n := range tfmodule.RequiredProviders {
names = append(names, n)
}
sort.Strings(names)
for _, name := range names {
for _, version := range tfmodule.RequiredProviders[name].VersionConstraints {
requirements = append(requirements, &Requirement{
Name: name,
Version: types.String(version),
})
}
}
return requirements
}
func loadResources(tfmodule *tfconfig.Module) []*Resource {
allResources := []map[string]*tfconfig.Resource{tfmodule.ManagedResources, tfmodule.DataResources}
discovered := make(map[string]*Resource)
for _, resource := range allResources {
for _, r := range resource {
var version string
if rv, ok := tfmodule.RequiredProviders[r.Provider.Name]; ok {
version = resourceVersion(rv.VersionConstraints)
}
var source string
if len(tfmodule.RequiredProviders[r.Provider.Name].Source) > 0 {
source = tfmodule.RequiredProviders[r.Provider.Name].Source
} else {
source = fmt.Sprintf("%s/%s", "hashicorp", r.Provider.Name)
}
rType := strings.TrimPrefix(r.Type, r.Provider.Name+"_")
key := fmt.Sprintf("%s.%s.%s.%s", r.Provider.Name, r.Mode, rType, r.Name)
discovered[key] = &Resource{
Type: rType,
Name: r.Name,
Mode: r.Mode.String(),
ProviderName: r.Provider.Name,
ProviderSource: source,
Version: types.String(version),
Position: Position{
Filename: r.Pos.Filename,
Line: r.Pos.Line,
},
}
}
}
resources := make([]*Resource, 0, len(discovered))
for _, resource := range discovered {
resources = append(resources, resource)
}
return resources
}
func resourceVersion(constraints []string) string {
if len(constraints) == 0 {
return "latest"
}
versionParts := strings.Split(constraints[len(constraints)-1], " ")
switch len(versionParts) {
case 1:
if _, err := strconv.Atoi(versionParts[0][0:1]); err != nil {
if versionParts[0][0:1] == "=" {
return versionParts[0][1:]
}
return "latest"
}
return versionParts[0]
case 2:
if versionParts[0] == "=" {
return versionParts[1]
}
}
return "latest"
}
func loadComments(filename string, lineNum int) string {
lines := reader.Lines{
FileName: filename,
LineNum: lineNum,
Condition: func(line string) bool {
return strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//")
},
Parser: func(line string) (string, bool) {
line = strings.TrimSpace(line)
line = strings.TrimPrefix(line, "#")
line = strings.TrimPrefix(line, "//")
line = strings.TrimSpace(line)
return line, true
},
}
comment, err := lines.Extract()
if err != nil {
return "" // absorb the error, we don't need to bubble it up or break the execution
}
return strings.Join(comment, " ")
}
func sortItems(tfmodule *Module, sortby *SortBy) { //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.
// inputs
switch {
case sortby.Type:
sortInputsByType(tfmodule.Inputs)
sortInputsByType(tfmodule.RequiredInputs)
sortInputsByType(tfmodule.OptionalInputs)
case sortby.Required:
sortInputsByRequired(tfmodule.Inputs)
sortInputsByRequired(tfmodule.RequiredInputs)
sortInputsByRequired(tfmodule.OptionalInputs)
case sortby.Name:
sortInputsByName(tfmodule.Inputs)
sortInputsByName(tfmodule.RequiredInputs)
sortInputsByName(tfmodule.OptionalInputs)
default:
sortInputsByPosition(tfmodule.Inputs)
sortInputsByPosition(tfmodule.RequiredInputs)
sortInputsByPosition(tfmodule.OptionalInputs)
}
// outputs
if sortby.Name || sortby.Required || sortby.Type {
sortOutputsByName(tfmodule.Outputs)
} else {
sortOutputsByPosition(tfmodule.Outputs)
}
// providers
if sortby.Name || sortby.Required || sortby.Type {
sortProvidersByName(tfmodule.Providers)
} else {
sortProvidersByPosition(tfmodule.Providers)
}
// resources (always sorted)
sortResourcesByType(tfmodule.Resources)
// modules
switch {
case sortby.Name || sortby.Required:
sortModulecallsByName(tfmodule.ModuleCalls)
case sortby.Type:
sortModulecallsBySource(tfmodule.ModuleCalls)
default:
sortModulecallsByPosition(tfmodule.ModuleCalls)
}
}

971
terraform/load_test.go Normal file
View File

@@ -0,0 +1,971 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"io/ioutil"
"path/filepath"
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadModuleWithOptions(t *testing.T) {
assert := assert.New(t)
options, _ := NewOptions().With(&Options{
Path: filepath.Join("testdata", "full-example"),
})
module, err := LoadWithOptions(options)
assert.Nil(err)
assert.Equal(true, module.HasHeader())
assert.Equal(false, module.HasFooter())
assert.Equal(true, module.HasInputs())
assert.Equal(true, module.HasOutputs())
assert.Equal(true, module.HasModuleCalls())
assert.Equal(true, module.HasProviders())
assert.Equal(true, module.HasRequirements())
options, _ = options.With(&Options{
FooterFromFile: "doc.tf",
ShowFooter: true,
})
// options.With and .WithOverwrite will not overwrite true with false
options.ShowHeader = false
module, err = LoadWithOptions(options)
assert.Nil(err)
assert.Equal(true, module.HasFooter())
assert.Equal(false, module.HasHeader())
}
func TestLoadModule(t *testing.T) {
tests := []struct {
name string
path string
wantErr bool
}{
{
name: "load module from path",
path: "full-example",
wantErr: false,
},
{
name: "load module from path",
path: "non-exist",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
_, err := loadModule(filepath.Join("testdata", tt.path))
if tt.wantErr {
assert.NotNil(err)
} else {
assert.Nil(err)
}
})
}
}
func TestGetFileFormat(t *testing.T) {
tests := []struct {
name string
filename string
expected string
}{
{
name: "get file format",
filename: "main.tf",
expected: ".tf",
},
{
name: "get file format",
filename: "main.file.tf",
expected: ".tf",
},
{
name: "get file format",
filename: "main_file.tf",
expected: ".tf",
},
{
name: "get file format",
filename: "main.file_tf",
expected: ".file_tf",
},
{
name: "get file format",
filename: "main_file_tf",
expected: "",
},
{
name: "get file format",
filename: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual := getFileFormat(tt.filename)
assert.Equal(tt.expected, actual)
})
}
}
func TestIsFileFormatSupported(t *testing.T) {
tests := []struct {
name string
filename string
expected bool
wantErr bool
errText string
section string
}{
{
name: "is file format supported",
filename: "main.adoc",
expected: true,
wantErr: false,
errText: "",
section: "header",
},
{
name: "is file format supported",
filename: "main.md",
expected: true,
wantErr: false,
errText: "",
section: "header",
},
{
name: "is file format supported",
filename: "main.tf",
expected: true,
wantErr: false,
errText: "",
section: "header",
},
{
name: "is file format supported",
filename: "main.txt",
expected: true,
wantErr: false,
errText: "",
section: "header",
},
{
name: "is file format supported",
filename: "main.doc",
expected: false,
wantErr: true,
errText: "only .adoc, .md, .tf, and .txt formats are supported to read header from",
section: "header",
},
{
name: "is file format supported",
filename: "",
expected: false,
wantErr: true,
errText: "--header-from value is missing",
section: "header",
}, {
name: "err message changes for footer",
filename: "main.doc",
expected: false,
wantErr: true,
errText: "only .adoc, .md, .tf, and .txt formats are supported to read footer from",
section: "footer",
},
{
name: "err message changes for footer",
filename: "",
expected: false,
wantErr: true,
errText: "--footer-from value is missing",
section: "footer",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual, err := isFileFormatSupported(tt.filename, tt.section)
if tt.wantErr {
assert.NotNil(err)
assert.Equal(tt.errText, err.Error())
} else {
assert.Nil(err)
assert.Equal(tt.expected, actual)
}
})
}
}
func TestLoadHeader(t *testing.T) {
tests := []struct {
name string
testData string
showHeader bool
expectedData func() (string, error)
}{
{
name: "loadHeader should return a string from file",
testData: "full-example",
showHeader: true,
expectedData: func() (string, error) {
path := filepath.Join("testdata", "expected", "full-example-mainTf-Header.golden")
data, err := ioutil.ReadFile(path)
return string(data), err
},
},
{
name: "loadHeader should return an empty string if not shown",
testData: "",
showHeader: false,
expectedData: func() (string, error) {
return "", nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
options, err := NewOptions().With(&Options{
Path: filepath.Join("testdata", tt.testData),
ShowHeader: tt.showHeader,
})
assert.Nil(err)
expected, err := tt.expectedData()
assert.Nil(err)
header, err := loadHeader(options)
assert.Nil(err)
assert.Equal(expected, header)
})
}
}
func TestLoadFooter(t *testing.T) {
tests := []struct {
name string
testData string
footerFile string
showFooter bool
expectedData func() (string, error)
}{
{
name: "loadFooter should return a string from file",
testData: "full-example",
footerFile: "main.tf",
showFooter: true,
expectedData: func() (string, error) {
path := filepath.Join("testdata", "expected", "full-example-mainTf-Header.golden")
data, err := ioutil.ReadFile(path)
return string(data), err
},
},
{
name: "loadHeader should return an empty string if not shown",
testData: "",
footerFile: "",
showFooter: false,
expectedData: func() (string, error) {
return "", nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
options, err := NewOptions().With(&Options{
Path: filepath.Join("testdata", tt.testData),
FooterFromFile: tt.footerFile,
ShowFooter: tt.showFooter,
})
assert.Nil(err)
expected, err := tt.expectedData()
assert.Nil(err)
header, err := loadFooter(options)
assert.Nil(err)
assert.Equal(expected, header)
})
}
}
func TestLoadSections(t *testing.T) {
tests := []struct {
name string
path string
file string
expected string
wantErr bool
errText string
section string
}{
{
name: "load module header from path",
path: "full-example",
file: "main.tf",
expected: "Example of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n\nEven inline **formatting** in _here_ is possible.\nand some [link](https://domain.com/)",
wantErr: false,
errText: "",
section: "header",
},
{
name: "load module header from path",
path: "full-example",
file: "doc.tf",
expected: "Custom Header:\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2",
wantErr: false,
errText: "",
section: "header",
},
{
name: "load module header from path",
path: "full-example",
file: "doc.md",
expected: "# Custom Header\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n",
wantErr: false,
errText: "",
section: "header",
},
{
name: "load module header from path",
path: "full-example",
file: "doc.adoc",
expected: "= Custom Header\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n",
wantErr: false,
errText: "",
section: "header",
},
{
name: "load module header from path",
path: "full-example",
file: "doc.txt",
expected: "# Custom Header\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n",
wantErr: false,
errText: "",
section: "header",
},
{
name: "load module header from path",
path: "no-inputs",
file: "main.tf",
expected: "",
wantErr: false,
errText: "",
section: "header",
},
{
name: "load module header from path",
path: "full-example",
file: "non-existent.tf",
expected: "",
wantErr: true,
errText: "stat testdata/full-example/non-existent.tf: no such file or directory",
section: "header",
},
{
name: "no error if header file is missing and is default 'main.tf'",
path: "inputs-lf",
file: "main.tf",
expected: "",
wantErr: false,
errText: "",
section: "header",
},
{
name: "error if footer file is missing even if 'main.tf'",
path: "inputs-lf",
file: "main.tf",
expected: "",
wantErr: true,
errText: "stat testdata/inputs-lf/main.tf: no such file or directory",
section: "footer",
},
{
name: "load module header from path",
path: "full-example",
file: "wrong-formate.docx",
expected: "",
wantErr: true,
errText: "only .adoc, .md, .tf, and .txt formats are supported to read footer from",
section: "footer",
},
{
name: "load module header from path",
path: "full-example",
file: "",
expected: "",
wantErr: true,
errText: "--header-from value is missing",
section: "header",
},
{
name: "load module header from path",
path: "empty-header",
file: "",
expected: "",
wantErr: true,
errText: "--header-from value is missing",
section: "header",
},
{
name: "load module footer from path",
path: "non-exist",
file: "",
expected: "",
wantErr: true,
errText: "--footer-from value is missing",
section: "footer",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
options := &Options{Path: filepath.Join("testdata", tt.path)}
actual, err := loadSection(options, tt.file, tt.section)
if tt.wantErr {
assert.NotNil(err)
assert.Equal(tt.errText, err.Error())
} else {
assert.Nil(err)
assert.Equal(tt.expected, actual)
}
})
}
}
func TestLoadInputs(t *testing.T) {
type expected struct {
inputs int
requireds int
optionals int
}
tests := []struct {
name string
path string
expected expected
}{
{
name: "load module inputs from path",
path: "full-example",
expected: expected{
inputs: 7,
requireds: 2,
optionals: 5,
},
},
{
name: "load module inputs from path",
path: "no-required-inputs",
expected: expected{
inputs: 6,
requireds: 0,
optionals: 6,
},
},
{
name: "load module inputs from path",
path: "no-optional-inputs",
expected: expected{
inputs: 6,
requireds: 6,
optionals: 0,
},
},
{
name: "load module inputs from path",
path: "no-inputs",
expected: expected{
inputs: 0,
requireds: 0,
optionals: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
module, _ := loadModule(filepath.Join("testdata", tt.path))
options := NewOptions()
inputs, requireds, optionals := loadInputs(module, options)
assert.Equal(tt.expected.inputs, len(inputs))
assert.Equal(tt.expected.requireds, len(requireds))
assert.Equal(tt.expected.optionals, len(optionals))
})
}
}
func TestLoadModulecalls(t *testing.T) {
tests := []struct {
name string
path string
expected int
}{
{
name: "load modulecalls from path",
path: "full-example",
expected: 2,
},
{
name: "load modulecalls from path",
path: "no-modulecalls",
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
module, _ := loadModule(filepath.Join("testdata", tt.path))
modulecalls := loadModulecalls(module)
assert.Equal(tt.expected, len(modulecalls))
})
}
}
func TestLoadInputsLineEnding(t *testing.T) {
tests := []struct {
name string
path string
expected string
}{
{
name: "load module inputs from file with lf line ending",
path: "inputs-lf",
expected: "The quick brown fox jumps\nover the lazy dog\n",
},
{
name: "load module inputs from file with crlf line ending",
path: "inputs-crlf",
expected: "The quick brown fox jumps\nover the lazy dog\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
module, _ := loadModule(filepath.Join("testdata", tt.path))
options := NewOptions()
inputs, _, _ := loadInputs(module, options)
assert.Equal(1, len(inputs))
assert.Equal(tt.expected, string(inputs[0].Description))
})
}
}
func TestLoadOutputs(t *testing.T) {
type expected struct {
outputs int
}
tests := []struct {
name string
path string
expected expected
}{
{
name: "load module outputs from path",
path: "full-example",
expected: expected{
outputs: 3,
},
},
{
name: "load module outputs from path",
path: "no-outputs",
expected: expected{
outputs: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
options := NewOptions()
module, _ := loadModule(filepath.Join("testdata", tt.path))
outputs, err := loadOutputs(module, options)
assert.Nil(err)
assert.Equal(tt.expected.outputs, len(outputs))
for _, v := range outputs {
assert.Equal(false, v.ShowValue)
}
})
}
}
func TestLoadOutputsValues(t *testing.T) {
type expected struct {
outputs int
}
tests := []struct {
name string
path string
outputPath string
expected expected
wantErr bool
}{
{
name: "load module outputs with values from path",
path: "full-example",
outputPath: "output-values.json",
expected: expected{
outputs: 3,
},
wantErr: false,
},
{
name: "load module outputs with values from path",
path: "full-example",
outputPath: "no-file.json",
expected: expected{},
wantErr: true,
},
{
name: "load module outputs with values from path",
path: "no-outputs",
outputPath: "no-file.json",
expected: expected{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
options, _ := NewOptions().With(&Options{
OutputValues: true,
OutputValuesPath: filepath.Join("testdata", tt.path, tt.outputPath),
})
module, _ := loadModule(filepath.Join("testdata", tt.path))
outputs, err := loadOutputs(module, options)
if tt.wantErr {
assert.NotNil(err)
} else {
assert.Nil(err)
assert.Equal(tt.expected.outputs, len(outputs))
for _, v := range outputs {
assert.Equal(true, v.ShowValue)
}
}
})
}
}
func TestLoadProviders(t *testing.T) {
type expected struct {
providers []string
}
tests := []struct {
name string
path string
lockfile bool
expected expected
}{
{
name: "load module providers from path",
path: "full-example",
lockfile: false,
expected: expected{
providers: []string{"aws->= 2.15.0", "null-", "tls-"},
},
},
{
name: "load module providers from path",
path: "with-lock-file",
lockfile: true,
expected: expected{
providers: []string{"aws-3.42.0", "null-3.1.0", "tls-3.1.0"},
},
},
{
name: "load module providers from path",
path: "with-lock-file",
lockfile: false,
expected: expected{
providers: []string{"aws->= 2.15.0", "null-", "tls-"},
},
},
{
name: "load module providers from path",
path: "no-providers",
expected: expected{
providers: []string{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
options, _ := NewOptions().With(&Options{
Path: filepath.Join("testdata", tt.path),
})
options.UseLockFile = tt.lockfile
module, _ := loadModule(filepath.Join("testdata", tt.path))
providers := loadProviders(module, options)
actual := []string{}
for _, p := range providers {
actual = append(actual, p.FullName()+"-"+string(p.Version))
}
sort.Strings(actual)
assert.Equal(tt.expected.providers, actual)
})
}
}
func TestLoadComments(t *testing.T) {
tests := []struct {
name string
path string
fileName string
lineNumber int
expected string
}{
{
name: "load resource comment from file",
path: "full-example",
fileName: "variables.tf",
lineNumber: 2,
expected: "D description",
},
{
name: "load resource comment from file",
path: "full-example",
fileName: "variables.tf",
lineNumber: 16,
expected: "A Description in multiple lines",
},
{
name: "load resource comment from file with wrong line number",
path: "full-example",
fileName: "variables.tf",
lineNumber: 100,
expected: "",
},
{
name: "load resource comment from non existing file",
path: "full-example",
fileName: "non-exist.tf",
lineNumber: 5,
expected: "",
},
{
name: "load resource comment from non existing file",
path: "non-exist",
fileName: "variables.tf",
lineNumber: 5,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual := loadComments(filepath.Join("testdata", tt.path, tt.fileName), tt.lineNumber)
assert.Equal(tt.expected, actual)
})
}
}
func TestReadComments(t *testing.T) {
tests := []struct {
name string
path string
fileName string
readComments bool
expected string
}{
{
name: "Validate description when 'ReadComments' is false",
path: "read-comments",
fileName: "variables.tf",
readComments: false,
expected: "",
},
{
name: "Validate description when 'ReadComments' is true",
path: "read-comments",
fileName: "variables.tf",
readComments: true,
expected: "B description",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
options := NewOptions()
options.ReadComments = tt.readComments
module, err := loadModule(filepath.Join("testdata", tt.path))
assert.Nil(err)
inputs, _, _ := loadInputs(module, options)
assert.Equal(1, len(inputs))
assert.Equal(tt.expected, string(inputs[0].Description))
outputs, _ := loadOutputs(module, options)
assert.Equal(1, len(outputs))
assert.Equal(tt.expected, string(outputs[0].Description))
})
}
}
func TestSortItems(t *testing.T) {
type expected struct {
inputs []string
required []string
optional []string
outputs []string
providers []string
}
tests := []struct {
name string
path string
sort *SortBy
expected expected
}{
{
name: "sort module items",
path: "full-example",
sort: &SortBy{Name: false, Required: false, Type: false},
expected: expected{
inputs: []string{"D", "B", "E", "A", "C", "F", "G"},
required: []string{"A", "F"},
optional: []string{"D", "B", "E", "C", "G"},
outputs: []string{"C", "A", "B"},
providers: []string{"tls", "aws", "null"},
},
},
{
name: "sort module items",
path: "full-example",
sort: &SortBy{Name: true, Required: false, Type: false},
expected: expected{
inputs: []string{"A", "B", "C", "D", "E", "F", "G"},
required: []string{"A", "F"},
optional: []string{"B", "C", "D", "E", "G"},
outputs: []string{"A", "B", "C"},
providers: []string{"aws", "null", "tls"},
},
},
{
name: "sort module items",
path: "full-example",
sort: &SortBy{Name: false, Required: true, Type: false},
expected: expected{
inputs: []string{"A", "F", "B", "C", "D", "E", "G"},
required: []string{"A", "F"},
optional: []string{"B", "C", "D", "E", "G"},
outputs: []string{"A", "B", "C"},
providers: []string{"aws", "null", "tls"},
},
},
{
name: "sort module items",
path: "full-example",
sort: &SortBy{Name: false, Required: false, Type: true},
expected: expected{
inputs: []string{"A", "F", "G", "B", "C", "D", "E"},
required: []string{"A", "F"},
optional: []string{"G", "B", "C", "D", "E"},
outputs: []string{"A", "B", "C"},
providers: []string{"aws", "null", "tls"},
},
},
{
name: "sort module items",
path: "full-example",
sort: &SortBy{Name: true, Required: true, Type: false},
expected: expected{
inputs: []string{"A", "F", "B", "C", "D", "E", "G"},
required: []string{"A", "F"},
optional: []string{"B", "C", "D", "E", "G"},
outputs: []string{"A", "B", "C"},
providers: []string{"aws", "null", "tls"},
},
},
{
name: "sort module items",
path: "full-example",
sort: &SortBy{Name: true, Required: false, Type: true},
expected: expected{
inputs: []string{"A", "F", "G", "B", "C", "D", "E"},
required: []string{"A", "F"},
optional: []string{"G", "B", "C", "D", "E"},
outputs: []string{"A", "B", "C"},
providers: []string{"aws", "null", "tls"},
},
},
{
name: "sort module items",
path: "full-example",
sort: &SortBy{Name: false, Required: true, Type: true},
expected: expected{
inputs: []string{"A", "F", "G", "B", "C", "D", "E"},
required: []string{"A", "F"},
optional: []string{"G", "B", "C", "D", "E"},
outputs: []string{"A", "B", "C"},
providers: []string{"aws", "null", "tls"},
},
},
{
name: "sort module items",
path: "full-example",
sort: &SortBy{Name: true, Required: true, Type: true},
expected: expected{
inputs: []string{"A", "F", "G", "B", "C", "D", "E"},
required: []string{"A", "F"},
optional: []string{"G", "B", "C", "D", "E"},
outputs: []string{"A", "B", "C"},
providers: []string{"aws", "null", "tls"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
path := filepath.Join("testdata", tt.path)
options, _ := NewOptions().With(&Options{
Path: path,
SortBy: tt.sort,
})
tfmodule, _ := loadModule(path)
module, err := loadModuleItems(tfmodule, options)
assert.Nil(err)
sortItems(module, tt.sort)
for i, v := range module.Inputs {
assert.Equal(tt.expected.inputs[i], v.Name)
}
for i, v := range module.RequiredInputs {
assert.Equal(tt.expected.required[i], v.Name)
}
for i, v := range module.OptionalInputs {
assert.Equal(tt.expected.optional[i], v.Name)
}
for i, v := range module.Outputs {
assert.Equal(tt.expected.outputs[i], v.Name)
}
for i, v := range module.Providers {
assert.Equal(tt.expected.providers[i], v.Name)
}
})
}
}

90
terraform/module.go Normal file
View File

@@ -0,0 +1,90 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"encoding/xml"
terraformsdk "github.com/terraform-docs/plugin-sdk/terraform"
)
// Module represents a Terraform module. It consists of
type Module struct {
XMLName xml.Name `json:"-" toml:"-" xml:"module" yaml:"-"`
Header string `json:"header" toml:"header" xml:"header" yaml:"header"`
Footer string `json:"footer" toml:"footer" xml:"footer" yaml:"footer"`
Inputs []*Input `json:"inputs" toml:"inputs" xml:"inputs>input" yaml:"inputs"`
ModuleCalls []*ModuleCall `json:"modules" toml:"modules" xml:"modules>module" yaml:"modules"`
Outputs []*Output `json:"outputs" toml:"outputs" xml:"outputs>output" yaml:"outputs"`
Providers []*Provider `json:"providers" toml:"providers" xml:"providers>provider" yaml:"providers"`
Requirements []*Requirement `json:"requirements" toml:"requirements" xml:"requirements>requirement" yaml:"requirements"`
Resources []*Resource `json:"resources" toml:"resources" xml:"resources>resource" yaml:"resources"`
RequiredInputs []*Input `json:"-" toml:"-" xml:"-" yaml:"-"`
OptionalInputs []*Input `json:"-" toml:"-" xml:"-" yaml:"-"`
}
// HasHeader indicates if the module has header.
func (m *Module) HasHeader() bool {
return len(m.Header) > 0
}
// HasFooter indicates if the module has footer.
func (m *Module) HasFooter() bool {
return len(m.Footer) > 0
}
// HasInputs indicates if the module has inputs.
func (m *Module) HasInputs() bool {
return len(m.Inputs) > 0
}
// HasModuleCalls indicates if the module has modulecalls.
func (m *Module) HasModuleCalls() bool {
return len(m.ModuleCalls) > 0
}
// HasOutputs indicates if the module has outputs.
func (m *Module) HasOutputs() bool {
return len(m.Outputs) > 0
}
// HasProviders indicates if the module has providers.
func (m *Module) HasProviders() bool {
return len(m.Providers) > 0
}
// HasRequirements indicates if the module has requirements.
func (m *Module) HasRequirements() bool {
return len(m.Requirements) > 0
}
// HasResources indicates if the module has resources.
func (m *Module) HasResources() bool {
return len(m.Resources) > 0
}
// Convert internal Module to its equivalent in plugin-sdk
func (m *Module) Convert() terraformsdk.Module {
return terraformsdk.NewModule(
terraformsdk.WithHeader(m.Header),
terraformsdk.WithFooter(m.Footer),
terraformsdk.WithInputs(inputs(m.Inputs).convert()),
terraformsdk.WithModuleCalls(modulecalls(m.ModuleCalls).convert()),
terraformsdk.WithOutputs(outputs(m.Outputs).convert()),
terraformsdk.WithProviders(providers(m.Providers).convert()),
terraformsdk.WithRequirements(requirements(m.Requirements).convert()),
terraformsdk.WithResources(resources(m.Resources).convert()),
terraformsdk.WithRequiredInputs(inputs(m.RequiredInputs).convert()),
terraformsdk.WithOptionalInputs(inputs(m.OptionalInputs).convert()),
)
}

73
terraform/modulecall.go Normal file
View File

@@ -0,0 +1,73 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"fmt"
"sort"
terraformsdk "github.com/terraform-docs/plugin-sdk/terraform"
)
// ModuleCall represents a submodule called by Terraform module.
type ModuleCall struct {
Name string `json:"name" toml:"name" xml:"name" yaml:"name"`
Source string `json:"source" toml:"source" xml:"source" yaml:"source"`
Version string `json:"version" toml:"version" xml:"version" yaml:"version"`
Position Position `json:"-" toml:"-" xml:"-" yaml:"-"`
}
// FullName returns full name of the modulecall, with version if available
func (mc *ModuleCall) FullName() string {
if mc.Version != "" {
return fmt.Sprintf("%s,%s", mc.Source, mc.Version)
}
return mc.Source
}
func sortModulecallsByName(x []*ModuleCall) {
sort.Slice(x, func(i, j int) bool {
return x[i].Name < x[j].Name
})
}
func sortModulecallsBySource(x []*ModuleCall) {
sort.Slice(x, func(i, j int) bool {
if x[i].Source == x[j].Source {
return x[i].Name < x[j].Name
}
return x[i].Source < x[j].Source
})
}
func sortModulecallsByPosition(x []*ModuleCall) {
sort.Slice(x, func(i, j int) bool {
return x[i].Position.Filename < x[j].Position.Filename || x[i].Position.Line < x[j].Position.Line
})
}
type modulecalls []*ModuleCall
func (mm modulecalls) convert() []*terraformsdk.ModuleCall {
list := []*terraformsdk.ModuleCall{}
for _, m := range mm {
list = append(list, &terraformsdk.ModuleCall{
Name: m.Name,
Source: m.Source,
Version: m.Version,
Position: terraformsdk.Position{
Filename: m.Position.Filename,
Line: m.Position.Line,
},
})
}
return list
}

View File

@@ -0,0 +1,123 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestModulecallName(t *testing.T) {
tests := map[string]struct {
module ModuleCall
expected string
}{
"WithoutVersion": {
module: ModuleCall{
Name: "provider",
Source: "bar",
},
expected: "bar",
},
"WithVersion": {
module: ModuleCall{
Name: "provider",
Source: "bar",
Version: "1.2.3",
},
expected: "bar,1.2.3",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.expected, tt.module.FullName())
})
}
}
func TestModulecallSort(t *testing.T) {
modules := sampleModulecalls()
tests := map[string]struct {
sortType func([]*ModuleCall)
expected []string
}{
"ByName": {
sortType: sortModulecallsByName,
expected: []string{"a", "b", "c", "d", "e", "f"},
},
"BySource": {
sortType: sortModulecallsBySource,
expected: []string{"f", "d", "c", "e", "a", "b"},
},
"ByPosition": {
sortType: sortModulecallsByPosition,
expected: []string{"b", "c", "a", "e", "d", "f"},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
tt.sortType(modules)
actual := make([]string, len(modules))
for k, i := range modules {
actual[k] = i.Name
}
assert.Equal(tt.expected, actual)
})
}
}
func sampleModulecalls() []*ModuleCall {
return []*ModuleCall{
{
Name: "a",
Source: "z",
Version: "1.2.3",
Position: Position{Filename: "foo/main.tf", Line: 35},
},
{
Name: "b",
Source: "z",
Version: "1.2.3",
Position: Position{Filename: "foo/main.tf", Line: 10},
},
{
Name: "c",
Source: "m",
Version: "1.2.3",
Position: Position{Filename: "foo/main.tf", Line: 23},
},
{
Name: "e",
Source: "x",
Version: "1.2.3",
Position: Position{Filename: "foo/main.tf", Line: 42},
},
{
Name: "d",
Source: "l",
Version: "1.2.3",
Position: Position{Filename: "foo/main.tf", Line: 51},
},
{
Name: "f",
Source: "a",
Version: "1.2.3",
Position: Position{Filename: "foo/main.tf", Line: 59},
},
}
}

78
terraform/options.go Normal file
View File

@@ -0,0 +1,78 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"errors"
"github.com/imdario/mergo"
)
// SortBy contains different sort criteria corresponding
// to available flags (e.g. name, required, etc)
type SortBy struct {
Name bool
Required bool
Type bool
}
// Options contains required options to load a Module from path
type Options struct {
Path string
ShowHeader bool
HeaderFromFile string
ShowFooter bool
FooterFromFile string
UseLockFile bool
SortBy *SortBy
OutputValues bool
OutputValuesPath string
ReadComments bool
}
// NewOptions returns new instance of Options
func NewOptions() *Options {
return &Options{
Path: "",
ShowHeader: true,
HeaderFromFile: "main.tf",
ShowFooter: false,
FooterFromFile: "",
UseLockFile: true,
SortBy: &SortBy{Name: false, Required: false, Type: false},
OutputValues: false,
OutputValuesPath: "",
ReadComments: true,
}
}
// With override options with existing Options
func (o *Options) With(override *Options) (*Options, error) {
if override == nil {
return nil, errors.New("cannot use nil as override value")
}
if err := mergo.Merge(o, *override); err != nil {
return nil, err
}
return o, nil
}
// WithOverwrite override options with existing Options and overwrites non-empty
// items in destination
func (o *Options) WithOverwrite(override *Options) (*Options, error) {
if override == nil {
return nil, errors.New("cannot use nil as override value")
}
if err := mergo.MergeWithOverwrite(o, *override); err != nil {
return nil, err
}
return o, nil
}

117
terraform/options_test.go Normal file
View File

@@ -0,0 +1,117 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOptionsWith(t *testing.T) {
assert := assert.New(t)
options := NewOptions()
assert.Equal(options.Path, "")
assert.Equal(options.OutputValues, false)
assert.Equal(options.OutputValuesPath, "")
_, err1 := options.With(&Options{
Path: "/path/to/foo",
})
assert.Nil(err1)
assert.Equal(options.Path, "/path/to/foo")
assert.Equal(options.OutputValues, false)
assert.Equal(options.OutputValuesPath, "")
_, err2 := options.With(&Options{
OutputValues: true,
OutputValuesPath: "/path/to/output/values",
})
assert.Nil(err2)
assert.Equal(options.Path, "/path/to/foo")
assert.Equal(options.OutputValues, true)
assert.Equal(options.OutputValuesPath, "/path/to/output/values")
_, err3 := options.With(&Options{
Path: "",
OutputValues: false,
})
assert.Nil(err3)
assert.NotEqual(options.Path, "")
assert.NotEqual(options.OutputValues, false)
}
func TestOptionsWithNil(t *testing.T) {
assert := assert.New(t)
options := NewOptions()
_, err := options.With(nil)
assert.NotNil(err)
}
func TestOptionsWithOverwrite(t *testing.T) {
assert := assert.New(t)
options := NewOptions()
assert.Equal(options.Path, "")
assert.Equal(options.HeaderFromFile, "main.tf")
assert.Equal(options.OutputValues, false)
assert.Equal(options.OutputValuesPath, "")
_, err1 := options.With(&Options{
Path: "/path/to/foo",
})
assert.Nil(err1)
assert.Equal(options.Path, "/path/to/foo")
assert.Equal(options.HeaderFromFile, "main.tf")
assert.Equal(options.OutputValues, false)
assert.Equal(options.OutputValuesPath, "")
_, err2 := options.WithOverwrite(&Options{
HeaderFromFile: "doc.tf",
OutputValues: true,
OutputValuesPath: "/path/to/output/values",
})
assert.Nil(err2)
assert.Equal(options.Path, "/path/to/foo")
assert.Equal(options.HeaderFromFile, "doc.tf")
assert.Equal(options.OutputValues, true)
assert.Equal(options.OutputValuesPath, "/path/to/output/values")
_, err3 := options.WithOverwrite(&Options{
Path: "",
OutputValues: false,
})
assert.Nil(err3)
assert.NotEqual(options.Path, "")
assert.Equal(options.HeaderFromFile, "doc.tf")
assert.NotEqual(options.OutputValues, false)
assert.Equal(options.OutputValuesPath, "/path/to/output/values")
}
func TestOptionsWithNilOverwrite(t *testing.T) {
assert := assert.New(t)
options := NewOptions()
_, err := options.WithOverwrite(nil)
assert.NotNil(err)
}

165
terraform/output.go Normal file
View File

@@ -0,0 +1,165 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"sort"
terraformsdk "github.com/terraform-docs/plugin-sdk/terraform"
"github.com/terraform-docs/terraform-docs/internal/types"
)
// Output represents a Terraform output.
type Output struct {
Name string `json:"name" toml:"name" xml:"name" yaml:"name"`
Description types.String `json:"description" toml:"description" xml:"description" yaml:"description"`
Value types.Value `json:"value,omitempty" toml:"value,omitempty" xml:"value,omitempty" yaml:"value,omitempty"`
Sensitive bool `json:"sensitive,omitempty" toml:"sensitive,omitempty" xml:"sensitive,omitempty" yaml:"sensitive,omitempty"`
Position Position `json:"-" toml:"-" xml:"-" yaml:"-"`
ShowValue bool `json:"-" toml:"-" xml:"-" yaml:"-"`
}
type withvalue struct {
Name string `json:"name" toml:"name" xml:"name" yaml:"name"`
Description types.String `json:"description" toml:"description" xml:"description" yaml:"description"`
Value types.Value `json:"value" toml:"value" xml:"value" yaml:"value"`
Sensitive bool `json:"sensitive" toml:"sensitive" xml:"sensitive" yaml:"sensitive"`
Position Position `json:"-" toml:"-" xml:"-" yaml:"-"`
ShowValue bool `json:"-" toml:"-" xml:"-" yaml:"-"`
}
// GetValue returns JSON representation of the 'Value', which is an 'interface'.
// If 'Value' is a primitive type, the primitive value of 'Value' will be returned
// and not the JSON formatted of it.
func (o *Output) GetValue() string {
if !o.ShowValue || o.Value == nil {
return ""
}
marshaled, err := json.MarshalIndent(o.Value, "", " ")
if err != nil {
panic(err)
}
value := string(marshaled)
if value == `null` {
return "" // types.Nil
}
return value // everything else
}
// HasDefault indicates if a Terraform output has a default value set.
func (o *Output) HasDefault() bool {
if !o.ShowValue || o.Value == nil {
return false
}
return o.Value.HasDefault()
}
// MarshalJSON custom yaml marshal function to take '--output-values' flag into
// consideration. It means if the flag is not set Value and Sensitive fields are
// set to 'omitempty', otherwise if output values are being shown 'omitempty' gets
// explicitly removed to show even empty and false values.
func (o *Output) MarshalJSON() ([]byte, error) {
fn := func(oo interface{}) ([]byte, error) {
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(oo); err != nil {
panic(err)
}
return buf.Bytes(), nil
}
if o.ShowValue {
return fn(withvalue(*o))
}
o.Value = nil // explicitly make empty
o.Sensitive = false // explicitly make empty
return fn(*o)
}
// MarshalXML custom xml marshal function to take '--output-values' flag into
// consideration. It means if the flag is not set Value and Sensitive fields
// are set to 'omitempty', otherwise if output values are being shown 'omitempty'
// gets explicitly removed to show even empty and false values.
func (o *Output) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
fn := func(v interface{}, name string) error {
return e.EncodeElement(v, xml.StartElement{Name: xml.Name{Local: name}})
}
err := e.EncodeToken(start)
if err != nil {
return err
}
fn(o.Name, "name") //nolint:errcheck,gosec
fn(o.Description, "description") //nolint:errcheck,gosec
if o.ShowValue {
fn(o.Value, "value") //nolint:errcheck,gosec
fn(o.Sensitive, "sensitive") //nolint:errcheck,gosec
}
return e.EncodeToken(start.End())
}
// MarshalYAML custom yaml marshal function to take '--output-values' flag into
// consideration. It means if the flag is not set Value and Sensitive fields are
// set to 'omitempty', otherwise if output values are being shown 'omitempty' gets
// explicitly removed to show even empty and false values.
func (o *Output) MarshalYAML() (interface{}, error) {
if o.ShowValue {
return withvalue(*o), nil
}
o.Value = nil // explicitly make empty
o.Sensitive = false // explicitly make empty
return *o, nil
}
// output is used for unmarshalling `terraform outputs --json` into
type output struct {
Sensitive bool `json:"sensitive"`
Type interface{} `json:"type"`
Value interface{} `json:"value"`
}
func sortOutputsByName(x []*Output) {
sort.Slice(x, func(i, j int) bool {
return x[i].Name < x[j].Name
})
}
func sortOutputsByPosition(x []*Output) {
sort.Slice(x, func(i, j int) bool {
if x[i].Position.Filename == x[j].Position.Filename {
return x[i].Position.Line < x[j].Position.Line
}
return x[i].Position.Filename < x[j].Position.Filename
})
}
type outputs []*Output
func (oo outputs) convert() []*terraformsdk.Output {
list := []*terraformsdk.Output{}
for _, o := range oo {
list = append(list, &terraformsdk.Output{
Name: o.Name,
Description: fmt.Sprintf("%v", o.Description.Raw()),
Value: nil,
Sensitive: o.Sensitive,
Position: terraformsdk.Position{
Filename: o.Position.Filename,
Line: o.Position.Line,
},
ShowValue: o.ShowValue,
})
}
return list
}

527
terraform/output_test.go Normal file
View File

@@ -0,0 +1,527 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"bytes"
"encoding/xml"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/terraform-docs/terraform-docs/internal/types"
)
func TestOutputValue(t *testing.T) {
outputs := sampleOutputs()
tests := []struct {
name string
output Output
expectValue string
expectDefault bool
}{
{
name: "output Value and HasDefault",
output: outputs[0],
expectValue: "",
expectDefault: false,
},
{
name: "output Value and HasDefault",
output: outputs[1],
expectValue: "",
expectDefault: false,
},
{
name: "output Value and HasDefault",
output: outputs[2],
expectValue: "false",
expectDefault: true,
},
{
name: "output Value and HasDefault",
output: outputs[3],
expectValue: "\"\"",
expectDefault: true,
},
{
name: "output Value and HasDefault",
output: outputs[4],
expectValue: "\"foo\"",
expectDefault: true,
},
{
name: "output Value and HasDefault",
output: outputs[5],
expectValue: "",
expectDefault: false,
},
{
name: "output Value and HasDefault",
output: outputs[6],
expectValue: "\"\\u003csensitive\\u003e\"",
expectDefault: true,
},
{
name: "output Value and HasDefault",
output: outputs[7],
expectValue: "[\n \"a\",\n \"b\",\n \"c\"\n]",
expectDefault: true,
},
{
name: "output Value and HasDefault",
output: outputs[8],
expectValue: "[]",
expectDefault: true,
},
{
name: "output Value and HasDefault",
output: outputs[9],
expectValue: "{\n \"a\": 1,\n \"b\": 2,\n \"c\": 3\n}",
expectDefault: true,
},
{
name: "output Value and HasDefault",
output: outputs[10],
expectValue: "{}",
expectDefault: true,
},
{
name: "output Value and HasDefault",
output: outputs[11],
expectValue: "",
expectDefault: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.expectValue, tt.output.GetValue())
assert.Equal(tt.expectDefault, tt.output.HasDefault())
})
}
}
func TestOutputMarshalJSON(t *testing.T) {
outputs := sampleOutputs()
tests := []struct {
name string
output Output
expected string
}{
{
name: "output marshal JSON",
output: outputs[0],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":null,\"sensitive\":false}\n",
},
{
name: "output marshal JSON",
output: outputs[1],
expected: "{\"name\":\"output\",\"description\":\"description\"}\n",
},
{
name: "output marshal JSON",
output: outputs[2],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":false,\"sensitive\":false}\n",
},
{
name: "output marshal JSON",
output: outputs[3],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":\"\",\"sensitive\":false}\n",
},
{
name: "output marshal JSON",
output: outputs[4],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":\"foo\",\"sensitive\":false}\n",
},
{
name: "output marshal JSON",
output: outputs[5],
expected: "{\"name\":\"output\",\"description\":\"description\"}\n",
},
{
name: "output marshal JSON",
output: outputs[6],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":\"<sensitive>\",\"sensitive\":true}\n",
},
{
name: "output marshal JSON",
output: outputs[7],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":[\"a\",\"b\",\"c\"],\"sensitive\":false}\n",
},
{
name: "output marshal JSON",
output: outputs[8],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":[],\"sensitive\":false}\n",
},
{
name: "output marshal JSON",
output: outputs[9],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":{\"a\":1,\"b\":2,\"c\":3},\"sensitive\":false}\n",
},
{
name: "output marshal JSON",
output: outputs[10],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":{},\"sensitive\":false}\n",
},
{
name: "output marshal JSON",
output: outputs[11],
expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":null,\"sensitive\":false}\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual, err := tt.output.MarshalJSON()
assert.Nil(err)
assert.Equal(tt.expected, string(actual))
})
}
}
func TestOutputMarshalXML(t *testing.T) {
outputs := sampleOutputs()
tests := []struct {
name string
output Output
expected string
}{
{
name: "output marshal XML",
output: outputs[0],
expected: "<output><name>output</name><description>description</description><value xsi:nil=\"true\"></value><sensitive>false</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[1],
expected: "<output><name>output</name><description>description</description></output>",
},
{
name: "output marshal XML",
output: outputs[2],
expected: "<output><name>output</name><description>description</description><value>false</value><sensitive>false</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[3],
expected: "<output><name>output</name><description>description</description><value></value><sensitive>false</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[4],
expected: "<output><name>output</name><description>description</description><value>foo</value><sensitive>false</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[5],
expected: "<output><name>output</name><description>description</description></output>",
},
{
name: "output marshal XML",
output: outputs[6],
expected: "<output><name>output</name><description>description</description><value>&lt;sensitive&gt;</value><sensitive>true</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[7],
expected: "<output><name>output</name><description>description</description><value><item>a</item><item>b</item><item>c</item></value><sensitive>false</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[8],
expected: "<output><name>output</name><description>description</description><value></value><sensitive>false</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[9],
expected: "<output><name>output</name><description>description</description><value><a>1</a><b>2</b><c>3</c></value><sensitive>false</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[10],
expected: "<output><name>output</name><description>description</description><value></value><sensitive>false</sensitive></output>",
},
{
name: "output marshal XML",
output: outputs[11],
expected: "<output><name>output</name><description>description</description><value xsi:nil=\"true\"></value><sensitive>false</sensitive></output>",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
var b bytes.Buffer
encoder := xml.NewEncoder(&b)
start := xml.StartElement{Name: xml.Name{Local: "output"}}
err := tt.output.MarshalXML(encoder, start)
assert.Nil(err)
err = encoder.Flush()
assert.Nil(err)
assert.Nil(err)
assert.Equal(tt.expected, b.String())
})
}
}
func TestOutputMarshalYAML(t *testing.T) {
outputs := sampleOutputs()
tests := []struct {
name string
output Output
expected string
}{
{
name: "output marshal JSON",
output: outputs[0],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[1],
expected: "terraform.Output",
},
{
name: "output marshal JSON",
output: outputs[2],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[3],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[4],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[5],
expected: "terraform.Output",
},
{
name: "output marshal JSON",
output: outputs[6],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[7],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[8],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[9],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[10],
expected: "terraform.withvalue",
},
{
name: "output marshal JSON",
output: outputs[11],
expected: "terraform.withvalue",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual, err := tt.output.MarshalYAML()
assert.Nil(err)
assert.Equal(tt.expected, reflect.TypeOf(actual).String())
})
}
}
func sampleOutputs() []Output {
name := "output"
description := types.String("description")
position := Position{Filename: "foo.tf", Line: 13}
return []Output{
{
Name: name,
Description: description,
Value: types.ValueOf(nil),
Sensitive: false,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Position: position,
ShowValue: false,
},
{
Name: name,
Description: description,
Value: types.ValueOf(false),
Sensitive: false,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Value: types.ValueOf(""),
Sensitive: false,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Value: types.ValueOf("foo"),
Sensitive: false,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Value: types.ValueOf("this should be hidden"),
Sensitive: false,
Position: position,
ShowValue: false,
},
{
Name: name,
Description: description,
Value: types.ValueOf("<sensitive>"),
Sensitive: true,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Value: types.ValueOf(types.List{"a", "b", "c"}.Underlying()),
Sensitive: false,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Value: types.ValueOf(types.List{}.Underlying()),
Sensitive: false,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Value: types.ValueOf(types.Map{"a": 1, "b": 2, "c": 3}.Underlying()),
Sensitive: false,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Value: types.ValueOf(types.Map{}.Underlying()),
Sensitive: false,
Position: position,
ShowValue: true,
},
{
Name: name,
Description: description,
Value: types.ValueOf(nil),
Sensitive: false,
Position: position,
ShowValue: true,
},
}
}
func TestOutputsSort(t *testing.T) {
outputs := sampleOutputsForSort()
tests := map[string]struct {
sortType func([]*Output)
expected []string
}{
"ByName": {
sortType: sortOutputsByName,
expected: []string{"a", "b", "c", "d", "e"},
},
"ByPosition": {
sortType: sortOutputsByPosition,
expected: []string{"d", "a", "e", "b", "c"},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
tt.sortType(outputs)
actual := make([]string, len(outputs))
for k, o := range outputs {
actual[k] = o.Name
}
assert.Equal(tt.expected, actual)
})
}
}
func sampleOutputsForSort() []*Output {
return []*Output{
{
Name: "a",
Description: types.String("description of a"),
Value: nil,
Position: Position{Filename: "foo/outputs.tf", Line: 25},
},
{
Name: "d",
Description: types.String("description of d"),
Value: nil,
Position: Position{Filename: "foo/outputs.tf", Line: 10},
},
{
Name: "e",
Description: types.String("description of e"),
Value: nil,
Position: Position{Filename: "foo/outputs.tf", Line: 33},
},
{
Name: "b",
Description: types.String("description of b"),
Value: nil,
Position: Position{Filename: "foo/outputs.tf", Line: 39},
},
{
Name: "c",
Description: types.String("description of c"),
Value: nil,
Position: Position{Filename: "foo/outputs.tf", Line: 42},
},
}
}

17
terraform/position.go Normal file
View File

@@ -0,0 +1,17 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
// Position represents position of Terraform item (input, output, provider, etc) in a file.
type Position struct {
Filename string `json:"-" toml:"-" xml:"-" yaml:"-"`
Line int `json:"-" toml:"-" xml:"-" yaml:"-"`
}

71
terraform/provider.go Normal file
View File

@@ -0,0 +1,71 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"fmt"
"sort"
terraformsdk "github.com/terraform-docs/plugin-sdk/terraform"
"github.com/terraform-docs/terraform-docs/internal/types"
)
// Provider represents a Terraform output.
type Provider struct {
Name string `json:"name" toml:"name" xml:"name" yaml:"name"`
Alias types.String `json:"alias" toml:"alias" xml:"alias" yaml:"alias"`
Version types.String `json:"version" toml:"version" xml:"version" yaml:"version"`
Position Position `json:"-" toml:"-" xml:"-" yaml:"-"`
}
// FullName returns full name of the provider, with alias if available
func (p *Provider) FullName() string {
if p.Alias != "" {
return fmt.Sprintf("%s.%s", p.Name, p.Alias)
}
return p.Name
}
func sortProvidersByName(x []*Provider) {
sort.Slice(x, func(i, j int) bool {
if x[i].Name == x[j].Name {
return x[i].Name == x[j].Name && x[i].Alias < x[j].Alias
}
return x[i].Name < x[j].Name
})
}
func sortProvidersByPosition(x []*Provider) {
sort.Slice(x, func(i, j int) bool {
if x[i].Position.Filename == x[j].Position.Filename {
return x[i].Position.Line < x[j].Position.Line
}
return x[i].Position.Filename < x[j].Position.Filename
})
}
type providers []*Provider
func (pp providers) convert() []*terraformsdk.Provider {
list := []*terraformsdk.Provider{}
for _, p := range pp {
list = append(list, &terraformsdk.Provider{
Name: p.Name,
Alias: fmt.Sprintf("%v", p.Alias.Raw()),
Version: fmt.Sprintf("%v", p.Version.Raw()),
Position: terraformsdk.Position{
Filename: p.Position.Filename,
Line: p.Position.Line,
},
})
}
return list
}

130
terraform/provider_test.go Normal file
View File

@@ -0,0 +1,130 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/terraform-docs/terraform-docs/internal/types"
)
func TestProviderName(t *testing.T) {
tests := map[string]struct {
provider Provider
expected string
}{
"WithoutAlias": {
provider: Provider{
Name: "provider",
Alias: types.String(""),
Version: types.String(">= 1.2.3"),
Position: Position{Filename: "foo.tf", Line: 13},
},
expected: "provider",
},
"WithAlias": {
provider: Provider{
Name: "provider",
Alias: types.String("alias"),
Version: types.String(">= 1.2.3"),
Position: Position{Filename: "foo.tf", Line: 13},
},
expected: "provider.alias",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.expected, tt.provider.FullName())
})
}
}
func TestProvidersSort(t *testing.T) {
providers := sampleProviders()
tests := map[string]struct {
sortType func([]*Provider)
expected []string
}{
"ByName": {
sortType: sortProvidersByName,
expected: []string{"a", "b", "c", "d", "d.a", "e", "e.a"},
},
"ByPosition": {
sortType: sortProvidersByPosition,
expected: []string{"e.a", "b", "d", "d.a", "a", "e", "c"},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
tt.sortType(providers)
actual := make([]string, len(providers))
for k, p := range providers {
actual[k] = p.FullName()
}
assert.Equal(tt.expected, actual)
})
}
}
func sampleProviders() []*Provider {
return []*Provider{
{
Name: "d",
Alias: types.String(""),
Version: types.String("1.3.2"),
Position: Position{Filename: "foo/main.tf", Line: 21},
},
{
Name: "d",
Alias: types.String("a"),
Version: types.String("> 1.x"),
Position: Position{Filename: "foo/main.tf", Line: 25},
},
{
Name: "b",
Alias: types.String(""),
Version: types.String("= 2.1.0"),
Position: Position{Filename: "foo/main.tf", Line: 13},
},
{
Name: "a",
Alias: types.String(""),
Version: types.String(""),
Position: Position{Filename: "foo/main.tf", Line: 39},
},
{
Name: "c",
Alias: types.String(""),
Version: types.String("~> 0.5.0"),
Position: Position{Filename: "foo/main.tf", Line: 53},
},
{
Name: "e",
Alias: types.String(""),
Version: types.String(""),
Position: Position{Filename: "foo/main.tf", Line: 47},
},
{
Name: "e",
Alias: types.String("a"),
Version: types.String("> 1.0"),
Position: Position{Filename: "foo/main.tf", Line: 5},
},
}
}

37
terraform/requirement.go Normal file
View File

@@ -0,0 +1,37 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"fmt"
terraformsdk "github.com/terraform-docs/plugin-sdk/terraform"
"github.com/terraform-docs/terraform-docs/internal/types"
)
// Requirement represents a requirement for Terraform module.
type Requirement struct {
Name string `json:"name" toml:"name" xml:"name" yaml:"name"`
Version types.String `json:"version" toml:"version" xml:"version" yaml:"version"`
}
type requirements []*Requirement
func (rr requirements) convert() []*terraformsdk.Requirement {
list := []*terraformsdk.Requirement{}
for _, r := range rr {
list = append(list, &terraformsdk.Requirement{
Name: r.Name,
Version: fmt.Sprintf("%v", r.Version.Raw()),
})
}
return list
}

103
terraform/resource.go Normal file
View File

@@ -0,0 +1,103 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"fmt"
"sort"
"strings"
terraformsdk "github.com/terraform-docs/plugin-sdk/terraform"
"github.com/terraform-docs/terraform-docs/internal/types"
)
// Resource represents a managed or data type that is created by the module
type Resource struct {
Type string `json:"type" toml:"type" xml:"type" yaml:"type"`
Name string `json:"name" toml:"name" xml:"name" yaml:"name"`
ProviderName string `json:"provider" toml:"provider" xml:"provider" yaml:"provider"`
ProviderSource string `json:"source" toml:"source" xml:"source" yaml:"source"`
Mode string `json:"mode" toml:"mode" xml:"mode" yaml:"mode"`
Version types.String `json:"version" toml:"version" xml:"version" yaml:"version"`
Position Position `json:"-" toml:"-" xml:"-" yaml:"-"`
}
// Spec returns the resource spec addresses a specific resource in the config.
// It takes the form: resource_type.resource_name[resource index]
// For more details, see:
// https://www.terraform.io/docs/cli/state/resource-addressing.html#resource-spec
func (r *Resource) Spec() string {
return r.ProviderName + "_" + r.Type + "." + r.Name
}
// GetMode returns normalized resource type as "resource" or "data source"
func (r *Resource) GetMode() string {
switch r.Mode {
case "managed":
return "resource"
case "data":
return "data source"
default:
return "invalid"
}
}
// URL returns a best guess at the URL for resource documentation
func (r *Resource) URL() string {
kind := ""
switch r.Mode {
case "managed":
kind = "resources"
case "data":
kind = "data-sources"
default:
return ""
}
if strings.Count(r.ProviderSource, "/") > 1 {
return ""
}
return fmt.Sprintf("https://registry.terraform.io/providers/%s/%s/docs/%s/%s", r.ProviderSource, r.Version, kind, r.Type)
}
func sortResourcesByType(x []*Resource) {
sort.Slice(x, func(i, j int) bool {
if x[i].Mode == x[j].Mode {
if x[i].Spec() == x[j].Spec() {
return x[i].Name <= x[j].Name
}
return x[i].Spec() < x[j].Spec()
}
return x[i].Mode > x[j].Mode
})
}
type resources []*Resource
func (rr resources) convert() []*terraformsdk.Resource {
list := []*terraformsdk.Resource{}
for _, r := range rr {
list = append(list, &terraformsdk.Resource{
Type: r.Type,
Name: r.Name,
ProviderName: r.ProviderName,
ProviderSource: r.ProviderSource,
Mode: r.Mode,
Version: fmt.Sprintf("%v", r.Version.Raw()),
Position: terraformsdk.Position{
Filename: r.Position.Filename,
Line: r.Position.Line,
},
})
}
return list
}

341
terraform/resource_test.go Normal file
View File

@@ -0,0 +1,341 @@
/*
Copyright 2021 The terraform-docs Authors.
Licensed under the MIT license (the "License"); you may not
use this file except in compliance with the License.
You may obtain a copy of the License at the LICENSE file in
the root directory of this source tree.
*/
package terraform
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/terraform-docs/terraform-docs/internal/types"
)
func TestResourceSpec(t *testing.T) {
assert := assert.New(t)
resource := Resource{
Type: "private_key",
Name: "baz",
ProviderName: "tls",
ProviderSource: "hashicorp/tls",
Mode: "managed",
Version: types.String("latest"),
}
assert.Equal("tls_private_key.baz", resource.Spec())
}
func TestPluginSdkConversion(t *testing.T) {
assert := assert.New(t)
resource := Resource{
Type: "private_key",
Name: "baz",
ProviderName: "tls",
ProviderSource: "hashicorp/tls",
Mode: "managed",
Version: types.String("latest"),
}
sdkResource := resources{&resource}.convert()[0]
assert.Equal(resource.Type, sdkResource.Type)
assert.Equal(resource.Name, sdkResource.Name)
assert.Equal(resource.ProviderName, sdkResource.ProviderName)
assert.Equal(resource.ProviderSource, sdkResource.ProviderSource)
assert.Equal(resource.Mode, sdkResource.Mode)
assert.Equal(resource.Version, types.String(sdkResource.Version))
}
func TestResourceMode(t *testing.T) {
tests := map[string]struct {
resource Resource
expectValue string
}{
"Managed": {
resource: Resource{
Type: "private_key",
ProviderName: "tls",
ProviderSource: "hashicorp/tls",
Mode: "managed",
Version: types.String("latest"),
},
expectValue: "resource",
},
"Data Source": {
resource: Resource{
Type: "caller_identity",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
Mode: "data",
Version: types.String("latest"),
},
expectValue: "data source",
},
"Invalid": {
resource: Resource{
Type: "caller_identity",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
Mode: "",
Version: types.String("latest"),
},
expectValue: "invalid",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.expectValue, tt.resource.GetMode())
})
}
}
func TestResourceURL(t *testing.T) {
tests := map[string]struct {
resource Resource
expectValue string
}{
"Generic URL construction": {
resource: Resource{
Type: "private_key",
ProviderName: "tls",
ProviderSource: "hashicorp/tls",
Mode: "managed",
Version: types.String("latest"),
},
expectValue: "https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key",
},
"Unable to construct URL": {
resource: Resource{
Type: "custom",
ProviderName: "nih",
ProviderSource: "http://nih.tld/some/path/to/provider/source",
Mode: "managed",
Version: types.String("latest"),
},
expectValue: "",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.expectValue, tt.resource.URL())
})
}
}
func TestResourcesSortedByType(t *testing.T) {
assert := assert.New(t)
resources := sampleResources()
sortResourcesByType(resources)
expected := []string{"a_a.a", "a_f.f", "b_b.b", "b_d.d", "c_c.c", "c_e.c", "c_e.d", "c_e_x.c", "c_e_x.d", "z_z.z", "a_a.a", "z_z.z", "a_a.a", "z_z.z"}
actual := make([]string, len(resources))
for k, i := range resources {
actual[k] = i.Spec()
}
assert.Equal(expected, actual)
}
func TestResourcesSortedByTypeAndMode(t *testing.T) {
assert := assert.New(t)
resources := sampleResources()
sortResourcesByType(resources)
expected := []string{"a_a.a (r)", "a_f.f (r)", "b_b.b (r)", "b_d.d (r)", "c_c.c (r)", "c_e.c (r)", "c_e.d (r)", "c_e_x.c (r)", "c_e_x.d (r)", "z_z.z (r)", "a_a.a (d)", "z_z.z (d)", "a_a.a", "z_z.z"}
actual := make([]string, len(resources))
for k, i := range resources {
mode := ""
switch i.Mode {
case "managed":
mode = " (r)"
case "data":
mode = " (d)"
}
actual[k] = i.Spec() + mode
}
assert.Equal(expected, actual)
}
func TestResourceVersion(t *testing.T) {
tests := map[string]struct {
constraint []string
expected string
}{
"exact version, without operator": {
constraint: []string{"1.2.3"},
expected: "1.2.3",
},
"exact version, with operator": {
constraint: []string{"= 1.2.3"},
expected: "1.2.3",
},
"exact version, with operator, without space": {
constraint: []string{"=1.2.3"},
expected: "1.2.3",
},
"exclude exact version, with space": {
constraint: []string{"!= 1.2.3"},
expected: "latest",
},
"exclude exact version, without space": {
constraint: []string{"!=1.2.3"},
expected: "latest",
},
"comparison version, with space": {
constraint: []string{"> 1.2.3"},
expected: "latest",
},
"comparison version, without space": {
constraint: []string{">1.2.3"},
expected: "latest",
},
"range version": {
constraint: []string{"> 1.2.3, < 2.0.0"},
expected: "latest",
},
"pessimistic version, with space": {
constraint: []string{"~> 1.2.3"},
expected: "latest",
},
"pessimistic version, without space": {
constraint: []string{"~>1.2.3"},
expected: "latest",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.expected, resourceVersion(tt.constraint))
})
}
}
func sampleResources() []*Resource {
return []*Resource{
{
Type: "e",
Name: "d",
ProviderName: "c",
ProviderSource: "hashicorp/e",
Mode: "managed",
Version: "1.5.0",
},
{
Type: "e",
Name: "c",
ProviderName: "c",
ProviderSource: "hashicorp/e",
Mode: "managed",
Version: "1.5.0",
},
{
Type: "e_x",
Name: "d",
ProviderName: "c",
ProviderSource: "hashicorp/e",
Mode: "managed",
Version: "1.5.0",
},
{
Type: "e_x",
Name: "c",
ProviderName: "c",
ProviderSource: "hashicorp/e",
Mode: "managed",
Version: "1.5.0",
},
{
Type: "a",
Name: "a",
ProviderName: "a",
ProviderSource: "hashicorp/a",
Mode: "managed",
Version: "1.1.0",
},
{
Type: "d",
Name: "d",
ProviderName: "b",
ProviderSource: "hashicorp/d",
Mode: "managed",
Version: "1.4.0",
},
{
Type: "b",
Name: "b",
ProviderName: "b",
ProviderSource: "hashicorp/b",
Mode: "managed",
Version: "1.2.0",
},
{
Type: "c",
Name: "c",
ProviderName: "c",
ProviderSource: "hashicorp/c",
Mode: "managed",
Version: "1.3.0",
},
{
Type: "f",
Name: "f",
ProviderName: "a",
ProviderSource: "hashicorp/f",
Mode: "managed",
Version: "1.6.0",
},
{
ProviderName: "z",
Type: "z",
Name: "z",
ProviderSource: "hashicorp/a",
Mode: "managed",
Version: "1.5.0",
},
{
ProviderName: "z",
Type: "z",
Name: "z",
ProviderSource: "hashicorp/a",
Mode: "data",
Version: "1.5.0",
},
{
ProviderName: "a",
Type: "a",
Name: "a",
ProviderSource: "hashicorp/a",
Mode: "data",
Version: "1.5.0",
},
{
ProviderName: "z",
Type: "z",
Name: "z",
ProviderSource: "hashicorp/a",
Mode: "",
Version: "1.5.0",
},
{
ProviderName: "a",
Type: "a",
Name: "a",
ProviderSource: "hashicorp/a",
Mode: "",
Version: "1.5.0",
},
}
}

View File

@@ -0,0 +1,7 @@
resource "tls_private_key" "baz" {}
data "aws_caller_identity" "current" {
provider = "aws"
}
resource "null_resource" "foo" {}

View File

@@ -0,0 +1,7 @@
Example of 'foo_bar' module in `foo_bar.tf`.
- list item 1
- list item 2
Even inline **formatting** in _here_ is possible.
and some [link](https://domain.com/)

View File

@@ -0,0 +1,6 @@
= Custom Header
Example of 'foo_bar' module in `foo_bar.tf`.
- list item 1
- list item 2

View File

@@ -0,0 +1,6 @@
# Custom Header
Example of 'foo_bar' module in `foo_bar.tf`.
- list item 1
- list item 2

View File

@@ -0,0 +1,8 @@
/**
* Custom Header:
*
* Example of 'foo_bar' module in `foo_bar.tf`.
*
* - list item 1
* - list item 2
*/

View File

@@ -0,0 +1,6 @@
# Custom Header
Example of 'foo_bar' module in `foo_bar.tf`.
- list item 1
- list item 2

33
terraform/testdata/full-example/main.tf vendored Normal file
View File

@@ -0,0 +1,33 @@
/**
* Example of 'foo_bar' module in `foo_bar.tf`.
*
* - list item 1
* - list item 2
*
* Even inline **formatting** in _here_ is possible.
* and some [link](https://domain.com/)
*/
terraform {
required_version = ">= 0.12"
required_providers {
aws = ">= 2.15.0"
}
}
resource "tls_private_key" "baz" {}
data "aws_caller_identity" "current" {
provider = "aws"
}
resource "null_resource" "foo" {}
module "foo" {
source = "bar"
version = "1.2.3"
}
module "foobar" {
source = "git@github.com:module/path?ref=v7.8.9"
}

View File

@@ -0,0 +1,17 @@
{
"C": {
"sensitive": true,
"type": "string",
"value": "sensitive-c"
},
"A": {
"sensitive": false,
"type": "string",
"value": "a value"
},
"B": {
"sensitive": false,
"type": "string",
"value": "b value"
}
}

View File

@@ -0,0 +1,14 @@
output C {
description = "It's unquoted output."
value = "c"
}
output "A" {
description = "A description"
value = "a"
}
// B description
output "B" {
value = "b"
}

View File

@@ -0,0 +1,30 @@
// D description
variable "D" {
default = "d"
}
variable "B" {
default = "b"
}
variable "E" {
default = ""
}
# A Description
# in multiple lines
variable A {}
variable "C" {
description = "C description"
default = "c"
}
variable "F" {
description = "F description"
}
variable "G" {
description = "G description"
default = null
}

View File

@@ -0,0 +1,7 @@
variable "multi-line-lf" {
type = string
description = <<-EOT
The quick brown fox jumps
over the lazy dog
EOT
}

View File

@@ -0,0 +1,7 @@
variable "multi-line-crlf" {
type = string
description = <<-EOT
The quick brown fox jumps
over the lazy dog
EOT
}

View File

View File

@@ -0,0 +1 @@
resource "tls_private_key" "baz" {}

View File

@@ -0,0 +1,18 @@
// D description
variable "D" {}
variable "B" {}
variable "E" {}
# A Description
# in multiple lines
variable A {}
variable "C" {
description = "C description"
}
variable "F" {
description = "F description"
}

View File

View File

View File

@@ -0,0 +1,28 @@
// D description
variable "D" {
default = "d"
}
variable "B" {
default = "b"
}
variable "E" {
default = ""
}
# A Description
# in multiple lines
variable A {
default = "a"
}
variable "C" {
description = "C description"
default = "c"
}
variable "F" {
description = "F description"
default = "f"
}

View File

@@ -0,0 +1,9 @@
// B description
variable "B" {
default = "b"
}
// B description
output "B" {
value = "b"
}

57
terraform/testdata/with-lock-file/.terraform.lock.hcl generated vendored Normal file
View File

@@ -0,0 +1,57 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "3.42.0"
constraints = ">= 2.15.0"
hashes = [
"h1:quV6hK7ewiHWBznGWCb/gJ6JAPm6UtouBUrhAjv6oRY=",
"zh:126c856a6eedddd8571f161a826a407ba5655a37a6241393560a96b8c4beca1a",
"zh:1a4868e6ac734b5fc2e79a4a889d176286b66664aad709435aa6acee5871d5b0",
"zh:40fed7637ab8ddeb93bef06aded35d970f0628025b97459ae805463e8aa0a58a",
"zh:68def3c0a5a1aac1db6372c51daef858b707f03052626d3427ac24cba6f2014d",
"zh:6db7ec9c8d1803a0b6f40a664aa892e0f8894562de83061fa7ac1bc51ff5e7e5",
"zh:7058abaad595930b3f97dc04e45c112b2dbf37d098372a849081f7081da2fb52",
"zh:8c25adb15a19da301c478aa1f4a4d8647cabdf8e5dae8331d4490f80ea718c26",
"zh:8e129b847401e39fcbc54817726dab877f36b7f00ff5ed76f7b43470abe99ff9",
"zh:d268bb267a2d6b39df7ddee8efa7c1ef7a15cf335dfa5f2e64c9dae9b623a1b8",
"zh:d6eeb3614a0ab50f8e9ab5666ae5754ea668ce327310e5b21b7f04a18d7611a8",
"zh:f5d3c58055dff6e38562b75d3edc908cb2f1e45c6914f6b00f4773359ce49324",
]
}
provider "registry.terraform.io/hashicorp/null" {
version = "3.1.0"
hashes = [
"h1:vpC6bgUQoJ0znqIKVFevOdq+YQw42bRq0u+H3nto8nA=",
"zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2",
"zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515",
"zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521",
"zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2",
"zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e",
"zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53",
"zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d",
"zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8",
"zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70",
"zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b",
"zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e",
]
}
provider "registry.terraform.io/hashicorp/tls" {
version = "3.1.0"
hashes = [
"h1:fUJX8Zxx38e2kBln+zWr1Tl41X+OuiE++REjrEyiOM4=",
"zh:3d46616b41fea215566f4a957b6d3a1aa43f1f75c26776d72a98bdba79439db6",
"zh:623a203817a6dafa86f1b4141b645159e07ec418c82fe40acd4d2a27543cbaa2",
"zh:668217e78b210a6572e7b0ecb4134a6781cc4d738f4f5d09eb756085b082592e",
"zh:95354df03710691773c8f50a32e31fca25f124b7f3d6078265fdf3c4e1384dca",
"zh:9f97ab190380430d57392303e3f36f4f7835c74ea83276baa98d6b9a997c3698",
"zh:a16f0bab665f8d933e95ca055b9c8d5707f1a0dd8c8ecca6c13091f40dc1e99d",
"zh:be274d5008c24dc0d6540c19e22dbb31ee6bfdd0b2cddd4d97f3cd8a8d657841",
"zh:d5faa9dce0a5fc9d26b2463cea5be35f8586ab75030e7fa4d4920cd73ee26989",
"zh:e9b672210b7fb410780e7b429975adcc76dd557738ecc7c890ea18942eb321a5",
"zh:eb1f8368573d2370605d6dbf60f9aaa5b64e55741d96b5fb026dbfe91de67c0d",
"zh:fc1e12b713837b85daf6c3bb703d7795eaf1c5177aebae1afcf811dd7009f4b0",
]
}

View File

@@ -0,0 +1,33 @@
/**
* Example of 'foo_bar' module in `foo_bar.tf`.
*
* - list item 1
* - list item 2
*
* Even inline **formatting** in _here_ is possible.
* and some [link](https://domain.com/)
*/
terraform {
required_version = ">= 0.12"
required_providers {
aws = ">= 2.15.0"
}
}
resource "tls_private_key" "baz" {}
data "aws_caller_identity" "current" {
provider = "aws"
}
resource "null_resource" "foo" {}
module "foo" {
source = "bar"
version = "1.2.3"
}
module "foobar" {
source = "git@github.com:module/path?ref=v7.8.9"
}

View File

@@ -0,0 +1,14 @@
output C {
description = "It's unquoted output."
value = "c"
}
output "A" {
description = "A description"
value = "a"
}
// B description
output "B" {
value = "b"
}

View File

@@ -0,0 +1,30 @@
// D description
variable "D" {
default = "d"
}
variable "B" {
default = "b"
}
variable "E" {
default = ""
}
# A Description
# in multiple lines
variable A {}
variable "C" {
description = "C description"
default = "c"
}
variable "F" {
description = "F description"
}
variable "G" {
description = "G description"
default = null
}