mirror of
https://github.com/terraform-docs/terraform-docs.git
synced 2026-03-27 12:58:35 +07:00
Template should be optional for output-mode 'replace'
Template is optional when writing to --output-file and --output-mode is 'replace'. This is useful in particular when user wants to directly output to a known file format, for example json, yaml, in which sourrounding comments will break linters or functionality of those formats. Signed-off-by: Khosrow Moossavi <khos2ow@gmail.com>
This commit is contained in:
@@ -44,6 +44,7 @@ corresponding default values (if applicable).
|
||||
|
||||
```yaml
|
||||
formatter: <FORMATTER_NAME>
|
||||
|
||||
header-from: main.tf
|
||||
footer-from: ""
|
||||
|
||||
@@ -145,7 +146,8 @@ flag) is not empty. Insersion behavior can be controlled by `output.mode` (or
|
||||
- `inject` (default)
|
||||
|
||||
Partially replace the `output-file` with generated output. This will fail if
|
||||
`output-file` doesn't exist.
|
||||
`output-file` doesn't exist. Also will fail if `output-file` doesn't already
|
||||
have surrounding comments.
|
||||
|
||||
- `replace`
|
||||
|
||||
@@ -153,8 +155,12 @@ flag) is not empty. Insersion behavior can be controlled by `output.mode` (or
|
||||
the `output-file` if it doesn't exist.
|
||||
|
||||
The output generated by formatters (`markdown`, `asciidoc`, etc) will first be
|
||||
inserted into a template before getting saved into the file. This template can
|
||||
be customized with `output.template` or `--output-template string` CLI flag.
|
||||
inserted into a template, if provided, before getting saved into the file. This
|
||||
template can be customized with `output.template` or `--output-template string`
|
||||
CLI flag.
|
||||
|
||||
**Note:** `output.template` is optional for mode `replace`.
|
||||
|
||||
The default template value is:
|
||||
|
||||
```go
|
||||
@@ -163,8 +169,7 @@ The default template value is:
|
||||
<!-- END_TF_DOCS -->
|
||||
```
|
||||
|
||||
This template consists of three items, all of them are mandatory and have to be on
|
||||
separate lines:
|
||||
This template consists of at least three lines (all of which are mandatory):
|
||||
|
||||
- begin comment
|
||||
- `{{ .Content }}` slug
|
||||
@@ -172,3 +177,25 @@ separate lines:
|
||||
|
||||
You may change the wording of comment as you wish, but the comment must be present
|
||||
in the template. Also note that `SPACE`s inside `{{ }}` are mandatory.
|
||||
|
||||
You may also add as many lines as you'd like before or after `{{ .Content }}` line.
|
||||
|
||||
**Note:** `{{ .Content }}` is mandatory if you want to customize template for mode
|
||||
`replace`. For example if you wish to output to YAML file with trailing comment, the
|
||||
following can be used:
|
||||
|
||||
```yaml
|
||||
formatter: yaml
|
||||
|
||||
output:
|
||||
file: output.yaml
|
||||
mode: replace
|
||||
template: |-
|
||||
# Example trailing comments block which will be placed at the top of the
|
||||
# 'output.yaml' file.
|
||||
#
|
||||
# Note that there's no <!-- BEGIN_TF_DOCS --> and <!-- END_TF_DOCS -->
|
||||
# which will break the integrity yaml file.
|
||||
|
||||
{{ .Content }}
|
||||
```
|
||||
|
||||
@@ -71,7 +71,7 @@ configuration file.
|
||||
|
||||
## Insert Output To File
|
||||
|
||||
Since `v0.12.0` generated output can be insterted directly into the file. There
|
||||
Since `v0.12.0`, generated output can be insterted directly into the file. There
|
||||
are two modes of insersion: `inject` (default) or `replace`. Take a look at [output]
|
||||
configuration for all the details.
|
||||
|
||||
|
||||
@@ -156,20 +156,35 @@ func defaultOutput() output {
|
||||
}
|
||||
}
|
||||
|
||||
func (o *output) validate() error {
|
||||
func (o *output) validate() 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 o.File != "" {
|
||||
if o.Mode == "" {
|
||||
return fmt.Errorf("value of '--output-mode' can't be empty")
|
||||
}
|
||||
|
||||
// Template is optional for mode 'replace'
|
||||
if o.Mode == outputModeReplace && o.Template == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if o.Template == "" {
|
||||
return fmt.Errorf("value of '--output-template' can't be empty")
|
||||
}
|
||||
|
||||
index := strings.Index(o.Template, outputContent)
|
||||
if index < 0 {
|
||||
if index := strings.Index(o.Template, outputContent); index < 0 {
|
||||
return fmt.Errorf("value of '--output-template' doesn't have '{{ .Content }}' (note that spaces inside '{{ }}' are mandatory)")
|
||||
}
|
||||
|
||||
// No extra validation is needed for mode 'replace',
|
||||
// the followings only apply for every other modes.
|
||||
if o.Mode == outputModeReplace {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := strings.Split(o.Template, "\n")
|
||||
if len(lines) < 3 {
|
||||
return fmt.Errorf("value of '--output-template' should contain at least 3 lines (begin comment, {{ .Content }}, and end comment)")
|
||||
|
||||
19
internal/cli/testdata/writer/mode-inject.golden
vendored
Normal file
19
internal/cli/testdata/writer/mode-inject.golden
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Foo
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
|
||||
## Bar
|
||||
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua.
|
||||
|
||||
- Ut enim ad minim veniam
|
||||
- quis nostrud exercitation
|
||||
|
||||
<!-- BEGIN_TF_DOCS -->
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
||||
<!-- END_TF_DOCS -->
|
||||
|
||||
## Baz
|
||||
|
||||
esse cillum dolore eu fugiat nulla pariatur.
|
||||
20
internal/cli/testdata/writer/mode-inject.md
vendored
Normal file
20
internal/cli/testdata/writer/mode-inject.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Foo
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
|
||||
## Bar
|
||||
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua.
|
||||
|
||||
- Ut enim ad minim veniam
|
||||
- quis nostrud exercitation
|
||||
|
||||
<!-- BEGIN_TF_DOCS -->
|
||||
ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit
|
||||
<!-- END_TF_DOCS -->
|
||||
|
||||
## Baz
|
||||
|
||||
esse cillum dolore eu fugiat nulla pariatur.
|
||||
3
internal/cli/testdata/writer/mode-replace-with-comment.golden
vendored
Normal file
3
internal/cli/testdata/writer/mode-replace-with-comment.golden
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<!-- BEGIN_TF_DOCS -->
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
||||
<!-- END_TF_DOCS -->
|
||||
1
internal/cli/testdata/writer/mode-replace-without-comment.golden
vendored
Normal file
1
internal/cli/testdata/writer/mode-replace-without-comment.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
||||
1
internal/cli/testdata/writer/mode-replace-without-template.golden
vendored
Normal file
1
internal/cli/testdata/writer/mode-replace-without-template.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
||||
20
internal/cli/testdata/writer/mode-replace.md
vendored
Normal file
20
internal/cli/testdata/writer/mode-replace.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Foo
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
|
||||
## Bar
|
||||
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua.
|
||||
|
||||
- Ut enim ad minim veniam
|
||||
- quis nostrud exercitation
|
||||
|
||||
<!-- BEGIN_TF_DOCS -->
|
||||
ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit
|
||||
<!-- END_TF_DOCS -->
|
||||
|
||||
## Baz
|
||||
|
||||
esse cillum dolore eu fugiat nulla pariatur.
|
||||
@@ -13,6 +13,7 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -55,12 +56,20 @@ type fileWriter struct {
|
||||
template string
|
||||
begin string
|
||||
end string
|
||||
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (fw *fileWriter) Write(p []byte) (int, error) {
|
||||
filename := filepath.Join(fw.dir, fw.file)
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if fw.template == "" {
|
||||
// template is optional for mode replace
|
||||
if fw.mode == outputModeReplace {
|
||||
return fw.write(filename, p)
|
||||
}
|
||||
return 0, errors.New(errTemplateEmpty)
|
||||
}
|
||||
|
||||
@@ -73,38 +82,45 @@ func (fw *fileWriter) Write(p []byte) (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
content := buf.String()
|
||||
filename := filepath.Join(fw.dir, fw.file)
|
||||
|
||||
if fw.mode == outputModeInject {
|
||||
f, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fc := string(f)
|
||||
if fc == "" {
|
||||
return 0, errors.New(errFileEmpty)
|
||||
}
|
||||
|
||||
before := strings.Index(fc, fw.begin)
|
||||
if before < 0 {
|
||||
return 0, errors.New(errBeginCommentMissing)
|
||||
}
|
||||
content = fc[:before] + content
|
||||
|
||||
after := strings.Index(fc, fw.end)
|
||||
if after < 0 {
|
||||
return 0, errors.New(errEndCommentMissing)
|
||||
}
|
||||
if after < before {
|
||||
return 0, errors.New(errEndCommentBeforeBegin)
|
||||
}
|
||||
content += fc[after+len(fw.end):]
|
||||
// Replace the content of 'filename' with generated output,
|
||||
// no further processing is reequired for mode 'replace'.
|
||||
if fw.mode == outputModeReplace {
|
||||
return fw.write(filename, buf.Bytes())
|
||||
}
|
||||
|
||||
n := len(content)
|
||||
err := os.WriteFile(filename, []byte(content), 0644)
|
||||
content := buf.String()
|
||||
|
||||
return n, err
|
||||
f, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fc := string(f)
|
||||
if fc == "" {
|
||||
return 0, errors.New(errFileEmpty)
|
||||
}
|
||||
|
||||
before := strings.Index(fc, fw.begin)
|
||||
if before < 0 {
|
||||
return 0, errors.New(errBeginCommentMissing)
|
||||
}
|
||||
content = fc[:before] + content
|
||||
|
||||
after := strings.Index(fc, fw.end)
|
||||
if after < 0 {
|
||||
return 0, errors.New(errEndCommentMissing)
|
||||
}
|
||||
if after < before {
|
||||
return 0, errors.New(errEndCommentBeforeBegin)
|
||||
}
|
||||
content += fc[after+len(fw.end):]
|
||||
|
||||
return fw.write(filename, []byte(content))
|
||||
}
|
||||
|
||||
func (fw *fileWriter) write(filename string, p []byte) (int, error) {
|
||||
if fw.writer != nil {
|
||||
return fw.writer.Write(p)
|
||||
}
|
||||
return len(p), os.WriteFile(filename, p, 0644)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,14 @@ the root directory of this source tree.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/terraform-docs/terraform-docs/internal/testutil"
|
||||
)
|
||||
|
||||
func TestFileWriter(t *testing.T) {
|
||||
@@ -26,14 +29,73 @@ func TestFileWriter(t *testing.T) {
|
||||
template string
|
||||
begin string
|
||||
end string
|
||||
writer io.Writer
|
||||
|
||||
expected string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// Successful writes
|
||||
"ModeInject": {
|
||||
file: "mode-inject.md",
|
||||
mode: "inject",
|
||||
template: OutputTemplate,
|
||||
begin: outputBeginComment,
|
||||
end: outputEndComment,
|
||||
writer: &bytes.Buffer{},
|
||||
|
||||
expected: "mode-inject",
|
||||
wantErr: false,
|
||||
errMsg: "",
|
||||
},
|
||||
"ModeReplaceWithComment": {
|
||||
file: "mode-replace.md",
|
||||
mode: "replace",
|
||||
template: OutputTemplate,
|
||||
begin: outputBeginComment,
|
||||
end: outputEndComment,
|
||||
writer: &bytes.Buffer{},
|
||||
|
||||
expected: "mode-replace-with-comment",
|
||||
wantErr: false,
|
||||
errMsg: "",
|
||||
},
|
||||
"ModeReplaceWithoutComment": {
|
||||
file: "mode-replace.md",
|
||||
mode: "replace",
|
||||
template: outputContent,
|
||||
begin: "",
|
||||
end: "",
|
||||
writer: &bytes.Buffer{},
|
||||
|
||||
expected: "mode-replace-without-comment",
|
||||
wantErr: false,
|
||||
errMsg: "",
|
||||
},
|
||||
"ModeReplaceWithoutTemplate": {
|
||||
file: "mode-replace.md",
|
||||
mode: "replace",
|
||||
template: "",
|
||||
begin: "",
|
||||
end: "",
|
||||
writer: &bytes.Buffer{},
|
||||
|
||||
expected: "mode-replace-without-template",
|
||||
wantErr: false,
|
||||
errMsg: "",
|
||||
},
|
||||
|
||||
// Error writes
|
||||
"ModeInjectNoFile": {
|
||||
file: "file-missing.md",
|
||||
mode: "inject",
|
||||
template: OutputTemplate,
|
||||
begin: outputBeginComment,
|
||||
end: outputEndComment,
|
||||
writer: nil,
|
||||
|
||||
expected: "",
|
||||
wantErr: true,
|
||||
errMsg: "open testdata/writer/file-missing.md: no such file or directory",
|
||||
},
|
||||
"EmptyTemplate": {
|
||||
@@ -42,6 +104,10 @@ func TestFileWriter(t *testing.T) {
|
||||
template: "",
|
||||
begin: outputBeginComment,
|
||||
end: outputEndComment,
|
||||
writer: nil,
|
||||
|
||||
expected: "",
|
||||
wantErr: true,
|
||||
errMsg: "template is missing",
|
||||
},
|
||||
"EmptyFile": {
|
||||
@@ -50,6 +116,10 @@ func TestFileWriter(t *testing.T) {
|
||||
template: OutputTemplate,
|
||||
begin: outputBeginComment,
|
||||
end: outputEndComment,
|
||||
writer: nil,
|
||||
|
||||
expected: "",
|
||||
wantErr: true,
|
||||
errMsg: "file content is empty",
|
||||
},
|
||||
"BeginCommentMissing": {
|
||||
@@ -58,6 +128,10 @@ func TestFileWriter(t *testing.T) {
|
||||
template: OutputTemplate,
|
||||
begin: outputBeginComment,
|
||||
end: outputEndComment,
|
||||
writer: nil,
|
||||
|
||||
expected: "",
|
||||
wantErr: true,
|
||||
errMsg: "begin comment is missing",
|
||||
},
|
||||
"EndCommentMissing": {
|
||||
@@ -66,6 +140,10 @@ func TestFileWriter(t *testing.T) {
|
||||
template: OutputTemplate,
|
||||
begin: outputBeginComment,
|
||||
end: outputEndComment,
|
||||
writer: nil,
|
||||
|
||||
expected: "",
|
||||
wantErr: true,
|
||||
errMsg: "end comment is missing",
|
||||
},
|
||||
"EndCommentBeforeBegin": {
|
||||
@@ -74,6 +152,10 @@ func TestFileWriter(t *testing.T) {
|
||||
template: OutputTemplate,
|
||||
begin: outputBeginComment,
|
||||
end: outputEndComment,
|
||||
writer: nil,
|
||||
|
||||
expected: "",
|
||||
wantErr: true,
|
||||
errMsg: "end comment is before begin comment",
|
||||
},
|
||||
}
|
||||
@@ -90,12 +172,26 @@ func TestFileWriter(t *testing.T) {
|
||||
template: tt.template,
|
||||
begin: tt.begin,
|
||||
end: tt.end,
|
||||
|
||||
writer: tt.writer,
|
||||
}
|
||||
|
||||
_, err := io.WriteString(writer, content)
|
||||
|
||||
assert.NotNil(err)
|
||||
assert.Equal(tt.errMsg, err.Error())
|
||||
if tt.wantErr {
|
||||
assert.NotNil(err)
|
||||
assert.Equal(tt.errMsg, err.Error())
|
||||
} else {
|
||||
assert.Nil(err)
|
||||
|
||||
w, ok := tt.writer.(*bytes.Buffer)
|
||||
assert.True(ok, "tt.writer is not a valid bytes.Buffer")
|
||||
|
||||
expected, err := testutil.GetExpected("writer", tt.expected)
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal(expected, w.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user