block.go (229 lines of code) (raw):

package golden import ( "encoding/json" "fmt" "github.com/emirpasic/gods/sets/hashset" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/lonegunmanb/go-defaults" "github.com/zclconf/go-cty/cty" "reflect" "strings" ) type BlockCustomizedRefType interface { CustomizedRefType() string } type BlockType interface { BlockType() string } type Block interface { Id() string Name() string Type() string BlockType() string Address() string HclBlock() *HclBlock EvalContext() *hcl.EvalContext BaseValues() map[string]cty.Value PreConditionCheck(*hcl.EvalContext) ([]PreCondition, error) AddressLength() int CanExecutePrePlan() bool Config() Config getDownstreams() []Block getForEach() *ForEach markExpanded() isReadyForRead() bool markReady() expandable() bool } func BlockToString(f Block) string { if s, ok := f.(fmt.Stringer); ok { return s.String() } marshal, _ := json.Marshal(f) return string(marshal) } var MetaAttributeNames = hashset.New("for_each", "depends_on") var MetaNestedBlockNames = hashset.New("precondition", "dynamic") func Decode(b Block) error { hb := b.HclBlock() if err := verifyDependsOn(b); err != nil { return err } zeroBlock(b) evalContext := b.EvalContext() if customDecode, ok := b.(CustomDecode); ok { return customDecode.Decode(hb, evalContext) } if baseDecode, ok := b.(BaseDecode); ok { err := baseDecode.BaseDecode(hb, evalContext) if err != nil { return err } } expandedHb, err := hb.ExpandDynamicBlocks(evalContext) if err != nil { return err } diag := gohcl.DecodeBody(cleanBodyForDecode(expandedHb.Body), evalContext, b) if diag.HasErrors() { return diag } // we need set defaults again, since gohcl.DecodeBody might erase default value set on those attribute has null values. defaults.SetDefaults(b) return nil } func zeroBlock(b Block) { nb, _ := wrapBlock(b.Config(), b.HclBlock()) v := reflect.ValueOf(b).Elem() vnb := reflect.ValueOf(nb).Elem() for i := 0; i < v.NumField(); i++ { field := v.Field(i) fieldNb := vnb.Field(i) if !field.CanSet() { continue } tag := v.Type().Field(i).Tag if _, ok := tag.Lookup("hcl"); ok || strings.Contains(string(tag), "attribute") { field.Set(fieldNb) } } } func verifyDependsOn(b Block) error { dependsOn, ok := b.HclBlock().Attributes()["depends_on"] if !ok { return nil } exprString := strings.TrimSpace(dependsOn.ExprString()) if !strings.HasPrefix(exprString, "[") && !strings.HasSuffix(exprString, "]") { return fmt.Errorf("`depends_on` must be a list of block address") } elements := strings.Split(strings.TrimSuffix(strings.TrimPrefix(exprString, "["), "]"), ",") for _, element := range elements { element = strings.Trim(element, " \t\r\n") if element == "" { continue } if !b.Config().ValidBlockAddress(element) { return fmt.Errorf("`depends_on` must be a list of block address, invalid address: %s", element) } } return nil } func cleanBodyForDecode(hb *hclsyntax.Body) *hclsyntax.Body { // Create a new hclsyntax.Body newBody := &hclsyntax.Body{ Attributes: make(hclsyntax.Attributes), } // Iterate over the attributes of the original body for attrName, attr := range hb.Attributes { if MetaAttributeNames.Contains(attrName) { continue } newBody.Attributes[attrName] = attr } for _, nb := range hb.Blocks { if MetaNestedBlockNames.Contains(nb.Type) { continue } cloneBody := *nb.Body cloneNb := *nb cloneNb.Body = cleanBodyForDecode(&cloneBody) newBody.Blocks = append(newBody.Blocks, &cloneNb) } return newBody } func SingleValues(blocks []SingleValueBlock) cty.Value { if len(blocks) == 0 { return cty.EmptyObjectVal } res := map[string]cty.Value{} for _, b := range blocks { res[b.Name()] = b.Value() } return cty.ObjectVal(res) } func Values[T Block](blocks []T) cty.Value { if len(blocks) == 0 { return cty.EmptyObjectVal } res := map[string]cty.Value{} valuesMap := map[string]map[string]cty.Value{} for _, b := range blocks { values, exists := valuesMap[b.Type()] if !exists { values = map[string]cty.Value{} valuesMap[b.Type()] = values } blockVal := blockToCtyValue(b) forEach := b.getForEach() if forEach == nil { values[b.Name()] = blockVal } else { m, ok := values[b.Name()] if !ok { m = cty.MapValEmpty(cty.EmptyObject) } nm := m.AsValueMap() if nm == nil { nm = make(map[string]cty.Value) } nm[CtyValueToString(forEach.key)] = blockVal values[b.Name()] = cty.ObjectVal(nm) } valuesMap[b.Type()] = values } for t, m := range valuesMap { res[t] = cty.ObjectVal(m) } return cty.ObjectVal(res) } func blockToCtyValue(b Block) cty.Value { blockValues := map[string]cty.Value{} baseCtyValues := b.BaseValues() ctyValues := Value(b) for k, v := range ctyValues { blockValues[k] = v } for k, v := range baseCtyValues { blockValues[k] = v } blockVal := cty.ObjectVal(blockValues) return blockVal } func concatLabels(labels []string) string { sb := strings.Builder{} for i, l := range labels { if l == "" { continue } sb.WriteString(l) if i != len(labels)-1 { sb.WriteString(".") } } return sb.String() } func blockAddress(b *HclBlock) string { if b == nil { return "" } sb := strings.Builder{} sb.WriteString(b.Type) sb.WriteString(".") sb.WriteString(concatLabels(b.Labels)) if b.ForEach != nil { sb.WriteString(fmt.Sprintf("[%s]", CtyValueToString(b.key))) } return sb.String() } // Not all `local` expression could be evaluated before for_each expansion, so we need to try to evaluate them. func prePlan(b Block) error { l, ok := b.(PrePlanBlock) if !ok { return nil } err := l.ExecuteBeforePlan() if err != nil { return err } b.markReady() return nil }