feat: Add support for .terraform-docs.yml config file (#272)

* Add support for .terraform-docs.yml config file

* add config reader

* add usage documentation and reference guide

* typo

* update docs
This commit is contained in:
Khosrow Moossavi
2020-07-13 22:06:30 -04:00
committed by GitHub
parent 04375a6c6a
commit fd97ec5930
28 changed files with 1039 additions and 91 deletions

View File

@@ -32,16 +32,21 @@ func Execute() error {
func NewCommand() *cobra.Command {
config := cli.DefaultConfig()
cmd := &cobra.Command{
Args: cobra.NoArgs,
Use: "terraform-docs",
Args: cobra.MaximumNArgs(1),
Use: "terraform-docs [PATH]",
Short: "A utility to generate documentation from Terraform modules in various output formats",
Long: "A utility to generate documentation from Terraform modules in various output formats",
Version: version.Full(),
SilenceUsage: true,
SilenceErrors: true,
Annotations: cli.Annotations("root"),
PreRunE: cli.PreRunEFunc(config),
RunE: cli.RunEFunc(config),
}
// flags
cmd.PersistentFlags().StringVarP(&config.File, "config", "c", ".terraform-docs.yml", "config file name")
cmd.PersistentFlags().StringSliceVar(&config.Sections.Show, "show", []string{}, "show section [header, inputs, outputs, providers, requirements]")
cmd.PersistentFlags().StringSliceVar(&config.Sections.Hide, "hide", []string{}, "hide section [header, inputs, outputs, providers, requirements]")
cmd.PersistentFlags().BoolVar(&config.Sections.ShowAll, "show-all", true, "show all sections")

57
docs/CONFIG_FILE.md Normal file
View File

@@ -0,0 +1,57 @@
# Config File Reference
All available options for `.terraform-docs.yml`. Note that not all of them can be used at the same time (e.g. `sections.hide` and `sections.show`)
```yaml
formatter: <FORMATTER_NAME>
header-from: main.tf
sections:
hide-all: false
hide:
- header
- inputs
- outputs
- providers
- requirements
show-all: true
show:
- header
- inputs
- outputs
- providers
- requirements
output-values:
enabled: false
from: ""
sort:
enabled: true
by:
- required
- type
settings:
color: true
escape: true
indent: 2
required: true
sensitive: true
```
Available options for `FORMATTER_NAME` are:
- `asciidoc`
- `asciidoc document`
- `asciidoc table`
- `json`
- `markdonw`
- `markdonw document`
- `markdonw table`
- `pretty`
- `tfvars hcl`
- `tfvars json`
- `toml`
- `xml`
- `yaml`

View File

@@ -6,9 +6,14 @@ A utility to generate documentation from Terraform modules in various output for
A utility to generate documentation from Terraform modules in various output formats
```
terraform-docs [PATH] [flags]
```
### Options
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
-h, --help help for terraform-docs
--hide strings hide section [header, inputs, outputs, providers, requirements]
@@ -39,4 +44,4 @@ A utility to generate documentation from Terraform modules in various output for
* [terraform-docs xml](/docs/formats/xml.md) - Generate XML of inputs and outputs
* [terraform-docs yaml](/docs/formats/yaml.md) - Generate YAML of inputs and outputs
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -8,6 +8,36 @@ Support for Terraform `v0.12.x` has been added in `terraform-docs` version `v0.8
Please refer to [Formats Guide](/docs/FORMATS_GUIDE.md) for guidance on output formats, execution syntax, CLI options, etc.
## Configuration File
`terraform-docs` can read the desired formatter and options from a file, instead of being passed to in CLI. This is a convenient way to share the configuation amongst teammates and also CI pipelines. To do so you can use `-c` or `--config` flag which accepts name of the config file (default to `.terraform-docs.yml`). Example `.terraform-docs.yml`:
```yaml
formatter: markdown table
header-from: doc.txt
sections:
hide:
- inputs
- outputs
settings:
indent: 4
required: false
```
when executed:
```bash
terraform-docs ./example/ # this will read the config above and:
#
# 1) generate output of 'Markdown Table'
# 2) read module 'Header' from 'doc.txt'
# 3) hide 'Inputs' and 'Outputs'
# 4) set 'Indention' to 4
# 5) hide 'Required' column
```
Please refer to [Config File Reference](/docs/CONFIG_FILE.md) for all the available confiuartion options.
## Control Visibility of Sections
Output generated by `terraform-docs` consists of different sections (header, requirements, providers, inputs, outputs) which are visible by default. The visibility of these can be controlled by one or combination of : `--show-all`, `--hide-all`, `--show <name>` and `--hide <name>`. For example:

View File

@@ -19,6 +19,7 @@ terraform-docs asciidoc document [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -419,4 +420,4 @@ generates the following output:
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -19,6 +19,7 @@ terraform-docs asciidoc table [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -375,4 +376,4 @@ generates the following output:
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -22,6 +22,7 @@ terraform-docs asciidoc [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -39,4 +40,4 @@ terraform-docs asciidoc [PATH] [flags]
* [terraform-docs asciidoc document](/docs/formats/asciidoc-document.md) - Generate AsciiDoc document of inputs and outputs
* [terraform-docs asciidoc table](/docs/formats/asciidoc-table.md) - Generate AsciiDoc tables of inputs and outputs
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -20,6 +20,7 @@ terraform-docs json [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -338,4 +339,4 @@ generates the following output:
}
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -19,6 +19,7 @@ terraform-docs markdown document [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--escape escape special characters (default true)
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
@@ -420,4 +421,4 @@ generates the following output:
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -19,6 +19,7 @@ terraform-docs markdown table [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--escape escape special characters (default true)
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
@@ -146,4 +147,4 @@ generates the following output:
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -23,6 +23,7 @@ terraform-docs markdown [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -40,4 +41,4 @@ terraform-docs markdown [PATH] [flags]
* [terraform-docs markdown document](/docs/formats/markdown-document.md) - Generate Markdown document of inputs and outputs
* [terraform-docs markdown table](/docs/formats/markdown-table.md) - Generate Markdown tables of inputs and outputs
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -20,6 +20,7 @@ terraform-docs pretty [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -242,4 +243,4 @@ generates the following output:
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -19,6 +19,7 @@ terraform-docs tfvars hcl [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -98,4 +99,4 @@ generates the following output:
with-url = ""
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -19,6 +19,7 @@ terraform-docs tfvars json [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -100,4 +101,4 @@ generates the following output:
}
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -15,6 +15,7 @@ Generate terraform.tfvars of inputs
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -32,4 +33,4 @@ Generate terraform.tfvars of inputs
* [terraform-docs tfvars hcl](/docs/formats/tfvars-hcl.md) - Generate HCL format of terraform.tfvars of inputs
* [terraform-docs tfvars json](/docs/formats/tfvars-json.md) - Generate JSON format of terraform.tfvars of inputs
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -19,6 +19,7 @@ terraform-docs toml [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -315,4 +316,4 @@ generates the following output:
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -19,6 +19,7 @@ terraform-docs xml [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -337,4 +338,4 @@ generates the following output:
</module>
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -19,6 +19,7 @@ terraform-docs yaml [PATH] [flags]
### Options inherited from parent commands
```
-c, --config string config file name (default ".terraform-docs.yml")
--header-from string relative path of a file to read header from (default "main.tf")
--hide strings hide section [header, inputs, outputs, providers, requirements]
--hide-all hide all sections (default false)
@@ -289,4 +290,4 @@ generates the following output:
version: '>= 2.2.0'
###### Auto generated by spf13/cobra on 24-May-2020
###### Auto generated by spf13/cobra on 13-Jul-2020

View File

@@ -0,0 +1,15 @@
formatter: markdown table
header-from: doc.txt
sections:
hide-all: true
show:
- header
- inputs
- providers
sort:
enabled: true
by:
- required
settings:
indent: 4
escape: false

View File

@@ -1,16 +1,10 @@
package cli
import (
"strings"
)
// Annotations returns set of annotations for cobra.Commands,
// specifically the 'command' namd and command 'kind'
// specifically the command 'name' and command 'kind'
func Annotations(cmd string) map[string]string {
annotations := make(map[string]string)
for _, s := range strings.Split(cmd, " ") {
annotations["command"] = s
return map[string]string{
"command": cmd,
"kind": "formatter",
}
annotations["kind"] = "formatter"
return annotations
}

View File

@@ -0,0 +1,35 @@
package cli
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommandAnnotations(t *testing.T) {
tests := []struct {
name string
command string
}{
{
name: "command annotations",
command: "foo",
},
{
name: "command annotations",
command: "foo bar",
},
{
name: "command annotations",
command: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual := Annotations(tt.command)
assert.Equal(tt.command, actual["command"])
assert.Equal("formatter", actual["kind"])
})
}
}

View File

@@ -2,15 +2,11 @@ package cli
import (
"fmt"
"strings"
"github.com/terraform-docs/terraform-docs/internal/module"
"github.com/terraform-docs/terraform-docs/pkg/print"
)
// list of flagset items which explicitly changed from CLI
var changedfs = make(map[string]bool)
type _sections struct {
NoHeader bool
NoInputs bool
@@ -19,26 +15,26 @@ type _sections struct {
NoRequirements bool
}
type sections struct {
Show []string
Hide []string
ShowAll bool
HideAll bool
Deprecated *_sections
Show []string `yaml:"show"`
Hide []string `yaml:"hide"`
ShowAll bool `yaml:"show-all"`
HideAll bool `yaml:"hide-all"`
Deprecated _sections `yaml:"-"`
header bool
inputs bool
outputs bool
providers bool
requirements bool
header bool `yaml:"-"`
inputs bool `yaml:"-"`
outputs bool `yaml:"-"`
providers bool `yaml:"-"`
requirements bool `yaml:"-"`
}
func defaultSections() *sections {
return &sections{
func defaultSections() sections {
return sections{
Show: []string{},
Hide: []string{},
ShowAll: true,
HideAll: false,
Deprecated: &_sections{
Deprecated: _sections{
NoHeader: false,
NoInputs: false,
NoOutputs: false,
@@ -110,12 +106,12 @@ func (s *sections) visibility(section string) bool {
}
type outputvalues struct {
Enabled bool
From string
Enabled bool `yaml:"enabled"`
From string `yaml:"from"`
}
func defaultOutputValues() *outputvalues {
return &outputvalues{
func defaultOutputValues() outputvalues {
return outputvalues{
Enabled: false,
From: "",
}
@@ -132,26 +128,28 @@ func (o *outputvalues) validate() error {
}
type sortby struct {
Required bool
Type bool
Required bool `name:"required"`
Type bool `name:"type"`
}
type _sort struct {
NoSort bool
}
type sort struct {
Enabled bool
By *sortby
Deprecated *_sort
Enabled bool `yaml:"enabled"`
ByList []string `yaml:"by"`
By sortby `yaml:"-"`
Deprecated _sort `yaml:"-"`
}
func defaultSort() *sort {
return &sort{
func defaultSort() sort {
return sort{
Enabled: true,
By: &sortby{
ByList: []string{},
By: sortby{
Required: false,
Type: false,
},
Deprecated: &_sort{
Deprecated: _sort{
NoSort: false,
},
}
@@ -164,6 +162,14 @@ func (s *sort) validate() error {
return fmt.Errorf("'--%s' and '--no-%s' can't be used together", item, item)
}
}
types := []string{"required", "type"}
for _, item := range s.ByList {
switch item {
case types[0], types[1]:
default:
return fmt.Errorf("'%s' is not a valid sort type", item)
}
}
if s.By.Required && s.By.Type {
return fmt.Errorf("'--sort-by-required' and '--sort-by-type' can't be used together")
}
@@ -177,22 +183,22 @@ type _settings struct {
NoSensitive bool
}
type settings struct {
Color bool
Escape bool
Indent int
Required bool
Sensitive bool
Deprecated *_settings
Color bool `yaml:"color"`
Escape bool `yaml:"escape"`
Indent int `yaml:"indent"`
Required bool `yaml:"required"`
Sensitive bool `yaml:"sensitive"`
Deprecated _settings `yaml:"-"`
}
func defaultSettings() *settings {
return &settings{
func defaultSettings() settings {
return settings{
Color: true,
Escape: true,
Indent: 2,
Required: true,
Sensitive: true,
Deprecated: &_settings{
Deprecated: _settings{
NoColor: false,
NoEscape: false,
NoRequired: false,
@@ -213,17 +219,19 @@ func (s *settings) validate() error {
// Config represents all the available config options that can be accessed and passed through CLI
type Config struct {
Formatter string
HeaderFrom string
Sections *sections
OutputValues *outputvalues
Sort *sort
Settings *settings
File string `yaml:"-"`
Formatter string `yaml:"formatter"`
HeaderFrom string `yaml:"header-from"`
Sections sections `yaml:"sections"`
OutputValues outputvalues `yaml:"output-values"`
Sort sort `yaml:"sort"`
Settings settings `yaml:"settings"`
}
// DefaultConfig returns new instance of Config with default values set
func DefaultConfig() *Config {
return &Config{
File: "",
Formatter: "",
HeaderFrom: "main.tf",
Sections: defaultSections(),
@@ -233,10 +241,8 @@ func DefaultConfig() *Config {
}
}
// normalize provided Config
func (c *Config) normalize(command string) {
c.Formatter = strings.Replace(command, "terraform-docs ", "", -1)
// process provided Config
func (c *Config) process() {
// sections
if c.Sections.HideAll && !changedfs["show-all"] {
c.Sections.ShowAll = false
@@ -251,27 +257,32 @@ func (c *Config) normalize(command string) {
c.Sections.requirements = c.Sections.visibility("requirements")
// sort
if !changedfs["sort"] {
if !changedfs["sort"] && changedfs["no-sort"] {
c.Sort.Enabled = !c.Sort.Deprecated.NoSort
}
// settings
if !changedfs["escape"] {
if !changedfs["escape"] && changedfs["no-escape"] {
c.Settings.Escape = !c.Settings.Deprecated.NoEscape
}
if !changedfs["color"] {
if !changedfs["color"] && changedfs["no-color"] {
c.Settings.Color = !c.Settings.Deprecated.NoColor
}
if !changedfs["required"] {
if !changedfs["required"] && changedfs["no-required"] {
c.Settings.Required = !c.Settings.Deprecated.NoRequired
}
if !changedfs["sensitive"] {
if !changedfs["sensitive"] && changedfs["no-sensitive"] {
c.Settings.Sensitive = !c.Settings.Deprecated.NoSensitive
}
}
// validate config and check for any misuse or misconfiguration
func (c *Config) validate() error {
// formatter
if c.Formatter == "" {
return fmt.Errorf("value of 'formatter' can't be empty")
}
// header-from
if c.HeaderFrom == "" {
return fmt.Errorf("value of '--header-from' can't be empty")
@@ -338,12 +349,3 @@ func (c *Config) extract() (*print.Settings, *module.Options) {
return settings, options
}
func contains(list []string, name string) bool {
for _, i := range list {
if i == name {
return true
}
}
return false
}

166
internal/cli/reader.go Normal file
View File

@@ -0,0 +1,166 @@
package cli
import (
"fmt"
"io/ioutil"
"os"
"reflect"
"gopkg.in/yaml.v3"
)
type cfgreader struct {
file string
config *Config
overrides Config
}
func (c *cfgreader) exist() (bool, error) {
if c.file == "" {
return false, fmt.Errorf("config file name is missing")
}
if info, err := os.Stat(c.file); os.IsNotExist(err) || info.IsDir() {
return false, err
}
return true, nil
}
func (c *cfgreader) parse() error {
if ok, err := c.exist(); !ok {
return err
}
content, err := ioutil.ReadFile(c.file)
if err != nil {
return err
}
c.overrides = *c.config
if err := yaml.Unmarshal(content, c.config); err != nil {
return err
}
if c.config.Sections.HideAll && !changedfs["show-all"] {
c.config.Sections.ShowAll = false
}
if !c.config.Sections.ShowAll && !changedfs["hide-all"] {
c.config.Sections.HideAll = true
}
for flag, enabled := range changedfs {
if !enabled {
continue
}
switch flag {
case "header-from":
if err := c.overrideValue(flag, c.config, &c.overrides); err != nil {
return err
}
case "show":
c.overrideShow()
case "hide":
c.overrideHide()
case "sort":
if err := c.overrideValue("enabled", &c.config.Sort, &c.overrides.Sort); err != nil {
return err
}
case "sort-by-required", "sort-by-type":
mapping := map[string]string{"sort-by-required": "required", "sort-by-type": "type"}
if !contains(c.config.Sort.ByList, mapping[flag]) {
c.config.Sort.ByList = append(c.config.Sort.ByList, mapping[flag])
}
el := reflect.ValueOf(&c.overrides.Sort.By).Elem()
field, err := c.findField(el, "name", mapping[flag])
if err != nil {
return err
}
if !el.FieldByName(field).Bool() {
c.config.Sort.ByList = remove(c.config.Sort.ByList, mapping[flag])
}
case "output-values", "output-values-from":
mapping := map[string]string{"output-values": "enabled", "output-values-from": "from"}
if err := c.overrideValue(mapping[flag], &c.config.OutputValues, &c.overrides.OutputValues); err != nil {
return err
}
case "color", "escape", "indent", "required", "sensitive":
if err := c.overrideValue(flag, &c.config.Settings, &c.overrides.Settings); err != nil {
return err
}
}
}
if err := c.updateSortTypes(); err != nil {
return err
}
return nil
}
func (c *cfgreader) overrideValue(name string, to interface{}, from interface{}) error {
if name == "" || name == "-" {
return fmt.Errorf("tag name cannot be blank or empty")
}
toEl := reflect.ValueOf(to).Elem()
field, err := c.findField(toEl, "yaml", name)
if err != nil {
return err
}
fromEl := reflect.ValueOf(from).Elem()
toEl.FieldByName(field).Set(fromEl.FieldByName(field))
return nil
}
func (c *cfgreader) overrideShow() {
for _, item := range c.overrides.Sections.Show {
if c.config.Sections.ShowAll {
if contains(c.config.Sections.Hide, item) {
c.config.Sections.Hide = remove(c.config.Sections.Hide, item)
c.config.Sections.Show = remove(c.config.Sections.Show, item)
}
} else {
if !contains(c.config.Sections.Show, item) {
c.config.Sections.Show = append(c.config.Sections.Show, item)
}
}
}
}
func (c *cfgreader) overrideHide() {
for _, item := range c.overrides.Sections.Hide {
if c.config.Sections.HideAll {
if contains(c.config.Sections.Show, item) {
c.config.Sections.Show = remove(c.config.Sections.Show, item)
c.config.Sections.Hide = remove(c.config.Sections.Hide, item)
}
} else {
if !contains(c.config.Sections.Hide, item) {
c.config.Sections.Hide = append(c.config.Sections.Hide, item)
}
}
}
}
func (c *cfgreader) updateSortTypes() error {
for _, item := range c.config.Sort.ByList {
el := reflect.ValueOf(&c.config.Sort.By).Elem()
field, err := c.findField(el, "name", item)
if err != nil {
return err
}
el.FieldByName(field).Set(reflect.ValueOf(true))
}
return nil
}
func (c *cfgreader) findField(el reflect.Value, tag string, value string) (string, error) {
for i := 0; i < el.NumField(); i++ {
f := el.Type().Field(i)
t := f.Tag.Get(tag)
if t == "" || t == "-" || t != value {
continue
}
return f.Name, nil
}
return "", fmt.Errorf("field with tag: '%s', value; '%s' not found or not readable", tag, value)
}

425
internal/cli/reader_test.go Normal file
View File

@@ -0,0 +1,425 @@
package cli
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExists(t *testing.T) {
tests := []struct {
name string
config cfgreader
expected bool
wantErr bool
errMsg string
}{
{
name: "config file exists",
config: cfgreader{file: "testdata/sample-config.yaml"},
expected: true,
wantErr: false,
errMsg: "",
},
{
name: "config file not found",
config: cfgreader{file: "testdata/noop.yaml"},
expected: false,
wantErr: true,
errMsg: "",
},
{
name: "config file empty",
config: cfgreader{file: ""},
expected: false,
wantErr: true,
errMsg: "config file name is missing",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual, err := tt.config.exist()
if tt.wantErr {
assert.NotNil(err)
if tt.errMsg != "" {
assert.Equal(tt.errMsg, err.Error())
}
} else {
assert.Nil(err)
}
assert.Equal(tt.expected, actual)
})
}
}
func TestOverrideValue(t *testing.T) {
config := DefaultConfig()
override := DefaultConfig()
tests := []struct {
name string
tag string
to func() interface{}
from func() interface{}
overrideFn func()
wantErr bool
errMsg string
}{
{
name: "override values of given field",
tag: "header-from",
to: func() interface{} { return config },
from: func() interface{} { return override },
overrideFn: func() { override.HeaderFrom = "foo.txt" },
wantErr: false,
errMsg: "",
},
{
name: "override values of given field",
tag: "enabled",
to: func() interface{} { return &config.Sort },
from: func() interface{} { return &override.Sort },
overrideFn: func() { override.Sort.Enabled = false },
wantErr: false,
errMsg: "",
},
{
name: "override values of given field",
tag: "color",
to: func() interface{} { return &config.Settings },
from: func() interface{} { return &override.Settings },
overrideFn: func() { override.Settings.Color = false },
wantErr: false,
errMsg: "",
},
{
name: "override values of unkwon field tag",
tag: "not-available",
to: func() interface{} { return config },
from: func() interface{} { return override },
overrideFn: func() {},
wantErr: true,
errMsg: "field with tag: 'yaml', value; 'not-available' not found or not readable",
},
{
name: "override values of blank field tag",
tag: "-",
to: func() interface{} { return config },
from: func() interface{} { return override },
overrideFn: func() {},
wantErr: true,
errMsg: "tag name cannot be blank or empty",
},
{
name: "override values of empty field tag",
tag: "",
to: func() interface{} { return config },
from: func() interface{} { return override },
overrideFn: func() {},
wantErr: true,
errMsg: "tag name cannot be blank or empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
c := cfgreader{config: config}
tt.overrideFn()
if !tt.wantErr {
// make sure before values are different
assert.NotEqual(override, config)
}
// then override property 'from' to 'to'
err := c.overrideValue(tt.tag, tt.to(), tt.from())
if tt.wantErr {
assert.NotNil(err)
assert.Equal(tt.errMsg, err.Error())
} else {
assert.Nil(err)
}
// then make sure values are the same now
assert.Equal(override, config)
})
}
}
func TestOverrideShow(t *testing.T) {
tests := []struct {
name string
show []string
hide []string
showall bool
overrideShow []string
expectedShow []string
expectedHide []string
}{
{
name: "override section show",
show: []string{""},
hide: []string{"inputs", "outputs"},
showall: true,
overrideShow: []string{"inputs"},
expectedShow: []string{""},
expectedHide: []string{"outputs"},
},
{
name: "override section show",
show: []string{"providers"},
hide: []string{"inputs"},
showall: true,
overrideShow: []string{"outputs"},
expectedShow: []string{"providers"},
expectedHide: []string{"inputs"},
},
{
name: "override section show",
show: []string{"inputs"},
hide: []string{"providers"},
showall: false,
overrideShow: []string{"outputs"},
expectedShow: []string{"inputs", "outputs"},
expectedHide: []string{"providers"},
},
{
name: "override section show",
show: []string{"inputs"},
hide: []string{"inputs"},
showall: false,
overrideShow: []string{"inputs"},
expectedShow: []string{"inputs"},
expectedHide: []string{"inputs"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
config := DefaultConfig()
override := DefaultConfig()
c := cfgreader{config: config, overrides: *override}
c.config.Sections.Show = tt.show
c.config.Sections.Hide = tt.hide
c.config.Sections.ShowAll = tt.showall
c.overrides.Sections.Show = tt.overrideShow
c.overrideShow()
assert.Equal(tt.expectedShow, c.config.Sections.Show)
assert.Equal(tt.expectedHide, c.config.Sections.Hide)
})
}
}
func TestOverrideHide(t *testing.T) {
tests := []struct {
name string
show []string
hide []string
hideall bool
overrideHide []string
expectedShow []string
expectedHide []string
}{
{
name: "override section hide",
show: []string{"inputs", "outputs"},
hide: []string{""},
hideall: true,
overrideHide: []string{"inputs"},
expectedShow: []string{"outputs"},
expectedHide: []string{""},
},
{
name: "override section hide",
show: []string{"inputs"},
hide: []string{"providers"},
hideall: true,
overrideHide: []string{"outputs"},
expectedShow: []string{"inputs"},
expectedHide: []string{"providers"},
},
{
name: "override section hide",
show: []string{"providers"},
hide: []string{"inputs"},
hideall: false,
overrideHide: []string{"outputs"},
expectedShow: []string{"providers"},
expectedHide: []string{"inputs", "outputs"},
},
{
name: "override section hide",
show: []string{"inputs"},
hide: []string{"inputs"},
hideall: false,
overrideHide: []string{"inputs"},
expectedShow: []string{"inputs"},
expectedHide: []string{"inputs"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
config := DefaultConfig()
override := DefaultConfig()
c := cfgreader{config: config, overrides: *override}
c.config.Sections.Show = tt.show
c.config.Sections.Hide = tt.hide
c.config.Sections.HideAll = tt.hideall
c.overrides.Sections.Hide = tt.overrideHide
c.overrideHide()
assert.Equal(tt.expectedShow, c.config.Sections.Show)
assert.Equal(tt.expectedHide, c.config.Sections.Hide)
})
}
}
func TestUpdateSortTypes(t *testing.T) {
tests := []struct {
name string
appendFn func(config *Config)
expectedFn func(config *Config) bool
wantErr bool
errMsg string
}{
{
name: "override values of given field",
appendFn: func(config *Config) { config.Sort.ByList = append(config.Sort.ByList, "required") },
expectedFn: func(config *Config) bool { return config.Sort.By.Required },
wantErr: false,
errMsg: "",
},
{
name: "override values of given field",
appendFn: func(config *Config) { config.Sort.ByList = append(config.Sort.ByList, "type") },
expectedFn: func(config *Config) bool { return config.Sort.By.Type },
wantErr: false,
errMsg: "",
},
{
name: "override values of given field",
appendFn: func(config *Config) { config.Sort.ByList = append(config.Sort.ByList, "unknown") },
expectedFn: func(config *Config) bool { return false },
wantErr: true,
errMsg: "field with tag: 'name', value; 'unknown' not found or not readable",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
config := DefaultConfig()
c := cfgreader{config: config}
tt.appendFn(config)
// make sure before values is false
assert.Equal(false, tt.expectedFn(config))
// then update sort types
err := c.updateSortTypes()
if tt.wantErr {
assert.NotNil(err)
assert.Equal(tt.errMsg, err.Error())
} else {
// then make sure values is true
assert.Nil(err)
assert.Equal(true, tt.expectedFn(config))
}
})
}
}
func TestFindField(t *testing.T) {
type sample struct {
A string `foo:"a"`
B string `bar:"b"`
C string `baz:"-"`
D string `fizz:"-"`
E string `buzz:""`
F string
}
tests := []struct {
name string
tag string
value string
expected string
wantErr bool
errMsg string
}{
{
name: "find field with given tag",
tag: "foo",
value: "a",
expected: "A",
wantErr: false,
errMsg: "",
},
{
name: "find field with given tag",
tag: "bar",
value: "b",
expected: "B",
wantErr: false,
errMsg: "",
},
{
name: "find field with tag none",
tag: "baz",
value: "-",
expected: "",
wantErr: true,
errMsg: "field with tag: 'baz', value; '-' not found or not readable",
},
{
name: "find field with tag none",
tag: "fizz",
value: "-",
expected: "",
wantErr: true,
errMsg: "field with tag: 'fizz', value; '-' not found or not readable",
},
{
name: "find field with tag empty",
tag: "buzz",
value: "",
expected: "",
wantErr: true,
errMsg: "field with tag: 'buzz', value; '' not found or not readable",
},
{
name: "find field with tag unknown",
tag: "unknown",
value: "unknown",
expected: "",
wantErr: true,
errMsg: "field with tag: 'unknown', value; 'unknown' not found or not readable",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
c := cfgreader{}
el := reflect.ValueOf(&sample{}).Elem()
actual, err := c.findField(el, tt.tag, tt.value)
if tt.wantErr {
assert.NotNil(err)
assert.Equal(tt.errMsg, err.Error())
} else {
assert.Nil(err)
assert.Equal(tt.expected, actual)
}
})
}
}

View File

@@ -2,6 +2,8 @@ package cli
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -10,16 +12,63 @@ import (
"github.com/terraform-docs/terraform-docs/internal/module"
)
// list of flagset items which are explicitly changed from CLI
var changedfs = make(map[string]bool)
// PreRunEFunc returns actual 'cobra.Command#PreRunE' function
// for 'formatter' commands. This functions reads and normalizes
// flags and arguments passed through CLI execution.
func PreRunEFunc(config *Config) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
formatter := cmd.Annotations["command"]
// root command must have an argument, otherwise we're going to show help
if formatter == "root" && len(args) == 0 {
cmd.Help() //nolint:errcheck
os.Exit(0)
}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
changedfs[f.Name] = f.Changed
})
config.normalize(cmd.CommandPath())
// read config file if provided and/or available
if config.File == "" {
return fmt.Errorf("value of '--config' can't be empty")
}
file := filepath.Join(args[0], config.File)
cfgreader := &cfgreader{
file: file,
config: config,
}
if found, err := cfgreader.exist(); !found {
// config is explicitly provided and file not found, this is an error
if changedfs["config"] {
return err
}
// config is not provided and file not found, only show an error for the root command
if formatter == "root" {
cmd.Help() //nolint:errcheck
os.Exit(0)
}
} else {
// config file is found, we're now going to parse it
if err := cfgreader.parse(); err != nil {
return err
}
}
// explicitly setting formatter to Config for non-root commands this
// will effectively override formattter properties from config file
// if 1) config file exists and 2) formatter is set and 3) explicitly
// a subcommand was executed in the terminal
if formatter != "root" {
config.Formatter = formatter
}
config.process()
if err := config.validate(); err != nil {
return err

View File

@@ -0,0 +1 @@
formatter: foo

29
internal/cli/util.go Normal file
View File

@@ -0,0 +1,29 @@
package cli
func contains(list []string, name string) bool {
for _, v := range list {
if v == name {
return true
}
}
return false
}
func index(list []string, name string) int {
for i, v := range list {
if v == name {
return i
}
}
return -1
}
func remove(list []string, name string) []string {
index := index(list, name)
if index < 0 {
return list
}
list[index] = list[len(list)-1]
list[len(list)-1] = ""
return list[:len(list)-1]
}

121
internal/cli/util_test.go Normal file
View File

@@ -0,0 +1,121 @@
package cli
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSliceContains(t *testing.T) {
list := []string{"foo", "bar", "buzz"}
tests := []struct {
name string
item string
expected bool
}{
{
name: "item exists in slice",
item: "foo",
expected: true,
},
{
name: "item exists in slice",
item: "bar",
expected: true,
},
{
name: "item not exist in slice",
item: "fizz",
expected: false,
},
{
name: "empty item",
item: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual := contains(list, tt.item)
assert.Equal(tt.expected, actual)
})
}
}
func TestSliceIndex(t *testing.T) {
list := []string{"foo", "bar", "buzz"}
tests := []struct {
name string
item string
expected int
}{
{
name: "index of item exists in slice",
item: "foo",
expected: 0,
},
{
name: "index of item exists in slice",
item: "bar",
expected: 1,
},
{
name: "index of item not exist in slice",
item: "fizz",
expected: -1,
},
{
name: "index of empty item",
item: "",
expected: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual := index(list, tt.item)
assert.Equal(tt.expected, actual)
})
}
}
func TestSliceRemove(t *testing.T) {
list := []string{"foo", "bar", "buzz"}
tests := []struct {
name string
item string
expected []string
}{
{
name: "remove item exists in slice",
item: "foo",
expected: []string{"buzz", "bar"},
},
{
name: "remove item exists in slice",
item: "bar",
expected: []string{"foo", "buzz"},
},
{
name: "remove item not exist in slice",
item: "fizz",
expected: []string{"foo", "bar", "buzz"},
},
{
name: "remove empty item",
item: "",
expected: []string{"foo", "bar", "buzz"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
cpy := make([]string, len(list))
copy(cpy, list)
actual := remove(cpy, tt.item)
assert.Equal(len(tt.expected), len(actual))
assert.Equal(tt.expected, actual)
})
}
}