Skip to content

Commit 8e622ee

Browse files
authored
Supports aws_db_instance (RDS) (#81)
1 parent 97ad90f commit 8e622ee

15 files changed

+298
-47
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ It can estimate Carbon Emissions of:
2727
- Amazon Web Services
2828
- [x] EC2 (including inline root, elastic, and ephemeral block storages)
2929
- [x] EBS Volumes
30+
- [x] RDS
3031

3132
The following will also be supported soon:
3233

3334
- Amazon Web Services
34-
- [ ] RDS
3535
- [ ] AutoScaling Group
3636
- Azure
3737
- [ ] Virtual Machines

doc/scope.md

+2
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ Data resources:
4343
|---|---|---|
4444
| `aws_instance`| No GPU | |
4545
| `aws_ebs_volume`| if size set, or if snapshot declared as data resource | |
46+
| `aws_db_instance` | | |
4647

4748
Data resources:
4849

4950
| Resource | Limitations | Comment |
5051
|---|---|---|
5152
| `aws_ami`| `ebs.volume_size` can be set, otherwise get it from image only if AWS credentials are provided| |
5253
| `aws_ebs_snapshot`| `volume_size` can be set, otherwise get it from image only if AWS credentials are provided| |
54+
| `aws_db_snapshot`| get it only if AWS credentials are provided| |
5355

5456

5557
_more to be implemented_

internal/plan/defaults.go

+5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import (
99
"github.com/aws/aws-sdk-go/aws/session"
1010
)
1111

12+
var defaultRegion *string
13+
1214
func getDefaultRegion() *string {
15+
if defaultRegion != nil {
16+
return defaultRegion
17+
}
1318
var region interface{}
1419
if region == nil {
1520
if os.Getenv("AWS_DEFAULT_REGION") != "" {

internal/plan/json_getters.go

+46-27
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func getSliceItems(context tfContext) ([]interface{}, error) {
8585
for _, pathRaw := range paths {
8686
path := pathRaw
8787
if strings.Contains(pathRaw, "${") {
88-
path, err = resolvePlaceholders(path, context.ParentContext)
88+
path, err = resolvePlaceholders(pathRaw, context.ParentContext)
8989
if err != nil {
9090
return nil, err
9191
}
@@ -98,7 +98,7 @@ func getSliceItems(context tfContext) ([]interface{}, error) {
9898
if len(jsonResults) == 0 && TfPlan != nil {
9999
jsonResults, err = utils.GetJSON(path, *TfPlan)
100100
if err != nil {
101-
return nil, errors.Wrapf(err, "Cannot get item: %v", path)
101+
return nil, errors.Wrapf(err, "Cannot get item in full plan: %v", path)
102102
}
103103
}
104104
for _, jsonResultsI := range jsonResults {
@@ -108,7 +108,9 @@ func getSliceItems(context tfContext) ([]interface{}, error) {
108108
if err != nil {
109109
return nil, err
110110
}
111-
results = append(results, result)
111+
if result != nil {
112+
results = append(results, result)
113+
}
112114
case []interface{}:
113115
for _, jsonResultI := range jsonResults {
114116
jsonResultI, ok := jsonResultI.(map[string]interface{})
@@ -119,7 +121,9 @@ func getSliceItems(context tfContext) ([]interface{}, error) {
119121
if err != nil {
120122
return nil, err
121123
}
122-
results = append(results, result)
124+
if result != nil {
125+
results = append(results, result)
126+
}
123127
}
124128
default:
125129
return nil, errors.Errorf("Not an map or an array of maps: %T", jsonResultsI)
@@ -146,7 +150,9 @@ func getItem(context tfContext, itemMappingProperties *ResourceMapping, jsonResu
146150
if err != nil {
147151
return nil, err
148152
}
149-
result[key] = property
153+
if property != nil {
154+
result[key] = property
155+
}
150156
}
151157
return result, nil
152158
}
@@ -198,7 +204,7 @@ func getValue(key string, context *tfContext) (*valueWithUnit, error) {
198204
for _, propertyMapping := range propertiesMappings {
199205
paths, err := readPaths(propertyMapping.Paths)
200206
if err != nil {
201-
return nil, err
207+
return nil, errors.Wrapf(err, "Cannot get paths for %v", context.ResourceAddress)
202208
}
203209
unit := propertyMapping.Unit
204210

@@ -208,25 +214,27 @@ func getValue(key string, context *tfContext) (*valueWithUnit, error) {
208214
}
209215
path := pathRaw
210216
if strings.Contains(pathRaw, "${") {
217+
fmt.Println("pathRaw: ", pathRaw)
211218
path, err = resolvePlaceholders(path, context)
212219
if err != nil {
213-
return nil, err
220+
return nil, errors.Wrapf(err, "Cannot resolve placeholders for %v", path)
214221
}
222+
fmt.Println("path: ", path)
215223
}
216224
valueFounds, err := utils.GetJSON(path, context.Resource)
217225
if err != nil {
218-
return nil, err
226+
return nil, errors.Wrapf(err, "Cannot get value for %v", path)
219227
}
220228
if len(valueFounds) == 0 && TfPlan != nil {
221229
// Try to resolve it against the whole plan
222230
valueFounds, err = utils.GetJSON(path, *TfPlan)
223231
if err != nil {
224-
return nil, err
232+
return nil, errors.Wrapf(err, "Cannot get value in the whole plan for %v", path)
225233
}
226234
}
227235
if len(valueFounds) > 0 {
228236
if len(valueFounds) > 1 {
229-
return nil, fmt.Errorf("Found more than one value for property %v of resource type %v", key, context.ResourceAddress)
237+
return nil, errors.Errorf("Found more than one value for property %v of resource type %v", key, context.ResourceAddress)
230238
}
231239
if valueFounds[0] == nil {
232240
continue
@@ -240,14 +248,14 @@ func getValue(key string, context *tfContext) (*valueWithUnit, error) {
240248
if ok {
241249
valueFound, err = applyRegex(valueFoundStr, &propertyMapping, context)
242250
if err != nil {
243-
return nil, err
251+
return nil, errors.Wrapf(err, "Cannot apply regex for %v", valueFoundStr)
244252
}
245253
}
246254
valueFoundStr, ok = valueFound.(string)
247255
if ok {
248256
valueFound, err = applyReference(valueFoundStr, &propertyMapping, context)
249257
if err != nil {
250-
return nil, err
258+
return nil, errors.Wrapf(err, "Cannot apply reference for %v", valueFoundStr)
251259
}
252260
}
253261
}
@@ -257,7 +265,7 @@ func getValue(key string, context *tfContext) (*valueWithUnit, error) {
257265
if ok {
258266
valueFound, err = getValueOfExpression(valueFoundMap, context)
259267
if err != nil {
260-
return nil, err
268+
return nil, errors.Wrapf(err, "Cannot get value of expression for %v", valueFoundMap)
261269
}
262270
}
263271

@@ -272,7 +280,7 @@ func getValue(key string, context *tfContext) (*valueWithUnit, error) {
272280
if valueFound == nil {
273281
defaultValue, err := getDefaultValue(key, context)
274282
if err != nil {
275-
return nil, err
283+
return nil, errors.Wrapf(err, "Cannot get default value for %v", key)
276284
}
277285

278286
if defaultValue != nil {
@@ -303,7 +311,13 @@ func resolvePlaceholders(input string, context *tfContext) (string, error) {
303311
if err != nil {
304312
return input, err
305313
}
306-
resolvedExpressions[placeholder] = resolved
314+
if resolved == nil {
315+
// It's ok to not find a value for a placeholder
316+
resolvedExpressions[placeholder] = ".not_found"
317+
} else {
318+
resolvedExpressions[placeholder] = *resolved
319+
}
320+
307321
}
308322

309323
// Replace placeholders in the input string with resolved expressions
@@ -317,32 +331,37 @@ func resolvePlaceholders(input string, context *tfContext) (string, error) {
317331
return resolvedString, nil
318332
}
319333

320-
func resolvePlaceholder(expression string, context *tfContext) (string, error) {
321-
result := ""
334+
func resolvePlaceholder(expression string, context *tfContext) (*string, error) {
322335
if strings.HasPrefix(expression, "this.") {
323336
thisProperty := strings.TrimPrefix(expression, "this")
324337
resource := context.Resource
325338
value, err := utils.GetJSON(thisProperty, resource)
326339
if err != nil {
327-
return "", errors.Wrapf(err, "Cannot get value for variable %s", expression)
340+
return nil, errors.Wrapf(err, "Cannot get value for variable %s", expression)
328341
}
329-
if value == nil {
330-
return "", errors.Errorf("No value found for variable %s", expression)
342+
if len(value) == 0 {
343+
return nil, nil
344+
}
345+
if value[0] == nil {
346+
return nil, nil
331347
}
332-
return fmt.Sprintf("%v", value[0]), err
348+
valueStr := fmt.Sprintf("%v", value[0])
349+
return &valueStr, err
333350
} else if strings.HasPrefix(expression, "config.") {
334351
configProperty := strings.TrimPrefix(expression, "config.")
335352
value := viper.GetFloat64(configProperty)
336-
return fmt.Sprintf("%v", value), nil
353+
valueStr := fmt.Sprintf("%v", value)
354+
return &valueStr, nil
337355
}
338356
variable, err := getVariable(expression, context)
339357
if err != nil {
340-
return "", err
358+
return nil, err
341359
}
342360
if variable != nil {
343-
result = fmt.Sprintf("%v", variable)
361+
valueStr := fmt.Sprintf("%v", variable)
362+
return &valueStr, nil
344363
}
345-
return result, nil
364+
return nil, nil
346365
}
347366

348367
func getDefaultValue(key string, context *tfContext) (*valueWithUnit, error) {
@@ -400,10 +419,10 @@ func getVariable(name string, context *tfContext) (interface{}, error) {
400419
}
401420
value, err := getValue(name, &variableContext)
402421
if err != nil {
403-
return nil, err
422+
return nil, errors.Wrapf(err, "Cannot get variable %v", name)
404423
}
405424
if value == nil {
406-
return nil, fmt.Errorf("Cannot get variable : %v", name)
425+
return nil, nil
407426
}
408427
return value.Value, nil
409428

internal/plan/mapping.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
var globalMappings *Mappings
1414

1515
// GetMapping returns the mapping of the terraform resources
16-
func getMapping() (*Mappings, error) {
16+
func GetMapping() (*Mappings, error) {
1717
if globalMappings != nil {
1818
return globalMappings, nil
1919
}

internal/plan/mappings/aws/general.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ general:
1313
json_data:
1414
aws_instances : "aws_instances.json"
1515
ignored_resources:
16-
- "aws_vpc"
16+
- "aws_vpc"
17+
- "aws_volume_attachment"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
compute_resource:
2+
aws_db_instance:
3+
paths:
4+
- .planned_values.root_module.resources[] | select(.type == "aws_db_instance")
5+
type: resource
6+
variables:
7+
properties:
8+
replicate_source_db:
9+
- paths:
10+
- '.configuration.root_module.resources[] | select(.address == "${this.address}") | .expressions.replicate_source_db?.references[]? | select(endswith("id")) | gsub("\\.id$"; "")'
11+
reference:
12+
paths:
13+
- .planned_values.root_module.resources[] | select(.address == "${key}")
14+
- .planned_values.root_module.child_modules[] | select(.address == ("${key}" | split(".")[0:2] | join("."))) | .resources[] | select(.name == ("${key}" | split(".")[2]))
15+
- .prior_state.values.root_module.resources[] | select(.address == "${key}")
16+
return_path: true
17+
properties:
18+
name:
19+
- paths: ".name"
20+
address:
21+
- paths: ".address"
22+
type:
23+
- paths: ".type"
24+
zone:
25+
- paths: ".values.availability_zone"
26+
region:
27+
- paths: ".values.availability_zone"
28+
regex:
29+
pattern: '^(.+-\d+)[a-z]+'
30+
group: 1
31+
- paths: ".configuration.provider_config.aws.expressions.region"
32+
replication_factor:
33+
- paths: '.values| if .multi_az then 2 else 1 end'
34+
vCPUs:
35+
- paths: ".values.instance_class"
36+
regex:
37+
pattern: '^db\.(.+)'
38+
group: 1
39+
reference:
40+
json_file: aws_instances
41+
property: "VCPU"
42+
memory:
43+
- paths: ".values.instance_class"
44+
unit: mb
45+
regex:
46+
pattern: '^db\.(.+)'
47+
group: 1
48+
reference:
49+
json_file: aws_instances
50+
property: "MemoryMb"
51+
storage:
52+
- type: list
53+
item:
54+
- paths:
55+
- '.values | select(.allocated_storage)'
56+
- '${replicate_source_db}.values | select(.allocated_storage)'
57+
properties:
58+
size:
59+
- paths: ".allocated_storage"
60+
unit: gb
61+
type:
62+
- paths: ".storage_type"
63+
default: gp2
64+
reference:
65+
general: disk_types
66+
- paths: '.prior_state.values.root_module.resources[] | select(.values.db_snapshot_identifier == "${this.values.snapshot_identifier}")'
67+
properties:
68+
size:
69+
- paths: ".values.allocated_storage"
70+
unit: gb
71+
type:
72+
- paths: "values.storage_type"
73+
default: gp2
74+
reference:
75+
general: disk_types

internal/plan/resources.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func GetResources(tfplan *map[string]interface{}) (map[string]resources.Resource
3434
resourcesMap := map[string]resources.Resource{}
3535

3636
// Get compute resources
37-
mapping, err := getMapping()
37+
mapping, err := GetMapping()
3838
if err != nil {
3939
errW := errors.Wrap(err, "Cannot get mapping")
4040
return nil, errW
@@ -118,7 +118,7 @@ func getResourcesOfType(resourceType string, mapping *ResourceMapping) ([]resour
118118
}
119119
log.Debugf(" Found %d resources of type '%s'", len(resourcesFound), resourceType)
120120
for _, resourceI := range resourcesFound {
121-
resourcesResultGot, err := getComputeResource(resourceI, mapping, resourcesResult)
121+
resourcesResultGot, err := GetComputeResource(resourceI, mapping, resourcesResult)
122122
if err != nil {
123123
errW := errors.Wrapf(err, "Cannot get compute resource for path %v", path)
124124
return nil, errW
@@ -133,7 +133,7 @@ func getResourcesOfType(resourceType string, mapping *ResourceMapping) ([]resour
133133

134134
}
135135

136-
func getComputeResource(resourceI interface{}, resourceMapping *ResourceMapping, resourcesResult []resources.Resource) ([]resources.Resource, error) {
136+
func GetComputeResource(resourceI interface{}, resourceMapping *ResourceMapping, resourcesResult []resources.Resource) ([]resources.Resource, error) {
137137
resource := resourceI.(map[string]interface{})
138138
resourceAddress := resource["address"].(string)
139139
providerName, ok := resource["provider_name"].(string)
@@ -293,6 +293,9 @@ func getComputeResource(resourceI interface{}, resourceMapping *ResourceMapping,
293293
}
294294

295295
for i, storageI := range storages {
296+
if storageI == nil {
297+
continue
298+
}
296299
storage, err := getStorage(storageI.(map[string]interface{}))
297300
if err != nil {
298301
return nil, errors.Wrapf(err, "Cannot get storage[%v] for %v", i, resourceAddress)
@@ -331,7 +334,19 @@ func getGPU(gpu map[string]interface{}) ([]string, error) {
331334
}
332335

333336
func getStorage(storageMap map[string]interface{}) (*storage, error) {
334-
storageSize := storageMap["size"].(*valueWithUnit)
337+
storageSize, ok := storageMap["size"].(*valueWithUnit)
338+
if !ok {
339+
// It can happen there is no size but type as been set by default. In this case, we ignore the storage
340+
// Can be fixed in the mapping by selecting only if the size property is set (example '.values | select(.allocated_storage)')
341+
log.Warnf("Cannot find storage size in storageMap '%v': %T", storageMap, storageMap)
342+
return nil, nil
343+
}
344+
if storageSize == nil {
345+
return nil, errors.Errorf("Storage size is nil '%v': %T", storageSize, storageSize)
346+
}
347+
if storageSize.Value == nil {
348+
return nil, errors.Errorf("Storage size value is nil '%v': %T", storageSize, storageSize)
349+
}
335350
storageSizeGb, err := decimal.NewFromString(fmt.Sprintf("%v", storageSize.Value))
336351
if err != nil {
337352
log.Fatal(err)

0 commit comments

Comments
 (0)