Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"HCL2"-based validate command #17539

Merged
merged 4 commits into from
Mar 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/local/backend_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
continue
}
if b.CLI != nil {
b.CLI.Warn(format.Diagnostic(diag, b.Colorize(), 72))
// FIXME: We don't have access to the source code cache
// in here, so we can't produce source code snippets
// from this codepath.
b.CLI.Warn(format.Diagnostic(diag, nil, b.Colorize(), 72))
} else {
desc := diag.Description()
log.Printf("[WARN] backend/local: %s", desc.Summary)
Expand Down
112 changes: 102 additions & 10 deletions command/format/diagnostic.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package format

import (
"bufio"
"bytes"
"fmt"
"strings"

"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcled"
"github.com/hashicorp/hcl2/hclparse"
"github.com/hashicorp/terraform/tfdiags"
"github.com/mitchellh/colorstring"
wordwrap "github.com/mitchellh/go-wordwrap"
Expand All @@ -16,7 +21,7 @@ import (
// at all. Although the long-form text parts of the message are wrapped,
// not all aspects of the message are guaranteed to fit within the specified
// terminal width.
func Diagnostic(diag tfdiags.Diagnostic, color *colorstring.Colorize, width int) string {
func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize, width int) string {
if diag == nil {
// No good reason to pass a nil diagnostic in here...
return ""
Expand All @@ -41,17 +46,74 @@ func Diagnostic(diag tfdiags.Diagnostic, color *colorstring.Colorize, width int)
// We don't wrap the summary, since we expect it to be terse, and since
// this is where we put the text of a native Go error it may not always
// be pure text that lends itself well to word-wrapping.
fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), desc.Summary)

if sourceRefs.Subject != nil {
fmt.Fprintf(&buf, color.Color("[bold]%s[reset] at %s\n\n"), desc.Summary, sourceRefs.Subject.StartString())
} else {
fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), desc.Summary)
}
// We'll borrow HCL's range implementation here, because it has some
// handy features to help us produce a nice source code snippet.
highlightRange := sourceRefs.Subject.ToHCL()
snippetRange := highlightRange
if sourceRefs.Context != nil {
snippetRange = sourceRefs.Context.ToHCL()
}
// Make sure the snippet includes the highlight. This should be true
// for any reasonable diagnostic, but we'll make sure.
snippetRange = hcl.RangeOver(snippetRange, highlightRange)

// TODO: also print out the relevant snippet of config source with the
// relevant section highlighted, so the user doesn't need to manually
// correlate back to config. Before we can do this, the HCL2 parser
// needs to be more deeply integrated so that we can use it to obtain
// the parsed source code and AST.
// We can't illustrate an empty range, so we'll turn such ranges into
// single-character ranges, which might not be totally valid (may point
// off the end of a line, or off the end of the file) but are good
// enough for the bounds checks we do below.
if snippetRange.Empty() {
snippetRange.End.Byte++
snippetRange.End.Column++
}
if highlightRange.Empty() {
highlightRange.End.Byte++
highlightRange.End.Column++
}

var src []byte
if sources != nil {
src = sources[snippetRange.Filename]
}
if src == nil {
// This should generally not happen, as long as sources are always
// loaded through the main loader. We may load things in other
// ways in weird cases, so we'll tolerate it at the expense of
// a not-so-helpful error message.
fmt.Fprintf(&buf, " on %s line %d:\n (source code not available)\n\n", highlightRange.Filename, highlightRange.Start.Line)
} else {
contextStr := sourceCodeContextStr(src, highlightRange)
if contextStr != "" {
contextStr = ", in " + contextStr
}
fmt.Fprintf(&buf, " on %s line %d%s:\n", highlightRange.Filename, highlightRange.Start.Line, contextStr)

sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines)
for sc.Scan() {
lineRange := sc.Range()
if !lineRange.Overlaps(snippetRange) {
continue
}
beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
if highlightedRange.Empty() {
fmt.Fprintf(&buf, "%4d: %s\n", lineRange.Start.Line, sc.Bytes())
} else {
before := beforeRange.SliceBytes(src)
highlighted := highlightedRange.SliceBytes(src)
after := afterRange.SliceBytes(src)
fmt.Fprintf(
&buf, color.Color("%4d: %s[underline]%s[reset]%s\n"),
lineRange.Start.Line,
before, highlighted, after,
)
}
}

buf.WriteByte('\n')
}
}

if desc.Detail != "" {
detail := desc.Detail
Expand All @@ -63,3 +125,33 @@ func Diagnostic(diag tfdiags.Diagnostic, color *colorstring.Colorize, width int)

return buf.String()
}

// sourceCodeContextStr attempts to find a user-friendly description of
// the location of the given range in the given source code.
//
// An empty string is returned if no suitable description is available, e.g.
// because the source is invalid, or because the offset is not inside any sort
// of identifiable container.
func sourceCodeContextStr(src []byte, rng hcl.Range) string {
filename := rng.Filename
offset := rng.Start.Byte

// We need to re-parse here to get a *hcl.File we can interrogate. This
// is not awesome since we presumably already parsed the file earlier too,
// but this re-parsing is architecturally simpler than retaining all of
// the hcl.File objects and we only do this in the case of an error anyway
// so the overhead here is not a big problem.
parser := hclparse.NewParser()
var file *hcl.File
var diags hcl.Diagnostics
if strings.HasSuffix(filename, ".json") {
file, diags = parser.ParseJSON(src, filename)
} else {
file, diags = parser.ParseHCL(src, filename)
}
if diags.HasErrors() {
return ""
}

return hcled.ContextString(file, offset)
}
33 changes: 33 additions & 0 deletions command/hook_module_install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package command

import (
"fmt"

version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/configs/configload"
"github.com/mitchellh/cli"
)

type uiModuleInstallHooks struct {
configload.InstallHooksImpl
Ui cli.Ui
ShowLocalPaths bool
}

var _ configload.InstallHooks = uiModuleInstallHooks{}

func (h uiModuleInstallHooks) Download(modulePath, packageAddr string, v *version.Version) {
if v != nil {
h.Ui.Info(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath))
} else {
h.Ui.Info(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath))
}
}

func (h uiModuleInstallHooks) Install(modulePath string, v *version.Version, localDir string) {
if h.ShowLocalPaths {
h.Ui.Info(fmt.Sprintf("- %s in %s", modulePath, localDir))
} else {
h.Ui.Info(fmt.Sprintf("- %s", modulePath))
}
}
8 changes: 7 additions & 1 deletion command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/helper/variables"
"github.com/hashicorp/terraform/helper/wrappedstreams"
Expand Down Expand Up @@ -103,6 +104,11 @@ type Meta struct {
// Private: do not set these
//----------------------------------------------------------

// configLoader is a shared configuration loader that is used by
// LoadConfig and other commands that access configuration files.
// It is initialized on first use.
configLoader *configload.Loader

// backendState is the currently active backend state
backendState *terraform.BackendState

Expand Down Expand Up @@ -547,7 +553,7 @@ func (m *Meta) showDiagnostics(vals ...interface{}) {
// For now, we don't have easy access to the writer that
// ui.Error (etc) are writing to and thus can't interrogate
// to see if it's a terminal and what size it is.
msg := format.Diagnostic(diag, m.Colorize(), 78)
msg := format.Diagnostic(diag, m.configSources(), m.Colorize(), 78)
switch diag.Severity() {
case tfdiags.Error:
m.Ui.Error(msg)
Expand Down
Loading