/
terraform.go
159 lines (136 loc) · 4.57 KB
/
terraform.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package terraform
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"get.porter.sh/porter/pkg/runtime"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
)
const (
// DefaultWorkingDir is the default working directory for Terraform.
DefaultWorkingDir = "terraform"
// DefaultClientVersion is the default version of the terraform cli.
DefaultClientVersion = "1.2.9"
// DefaultInitFile is the default file used to initialize terraform providers during build.
DefaultInitFile = ""
)
// Mixin is the logic behind the terraform mixin
type Mixin struct {
runtime.RuntimeConfig
config MixinConfig
userAgent string
}
// New terraform mixin client, initialized with useful defaults.
func New() *Mixin {
return NewFor(runtime.NewConfig())
}
func NewFor(cfg runtime.RuntimeConfig) *Mixin {
m := &Mixin{
RuntimeConfig: cfg,
config: MixinConfig{
WorkingDir: DefaultWorkingDir,
ClientVersion: DefaultClientVersion,
InitFile: DefaultInitFile,
},
}
m.SetUserAgent()
return m
}
func (m *Mixin) getPayloadData() ([]byte, error) {
reader := bufio.NewReader(m.In)
data, err := ioutil.ReadAll(reader)
if err != nil {
return nil, errors.Wrap(err, "could not read the payload from STDIN")
}
return data, nil
}
func (m *Mixin) getOutput(ctx context.Context, outputName string) ([]byte, error) {
// Using -json instead of -raw because terraform only allows for string, bool,
// and number output types when using -raw. This means that the outputs will
// need to be unencoded to raw string because -json does json compliant html
// special character encoding.
cmd := m.NewCommand(ctx, "terraform", "output", "-json", outputName)
cmd.Stderr = m.Err
// Terraform appears to auto-append a newline character when printing outputs
// Trim this character before returning the output
out, err := cmd.Output()
if err != nil {
prettyCmd := fmt.Sprintf("%s %s", cmd.Path, strings.Join(cmd.Args, " "))
return nil, errors.Wrap(err, fmt.Sprintf("couldn't run command %s", prettyCmd))
}
// Implement a custom JSON encoder that doesn't do HTML escaping. This allows
// for recrusive decoding of complex JSON objects using the unmarshal and then
// re-encoding it but skipping the html escaping. This allows for complex types
// like maps to be represented as a byte slice without having go types be
// part of that byte slice, eg: without the re-encoding, a JSON byte slice with
// '{"foo": "bar"}' would become map[foo:bar].
var outDat interface{}
err = json.Unmarshal(out, &outDat)
if err != nil {
return []byte{}, err
}
// If the output is a string then don't re-encode it, just return the decoded
// json string. Re-encoding the string will wrap it in quotes which breaks
// the compabiltity with -raw
if outStr, ok := outDat.(string); ok {
return []byte(outStr), nil
}
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err = encoder.Encode(outDat)
if err != nil {
return []byte{}, err
}
return bytes.TrimRight(buffer.Bytes(), "\n"), nil
}
func (m *Mixin) handleOutputs(ctx context.Context, outputs []Output) error {
var bigErr *multierror.Error
for _, output := range outputs {
bytes, err := m.getOutput(ctx, output.Name)
if err != nil {
bigErr = multierror.Append(bigErr, err)
continue
}
err = m.Context.WriteMixinOutputToFile(output.Name, bytes)
if err != nil {
bigErr = multierror.Append(bigErr, errors.Wrapf(err, "unable to persist output '%s'", output.Name))
}
if output.DestinationFile != "" {
err = m.Context.FileSystem.MkdirAll(filepath.Dir(output.DestinationFile), 0700)
if err != nil {
bigErr = multierror.Append(bigErr, errors.Wrapf(err, "unable to create destination directory for output '%s'", output.Name))
}
err = m.Context.FileSystem.WriteFile(output.DestinationFile, bytes, 0700)
if err != nil {
bigErr = multierror.Append(bigErr, errors.Wrapf(err, "unable to copy output '%s' to '%s'", output.Name, output.DestinationFile))
}
}
}
return bigErr.ErrorOrNil()
}
// commandPreRun runs setup tasks applicable for every action
func (m *Mixin) commandPreRun(ctx context.Context, step *Step) error {
if step.LogLevel != "" {
os.Setenv("TF_LOG", step.LogLevel)
}
// First, change to specified working dir
m.Chdir(m.config.WorkingDir)
if m.DebugMode {
fmt.Fprintln(m.Err, "Terraform working directory is", m.Getwd())
}
// Initialize Terraform
fmt.Println("Initializing Terraform...")
err := m.Init(ctx, step.BackendConfig)
if err != nil {
return fmt.Errorf("could not init terraform, %s", err)
}
return nil
}