Skip to content

Commit 3a80065

Browse files
Config Generation: Even more references (#1595)
This PR builds upon #1585 and finds refs also from computed attributes An example is the `org-prefs` test added. The org ID is a computed attribute but it is being figured out as a reference
1 parent e1c0dc1 commit 3a80065

File tree

7 files changed

+108
-74
lines changed

7 files changed

+108
-74
lines changed

pkg/generate/cloud.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,17 @@ func generateCloudResources(ctx context.Context, cfg *Config) ([]stack, error) {
7272
return nil, err
7373
}
7474

75-
log.Println("Post-processing for cloud")
76-
if err := stripDefaults(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), nil); err != nil {
75+
postprocessor := &postprocessor{}
76+
if postprocessor.plannedState, err = getPlannedState(ctx, cfg); err != nil {
7777
return nil, err
7878
}
79-
if err := wrapJSONFieldsInFunction(filepath.Join(cfg.OutputDir, "cloud-resources.tf")); err != nil {
79+
if err := postprocessor.stripDefaults(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), nil); err != nil {
8080
return nil, err
8181
}
82-
if err := replaceReferences(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), nil); err != nil {
82+
if err := postprocessor.wrapJSONFieldsInFunction(filepath.Join(cfg.OutputDir, "cloud-resources.tf")); err != nil {
83+
return nil, err
84+
}
85+
if err := postprocessor.replaceReferences(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), nil); err != nil {
8386
return nil, err
8487
}
8588

pkg/generate/generate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ func generateImportBlocks(ctx context.Context, client *common.Client, listerData
138138
}
139139
return
140140
}
141+
sort.Strings(ids)
141142

142143
// Write blocks like these
143144
// import {

pkg/generate/grafana.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,20 @@ func generateGrafanaResources(ctx context.Context, cfg *Config, stack stack, gen
7676
stripDefaultsExtraFields["org_id"] = `"1"` // Remove org_id if it's the default
7777
}
7878

79-
if err := stripDefaults(generatedFilename("resources.tf"), stripDefaultsExtraFields); err != nil {
79+
postprocessor := &postprocessor{}
80+
if postprocessor.plannedState, err = getPlannedState(ctx, cfg); err != nil {
8081
return err
8182
}
82-
if err := abstractDashboards(generatedFilename("resources.tf")); err != nil {
83+
if err := postprocessor.stripDefaults(generatedFilename("resources.tf"), stripDefaultsExtraFields); err != nil {
8384
return err
8485
}
85-
if err := wrapJSONFieldsInFunction(generatedFilename("resources.tf")); err != nil {
86+
if err := postprocessor.abstractDashboards(generatedFilename("resources.tf")); err != nil {
8687
return err
8788
}
88-
if err := replaceReferences(generatedFilename("resources.tf"), []string{
89+
if err := postprocessor.wrapJSONFieldsInFunction(generatedFilename("resources.tf")); err != nil {
90+
return err
91+
}
92+
if err := postprocessor.replaceReferences(generatedFilename("resources.tf"), []string{
8993
"*.org_id=grafana_organization.id",
9094
}); err != nil {
9195
return err

pkg/generate/postprocessing.go

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/hashicorp/hcl/v2"
1414
"github.com/hashicorp/hcl/v2/hclsyntax"
1515
"github.com/hashicorp/hcl/v2/hclwrite"
16+
tfjson "github.com/hashicorp/terraform-json"
1617
"github.com/zclconf/go-cty/cty"
1718
)
1819

@@ -138,9 +139,12 @@ var knownReferences = []string{
138139
"grafana_team_preferences.team_id=grafana_team.id",
139140
}
140141

141-
// TODO: Also find references from the state (computed fields, like ID)
142-
func replaceReferences(fpath string, extraKnownReferences []string) error {
143-
file, err := readHCLFile(fpath)
142+
type postprocessor struct {
143+
plannedState *tfjson.Plan
144+
}
145+
146+
func (p *postprocessor) replaceReferences(fpath string, extraKnownReferences []string) error {
147+
file, err := p.readHCLFile(fpath)
144148
if err != nil {
145149
return err
146150
}
@@ -149,17 +153,23 @@ func replaceReferences(fpath string, extraKnownReferences []string) error {
149153

150154
knownReferences := knownReferences
151155
knownReferences = append(knownReferences, extraKnownReferences...)
152-
// Find all resources. This map will be used to search for references
153-
resourcesBlocks := map[string]*hclwrite.Block{}
156+
157+
plannedResources := p.plannedState.PlannedValues.RootModule.Resources
158+
154159
for _, block := range file.Body().Blocks() {
155-
if block.Type() == "resource" {
156-
resourcesBlocks[block.Labels()[0]+"."+block.Labels()[1]] = block
160+
var blockResource *tfjson.StateResource
161+
for _, plannedResource := range plannedResources {
162+
if plannedResource.Type == block.Labels()[0] && plannedResource.Name == block.Labels()[1] {
163+
blockResource = plannedResource
164+
break
165+
}
166+
}
167+
if blockResource == nil {
168+
return fmt.Errorf("resource %s.%s not found in planned state", block.Labels()[0], block.Labels()[1])
157169
}
158-
}
159170

160-
for _, block := range file.Body().Blocks() {
161-
for attrName, attr := range block.Body().Attributes() {
162-
attrValue := string(attr.Expr().BuildTokens(nil).Bytes())
171+
for attrName := range block.Body().Attributes() {
172+
attrValue := blockResource.AttributeValues[attrName]
163173
attrReplaced := false
164174

165175
// Check the field name. If it has a possible reference, we have to search for it in the resources
@@ -178,20 +188,19 @@ func replaceReferences(fpath string, extraKnownReferences []string) error {
178188
refToResource := strings.Split(refTo, ".")[0]
179189
refToAttr := strings.Split(refTo, ".")[1]
180190

181-
for possibleResourceRefName, possibleResourceRef := range resourcesBlocks {
182-
if strings.HasPrefix(possibleResourceRefName, refToResource+".") {
183-
valueFromRef := ""
184-
if possibleResourceRef.Body().GetAttribute(refToAttr) != nil {
185-
valueFromRef = string(possibleResourceRef.Body().GetAttribute(refToAttr).Expr().BuildTokens(nil).Bytes())
186-
}
187-
// If the value from the first block matches the value from the second block, we have a reference
188-
if attrValue == valueFromRef {
189-
// Replace the value with the reference
190-
block.Body().SetAttributeTraversal(attrName, traversal(possibleResourceRefName, refToAttr))
191-
hasChanges = true
192-
attrReplaced = true
193-
break
194-
}
191+
for _, plannedResource := range plannedResources {
192+
if plannedResource.Type != refToResource {
193+
continue
194+
}
195+
196+
valueFromRef := plannedResource.AttributeValues[refToAttr]
197+
// If the value from the first block matches the value from the second block, we have a reference
198+
if attrValue == valueFromRef {
199+
// Replace the value with the reference
200+
block.Body().SetAttributeTraversal(attrName, traversal(plannedResource.Type, plannedResource.Name, refToAttr))
201+
hasChanges = true
202+
attrReplaced = true
203+
break
195204
}
196205
}
197206
}
@@ -206,15 +215,15 @@ func replaceReferences(fpath string, extraKnownReferences []string) error {
206215
return nil
207216
}
208217

209-
func stripDefaults(fpath string, extraFieldsToRemove map[string]any) error {
210-
file, err := readHCLFile(fpath)
218+
func (p *postprocessor) stripDefaults(fpath string, extraFieldsToRemove map[string]any) error {
219+
file, err := p.readHCLFile(fpath)
211220
if err != nil {
212221
return err
213222
}
214223

215224
hasChanges := false
216225
for _, block := range file.Body().Blocks() {
217-
if s := stripDefaultsFromBlock(block, extraFieldsToRemove); s {
226+
if s := p.stripDefaultsFromBlock(block, extraFieldsToRemove); s {
218227
hasChanges = true
219228
}
220229
}
@@ -225,8 +234,8 @@ func stripDefaults(fpath string, extraFieldsToRemove map[string]any) error {
225234
return nil
226235
}
227236

228-
func wrapJSONFieldsInFunction(fpath string) error {
229-
file, err := readHCLFile(fpath)
237+
func (p *postprocessor) wrapJSONFieldsInFunction(fpath string) error {
238+
file, err := p.readHCLFile(fpath)
230239
if err != nil {
231240
return err
232241
}
@@ -235,7 +244,7 @@ func wrapJSONFieldsInFunction(fpath string) error {
235244
// Find json attributes and use jsonencode
236245
for _, block := range file.Body().Blocks() {
237246
for key, attr := range block.Body().Attributes() {
238-
asMap, err := attributeToMap(attr)
247+
asMap, err := p.attributeToMap(attr)
239248
if err != nil || asMap == nil {
240249
continue
241250
}
@@ -252,11 +261,11 @@ func wrapJSONFieldsInFunction(fpath string) error {
252261
return nil
253262
}
254263

255-
func abstractDashboards(fpath string) error {
264+
func (p *postprocessor) abstractDashboards(fpath string) error {
256265
fDir := filepath.Dir(fpath)
257266
outPath := filepath.Join(fDir, "files")
258267

259-
file, err := readHCLFile(fpath)
268+
file, err := p.readHCLFile(fpath)
260269
if err != nil {
261270
return err
262271
}
@@ -269,7 +278,7 @@ func abstractDashboards(fpath string) error {
269278
continue
270279
}
271280

272-
dashboard, err := attributeToJSON(block.Body().GetAttribute("config_json"))
281+
dashboard, err := p.attributeToJSON(block.Body().GetAttribute("config_json"))
273282
if err != nil {
274283
return err
275284
}
@@ -316,7 +325,7 @@ func abstractDashboards(fpath string) error {
316325
return nil
317326
}
318327

319-
func attributeToMap(attr *hclwrite.Attribute) (map[string]interface{}, error) {
328+
func (p *postprocessor) attributeToMap(attr *hclwrite.Attribute) (map[string]interface{}, error) {
320329
var err error
321330

322331
// Convert jsonencode to raw json
@@ -345,8 +354,8 @@ func attributeToMap(attr *hclwrite.Attribute) (map[string]interface{}, error) {
345354
return dashboardMap, nil
346355
}
347356

348-
func attributeToJSON(attr *hclwrite.Attribute) ([]byte, error) {
349-
jsonMap, err := attributeToMap(attr)
357+
func (p *postprocessor) attributeToJSON(attr *hclwrite.Attribute) ([]byte, error) {
358+
jsonMap, err := p.attributeToMap(attr)
350359
if err != nil || jsonMap == nil {
351360
return nil, err
352361
}
@@ -359,7 +368,7 @@ func attributeToJSON(attr *hclwrite.Attribute) ([]byte, error) {
359368
return jsonMarshalled, nil
360369
}
361370

362-
func readHCLFile(fpath string) (*hclwrite.File, error) {
371+
func (p *postprocessor) readHCLFile(fpath string) (*hclwrite.File, error) {
363372
src, err := os.ReadFile(fpath)
364373
if err != nil {
365374
return nil, err
@@ -373,10 +382,10 @@ func readHCLFile(fpath string) (*hclwrite.File, error) {
373382
return file, nil
374383
}
375384

376-
func stripDefaultsFromBlock(block *hclwrite.Block, extraFieldsToRemove map[string]any) bool {
385+
func (p *postprocessor) stripDefaultsFromBlock(block *hclwrite.Block, extraFieldsToRemove map[string]any) bool {
377386
hasChanges := false
378387
for _, innblock := range block.Body().Blocks() {
379-
if s := stripDefaultsFromBlock(innblock, extraFieldsToRemove); s {
388+
if s := p.stripDefaultsFromBlock(innblock, extraFieldsToRemove); s {
380389
hasChanges = true
381390
}
382391
if len(innblock.Body().Attributes()) == 0 && len(innblock.Body().Blocks()) == 0 {
@@ -405,7 +414,7 @@ func stripDefaultsFromBlock(block *hclwrite.Block, extraFieldsToRemove map[strin
405414
if name == key {
406415
toRemove := false
407416
fieldValue := strings.TrimSpace(string(attribute.Expr().BuildTokens(nil).Bytes()))
408-
fieldValue, err := extractJSONEncode(fieldValue)
417+
fieldValue, err := p.extractJSONEncode(fieldValue)
409418
if err != nil {
410419
continue
411420
}
@@ -425,6 +434,16 @@ func stripDefaultsFromBlock(block *hclwrite.Block, extraFieldsToRemove map[strin
425434
}
426435
return hasChanges
427436
}
437+
func (p *postprocessor) extractJSONEncode(value string) (string, error) {
438+
if !strings.HasPrefix(value, "jsonencode(") {
439+
return "", nil
440+
}
441+
value = strings.TrimPrefix(value, "jsonencode(")
442+
value = strings.TrimSuffix(value, ")")
443+
444+
b, err := json.MarshalIndent(value, "", " ")
445+
return string(b), err
446+
}
428447

429448
// BELOW IS FROM https://github.com/hashicorp/terraform/blob/main/internal/configs/hcl2shim/values.go
430449

@@ -472,14 +491,3 @@ func HCL2ValueFromConfigValue(v interface{}) cty.Value {
472491
panic(fmt.Errorf("can't convert %#v to cty.Value", v))
473492
}
474493
}
475-
476-
func extractJSONEncode(value string) (string, error) {
477-
if !strings.HasPrefix(value, "jsonencode(") {
478-
return "", nil
479-
}
480-
value = strings.TrimPrefix(value, "jsonencode(")
481-
value = strings.TrimSuffix(value, ")")
482-
483-
b, err := json.MarshalIndent(value, "", " ")
484-
return string(b), err
485-
}

pkg/generate/terraform_state.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package generate
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
"path/filepath"
68

9+
"github.com/hashicorp/terraform-exec/tfexec"
710
tfjson "github.com/hashicorp/terraform-json"
811
)
912

@@ -14,3 +17,18 @@ func getState(ctx context.Context, cfg *Config) (*tfjson.State, error) {
1417
}
1518
return state, nil
1619
}
20+
21+
func getPlannedState(ctx context.Context, cfg *Config) (*tfjson.Plan, error) {
22+
tempWorkingDir, err := os.MkdirTemp("", "terraform-generate")
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to create temporary working directory: %w", err)
25+
}
26+
defer os.RemoveAll(tempWorkingDir)
27+
28+
planFile := filepath.Join(tempWorkingDir, "plan.tfplan")
29+
if _, err := cfg.Terraform.Plan(ctx, tfexec.Out(planFile)); err != nil {
30+
return nil, fmt.Errorf("failed to read terraform plan: %w", err)
31+
}
32+
33+
return cfg.Terraform.ShowPlanFile(ctx, planFile)
34+
}

pkg/generate/testdata/generate/alerting-in-org/imports.tf

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,18 @@ import {
1313
id = "2:My Mute Timing"
1414
}
1515

16-
import {
17-
to = grafana_notification_policy._2_policy
18-
id = "2:policy"
19-
}
20-
2116
import {
2217
to = grafana_notification_policy._1_policy
2318
id = "1:policy"
2419
}
2520

2621
import {
27-
to = grafana_organization._2
28-
id = "2"
22+
to = grafana_notification_policy._2_policy
23+
id = "2:policy"
2924
}
3025

3126
import {
32-
to = grafana_organization_preferences._2
27+
to = grafana_organization._2
3328
id = "2"
3429
}
3530

@@ -38,6 +33,11 @@ import {
3833
id = "1"
3934
}
4035

36+
import {
37+
to = grafana_organization_preferences._2
38+
id = "2"
39+
}
40+
4141
import {
4242
to = grafana_rule_group._2_alert-rule-folder_My_Rule_Group
4343
id = "2:alert-rule-folder:My Rule Group"

0 commit comments

Comments
 (0)