-
Notifications
You must be signed in to change notification settings - Fork 6
/
terraform_plan.go
208 lines (187 loc) · 6.68 KB
/
terraform_plan.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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package parsers
import (
"encoding/json"
"fmt"
"math/rand"
"reflect"
"strconv"
"github.com/pkg/errors"
)
type ResourceActions []string
type TerraformPlanResource struct {
Address string // "aws_cloudwatch_log_group.terra_ci",
Mode string // "managed",
Type string // "aws_cloudwatch_log_group",
Name string // "terra_ci",
Index interface{} // Can be either an integer or a string (e.g. 1, "10.0.101.0/24", "rtb-00cf8381520103cfb")
}
type TerraformPlanResourceChange struct {
TerraformPlanResource
Change struct {
Actions ResourceActions
Before map[string]interface{} // will be null when the action is `create`
After map[string]interface{} // will be null when then action is `delete`
}
Expressions interface{} `json:"expressions"`
}
type TerraformPlanModule struct {
Resources []TerraformPlanResourceChange `json:"resources"`
}
type TerraformPlanConfiguration struct {
RootModule TerraformPlanModule `json:"root_module"`
}
type TerraformPlanJson struct {
ResourceChanges []TerraformPlanResourceChange `json:"resource_changes"`
Configuration TerraformPlanConfiguration `json:"configuration"`
}
type TerraformScanInput map[string]map[string]map[string]interface{}
func getValidResourceActionsForDeltaScan() []ResourceActions {
return []ResourceActions{{`create`}, {`update`}, {`create`, `delete`}, {`delete`, `create`}}
}
func getValidResourceActionsForFullScan() []ResourceActions {
return append(getValidResourceActionsForDeltaScan(), ResourceActions{`no-op`})
}
func parseTerraformPlan(planJson TerraformPlanJson, isFullScan bool) TerraformScanInput {
scanInput := TerraformScanInput{
"resource": map[string]map[string]interface{}{},
"data": map[string]map[string]interface{}{},
}
for _, resource := range planJson.ResourceChanges {
// checks if valid action, if invalid skip loop iteration
if !isValidResourceActions(resource.Change.Actions, isFullScan) {
continue
}
// get correct mode for scanInput
var mode string
if resource.Mode == "data" {
mode = "data"
} else {
mode = "resource"
}
// even though we only support resource or data options, we do this as a sanity check
if _, ok := scanInput[mode]; ok {
// scanInput's mode is set, add item to mode
if _, ok := scanInput[mode][resource.Type]; ok {
// scanInput[mode][resource.Type] resource type already created, adding another resource under it with a new name
scanInput[mode][resource.Type][getResourceName(resource)] = resource.Change.After
} else {
// set new resource type with its values
scanInput[mode][resource.Type] = map[string]interface{}{getResourceName(resource): resource.Change.After}
}
}
}
// check root module for references in first depth of attributes
for _, resource := range planJson.Configuration.RootModule.Resources {
// don't care about references in data sources for time being
if resource.Mode == "data" {
continue
}
mode := "resource"
// only update the references in resources that have some resolved attributes already
if resolvedResource, ok := scanInput[mode][resource.Type][getResourceName(resource)].(map[string]interface{}); ok && resolvedResource != nil {
expressions := getExpressions(resource.Expressions)
for k, v := range expressions {
// only add non existing attributes. If we already have resolved value do not overwrite it with reference
if _, ok := resolvedResource[k]; !ok {
resolvedResource[k] = v
}
}
scanInput[mode][resource.Type][getResourceName(resource)] = resolvedResource
}
}
return scanInput
}
func getExpressions(expressions interface{}) map[string]interface{} {
result := make(map[string]interface{})
// expressions can be nested. we are only doing 1 depth to resolve top level depenencies
expressionMap, ok := expressions.(map[string]interface{})
if !ok {
return nil
}
for k, v := range expressionMap {
referenceKey, ok := getReference(v)
if ok {
result[k] = referenceKey
}
}
return result
}
// this is very naive implementation
// the referenences can be composed of number of keys
// we only going to use the first reference for time being
func getReference(value interface{}) (interface{}, bool) {
v, ok := value.(map[string]interface{})
if !ok {
return "", false
}
// we are only interested with "references" values
referencesInt, ok := v["references"]
if !ok {
return "", false
}
references, ok := referencesInt.([]interface{})
if !ok {
return "", false
}
return references[0], true
}
func getResourceName(resource TerraformPlanResourceChange) string {
if resource.Index == nil {
return resource.Name
} else {
// if an index field is present, use name + index to diffrentiate multi-instance resources
// e.g resource 1 with same type & name but different index
// "type": "aws_route",
// "name": "private",
// "index": "rtb-00cf8381520103cfb",
// e.g resource 2 with same type & name but different index
// "type": "aws_route",
// "name": "private",
// "index": "rtb-030b64d80cb5e9da7",
var indexKey string = mapResourceIndexToStringKey(resource.Index)
return fmt.Sprintf(`%s["%s"]`, resource.Name, indexKey)
}
}
func mapResourceIndexToStringKey(resourceIndex interface{}) string {
var indexType reflect.Kind = reflect.TypeOf(resourceIndex).Kind()
var indexKey string
if indexType == reflect.Int {
indexKey = strconv.Itoa(resourceIndex.(int))
} else if indexType == reflect.String {
indexKey = resourceIndex.(string)
// In some cases the JSON Unmarshal will decode an Integer as a Float, therefore the two following checks
} else if indexType == reflect.Float32 {
indexKey = strconv.Itoa(int(resourceIndex.(float32)))
} else if indexType == reflect.Float64 {
indexKey = strconv.Itoa(int(resourceIndex.(float64)))
} else {
// If some unknown value was used here, we'll generate some random integer.
indexKey = strconv.Itoa(rand.Intn(10000))
}
return indexKey
}
func isValidResourceActions(resourceAction ResourceActions, isFullScan bool) bool {
var validActions []ResourceActions
if isFullScan {
validActions = getValidResourceActionsForFullScan()
} else {
validActions = getValidResourceActionsForDeltaScan()
}
for _, validAction := range validActions {
if reflect.DeepEqual(validAction, resourceAction) {
return true
}
}
return false
}
func ParseTerraformPlan(p []byte, v *interface{}) error {
var tfPlanJson TerraformPlanJson
if err := json.Unmarshal(p, &tfPlanJson); err != nil {
return errors.Wrap(err, "failed to parse terraform-plan json payload")
}
// Currently being used only by Terraform Cloud integration
// It was decided that using Full Scan as the default scan is the right approach
// In the future this will be configurable
*v = parseTerraformPlan(tfPlanJson, true)
return nil
}