diff options
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/command/format')
6 files changed, 2206 insertions, 0 deletions
diff --git a/vendor/github.com/hashicorp/terraform/command/format/diagnostic.go b/vendor/github.com/hashicorp/terraform/command/format/diagnostic.go new file mode 100644 index 0000000..3dd9238 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/command/format/diagnostic.go | |||
@@ -0,0 +1,295 @@ | |||
1 | package format | ||
2 | |||
3 | import ( | ||
4 | "bufio" | ||
5 | "bytes" | ||
6 | "fmt" | ||
7 | "sort" | ||
8 | "strings" | ||
9 | |||
10 | "github.com/hashicorp/hcl2/hcl" | ||
11 | "github.com/hashicorp/hcl2/hcled" | ||
12 | "github.com/hashicorp/hcl2/hclparse" | ||
13 | "github.com/hashicorp/terraform/tfdiags" | ||
14 | "github.com/mitchellh/colorstring" | ||
15 | wordwrap "github.com/mitchellh/go-wordwrap" | ||
16 | "github.com/zclconf/go-cty/cty" | ||
17 | ) | ||
18 | |||
19 | // Diagnostic formats a single diagnostic message. | ||
20 | // | ||
21 | // The width argument specifies at what column the diagnostic messages will | ||
22 | // be wrapped. If set to zero, messages will not be wrapped by this function | ||
23 | // at all. Although the long-form text parts of the message are wrapped, | ||
24 | // not all aspects of the message are guaranteed to fit within the specified | ||
25 | // terminal width. | ||
26 | func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize, width int) string { | ||
27 | if diag == nil { | ||
28 | // No good reason to pass a nil diagnostic in here... | ||
29 | return "" | ||
30 | } | ||
31 | |||
32 | var buf bytes.Buffer | ||
33 | |||
34 | switch diag.Severity() { | ||
35 | case tfdiags.Error: | ||
36 | buf.WriteString(color.Color("\n[bold][red]Error: [reset]")) | ||
37 | case tfdiags.Warning: | ||
38 | buf.WriteString(color.Color("\n[bold][yellow]Warning: [reset]")) | ||
39 | default: | ||
40 | // Clear out any coloring that might be applied by Terraform's UI helper, | ||
41 | // so our result is not context-sensitive. | ||
42 | buf.WriteString(color.Color("\n[reset]")) | ||
43 | } | ||
44 | |||
45 | desc := diag.Description() | ||
46 | sourceRefs := diag.Source() | ||
47 | |||
48 | // We don't wrap the summary, since we expect it to be terse, and since | ||
49 | // this is where we put the text of a native Go error it may not always | ||
50 | // be pure text that lends itself well to word-wrapping. | ||
51 | fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), desc.Summary) | ||
52 | |||
53 | if sourceRefs.Subject != nil { | ||
54 | // We'll borrow HCL's range implementation here, because it has some | ||
55 | // handy features to help us produce a nice source code snippet. | ||
56 | highlightRange := sourceRefs.Subject.ToHCL() | ||
57 | snippetRange := highlightRange | ||
58 | if sourceRefs.Context != nil { | ||
59 | snippetRange = sourceRefs.Context.ToHCL() | ||
60 | } | ||
61 | |||
62 | // Make sure the snippet includes the highlight. This should be true | ||
63 | // for any reasonable diagnostic, but we'll make sure. | ||
64 | snippetRange = hcl.RangeOver(snippetRange, highlightRange) | ||
65 | if snippetRange.Empty() { | ||
66 | snippetRange.End.Byte++ | ||
67 | snippetRange.End.Column++ | ||
68 | } | ||
69 | if highlightRange.Empty() { | ||
70 | highlightRange.End.Byte++ | ||
71 | highlightRange.End.Column++ | ||
72 | } | ||
73 | |||
74 | var src []byte | ||
75 | if sources != nil { | ||
76 | src = sources[snippetRange.Filename] | ||
77 | } | ||
78 | if src == nil { | ||
79 | // This should generally not happen, as long as sources are always | ||
80 | // loaded through the main loader. We may load things in other | ||
81 | // ways in weird cases, so we'll tolerate it at the expense of | ||
82 | // a not-so-helpful error message. | ||
83 | fmt.Fprintf(&buf, " on %s line %d:\n (source code not available)\n", highlightRange.Filename, highlightRange.Start.Line) | ||
84 | } else { | ||
85 | file, offset := parseRange(src, highlightRange) | ||
86 | |||
87 | headerRange := highlightRange | ||
88 | |||
89 | contextStr := hcled.ContextString(file, offset-1) | ||
90 | if contextStr != "" { | ||
91 | contextStr = ", in " + contextStr | ||
92 | } | ||
93 | |||
94 | fmt.Fprintf(&buf, " on %s line %d%s:\n", headerRange.Filename, headerRange.Start.Line, contextStr) | ||
95 | |||
96 | // Config snippet rendering | ||
97 | sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines) | ||
98 | for sc.Scan() { | ||
99 | lineRange := sc.Range() | ||
100 | if !lineRange.Overlaps(snippetRange) { | ||
101 | continue | ||
102 | } | ||
103 | beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange) | ||
104 | before := beforeRange.SliceBytes(src) | ||
105 | highlighted := highlightedRange.SliceBytes(src) | ||
106 | after := afterRange.SliceBytes(src) | ||
107 | fmt.Fprintf( | ||
108 | &buf, color.Color("%4d: %s[underline]%s[reset]%s\n"), | ||
109 | lineRange.Start.Line, | ||
110 | before, highlighted, after, | ||
111 | ) | ||
112 | } | ||
113 | |||
114 | } | ||
115 | |||
116 | if fromExpr := diag.FromExpr(); fromExpr != nil { | ||
117 | // We may also be able to generate information about the dynamic | ||
118 | // values of relevant variables at the point of evaluation, then. | ||
119 | // This is particularly useful for expressions that get evaluated | ||
120 | // multiple times with different values, such as blocks using | ||
121 | // "count" and "for_each", or within "for" expressions. | ||
122 | expr := fromExpr.Expression | ||
123 | ctx := fromExpr.EvalContext | ||
124 | vars := expr.Variables() | ||
125 | stmts := make([]string, 0, len(vars)) | ||
126 | seen := make(map[string]struct{}, len(vars)) | ||
127 | Traversals: | ||
128 | for _, traversal := range vars { | ||
129 | for len(traversal) > 1 { | ||
130 | val, diags := traversal.TraverseAbs(ctx) | ||
131 | if diags.HasErrors() { | ||
132 | // Skip anything that generates errors, since we probably | ||
133 | // already have the same error in our diagnostics set | ||
134 | // already. | ||
135 | traversal = traversal[:len(traversal)-1] | ||
136 | continue | ||
137 | } | ||
138 | |||
139 | traversalStr := traversalStr(traversal) | ||
140 | if _, exists := seen[traversalStr]; exists { | ||
141 | continue Traversals // don't show duplicates when the same variable is referenced multiple times | ||
142 | } | ||
143 | switch { | ||
144 | case !val.IsKnown(): | ||
145 | // Can't say anything about this yet, then. | ||
146 | continue Traversals | ||
147 | case val.IsNull(): | ||
148 | stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is null"), traversalStr)) | ||
149 | default: | ||
150 | stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is %s"), traversalStr, compactValueStr(val))) | ||
151 | } | ||
152 | seen[traversalStr] = struct{}{} | ||
153 | } | ||
154 | } | ||
155 | |||
156 | sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly? | ||
157 | |||
158 | if len(stmts) > 0 { | ||
159 | fmt.Fprint(&buf, color.Color(" [dark_gray]|----------------[reset]\n")) | ||
160 | } | ||
161 | for _, stmt := range stmts { | ||
162 | fmt.Fprintf(&buf, color.Color(" [dark_gray]|[reset] %s\n"), stmt) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | buf.WriteByte('\n') | ||
167 | } | ||
168 | |||
169 | if desc.Detail != "" { | ||
170 | detail := desc.Detail | ||
171 | if width != 0 { | ||
172 | detail = wordwrap.WrapString(detail, uint(width)) | ||
173 | } | ||
174 | fmt.Fprintf(&buf, "%s\n", detail) | ||
175 | } | ||
176 | |||
177 | return buf.String() | ||
178 | } | ||
179 | |||
180 | func parseRange(src []byte, rng hcl.Range) (*hcl.File, int) { | ||
181 | filename := rng.Filename | ||
182 | offset := rng.Start.Byte | ||
183 | |||
184 | // We need to re-parse here to get a *hcl.File we can interrogate. This | ||
185 | // is not awesome since we presumably already parsed the file earlier too, | ||
186 | // but this re-parsing is architecturally simpler than retaining all of | ||
187 | // the hcl.File objects and we only do this in the case of an error anyway | ||
188 | // so the overhead here is not a big problem. | ||
189 | parser := hclparse.NewParser() | ||
190 | var file *hcl.File | ||
191 | var diags hcl.Diagnostics | ||
192 | if strings.HasSuffix(filename, ".json") { | ||
193 | file, diags = parser.ParseJSON(src, filename) | ||
194 | } else { | ||
195 | file, diags = parser.ParseHCL(src, filename) | ||
196 | } | ||
197 | if diags.HasErrors() { | ||
198 | return file, offset | ||
199 | } | ||
200 | |||
201 | return file, offset | ||
202 | } | ||
203 | |||
204 | // traversalStr produces a representation of an HCL traversal that is compact, | ||
205 | // resembles HCL native syntax, and is suitable for display in the UI. | ||
206 | func traversalStr(traversal hcl.Traversal) string { | ||
207 | // This is a specialized subset of traversal rendering tailored to | ||
208 | // producing helpful contextual messages in diagnostics. It is not | ||
209 | // comprehensive nor intended to be used for other purposes. | ||
210 | |||
211 | var buf bytes.Buffer | ||
212 | for _, step := range traversal { | ||
213 | switch tStep := step.(type) { | ||
214 | case hcl.TraverseRoot: | ||
215 | buf.WriteString(tStep.Name) | ||
216 | case hcl.TraverseAttr: | ||
217 | buf.WriteByte('.') | ||
218 | buf.WriteString(tStep.Name) | ||
219 | case hcl.TraverseIndex: | ||
220 | buf.WriteByte('[') | ||
221 | if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() { | ||
222 | buf.WriteString(compactValueStr(tStep.Key)) | ||
223 | } else { | ||
224 | // We'll just use a placeholder for more complex values, | ||
225 | // since otherwise our result could grow ridiculously long. | ||
226 | buf.WriteString("...") | ||
227 | } | ||
228 | buf.WriteByte(']') | ||
229 | } | ||
230 | } | ||
231 | return buf.String() | ||
232 | } | ||
233 | |||
234 | // compactValueStr produces a compact, single-line summary of a given value | ||
235 | // that is suitable for display in the UI. | ||
236 | // | ||
237 | // For primitives it returns a full representation, while for more complex | ||
238 | // types it instead summarizes the type, size, etc to produce something | ||
239 | // that is hopefully still somewhat useful but not as verbose as a rendering | ||
240 | // of the entire data structure. | ||
241 | func compactValueStr(val cty.Value) string { | ||
242 | // This is a specialized subset of value rendering tailored to producing | ||
243 | // helpful but concise messages in diagnostics. It is not comprehensive | ||
244 | // nor intended to be used for other purposes. | ||
245 | |||
246 | ty := val.Type() | ||
247 | switch { | ||
248 | case val.IsNull(): | ||
249 | return "null" | ||
250 | case !val.IsKnown(): | ||
251 | // Should never happen here because we should filter before we get | ||
252 | // in here, but we'll do something reasonable rather than panic. | ||
253 | return "(not yet known)" | ||
254 | case ty == cty.Bool: | ||
255 | if val.True() { | ||
256 | return "true" | ||
257 | } | ||
258 | return "false" | ||
259 | case ty == cty.Number: | ||
260 | bf := val.AsBigFloat() | ||
261 | return bf.Text('g', 10) | ||
262 | case ty == cty.String: | ||
263 | // Go string syntax is not exactly the same as HCL native string syntax, | ||
264 | // but we'll accept the minor edge-cases where this is different here | ||
265 | // for now, just to get something reasonable here. | ||
266 | return fmt.Sprintf("%q", val.AsString()) | ||
267 | case ty.IsCollectionType() || ty.IsTupleType(): | ||
268 | l := val.LengthInt() | ||
269 | switch l { | ||
270 | case 0: | ||
271 | return "empty " + ty.FriendlyName() | ||
272 | case 1: | ||
273 | return ty.FriendlyName() + " with 1 element" | ||
274 | default: | ||
275 | return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l) | ||
276 | } | ||
277 | case ty.IsObjectType(): | ||
278 | atys := ty.AttributeTypes() | ||
279 | l := len(atys) | ||
280 | switch l { | ||
281 | case 0: | ||
282 | return "object with no attributes" | ||
283 | case 1: | ||
284 | var name string | ||
285 | for k := range atys { | ||
286 | name = k | ||
287 | } | ||
288 | return fmt.Sprintf("object with 1 attribute %q", name) | ||
289 | default: | ||
290 | return fmt.Sprintf("object with %d attributes", l) | ||
291 | } | ||
292 | default: | ||
293 | return ty.FriendlyName() | ||
294 | } | ||
295 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/command/format/diff.go b/vendor/github.com/hashicorp/terraform/command/format/diff.go new file mode 100644 index 0000000..c726f0e --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/command/format/diff.go | |||
@@ -0,0 +1,1192 @@ | |||
1 | package format | ||
2 | |||
3 | import ( | ||
4 | "bufio" | ||
5 | "bytes" | ||
6 | "fmt" | ||
7 | "sort" | ||
8 | "strings" | ||
9 | |||
10 | "github.com/mitchellh/colorstring" | ||
11 | "github.com/zclconf/go-cty/cty" | ||
12 | ctyjson "github.com/zclconf/go-cty/cty/json" | ||
13 | |||
14 | "github.com/hashicorp/terraform/addrs" | ||
15 | "github.com/hashicorp/terraform/configs/configschema" | ||
16 | "github.com/hashicorp/terraform/plans" | ||
17 | "github.com/hashicorp/terraform/plans/objchange" | ||
18 | "github.com/hashicorp/terraform/states" | ||
19 | ) | ||
20 | |||
21 | // ResourceChange returns a string representation of a change to a particular | ||
22 | // resource, for inclusion in user-facing plan output. | ||
23 | // | ||
24 | // The resource schema must be provided along with the change so that the | ||
25 | // formatted change can reflect the configuration structure for the associated | ||
26 | // resource. | ||
27 | // | ||
28 | // If "color" is non-nil, it will be used to color the result. Otherwise, | ||
29 | // no color codes will be included. | ||
30 | func ResourceChange( | ||
31 | change *plans.ResourceInstanceChangeSrc, | ||
32 | tainted bool, | ||
33 | schema *configschema.Block, | ||
34 | color *colorstring.Colorize, | ||
35 | ) string { | ||
36 | addr := change.Addr | ||
37 | var buf bytes.Buffer | ||
38 | |||
39 | if color == nil { | ||
40 | color = &colorstring.Colorize{ | ||
41 | Colors: colorstring.DefaultColors, | ||
42 | Disable: true, | ||
43 | Reset: false, | ||
44 | } | ||
45 | } | ||
46 | |||
47 | dispAddr := addr.String() | ||
48 | if change.DeposedKey != states.NotDeposed { | ||
49 | dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey) | ||
50 | } | ||
51 | |||
52 | switch change.Action { | ||
53 | case plans.Create: | ||
54 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr))) | ||
55 | case plans.Read: | ||
56 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be read during apply\n # (config refers to values not yet known)", dispAddr))) | ||
57 | case plans.Update: | ||
58 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr))) | ||
59 | case plans.CreateThenDelete, plans.DeleteThenCreate: | ||
60 | if tainted { | ||
61 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced", dispAddr))) | ||
62 | } else { | ||
63 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced", dispAddr))) | ||
64 | } | ||
65 | case plans.Delete: | ||
66 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]destroyed", dispAddr))) | ||
67 | default: | ||
68 | // should never happen, since the above is exhaustive | ||
69 | buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr)) | ||
70 | } | ||
71 | buf.WriteString(color.Color("[reset]\n")) | ||
72 | |||
73 | switch change.Action { | ||
74 | case plans.Create: | ||
75 | buf.WriteString(color.Color("[green] +[reset] ")) | ||
76 | case plans.Read: | ||
77 | buf.WriteString(color.Color("[cyan] <=[reset] ")) | ||
78 | case plans.Update: | ||
79 | buf.WriteString(color.Color("[yellow] ~[reset] ")) | ||
80 | case plans.DeleteThenCreate: | ||
81 | buf.WriteString(color.Color("[red]-[reset]/[green]+[reset] ")) | ||
82 | case plans.CreateThenDelete: | ||
83 | buf.WriteString(color.Color("[green]+[reset]/[red]-[reset] ")) | ||
84 | case plans.Delete: | ||
85 | buf.WriteString(color.Color("[red] -[reset] ")) | ||
86 | default: | ||
87 | buf.WriteString(color.Color("??? ")) | ||
88 | } | ||
89 | |||
90 | switch addr.Resource.Resource.Mode { | ||
91 | case addrs.ManagedResourceMode: | ||
92 | buf.WriteString(fmt.Sprintf( | ||
93 | "resource %q %q", | ||
94 | addr.Resource.Resource.Type, | ||
95 | addr.Resource.Resource.Name, | ||
96 | )) | ||
97 | case addrs.DataResourceMode: | ||
98 | buf.WriteString(fmt.Sprintf( | ||
99 | "data %q %q ", | ||
100 | addr.Resource.Resource.Type, | ||
101 | addr.Resource.Resource.Name, | ||
102 | )) | ||
103 | default: | ||
104 | // should never happen, since the above is exhaustive | ||
105 | buf.WriteString(addr.String()) | ||
106 | } | ||
107 | |||
108 | buf.WriteString(" {") | ||
109 | |||
110 | p := blockBodyDiffPrinter{ | ||
111 | buf: &buf, | ||
112 | color: color, | ||
113 | action: change.Action, | ||
114 | requiredReplace: change.RequiredReplace, | ||
115 | } | ||
116 | |||
117 | // Most commonly-used resources have nested blocks that result in us | ||
118 | // going at least three traversals deep while we recurse here, so we'll | ||
119 | // start with that much capacity and then grow as needed for deeper | ||
120 | // structures. | ||
121 | path := make(cty.Path, 0, 3) | ||
122 | |||
123 | changeV, err := change.Decode(schema.ImpliedType()) | ||
124 | if err != nil { | ||
125 | // Should never happen in here, since we've already been through | ||
126 | // loads of layers of encode/decode of the planned changes before now. | ||
127 | panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err)) | ||
128 | } | ||
129 | |||
130 | // We currently have an opt-out that permits the legacy SDK to return values | ||
131 | // that defy our usual conventions around handling of nesting blocks. To | ||
132 | // avoid the rendering code from needing to handle all of these, we'll | ||
133 | // normalize first. | ||
134 | // (Ideally we'd do this as part of the SDK opt-out implementation in core, | ||
135 | // but we've added it here for now to reduce risk of unexpected impacts | ||
136 | // on other code in core.) | ||
137 | changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) | ||
138 | changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) | ||
139 | |||
140 | bodyWritten := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) | ||
141 | if bodyWritten { | ||
142 | buf.WriteString("\n") | ||
143 | buf.WriteString(strings.Repeat(" ", 4)) | ||
144 | } | ||
145 | buf.WriteString("}\n") | ||
146 | |||
147 | return buf.String() | ||
148 | } | ||
149 | |||
150 | type blockBodyDiffPrinter struct { | ||
151 | buf *bytes.Buffer | ||
152 | color *colorstring.Colorize | ||
153 | action plans.Action | ||
154 | requiredReplace cty.PathSet | ||
155 | } | ||
156 | |||
157 | const forcesNewResourceCaption = " [red]# forces replacement[reset]" | ||
158 | |||
159 | // writeBlockBodyDiff writes attribute or block differences | ||
160 | // and returns true if any differences were found and written | ||
161 | func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) bool { | ||
162 | path = ctyEnsurePathCapacity(path, 1) | ||
163 | |||
164 | bodyWritten := false | ||
165 | blankBeforeBlocks := false | ||
166 | { | ||
167 | attrNames := make([]string, 0, len(schema.Attributes)) | ||
168 | attrNameLen := 0 | ||
169 | for name := range schema.Attributes { | ||
170 | oldVal := ctyGetAttrMaybeNull(old, name) | ||
171 | newVal := ctyGetAttrMaybeNull(new, name) | ||
172 | if oldVal.IsNull() && newVal.IsNull() { | ||
173 | // Skip attributes where both old and new values are null | ||
174 | // (we do this early here so that we'll do our value alignment | ||
175 | // based on the longest attribute name that has a change, rather | ||
176 | // than the longest attribute name in the full set.) | ||
177 | continue | ||
178 | } | ||
179 | |||
180 | attrNames = append(attrNames, name) | ||
181 | if len(name) > attrNameLen { | ||
182 | attrNameLen = len(name) | ||
183 | } | ||
184 | } | ||
185 | sort.Strings(attrNames) | ||
186 | if len(attrNames) > 0 { | ||
187 | blankBeforeBlocks = true | ||
188 | } | ||
189 | |||
190 | for _, name := range attrNames { | ||
191 | attrS := schema.Attributes[name] | ||
192 | oldVal := ctyGetAttrMaybeNull(old, name) | ||
193 | newVal := ctyGetAttrMaybeNull(new, name) | ||
194 | |||
195 | bodyWritten = true | ||
196 | p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path) | ||
197 | } | ||
198 | } | ||
199 | |||
200 | { | ||
201 | blockTypeNames := make([]string, 0, len(schema.BlockTypes)) | ||
202 | for name := range schema.BlockTypes { | ||
203 | blockTypeNames = append(blockTypeNames, name) | ||
204 | } | ||
205 | sort.Strings(blockTypeNames) | ||
206 | |||
207 | for _, name := range blockTypeNames { | ||
208 | blockS := schema.BlockTypes[name] | ||
209 | oldVal := ctyGetAttrMaybeNull(old, name) | ||
210 | newVal := ctyGetAttrMaybeNull(new, name) | ||
211 | |||
212 | bodyWritten = true | ||
213 | p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path) | ||
214 | |||
215 | // Always include a blank for any subsequent block types. | ||
216 | blankBeforeBlocks = true | ||
217 | } | ||
218 | } | ||
219 | |||
220 | return bodyWritten | ||
221 | } | ||
222 | |||
223 | func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) { | ||
224 | path = append(path, cty.GetAttrStep{Name: name}) | ||
225 | p.buf.WriteString("\n") | ||
226 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
227 | showJustNew := false | ||
228 | var action plans.Action | ||
229 | switch { | ||
230 | case old.IsNull(): | ||
231 | action = plans.Create | ||
232 | showJustNew = true | ||
233 | case new.IsNull(): | ||
234 | action = plans.Delete | ||
235 | case ctyEqualWithUnknown(old, new): | ||
236 | action = plans.NoOp | ||
237 | showJustNew = true | ||
238 | default: | ||
239 | action = plans.Update | ||
240 | } | ||
241 | |||
242 | p.writeActionSymbol(action) | ||
243 | |||
244 | p.buf.WriteString(p.color.Color("[bold]")) | ||
245 | p.buf.WriteString(name) | ||
246 | p.buf.WriteString(p.color.Color("[reset]")) | ||
247 | p.buf.WriteString(strings.Repeat(" ", nameLen-len(name))) | ||
248 | p.buf.WriteString(" = ") | ||
249 | |||
250 | if attrS.Sensitive { | ||
251 | p.buf.WriteString("(sensitive value)") | ||
252 | } else { | ||
253 | switch { | ||
254 | case showJustNew: | ||
255 | p.writeValue(new, action, indent+2) | ||
256 | if p.pathForcesNewResource(path) { | ||
257 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
258 | } | ||
259 | default: | ||
260 | // We show new even if it is null to emphasize the fact | ||
261 | // that it is being unset, since otherwise it is easy to | ||
262 | // misunderstand that the value is still set to the old value. | ||
263 | p.writeValueDiff(old, new, indent+2, path) | ||
264 | } | ||
265 | } | ||
266 | } | ||
267 | |||
268 | func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) { | ||
269 | path = append(path, cty.GetAttrStep{Name: name}) | ||
270 | if old.IsNull() && new.IsNull() { | ||
271 | // Nothing to do if both old and new is null | ||
272 | return | ||
273 | } | ||
274 | |||
275 | // Where old/new are collections representing a nesting mode other than | ||
276 | // NestingSingle, we assume the collection value can never be unknown | ||
277 | // since we always produce the container for the nested objects, even if | ||
278 | // the objects within are computed. | ||
279 | |||
280 | switch blockS.Nesting { | ||
281 | case configschema.NestingSingle, configschema.NestingGroup: | ||
282 | var action plans.Action | ||
283 | eqV := new.Equals(old) | ||
284 | switch { | ||
285 | case old.IsNull(): | ||
286 | action = plans.Create | ||
287 | case new.IsNull(): | ||
288 | action = plans.Delete | ||
289 | case !new.IsWhollyKnown() || !old.IsWhollyKnown(): | ||
290 | // "old" should actually always be known due to our contract | ||
291 | // that old values must never be unknown, but we'll allow it | ||
292 | // anyway to be robust. | ||
293 | action = plans.Update | ||
294 | case !eqV.IsKnown() || !eqV.True(): | ||
295 | action = plans.Update | ||
296 | } | ||
297 | |||
298 | if blankBefore { | ||
299 | p.buf.WriteRune('\n') | ||
300 | } | ||
301 | p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path) | ||
302 | case configschema.NestingList: | ||
303 | // For the sake of handling nested blocks, we'll treat a null list | ||
304 | // the same as an empty list since the config language doesn't | ||
305 | // distinguish these anyway. | ||
306 | old = ctyNullBlockListAsEmpty(old) | ||
307 | new = ctyNullBlockListAsEmpty(new) | ||
308 | |||
309 | oldItems := ctyCollectionValues(old) | ||
310 | newItems := ctyCollectionValues(new) | ||
311 | |||
312 | // Here we intentionally preserve the index-based correspondance | ||
313 | // between old and new, rather than trying to detect insertions | ||
314 | // and removals in the list, because this more accurately reflects | ||
315 | // how Terraform Core and providers will understand the change, | ||
316 | // particularly when the nested block contains computed attributes | ||
317 | // that will themselves maintain correspondance by index. | ||
318 | |||
319 | // commonLen is number of elements that exist in both lists, which | ||
320 | // will be presented as updates (~). Any additional items in one | ||
321 | // of the lists will be presented as either creates (+) or deletes (-) | ||
322 | // depending on which list they belong to. | ||
323 | var commonLen int | ||
324 | switch { | ||
325 | case len(oldItems) < len(newItems): | ||
326 | commonLen = len(oldItems) | ||
327 | default: | ||
328 | commonLen = len(newItems) | ||
329 | } | ||
330 | |||
331 | if blankBefore && (len(oldItems) > 0 || len(newItems) > 0) { | ||
332 | p.buf.WriteRune('\n') | ||
333 | } | ||
334 | |||
335 | for i := 0; i < commonLen; i++ { | ||
336 | path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) | ||
337 | oldItem := oldItems[i] | ||
338 | newItem := newItems[i] | ||
339 | action := plans.Update | ||
340 | if oldItem.RawEquals(newItem) { | ||
341 | action = plans.NoOp | ||
342 | } | ||
343 | p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path) | ||
344 | } | ||
345 | for i := commonLen; i < len(oldItems); i++ { | ||
346 | path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) | ||
347 | oldItem := oldItems[i] | ||
348 | newItem := cty.NullVal(oldItem.Type()) | ||
349 | p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path) | ||
350 | } | ||
351 | for i := commonLen; i < len(newItems); i++ { | ||
352 | path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) | ||
353 | newItem := newItems[i] | ||
354 | oldItem := cty.NullVal(newItem.Type()) | ||
355 | p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path) | ||
356 | } | ||
357 | case configschema.NestingSet: | ||
358 | // For the sake of handling nested blocks, we'll treat a null set | ||
359 | // the same as an empty set since the config language doesn't | ||
360 | // distinguish these anyway. | ||
361 | old = ctyNullBlockSetAsEmpty(old) | ||
362 | new = ctyNullBlockSetAsEmpty(new) | ||
363 | |||
364 | oldItems := ctyCollectionValues(old) | ||
365 | newItems := ctyCollectionValues(new) | ||
366 | |||
367 | if (len(oldItems) + len(newItems)) == 0 { | ||
368 | // Nothing to do if both sets are empty | ||
369 | return | ||
370 | } | ||
371 | |||
372 | allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) | ||
373 | allItems = append(allItems, oldItems...) | ||
374 | allItems = append(allItems, newItems...) | ||
375 | all := cty.SetVal(allItems) | ||
376 | |||
377 | if blankBefore { | ||
378 | p.buf.WriteRune('\n') | ||
379 | } | ||
380 | |||
381 | for it := all.ElementIterator(); it.Next(); { | ||
382 | _, val := it.Element() | ||
383 | var action plans.Action | ||
384 | var oldValue, newValue cty.Value | ||
385 | switch { | ||
386 | case !val.IsKnown(): | ||
387 | action = plans.Update | ||
388 | newValue = val | ||
389 | case !old.HasElement(val).True(): | ||
390 | action = plans.Create | ||
391 | oldValue = cty.NullVal(val.Type()) | ||
392 | newValue = val | ||
393 | case !new.HasElement(val).True(): | ||
394 | action = plans.Delete | ||
395 | oldValue = val | ||
396 | newValue = cty.NullVal(val.Type()) | ||
397 | default: | ||
398 | action = plans.NoOp | ||
399 | oldValue = val | ||
400 | newValue = val | ||
401 | } | ||
402 | path := append(path, cty.IndexStep{Key: val}) | ||
403 | p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path) | ||
404 | } | ||
405 | |||
406 | case configschema.NestingMap: | ||
407 | // For the sake of handling nested blocks, we'll treat a null map | ||
408 | // the same as an empty map since the config language doesn't | ||
409 | // distinguish these anyway. | ||
410 | old = ctyNullBlockMapAsEmpty(old) | ||
411 | new = ctyNullBlockMapAsEmpty(new) | ||
412 | |||
413 | oldItems := old.AsValueMap() | ||
414 | newItems := new.AsValueMap() | ||
415 | if (len(oldItems) + len(newItems)) == 0 { | ||
416 | // Nothing to do if both maps are empty | ||
417 | return | ||
418 | } | ||
419 | |||
420 | allKeys := make(map[string]bool) | ||
421 | for k := range oldItems { | ||
422 | allKeys[k] = true | ||
423 | } | ||
424 | for k := range newItems { | ||
425 | allKeys[k] = true | ||
426 | } | ||
427 | allKeysOrder := make([]string, 0, len(allKeys)) | ||
428 | for k := range allKeys { | ||
429 | allKeysOrder = append(allKeysOrder, k) | ||
430 | } | ||
431 | sort.Strings(allKeysOrder) | ||
432 | |||
433 | if blankBefore { | ||
434 | p.buf.WriteRune('\n') | ||
435 | } | ||
436 | |||
437 | for _, k := range allKeysOrder { | ||
438 | var action plans.Action | ||
439 | oldValue := oldItems[k] | ||
440 | newValue := newItems[k] | ||
441 | switch { | ||
442 | case oldValue == cty.NilVal: | ||
443 | oldValue = cty.NullVal(newValue.Type()) | ||
444 | action = plans.Create | ||
445 | case newValue == cty.NilVal: | ||
446 | newValue = cty.NullVal(oldValue.Type()) | ||
447 | action = plans.Delete | ||
448 | case !newValue.RawEquals(oldValue): | ||
449 | action = plans.Update | ||
450 | default: | ||
451 | action = plans.NoOp | ||
452 | } | ||
453 | |||
454 | path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) | ||
455 | p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path) | ||
456 | } | ||
457 | } | ||
458 | } | ||
459 | |||
460 | func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) { | ||
461 | p.buf.WriteString("\n") | ||
462 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
463 | p.writeActionSymbol(action) | ||
464 | |||
465 | if label != nil { | ||
466 | fmt.Fprintf(p.buf, "%s %q {", name, *label) | ||
467 | } else { | ||
468 | fmt.Fprintf(p.buf, "%s {", name) | ||
469 | } | ||
470 | |||
471 | if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { | ||
472 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
473 | } | ||
474 | |||
475 | bodyWritten := p.writeBlockBodyDiff(blockS, old, new, indent+4, path) | ||
476 | if bodyWritten { | ||
477 | p.buf.WriteString("\n") | ||
478 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
479 | } | ||
480 | p.buf.WriteString("}") | ||
481 | } | ||
482 | |||
483 | func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) { | ||
484 | if !val.IsKnown() { | ||
485 | p.buf.WriteString("(known after apply)") | ||
486 | return | ||
487 | } | ||
488 | if val.IsNull() { | ||
489 | p.buf.WriteString(p.color.Color("[dark_gray]null[reset]")) | ||
490 | return | ||
491 | } | ||
492 | |||
493 | ty := val.Type() | ||
494 | |||
495 | switch { | ||
496 | case ty.IsPrimitiveType(): | ||
497 | switch ty { | ||
498 | case cty.String: | ||
499 | { | ||
500 | // Special behavior for JSON strings containing array or object | ||
501 | src := []byte(val.AsString()) | ||
502 | ty, err := ctyjson.ImpliedType(src) | ||
503 | // check for the special case of "null", which decodes to nil, | ||
504 | // and just allow it to be printed out directly | ||
505 | if err == nil && !ty.IsPrimitiveType() && val.AsString() != "null" { | ||
506 | jv, err := ctyjson.Unmarshal(src, ty) | ||
507 | if err == nil { | ||
508 | p.buf.WriteString("jsonencode(") | ||
509 | if jv.LengthInt() == 0 { | ||
510 | p.writeValue(jv, action, 0) | ||
511 | } else { | ||
512 | p.buf.WriteByte('\n') | ||
513 | p.buf.WriteString(strings.Repeat(" ", indent+4)) | ||
514 | p.writeValue(jv, action, indent+4) | ||
515 | p.buf.WriteByte('\n') | ||
516 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
517 | } | ||
518 | p.buf.WriteByte(')') | ||
519 | break // don't *also* do the normal behavior below | ||
520 | } | ||
521 | } | ||
522 | } | ||
523 | fmt.Fprintf(p.buf, "%q", val.AsString()) | ||
524 | case cty.Bool: | ||
525 | if val.True() { | ||
526 | p.buf.WriteString("true") | ||
527 | } else { | ||
528 | p.buf.WriteString("false") | ||
529 | } | ||
530 | case cty.Number: | ||
531 | bf := val.AsBigFloat() | ||
532 | p.buf.WriteString(bf.Text('f', -1)) | ||
533 | default: | ||
534 | // should never happen, since the above is exhaustive | ||
535 | fmt.Fprintf(p.buf, "%#v", val) | ||
536 | } | ||
537 | case ty.IsListType() || ty.IsSetType() || ty.IsTupleType(): | ||
538 | p.buf.WriteString("[") | ||
539 | |||
540 | it := val.ElementIterator() | ||
541 | for it.Next() { | ||
542 | _, val := it.Element() | ||
543 | |||
544 | p.buf.WriteString("\n") | ||
545 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
546 | p.writeActionSymbol(action) | ||
547 | p.writeValue(val, action, indent+4) | ||
548 | p.buf.WriteString(",") | ||
549 | } | ||
550 | |||
551 | if val.LengthInt() > 0 { | ||
552 | p.buf.WriteString("\n") | ||
553 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
554 | } | ||
555 | p.buf.WriteString("]") | ||
556 | case ty.IsMapType(): | ||
557 | p.buf.WriteString("{") | ||
558 | |||
559 | keyLen := 0 | ||
560 | for it := val.ElementIterator(); it.Next(); { | ||
561 | key, _ := it.Element() | ||
562 | if keyStr := key.AsString(); len(keyStr) > keyLen { | ||
563 | keyLen = len(keyStr) | ||
564 | } | ||
565 | } | ||
566 | |||
567 | for it := val.ElementIterator(); it.Next(); { | ||
568 | key, val := it.Element() | ||
569 | |||
570 | p.buf.WriteString("\n") | ||
571 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
572 | p.writeActionSymbol(action) | ||
573 | p.writeValue(key, action, indent+4) | ||
574 | p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString()))) | ||
575 | p.buf.WriteString(" = ") | ||
576 | p.writeValue(val, action, indent+4) | ||
577 | } | ||
578 | |||
579 | if val.LengthInt() > 0 { | ||
580 | p.buf.WriteString("\n") | ||
581 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
582 | } | ||
583 | p.buf.WriteString("}") | ||
584 | case ty.IsObjectType(): | ||
585 | p.buf.WriteString("{") | ||
586 | |||
587 | atys := ty.AttributeTypes() | ||
588 | attrNames := make([]string, 0, len(atys)) | ||
589 | nameLen := 0 | ||
590 | for attrName := range atys { | ||
591 | attrNames = append(attrNames, attrName) | ||
592 | if len(attrName) > nameLen { | ||
593 | nameLen = len(attrName) | ||
594 | } | ||
595 | } | ||
596 | sort.Strings(attrNames) | ||
597 | |||
598 | for _, attrName := range attrNames { | ||
599 | val := val.GetAttr(attrName) | ||
600 | |||
601 | p.buf.WriteString("\n") | ||
602 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
603 | p.writeActionSymbol(action) | ||
604 | p.buf.WriteString(attrName) | ||
605 | p.buf.WriteString(strings.Repeat(" ", nameLen-len(attrName))) | ||
606 | p.buf.WriteString(" = ") | ||
607 | p.writeValue(val, action, indent+4) | ||
608 | } | ||
609 | |||
610 | if len(attrNames) > 0 { | ||
611 | p.buf.WriteString("\n") | ||
612 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
613 | } | ||
614 | p.buf.WriteString("}") | ||
615 | } | ||
616 | } | ||
617 | |||
618 | func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) { | ||
619 | ty := old.Type() | ||
620 | typesEqual := ctyTypesEqual(ty, new.Type()) | ||
621 | |||
622 | // We have some specialized diff implementations for certain complex | ||
623 | // values where it's useful to see a visualization of the diff of | ||
624 | // the nested elements rather than just showing the entire old and | ||
625 | // new values verbatim. | ||
626 | // However, these specialized implementations can apply only if both | ||
627 | // values are known and non-null. | ||
628 | if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual { | ||
629 | switch { | ||
630 | case ty == cty.String: | ||
631 | // We have special behavior for both multi-line strings in general | ||
632 | // and for strings that can parse as JSON. For the JSON handling | ||
633 | // to apply, both old and new must be valid JSON. | ||
634 | // For single-line strings that don't parse as JSON we just fall | ||
635 | // out of this switch block and do the default old -> new rendering. | ||
636 | oldS := old.AsString() | ||
637 | newS := new.AsString() | ||
638 | |||
639 | { | ||
640 | // Special behavior for JSON strings containing object or | ||
641 | // list values. | ||
642 | oldBytes := []byte(oldS) | ||
643 | newBytes := []byte(newS) | ||
644 | oldType, oldErr := ctyjson.ImpliedType(oldBytes) | ||
645 | newType, newErr := ctyjson.ImpliedType(newBytes) | ||
646 | if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) { | ||
647 | oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType) | ||
648 | newJV, newErr := ctyjson.Unmarshal(newBytes, newType) | ||
649 | if oldErr == nil && newErr == nil { | ||
650 | if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace | ||
651 | p.buf.WriteString("jsonencode(") | ||
652 | p.buf.WriteByte('\n') | ||
653 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
654 | p.writeActionSymbol(plans.Update) | ||
655 | p.writeValueDiff(oldJV, newJV, indent+4, path) | ||
656 | p.buf.WriteByte('\n') | ||
657 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
658 | p.buf.WriteByte(')') | ||
659 | } else { | ||
660 | // if they differ only in insigificant whitespace | ||
661 | // then we'll note that but still expand out the | ||
662 | // effective value. | ||
663 | if p.pathForcesNewResource(path) { | ||
664 | p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]")) | ||
665 | } else { | ||
666 | p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]")) | ||
667 | } | ||
668 | p.buf.WriteByte('\n') | ||
669 | p.buf.WriteString(strings.Repeat(" ", indent+4)) | ||
670 | p.writeValue(oldJV, plans.NoOp, indent+4) | ||
671 | p.buf.WriteByte('\n') | ||
672 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
673 | p.buf.WriteByte(')') | ||
674 | } | ||
675 | return | ||
676 | } | ||
677 | } | ||
678 | } | ||
679 | |||
680 | if strings.Index(oldS, "\n") < 0 && strings.Index(newS, "\n") < 0 { | ||
681 | break | ||
682 | } | ||
683 | |||
684 | p.buf.WriteString("<<~EOT") | ||
685 | if p.pathForcesNewResource(path) { | ||
686 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
687 | } | ||
688 | p.buf.WriteString("\n") | ||
689 | |||
690 | var oldLines, newLines []cty.Value | ||
691 | { | ||
692 | r := strings.NewReader(oldS) | ||
693 | sc := bufio.NewScanner(r) | ||
694 | for sc.Scan() { | ||
695 | oldLines = append(oldLines, cty.StringVal(sc.Text())) | ||
696 | } | ||
697 | } | ||
698 | { | ||
699 | r := strings.NewReader(newS) | ||
700 | sc := bufio.NewScanner(r) | ||
701 | for sc.Scan() { | ||
702 | newLines = append(newLines, cty.StringVal(sc.Text())) | ||
703 | } | ||
704 | } | ||
705 | |||
706 | diffLines := ctySequenceDiff(oldLines, newLines) | ||
707 | for _, diffLine := range diffLines { | ||
708 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
709 | p.writeActionSymbol(diffLine.Action) | ||
710 | |||
711 | switch diffLine.Action { | ||
712 | case plans.NoOp, plans.Delete: | ||
713 | p.buf.WriteString(diffLine.Before.AsString()) | ||
714 | case plans.Create: | ||
715 | p.buf.WriteString(diffLine.After.AsString()) | ||
716 | default: | ||
717 | // Should never happen since the above covers all | ||
718 | // actions that ctySequenceDiff can return for strings | ||
719 | p.buf.WriteString(diffLine.After.AsString()) | ||
720 | |||
721 | } | ||
722 | p.buf.WriteString("\n") | ||
723 | } | ||
724 | |||
725 | p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol | ||
726 | p.buf.WriteString("EOT") | ||
727 | |||
728 | return | ||
729 | |||
730 | case ty.IsSetType(): | ||
731 | p.buf.WriteString("[") | ||
732 | if p.pathForcesNewResource(path) { | ||
733 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
734 | } | ||
735 | p.buf.WriteString("\n") | ||
736 | |||
737 | var addedVals, removedVals, allVals []cty.Value | ||
738 | for it := old.ElementIterator(); it.Next(); { | ||
739 | _, val := it.Element() | ||
740 | allVals = append(allVals, val) | ||
741 | if new.HasElement(val).False() { | ||
742 | removedVals = append(removedVals, val) | ||
743 | } | ||
744 | } | ||
745 | for it := new.ElementIterator(); it.Next(); { | ||
746 | _, val := it.Element() | ||
747 | allVals = append(allVals, val) | ||
748 | if val.IsKnown() && old.HasElement(val).False() { | ||
749 | addedVals = append(addedVals, val) | ||
750 | } | ||
751 | } | ||
752 | |||
753 | var all, added, removed cty.Value | ||
754 | if len(allVals) > 0 { | ||
755 | all = cty.SetVal(allVals) | ||
756 | } else { | ||
757 | all = cty.SetValEmpty(ty.ElementType()) | ||
758 | } | ||
759 | if len(addedVals) > 0 { | ||
760 | added = cty.SetVal(addedVals) | ||
761 | } else { | ||
762 | added = cty.SetValEmpty(ty.ElementType()) | ||
763 | } | ||
764 | if len(removedVals) > 0 { | ||
765 | removed = cty.SetVal(removedVals) | ||
766 | } else { | ||
767 | removed = cty.SetValEmpty(ty.ElementType()) | ||
768 | } | ||
769 | |||
770 | for it := all.ElementIterator(); it.Next(); { | ||
771 | _, val := it.Element() | ||
772 | |||
773 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
774 | |||
775 | var action plans.Action | ||
776 | switch { | ||
777 | case !val.IsKnown(): | ||
778 | action = plans.Update | ||
779 | case added.HasElement(val).True(): | ||
780 | action = plans.Create | ||
781 | case removed.HasElement(val).True(): | ||
782 | action = plans.Delete | ||
783 | default: | ||
784 | action = plans.NoOp | ||
785 | } | ||
786 | |||
787 | p.writeActionSymbol(action) | ||
788 | p.writeValue(val, action, indent+4) | ||
789 | p.buf.WriteString(",\n") | ||
790 | } | ||
791 | |||
792 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
793 | p.buf.WriteString("]") | ||
794 | return | ||
795 | case ty.IsListType() || ty.IsTupleType(): | ||
796 | p.buf.WriteString("[") | ||
797 | if p.pathForcesNewResource(path) { | ||
798 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
799 | } | ||
800 | p.buf.WriteString("\n") | ||
801 | |||
802 | elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice()) | ||
803 | for _, elemDiff := range elemDiffs { | ||
804 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
805 | p.writeActionSymbol(elemDiff.Action) | ||
806 | switch elemDiff.Action { | ||
807 | case plans.NoOp, plans.Delete: | ||
808 | p.writeValue(elemDiff.Before, elemDiff.Action, indent+4) | ||
809 | case plans.Update: | ||
810 | p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path) | ||
811 | case plans.Create: | ||
812 | p.writeValue(elemDiff.After, elemDiff.Action, indent+4) | ||
813 | default: | ||
814 | // Should never happen since the above covers all | ||
815 | // actions that ctySequenceDiff can return. | ||
816 | p.writeValue(elemDiff.After, elemDiff.Action, indent+4) | ||
817 | } | ||
818 | |||
819 | p.buf.WriteString(",\n") | ||
820 | } | ||
821 | |||
822 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
823 | p.buf.WriteString("]") | ||
824 | return | ||
825 | |||
826 | case ty.IsMapType(): | ||
827 | p.buf.WriteString("{") | ||
828 | if p.pathForcesNewResource(path) { | ||
829 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
830 | } | ||
831 | p.buf.WriteString("\n") | ||
832 | |||
833 | var allKeys []string | ||
834 | keyLen := 0 | ||
835 | for it := old.ElementIterator(); it.Next(); { | ||
836 | k, _ := it.Element() | ||
837 | keyStr := k.AsString() | ||
838 | allKeys = append(allKeys, keyStr) | ||
839 | if len(keyStr) > keyLen { | ||
840 | keyLen = len(keyStr) | ||
841 | } | ||
842 | } | ||
843 | for it := new.ElementIterator(); it.Next(); { | ||
844 | k, _ := it.Element() | ||
845 | keyStr := k.AsString() | ||
846 | allKeys = append(allKeys, keyStr) | ||
847 | if len(keyStr) > keyLen { | ||
848 | keyLen = len(keyStr) | ||
849 | } | ||
850 | } | ||
851 | |||
852 | sort.Strings(allKeys) | ||
853 | |||
854 | lastK := "" | ||
855 | for i, k := range allKeys { | ||
856 | if i > 0 && lastK == k { | ||
857 | continue // skip duplicates (list is sorted) | ||
858 | } | ||
859 | lastK = k | ||
860 | |||
861 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
862 | kV := cty.StringVal(k) | ||
863 | var action plans.Action | ||
864 | if old.HasIndex(kV).False() { | ||
865 | action = plans.Create | ||
866 | } else if new.HasIndex(kV).False() { | ||
867 | action = plans.Delete | ||
868 | } else if eqV := old.Index(kV).Equals(new.Index(kV)); eqV.IsKnown() && eqV.True() { | ||
869 | action = plans.NoOp | ||
870 | } else { | ||
871 | action = plans.Update | ||
872 | } | ||
873 | |||
874 | path := append(path, cty.IndexStep{Key: kV}) | ||
875 | |||
876 | p.writeActionSymbol(action) | ||
877 | p.writeValue(kV, action, indent+4) | ||
878 | p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) | ||
879 | p.buf.WriteString(" = ") | ||
880 | switch action { | ||
881 | case plans.Create, plans.NoOp: | ||
882 | v := new.Index(kV) | ||
883 | p.writeValue(v, action, indent+4) | ||
884 | case plans.Delete: | ||
885 | oldV := old.Index(kV) | ||
886 | newV := cty.NullVal(oldV.Type()) | ||
887 | p.writeValueDiff(oldV, newV, indent+4, path) | ||
888 | default: | ||
889 | oldV := old.Index(kV) | ||
890 | newV := new.Index(kV) | ||
891 | p.writeValueDiff(oldV, newV, indent+4, path) | ||
892 | } | ||
893 | |||
894 | p.buf.WriteByte('\n') | ||
895 | } | ||
896 | |||
897 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
898 | p.buf.WriteString("}") | ||
899 | return | ||
900 | case ty.IsObjectType(): | ||
901 | p.buf.WriteString("{") | ||
902 | p.buf.WriteString("\n") | ||
903 | |||
904 | forcesNewResource := p.pathForcesNewResource(path) | ||
905 | |||
906 | var allKeys []string | ||
907 | keyLen := 0 | ||
908 | for it := old.ElementIterator(); it.Next(); { | ||
909 | k, _ := it.Element() | ||
910 | keyStr := k.AsString() | ||
911 | allKeys = append(allKeys, keyStr) | ||
912 | if len(keyStr) > keyLen { | ||
913 | keyLen = len(keyStr) | ||
914 | } | ||
915 | } | ||
916 | for it := new.ElementIterator(); it.Next(); { | ||
917 | k, _ := it.Element() | ||
918 | keyStr := k.AsString() | ||
919 | allKeys = append(allKeys, keyStr) | ||
920 | if len(keyStr) > keyLen { | ||
921 | keyLen = len(keyStr) | ||
922 | } | ||
923 | } | ||
924 | |||
925 | sort.Strings(allKeys) | ||
926 | |||
927 | lastK := "" | ||
928 | for i, k := range allKeys { | ||
929 | if i > 0 && lastK == k { | ||
930 | continue // skip duplicates (list is sorted) | ||
931 | } | ||
932 | lastK = k | ||
933 | |||
934 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
935 | kV := k | ||
936 | var action plans.Action | ||
937 | if !old.Type().HasAttribute(kV) { | ||
938 | action = plans.Create | ||
939 | } else if !new.Type().HasAttribute(kV) { | ||
940 | action = plans.Delete | ||
941 | } else if eqV := old.GetAttr(kV).Equals(new.GetAttr(kV)); eqV.IsKnown() && eqV.True() { | ||
942 | action = plans.NoOp | ||
943 | } else { | ||
944 | action = plans.Update | ||
945 | } | ||
946 | |||
947 | path := append(path, cty.GetAttrStep{Name: kV}) | ||
948 | |||
949 | p.writeActionSymbol(action) | ||
950 | p.buf.WriteString(k) | ||
951 | p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) | ||
952 | p.buf.WriteString(" = ") | ||
953 | |||
954 | switch action { | ||
955 | case plans.Create, plans.NoOp: | ||
956 | v := new.GetAttr(kV) | ||
957 | p.writeValue(v, action, indent+4) | ||
958 | case plans.Delete: | ||
959 | oldV := old.GetAttr(kV) | ||
960 | newV := cty.NullVal(oldV.Type()) | ||
961 | p.writeValueDiff(oldV, newV, indent+4, path) | ||
962 | default: | ||
963 | oldV := old.GetAttr(kV) | ||
964 | newV := new.GetAttr(kV) | ||
965 | p.writeValueDiff(oldV, newV, indent+4, path) | ||
966 | } | ||
967 | |||
968 | p.buf.WriteString("\n") | ||
969 | } | ||
970 | |||
971 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
972 | p.buf.WriteString("}") | ||
973 | |||
974 | if forcesNewResource { | ||
975 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
976 | } | ||
977 | return | ||
978 | } | ||
979 | } | ||
980 | |||
981 | // In all other cases, we just show the new and old values as-is | ||
982 | p.writeValue(old, plans.Delete, indent) | ||
983 | if new.IsNull() { | ||
984 | p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] ")) | ||
985 | } else { | ||
986 | p.buf.WriteString(p.color.Color(" [yellow]->[reset] ")) | ||
987 | } | ||
988 | |||
989 | p.writeValue(new, plans.Create, indent) | ||
990 | if p.pathForcesNewResource(path) { | ||
991 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
992 | } | ||
993 | } | ||
994 | |||
995 | // writeActionSymbol writes a symbol to represent the given action, followed | ||
996 | // by a space. | ||
997 | // | ||
998 | // It only supports the actions that can be represented with a single character: | ||
999 | // Create, Delete, Update and NoAction. | ||
1000 | func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) { | ||
1001 | switch action { | ||
1002 | case plans.Create: | ||
1003 | p.buf.WriteString(p.color.Color("[green]+[reset] ")) | ||
1004 | case plans.Delete: | ||
1005 | p.buf.WriteString(p.color.Color("[red]-[reset] ")) | ||
1006 | case plans.Update: | ||
1007 | p.buf.WriteString(p.color.Color("[yellow]~[reset] ")) | ||
1008 | case plans.NoOp: | ||
1009 | p.buf.WriteString(" ") | ||
1010 | default: | ||
1011 | // Should never happen | ||
1012 | p.buf.WriteString(p.color.Color("? ")) | ||
1013 | } | ||
1014 | } | ||
1015 | |||
1016 | func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool { | ||
1017 | if !p.action.IsReplace() { | ||
1018 | // "requiredReplace" only applies when the instance is being replaced | ||
1019 | return false | ||
1020 | } | ||
1021 | return p.requiredReplace.Has(path) | ||
1022 | } | ||
1023 | |||
1024 | func ctyEmptyString(value cty.Value) bool { | ||
1025 | if !value.IsNull() && value.IsKnown() { | ||
1026 | valueType := value.Type() | ||
1027 | if valueType == cty.String && value.AsString() == "" { | ||
1028 | return true | ||
1029 | } | ||
1030 | } | ||
1031 | return false | ||
1032 | } | ||
1033 | |||
1034 | func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value { | ||
1035 | attrType := val.Type().AttributeType(name) | ||
1036 | |||
1037 | if val.IsNull() { | ||
1038 | return cty.NullVal(attrType) | ||
1039 | } | ||
1040 | |||
1041 | // We treat "" as null here | ||
1042 | // as existing SDK doesn't support null yet. | ||
1043 | // This allows us to avoid spurious diffs | ||
1044 | // until we introduce null to the SDK. | ||
1045 | attrValue := val.GetAttr(name) | ||
1046 | if ctyEmptyString(attrValue) { | ||
1047 | return cty.NullVal(attrType) | ||
1048 | } | ||
1049 | |||
1050 | return attrValue | ||
1051 | } | ||
1052 | |||
1053 | func ctyCollectionValues(val cty.Value) []cty.Value { | ||
1054 | if !val.IsKnown() || val.IsNull() { | ||
1055 | return nil | ||
1056 | } | ||
1057 | |||
1058 | ret := make([]cty.Value, 0, val.LengthInt()) | ||
1059 | for it := val.ElementIterator(); it.Next(); { | ||
1060 | _, value := it.Element() | ||
1061 | ret = append(ret, value) | ||
1062 | } | ||
1063 | return ret | ||
1064 | } | ||
1065 | |||
1066 | // ctySequenceDiff returns differences between given sequences of cty.Value(s) | ||
1067 | // in the form of Create, Delete, or Update actions (for objects). | ||
1068 | func ctySequenceDiff(old, new []cty.Value) []*plans.Change { | ||
1069 | var ret []*plans.Change | ||
1070 | lcs := objchange.LongestCommonSubsequence(old, new) | ||
1071 | var oldI, newI, lcsI int | ||
1072 | for oldI < len(old) || newI < len(new) || lcsI < len(lcs) { | ||
1073 | for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) { | ||
1074 | isObjectDiff := old[oldI].Type().IsObjectType() && (newI >= len(new) || new[newI].Type().IsObjectType()) | ||
1075 | if isObjectDiff && newI < len(new) { | ||
1076 | ret = append(ret, &plans.Change{ | ||
1077 | Action: plans.Update, | ||
1078 | Before: old[oldI], | ||
1079 | After: new[newI], | ||
1080 | }) | ||
1081 | oldI++ | ||
1082 | newI++ // we also consume the next "new" in this case | ||
1083 | continue | ||
1084 | } | ||
1085 | |||
1086 | ret = append(ret, &plans.Change{ | ||
1087 | Action: plans.Delete, | ||
1088 | Before: old[oldI], | ||
1089 | After: cty.NullVal(old[oldI].Type()), | ||
1090 | }) | ||
1091 | oldI++ | ||
1092 | } | ||
1093 | for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) { | ||
1094 | ret = append(ret, &plans.Change{ | ||
1095 | Action: plans.Create, | ||
1096 | Before: cty.NullVal(new[newI].Type()), | ||
1097 | After: new[newI], | ||
1098 | }) | ||
1099 | newI++ | ||
1100 | } | ||
1101 | if lcsI < len(lcs) { | ||
1102 | ret = append(ret, &plans.Change{ | ||
1103 | Action: plans.NoOp, | ||
1104 | Before: lcs[lcsI], | ||
1105 | After: lcs[lcsI], | ||
1106 | }) | ||
1107 | |||
1108 | // All of our indexes advance together now, since the line | ||
1109 | // is common to all three sequences. | ||
1110 | lcsI++ | ||
1111 | oldI++ | ||
1112 | newI++ | ||
1113 | } | ||
1114 | } | ||
1115 | return ret | ||
1116 | } | ||
1117 | |||
1118 | func ctyEqualWithUnknown(old, new cty.Value) bool { | ||
1119 | if !old.IsWhollyKnown() || !new.IsWhollyKnown() { | ||
1120 | return false | ||
1121 | } | ||
1122 | return old.Equals(new).True() | ||
1123 | } | ||
1124 | |||
1125 | // ctyTypesEqual checks equality of two types more loosely | ||
1126 | // by avoiding checks of object/tuple elements | ||
1127 | // as we render differences on element-by-element basis anyway | ||
1128 | func ctyTypesEqual(oldT, newT cty.Type) bool { | ||
1129 | if oldT.IsObjectType() && newT.IsObjectType() { | ||
1130 | return true | ||
1131 | } | ||
1132 | if oldT.IsTupleType() && newT.IsTupleType() { | ||
1133 | return true | ||
1134 | } | ||
1135 | return oldT.Equals(newT) | ||
1136 | } | ||
1137 | |||
1138 | func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path { | ||
1139 | if cap(path)-len(path) >= minExtra { | ||
1140 | return path | ||
1141 | } | ||
1142 | newCap := cap(path) * 2 | ||
1143 | if newCap < (len(path) + minExtra) { | ||
1144 | newCap = len(path) + minExtra | ||
1145 | } | ||
1146 | newPath := make(cty.Path, len(path), newCap) | ||
1147 | copy(newPath, path) | ||
1148 | return newPath | ||
1149 | } | ||
1150 | |||
1151 | // ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil | ||
1152 | // or returns an empty value of a suitable type to serve as a placeholder for it. | ||
1153 | // | ||
1154 | // In particular, this function handles the special situation where a "list" is | ||
1155 | // actually represented as a tuple type where nested blocks contain | ||
1156 | // dynamically-typed values. | ||
1157 | func ctyNullBlockListAsEmpty(in cty.Value) cty.Value { | ||
1158 | if !in.IsNull() { | ||
1159 | return in | ||
1160 | } | ||
1161 | if ty := in.Type(); ty.IsListType() { | ||
1162 | return cty.ListValEmpty(ty.ElementType()) | ||
1163 | } | ||
1164 | return cty.EmptyTupleVal // must need a tuple, then | ||
1165 | } | ||
1166 | |||
1167 | // ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil | ||
1168 | // or returns an empty value of a suitable type to serve as a placeholder for it. | ||
1169 | // | ||
1170 | // In particular, this function handles the special situation where a "map" is | ||
1171 | // actually represented as an object type where nested blocks contain | ||
1172 | // dynamically-typed values. | ||
1173 | func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value { | ||
1174 | if !in.IsNull() { | ||
1175 | return in | ||
1176 | } | ||
1177 | if ty := in.Type(); ty.IsMapType() { | ||
1178 | return cty.MapValEmpty(ty.ElementType()) | ||
1179 | } | ||
1180 | return cty.EmptyObjectVal // must need an object, then | ||
1181 | } | ||
1182 | |||
1183 | // ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil | ||
1184 | // or returns an empty value of a suitable type to serve as a placeholder for it. | ||
1185 | func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value { | ||
1186 | if !in.IsNull() { | ||
1187 | return in | ||
1188 | } | ||
1189 | // Dynamically-typed attributes are not supported inside blocks backed by | ||
1190 | // sets, so our result here is always a set. | ||
1191 | return cty.SetValEmpty(in.Type().ElementType()) | ||
1192 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/command/format/format.go b/vendor/github.com/hashicorp/terraform/command/format/format.go new file mode 100644 index 0000000..aa8d7de --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/command/format/format.go | |||
@@ -0,0 +1,8 @@ | |||
1 | // Package format contains helpers for formatting various Terraform | ||
2 | // structures for human-readabout output. | ||
3 | // | ||
4 | // This package is used by the official Terraform CLI in formatting any | ||
5 | // output and is exported to encourage non-official frontends to mimic the | ||
6 | // output formatting as much as possible so that text formats of Terraform | ||
7 | // structures have a consistent look and feel. | ||
8 | package format | ||
diff --git a/vendor/github.com/hashicorp/terraform/command/format/object_id.go b/vendor/github.com/hashicorp/terraform/command/format/object_id.go new file mode 100644 index 0000000..85ebbfe --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/command/format/object_id.go | |||
@@ -0,0 +1,123 @@ | |||
1 | package format | ||
2 | |||
3 | import ( | ||
4 | "github.com/zclconf/go-cty/cty" | ||
5 | ) | ||
6 | |||
7 | // ObjectValueID takes a value that is assumed to be an object representation | ||
8 | // of some resource instance object and attempts to heuristically find an | ||
9 | // attribute of it that is likely to be a unique identifier in the remote | ||
10 | // system that it belongs to which will be useful to the user. | ||
11 | // | ||
12 | // If such an attribute is found, its name and string value intended for | ||
13 | // display are returned. Both returned strings are empty if no such attribute | ||
14 | // exists, in which case the caller should assume that the resource instance | ||
15 | // address within the Terraform configuration is the best available identifier. | ||
16 | // | ||
17 | // This is only a best-effort sort of thing, relying on naming conventions in | ||
18 | // our resource type schemas. The result is not guaranteed to be unique, but | ||
19 | // should generally be suitable for display to an end-user anyway. | ||
20 | // | ||
21 | // This function will panic if the given value is not of an object type. | ||
22 | func ObjectValueID(obj cty.Value) (k, v string) { | ||
23 | if obj.IsNull() || !obj.IsKnown() { | ||
24 | return "", "" | ||
25 | } | ||
26 | |||
27 | atys := obj.Type().AttributeTypes() | ||
28 | |||
29 | switch { | ||
30 | |||
31 | case atys["id"] == cty.String: | ||
32 | v := obj.GetAttr("id") | ||
33 | if v.IsKnown() && !v.IsNull() { | ||
34 | return "id", v.AsString() | ||
35 | } | ||
36 | |||
37 | case atys["name"] == cty.String: | ||
38 | // "name" isn't always globally unique, but if there isn't also an | ||
39 | // "id" then it _often_ is, in practice. | ||
40 | v := obj.GetAttr("name") | ||
41 | if v.IsKnown() && !v.IsNull() { | ||
42 | return "name", v.AsString() | ||
43 | } | ||
44 | } | ||
45 | |||
46 | return "", "" | ||
47 | } | ||
48 | |||
49 | // ObjectValueName takes a value that is assumed to be an object representation | ||
50 | // of some resource instance object and attempts to heuristically find an | ||
51 | // attribute of it that is likely to be a human-friendly name in the remote | ||
52 | // system that it belongs to which will be useful to the user. | ||
53 | // | ||
54 | // If such an attribute is found, its name and string value intended for | ||
55 | // display are returned. Both returned strings are empty if no such attribute | ||
56 | // exists, in which case the caller should assume that the resource instance | ||
57 | // address within the Terraform configuration is the best available identifier. | ||
58 | // | ||
59 | // This is only a best-effort sort of thing, relying on naming conventions in | ||
60 | // our resource type schemas. The result is not guaranteed to be unique, but | ||
61 | // should generally be suitable for display to an end-user anyway. | ||
62 | // | ||
63 | // Callers that use both ObjectValueName and ObjectValueID at the same time | ||
64 | // should be prepared to get the same attribute key and value from both in | ||
65 | // some cases, since there is overlap betweek the id-extraction and | ||
66 | // name-extraction heuristics. | ||
67 | // | ||
68 | // This function will panic if the given value is not of an object type. | ||
69 | func ObjectValueName(obj cty.Value) (k, v string) { | ||
70 | if obj.IsNull() || !obj.IsKnown() { | ||
71 | return "", "" | ||
72 | } | ||
73 | |||
74 | atys := obj.Type().AttributeTypes() | ||
75 | |||
76 | switch { | ||
77 | |||
78 | case atys["name"] == cty.String: | ||
79 | v := obj.GetAttr("name") | ||
80 | if v.IsKnown() && !v.IsNull() { | ||
81 | return "name", v.AsString() | ||
82 | } | ||
83 | |||
84 | case atys["tags"].IsMapType() && atys["tags"].ElementType() == cty.String: | ||
85 | tags := obj.GetAttr("tags") | ||
86 | if tags.IsNull() || !tags.IsWhollyKnown() { | ||
87 | break | ||
88 | } | ||
89 | |||
90 | switch { | ||
91 | case tags.HasIndex(cty.StringVal("name")).RawEquals(cty.True): | ||
92 | v := tags.Index(cty.StringVal("name")) | ||
93 | if v.IsKnown() && !v.IsNull() { | ||
94 | return "tags.name", v.AsString() | ||
95 | } | ||
96 | case tags.HasIndex(cty.StringVal("Name")).RawEquals(cty.True): | ||
97 | // AWS-style naming convention | ||
98 | v := tags.Index(cty.StringVal("Name")) | ||
99 | if v.IsKnown() && !v.IsNull() { | ||
100 | return "tags.Name", v.AsString() | ||
101 | } | ||
102 | } | ||
103 | } | ||
104 | |||
105 | return "", "" | ||
106 | } | ||
107 | |||
108 | // ObjectValueIDOrName is a convenience wrapper around both ObjectValueID | ||
109 | // and ObjectValueName (in that preference order) to try to extract some sort | ||
110 | // of human-friendly descriptive string value for an object as additional | ||
111 | // context about an object when it is being displayed in a compact way (where | ||
112 | // not all of the attributes are visible.) | ||
113 | // | ||
114 | // Just as with the two functions it wraps, it is a best-effort and may return | ||
115 | // two empty strings if no suitable attribute can be found for a given object. | ||
116 | func ObjectValueIDOrName(obj cty.Value) (k, v string) { | ||
117 | k, v = ObjectValueID(obj) | ||
118 | if k != "" { | ||
119 | return | ||
120 | } | ||
121 | k, v = ObjectValueName(obj) | ||
122 | return | ||
123 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/command/format/plan.go b/vendor/github.com/hashicorp/terraform/command/format/plan.go new file mode 100644 index 0000000..098653f --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/command/format/plan.go | |||
@@ -0,0 +1,302 @@ | |||
1 | package format | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "fmt" | ||
6 | "log" | ||
7 | "sort" | ||
8 | "strings" | ||
9 | |||
10 | "github.com/mitchellh/colorstring" | ||
11 | |||
12 | "github.com/hashicorp/terraform/addrs" | ||
13 | "github.com/hashicorp/terraform/plans" | ||
14 | "github.com/hashicorp/terraform/states" | ||
15 | "github.com/hashicorp/terraform/terraform" | ||
16 | ) | ||
17 | |||
18 | // Plan is a representation of a plan optimized for display to | ||
19 | // an end-user, as opposed to terraform.Plan which is for internal use. | ||
20 | // | ||
21 | // DisplayPlan excludes implementation details that may otherwise appear | ||
22 | // in the main plan, such as destroy actions on data sources (which are | ||
23 | // there only to clean up the state). | ||
24 | type Plan struct { | ||
25 | Resources []*InstanceDiff | ||
26 | } | ||
27 | |||
28 | // InstanceDiff is a representation of an instance diff optimized | ||
29 | // for display, in conjunction with DisplayPlan. | ||
30 | type InstanceDiff struct { | ||
31 | Addr *terraform.ResourceAddress | ||
32 | Action plans.Action | ||
33 | |||
34 | // Attributes describes changes to the attributes of the instance. | ||
35 | // | ||
36 | // For destroy diffs this is always nil. | ||
37 | Attributes []*AttributeDiff | ||
38 | |||
39 | Tainted bool | ||
40 | Deposed bool | ||
41 | } | ||
42 | |||
43 | // AttributeDiff is a representation of an attribute diff optimized | ||
44 | // for display, in conjunction with DisplayInstanceDiff. | ||
45 | type AttributeDiff struct { | ||
46 | // Path is a dot-delimited traversal through possibly many levels of list and map structure, | ||
47 | // intended for display purposes only. | ||
48 | Path string | ||
49 | |||
50 | Action plans.Action | ||
51 | |||
52 | OldValue string | ||
53 | NewValue string | ||
54 | |||
55 | NewComputed bool | ||
56 | Sensitive bool | ||
57 | ForcesNew bool | ||
58 | } | ||
59 | |||
60 | // PlanStats gives summary counts for a Plan. | ||
61 | type PlanStats struct { | ||
62 | ToAdd, ToChange, ToDestroy int | ||
63 | } | ||
64 | |||
65 | // NewPlan produces a display-oriented Plan from a terraform.Plan. | ||
66 | func NewPlan(changes *plans.Changes) *Plan { | ||
67 | log.Printf("[TRACE] NewPlan for %#v", changes) | ||
68 | ret := &Plan{} | ||
69 | if changes == nil { | ||
70 | // Nothing to do! | ||
71 | return ret | ||
72 | } | ||
73 | |||
74 | for _, rc := range changes.Resources { | ||
75 | addr := rc.Addr | ||
76 | log.Printf("[TRACE] NewPlan found %s (%s)", addr, rc.Action) | ||
77 | dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode | ||
78 | |||
79 | // We create "delete" actions for data resources so we can clean | ||
80 | // up their entries in state, but this is an implementation detail | ||
81 | // that users shouldn't see. | ||
82 | if dataSource && rc.Action == plans.Delete { | ||
83 | continue | ||
84 | } | ||
85 | |||
86 | // For now we'll shim this to work with our old types. | ||
87 | // TODO: Update for the new plan types, ideally also switching over to | ||
88 | // a structural diff renderer instead of a flat renderer. | ||
89 | did := &InstanceDiff{ | ||
90 | Addr: terraform.NewLegacyResourceInstanceAddress(addr), | ||
91 | Action: rc.Action, | ||
92 | } | ||
93 | |||
94 | if rc.DeposedKey != states.NotDeposed { | ||
95 | did.Deposed = true | ||
96 | } | ||
97 | |||
98 | // Since this is just a temporary stub implementation on the way | ||
99 | // to us replacing this with the structural diff renderer, we currently | ||
100 | // don't include any attributes here. | ||
101 | // FIXME: Implement the structural diff renderer to replace this | ||
102 | // codepath altogether. | ||
103 | |||
104 | ret.Resources = append(ret.Resources, did) | ||
105 | } | ||
106 | |||
107 | // Sort the instance diffs by their addresses for display. | ||
108 | sort.Slice(ret.Resources, func(i, j int) bool { | ||
109 | iAddr := ret.Resources[i].Addr | ||
110 | jAddr := ret.Resources[j].Addr | ||
111 | return iAddr.Less(jAddr) | ||
112 | }) | ||
113 | |||
114 | return ret | ||
115 | } | ||
116 | |||
117 | // Format produces and returns a text representation of the receiving plan | ||
118 | // intended for display in a terminal. | ||
119 | // | ||
120 | // If color is not nil, it is used to colorize the output. | ||
121 | func (p *Plan) Format(color *colorstring.Colorize) string { | ||
122 | if p.Empty() { | ||
123 | return "This plan does nothing." | ||
124 | } | ||
125 | |||
126 | if color == nil { | ||
127 | color = &colorstring.Colorize{ | ||
128 | Colors: colorstring.DefaultColors, | ||
129 | Reset: false, | ||
130 | } | ||
131 | } | ||
132 | |||
133 | // Find the longest path length of all the paths that are changing, | ||
134 | // so we can align them all. | ||
135 | keyLen := 0 | ||
136 | for _, r := range p.Resources { | ||
137 | for _, attr := range r.Attributes { | ||
138 | key := attr.Path | ||
139 | |||
140 | if len(key) > keyLen { | ||
141 | keyLen = len(key) | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | |||
146 | buf := new(bytes.Buffer) | ||
147 | for _, r := range p.Resources { | ||
148 | formatPlanInstanceDiff(buf, r, keyLen, color) | ||
149 | } | ||
150 | |||
151 | return strings.TrimSpace(buf.String()) | ||
152 | } | ||
153 | |||
154 | // Stats returns statistics about the plan | ||
155 | func (p *Plan) Stats() PlanStats { | ||
156 | var ret PlanStats | ||
157 | for _, r := range p.Resources { | ||
158 | switch r.Action { | ||
159 | case plans.Create: | ||
160 | ret.ToAdd++ | ||
161 | case plans.Update: | ||
162 | ret.ToChange++ | ||
163 | case plans.DeleteThenCreate, plans.CreateThenDelete: | ||
164 | ret.ToAdd++ | ||
165 | ret.ToDestroy++ | ||
166 | case plans.Delete: | ||
167 | ret.ToDestroy++ | ||
168 | } | ||
169 | } | ||
170 | return ret | ||
171 | } | ||
172 | |||
173 | // ActionCounts returns the number of diffs for each action type | ||
174 | func (p *Plan) ActionCounts() map[plans.Action]int { | ||
175 | ret := map[plans.Action]int{} | ||
176 | for _, r := range p.Resources { | ||
177 | ret[r.Action]++ | ||
178 | } | ||
179 | return ret | ||
180 | } | ||
181 | |||
182 | // Empty returns true if there is at least one resource diff in the receiving plan. | ||
183 | func (p *Plan) Empty() bool { | ||
184 | return len(p.Resources) == 0 | ||
185 | } | ||
186 | |||
187 | // DiffActionSymbol returns a string that, once passed through a | ||
188 | // colorstring.Colorize, will produce a result that can be written | ||
189 | // to a terminal to produce a symbol made of three printable | ||
190 | // characters, possibly interspersed with VT100 color codes. | ||
191 | func DiffActionSymbol(action plans.Action) string { | ||
192 | switch action { | ||
193 | case plans.DeleteThenCreate: | ||
194 | return "[red]-[reset]/[green]+[reset]" | ||
195 | case plans.CreateThenDelete: | ||
196 | return "[green]+[reset]/[red]-[reset]" | ||
197 | case plans.Create: | ||
198 | return " [green]+[reset]" | ||
199 | case plans.Delete: | ||
200 | return " [red]-[reset]" | ||
201 | case plans.Read: | ||
202 | return " [cyan]<=[reset]" | ||
203 | case plans.Update: | ||
204 | return " [yellow]~[reset]" | ||
205 | default: | ||
206 | return " ?" | ||
207 | } | ||
208 | } | ||
209 | |||
210 | // formatPlanInstanceDiff writes the text representation of the given instance diff | ||
211 | // to the given buffer, using the given colorizer. | ||
212 | func formatPlanInstanceDiff(buf *bytes.Buffer, r *InstanceDiff, keyLen int, colorizer *colorstring.Colorize) { | ||
213 | addrStr := r.Addr.String() | ||
214 | |||
215 | // Determine the color for the text (green for adding, yellow | ||
216 | // for change, red for delete), and symbol, and output the | ||
217 | // resource header. | ||
218 | color := "yellow" | ||
219 | symbol := DiffActionSymbol(r.Action) | ||
220 | oldValues := true | ||
221 | switch r.Action { | ||
222 | case plans.DeleteThenCreate, plans.CreateThenDelete: | ||
223 | color = "yellow" | ||
224 | case plans.Create: | ||
225 | color = "green" | ||
226 | oldValues = false | ||
227 | case plans.Delete: | ||
228 | color = "red" | ||
229 | case plans.Read: | ||
230 | color = "cyan" | ||
231 | oldValues = false | ||
232 | } | ||
233 | |||
234 | var extraStr string | ||
235 | if r.Tainted { | ||
236 | extraStr = extraStr + " (tainted)" | ||
237 | } | ||
238 | if r.Deposed { | ||
239 | extraStr = extraStr + " (deposed)" | ||
240 | } | ||
241 | if r.Action.IsReplace() { | ||
242 | extraStr = extraStr + colorizer.Color(" [red][bold](new resource required)") | ||
243 | } | ||
244 | |||
245 | buf.WriteString( | ||
246 | colorizer.Color(fmt.Sprintf( | ||
247 | "[%s]%s [%s]%s%s\n", | ||
248 | color, symbol, color, addrStr, extraStr, | ||
249 | )), | ||
250 | ) | ||
251 | |||
252 | for _, attr := range r.Attributes { | ||
253 | |||
254 | v := attr.NewValue | ||
255 | var dispV string | ||
256 | switch { | ||
257 | case v == "" && attr.NewComputed: | ||
258 | dispV = "<computed>" | ||
259 | case attr.Sensitive: | ||
260 | dispV = "<sensitive>" | ||
261 | default: | ||
262 | dispV = fmt.Sprintf("%q", v) | ||
263 | } | ||
264 | |||
265 | updateMsg := "" | ||
266 | switch { | ||
267 | case attr.ForcesNew && r.Action.IsReplace(): | ||
268 | updateMsg = colorizer.Color(" [red](forces new resource)") | ||
269 | case attr.Sensitive && oldValues: | ||
270 | updateMsg = colorizer.Color(" [yellow](attribute changed)") | ||
271 | } | ||
272 | |||
273 | if oldValues { | ||
274 | u := attr.OldValue | ||
275 | var dispU string | ||
276 | switch { | ||
277 | case attr.Sensitive: | ||
278 | dispU = "<sensitive>" | ||
279 | default: | ||
280 | dispU = fmt.Sprintf("%q", u) | ||
281 | } | ||
282 | buf.WriteString(fmt.Sprintf( | ||
283 | " %s:%s %s => %s%s\n", | ||
284 | attr.Path, | ||
285 | strings.Repeat(" ", keyLen-len(attr.Path)), | ||
286 | dispU, dispV, | ||
287 | updateMsg, | ||
288 | )) | ||
289 | } else { | ||
290 | buf.WriteString(fmt.Sprintf( | ||
291 | " %s:%s %s%s\n", | ||
292 | attr.Path, | ||
293 | strings.Repeat(" ", keyLen-len(attr.Path)), | ||
294 | dispV, | ||
295 | updateMsg, | ||
296 | )) | ||
297 | } | ||
298 | } | ||
299 | |||
300 | // Write the reset color so we don't bleed color into later text | ||
301 | buf.WriteString(colorizer.Color("[reset]\n")) | ||
302 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/command/format/state.go b/vendor/github.com/hashicorp/terraform/command/format/state.go new file mode 100644 index 0000000..f411ef9 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/command/format/state.go | |||
@@ -0,0 +1,286 @@ | |||
1 | package format | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "fmt" | ||
6 | "sort" | ||
7 | "strings" | ||
8 | |||
9 | "github.com/zclconf/go-cty/cty" | ||
10 | |||
11 | "github.com/hashicorp/terraform/addrs" | ||
12 | "github.com/hashicorp/terraform/configs/configschema" | ||
13 | "github.com/hashicorp/terraform/plans" | ||
14 | "github.com/hashicorp/terraform/states" | ||
15 | "github.com/hashicorp/terraform/terraform" | ||
16 | "github.com/mitchellh/colorstring" | ||
17 | ) | ||
18 | |||
19 | // StateOpts are the options for formatting a state. | ||
20 | type StateOpts struct { | ||
21 | // State is the state to format. This is required. | ||
22 | State *states.State | ||
23 | |||
24 | // Schemas are used to decode attributes. This is required. | ||
25 | Schemas *terraform.Schemas | ||
26 | |||
27 | // Color is the colorizer. This is optional. | ||
28 | Color *colorstring.Colorize | ||
29 | } | ||
30 | |||
31 | // State takes a state and returns a string | ||
32 | func State(opts *StateOpts) string { | ||
33 | if opts.Color == nil { | ||
34 | panic("colorize not given") | ||
35 | } | ||
36 | |||
37 | if opts.Schemas == nil { | ||
38 | panic("schemas not given") | ||
39 | } | ||
40 | |||
41 | s := opts.State | ||
42 | if len(s.Modules) == 0 { | ||
43 | return "The state file is empty. No resources are represented." | ||
44 | } | ||
45 | |||
46 | buf := bytes.NewBufferString("[reset]") | ||
47 | p := blockBodyDiffPrinter{ | ||
48 | buf: buf, | ||
49 | color: opts.Color, | ||
50 | action: plans.NoOp, | ||
51 | } | ||
52 | |||
53 | // Format all the modules | ||
54 | for _, m := range s.Modules { | ||
55 | formatStateModule(p, m, opts.Schemas) | ||
56 | } | ||
57 | |||
58 | // Write the outputs for the root module | ||
59 | m := s.RootModule() | ||
60 | |||
61 | if m.OutputValues != nil { | ||
62 | if len(m.OutputValues) > 0 { | ||
63 | p.buf.WriteString("Outputs:\n\n") | ||
64 | } | ||
65 | |||
66 | // Sort the outputs | ||
67 | ks := make([]string, 0, len(m.OutputValues)) | ||
68 | for k := range m.OutputValues { | ||
69 | ks = append(ks, k) | ||
70 | } | ||
71 | sort.Strings(ks) | ||
72 | |||
73 | // Output each output k/v pair | ||
74 | for _, k := range ks { | ||
75 | v := m.OutputValues[k] | ||
76 | p.buf.WriteString(fmt.Sprintf("%s = ", k)) | ||
77 | p.writeValue(v.Value, plans.NoOp, 0) | ||
78 | p.buf.WriteString("\n\n") | ||
79 | } | ||
80 | } | ||
81 | |||
82 | return opts.Color.Color(strings.TrimSpace(p.buf.String())) | ||
83 | |||
84 | } | ||
85 | |||
86 | func formatStateModule(p blockBodyDiffPrinter, m *states.Module, schemas *terraform.Schemas) { | ||
87 | // First get the names of all the resources so we can show them | ||
88 | // in alphabetical order. | ||
89 | names := make([]string, 0, len(m.Resources)) | ||
90 | for name := range m.Resources { | ||
91 | names = append(names, name) | ||
92 | } | ||
93 | sort.Strings(names) | ||
94 | |||
95 | // Go through each resource and begin building up the output. | ||
96 | for _, key := range names { | ||
97 | for k, v := range m.Resources[key].Instances { | ||
98 | addr := m.Resources[key].Addr | ||
99 | |||
100 | taintStr := "" | ||
101 | if v.Current.Status == 'T' { | ||
102 | taintStr = "(tainted)" | ||
103 | } | ||
104 | p.buf.WriteString(fmt.Sprintf("# %s: %s\n", addr.Absolute(m.Addr).Instance(k), taintStr)) | ||
105 | |||
106 | var schema *configschema.Block | ||
107 | provider := m.Resources[key].ProviderConfig.ProviderConfig.StringCompact() | ||
108 | if _, exists := schemas.Providers[provider]; !exists { | ||
109 | // This should never happen in normal use because we should've | ||
110 | // loaded all of the schemas and checked things prior to this | ||
111 | // point. We can't return errors here, but since this is UI code | ||
112 | // we will try to do _something_ reasonable. | ||
113 | p.buf.WriteString(fmt.Sprintf("# missing schema for provider %q\n\n", provider)) | ||
114 | continue | ||
115 | } | ||
116 | |||
117 | switch addr.Mode { | ||
118 | case addrs.ManagedResourceMode: | ||
119 | schema, _ = schemas.ResourceTypeConfig( | ||
120 | provider, | ||
121 | addr.Mode, | ||
122 | addr.Type, | ||
123 | ) | ||
124 | if schema == nil { | ||
125 | p.buf.WriteString(fmt.Sprintf( | ||
126 | "# missing schema for provider %q resource type %s\n\n", provider, addr.Type)) | ||
127 | continue | ||
128 | } | ||
129 | |||
130 | p.buf.WriteString(fmt.Sprintf( | ||
131 | "resource %q %q {", | ||
132 | addr.Type, | ||
133 | addr.Name, | ||
134 | )) | ||
135 | case addrs.DataResourceMode: | ||
136 | schema, _ = schemas.ResourceTypeConfig( | ||
137 | provider, | ||
138 | addr.Mode, | ||
139 | addr.Type, | ||
140 | ) | ||
141 | if schema == nil { | ||
142 | p.buf.WriteString(fmt.Sprintf( | ||
143 | "# missing schema for provider %q data source %s\n\n", provider, addr.Type)) | ||
144 | continue | ||
145 | } | ||
146 | |||
147 | p.buf.WriteString(fmt.Sprintf( | ||
148 | "data %q %q {", | ||
149 | addr.Type, | ||
150 | addr.Name, | ||
151 | )) | ||
152 | default: | ||
153 | // should never happen, since the above is exhaustive | ||
154 | p.buf.WriteString(addr.String()) | ||
155 | } | ||
156 | |||
157 | val, err := v.Current.Decode(schema.ImpliedType()) | ||
158 | if err != nil { | ||
159 | fmt.Println(err.Error()) | ||
160 | break | ||
161 | } | ||
162 | |||
163 | path := make(cty.Path, 0, 3) | ||
164 | bodyWritten := p.writeBlockBodyDiff(schema, val.Value, val.Value, 2, path) | ||
165 | if bodyWritten { | ||
166 | p.buf.WriteString("\n") | ||
167 | } | ||
168 | |||
169 | p.buf.WriteString("}\n\n") | ||
170 | } | ||
171 | } | ||
172 | p.buf.WriteString("[reset]\n") | ||
173 | } | ||
174 | |||
175 | func formatNestedList(indent string, outputList []interface{}) string { | ||
176 | outputBuf := new(bytes.Buffer) | ||
177 | outputBuf.WriteString(fmt.Sprintf("%s[", indent)) | ||
178 | |||
179 | lastIdx := len(outputList) - 1 | ||
180 | |||
181 | for i, value := range outputList { | ||
182 | outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, " ", value)) | ||
183 | if i != lastIdx { | ||
184 | outputBuf.WriteString(",") | ||
185 | } | ||
186 | } | ||
187 | |||
188 | outputBuf.WriteString(fmt.Sprintf("\n%s]", indent)) | ||
189 | return strings.TrimPrefix(outputBuf.String(), "\n") | ||
190 | } | ||
191 | |||
192 | func formatListOutput(indent, outputName string, outputList []interface{}) string { | ||
193 | keyIndent := "" | ||
194 | |||
195 | outputBuf := new(bytes.Buffer) | ||
196 | |||
197 | if outputName != "" { | ||
198 | outputBuf.WriteString(fmt.Sprintf("%s%s = [", indent, outputName)) | ||
199 | keyIndent = " " | ||
200 | } | ||
201 | |||
202 | lastIdx := len(outputList) - 1 | ||
203 | |||
204 | for i, value := range outputList { | ||
205 | switch typedValue := value.(type) { | ||
206 | case string: | ||
207 | outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, keyIndent, value)) | ||
208 | case []interface{}: | ||
209 | outputBuf.WriteString(fmt.Sprintf("\n%s%s", indent, | ||
210 | formatNestedList(indent+keyIndent, typedValue))) | ||
211 | case map[string]interface{}: | ||
212 | outputBuf.WriteString(fmt.Sprintf("\n%s%s", indent, | ||
213 | formatNestedMap(indent+keyIndent, typedValue))) | ||
214 | } | ||
215 | |||
216 | if lastIdx != i { | ||
217 | outputBuf.WriteString(",") | ||
218 | } | ||
219 | } | ||
220 | |||
221 | if outputName != "" { | ||
222 | if len(outputList) > 0 { | ||
223 | outputBuf.WriteString(fmt.Sprintf("\n%s]", indent)) | ||
224 | } else { | ||
225 | outputBuf.WriteString("]") | ||
226 | } | ||
227 | } | ||
228 | |||
229 | return strings.TrimPrefix(outputBuf.String(), "\n") | ||
230 | } | ||
231 | |||
232 | func formatNestedMap(indent string, outputMap map[string]interface{}) string { | ||
233 | ks := make([]string, 0, len(outputMap)) | ||
234 | for k, _ := range outputMap { | ||
235 | ks = append(ks, k) | ||
236 | } | ||
237 | sort.Strings(ks) | ||
238 | |||
239 | outputBuf := new(bytes.Buffer) | ||
240 | outputBuf.WriteString(fmt.Sprintf("%s{", indent)) | ||
241 | |||
242 | lastIdx := len(outputMap) - 1 | ||
243 | for i, k := range ks { | ||
244 | v := outputMap[k] | ||
245 | outputBuf.WriteString(fmt.Sprintf("\n%s%s = %v", indent+" ", k, v)) | ||
246 | |||
247 | if lastIdx != i { | ||
248 | outputBuf.WriteString(",") | ||
249 | } | ||
250 | } | ||
251 | |||
252 | outputBuf.WriteString(fmt.Sprintf("\n%s}", indent)) | ||
253 | |||
254 | return strings.TrimPrefix(outputBuf.String(), "\n") | ||
255 | } | ||
256 | |||
257 | func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string { | ||
258 | ks := make([]string, 0, len(outputMap)) | ||
259 | for k, _ := range outputMap { | ||
260 | ks = append(ks, k) | ||
261 | } | ||
262 | sort.Strings(ks) | ||
263 | |||
264 | keyIndent := "" | ||
265 | |||
266 | outputBuf := new(bytes.Buffer) | ||
267 | if outputName != "" { | ||
268 | outputBuf.WriteString(fmt.Sprintf("%s%s = {", indent, outputName)) | ||
269 | keyIndent = " " | ||
270 | } | ||
271 | |||
272 | for _, k := range ks { | ||
273 | v := outputMap[k] | ||
274 | outputBuf.WriteString(fmt.Sprintf("\n%s%s%s = %v", indent, keyIndent, k, v)) | ||
275 | } | ||
276 | |||
277 | if outputName != "" { | ||
278 | if len(outputMap) > 0 { | ||
279 | outputBuf.WriteString(fmt.Sprintf("\n%s}", indent)) | ||
280 | } else { | ||
281 | outputBuf.WriteString("}") | ||
282 | } | ||
283 | } | ||
284 | |||
285 | return strings.TrimPrefix(outputBuf.String(), "\n") | ||
286 | } | ||