// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package protohcl import ( "fmt" "sort" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "google.golang.org/protobuf/reflect/protoreflect" ) // MessageDecoder provides an abstract way to decode protobuf messages from HCL // blocks or objects. type MessageDecoder interface { // EachField calls the given iterator for each field provided in the HCL source. EachField(iter FieldIterator) error // SkipFields returns a MessageDecoder that skips over the given fields. It is // primarily used for doing two-pass decoding of protobuf `Any` fields. SkipFields(fields ...string) MessageDecoder } // IterField represents a field discovered by the MessageDecoder. type IterField struct { // Name is the HCL name of the field. Name string // Desc is the protobuf field descriptor. Desc protoreflect.FieldDescriptor // Val is the field value, only if it was given using HCL attribute syntax. Val *cty.Value // Blocks contains the HCL blocks that were given for this field. Blocks []*hcl.Block // Range determines where in the HCL source the field was given, it is useful // for error messages. Range hcl.Range } // FieldIterator is given to MessageDecoder.EachField to iterate over all of the // fields in a given HCL block or object. type FieldIterator struct { // IgnoreUnknown instructs the MessageDecoder to skip over any fields not // included in Desc. IgnoreUnknown bool // Desc is the protobuf descriptor for the message the caller is decoding into. // It is used to determine which fields are valid. Desc protoreflect.MessageDescriptor // Func is called for each field in the given HCL block or object. Func func(field *IterField) error } func newBodyDecoder( body hcl.Body, namer FieldNamer, functions map[string]function.Function, ) MessageDecoder { return bodyDecoder{ body: body, namer: namer, functions: functions, skipFields: make(map[string]struct{}), } } type bodyDecoder struct { body hcl.Body namer FieldNamer functions map[string]function.Function skipFields map[string]struct{} } func (bd bodyDecoder) EachField(iter FieldIterator) error { schema, err := bd.schema(iter.Desc) if err != nil { return err } var ( content *hcl.BodyContent diags hcl.Diagnostics ) if iter.IgnoreUnknown { content, _, diags = bd.body.PartialContent(schema) } else { content, diags = bd.body.Content(schema) } if diags.HasErrors() { return diags } fields := make([]*IterField, 0) for _, attr := range content.Attributes { if _, ok := bd.skipFields[attr.Name]; ok { continue } desc := bd.namer.GetField(iter.Desc.Fields(), attr.Name) val, err := attr.Expr.Value(&hcl.EvalContext{Functions: bd.functions}) if err != nil { return err } fields = append(fields, &IterField{ Name: attr.Name, Desc: desc, Val: &val, Range: attr.Expr.Range(), }) } for blockType, blocks := range content.Blocks.ByType() { if _, ok := bd.skipFields[blockType]; ok { continue } desc := bd.namer.GetField(iter.Desc.Fields(), blockType) fields = append(fields, &IterField{ Name: blockType, Desc: desc, Blocks: blocks, }) } // Always handle Any fields last, as decoding them may require type information // gathered from other fields (e.g. as in the case of Resource GVKs). sort.Slice(fields, func(a, b int) bool { if isAnyField(fields[b].Desc) && !isAnyField(fields[a].Desc) { return true } return a < b }) for _, field := range fields { if err := iter.Func(field); err != nil { return err } } return nil } func (bd bodyDecoder) SkipFields(fields ...string) MessageDecoder { skip := make(map[string]struct{}, len(fields)+len(bd.skipFields)) for k, v := range bd.skipFields { skip[k] = v } for _, field := range fields { skip[field] = struct{}{} } // Note: we rely on the fact bd isn't a pointer to copy the struct here. bd.skipFields = skip return bd } func (bd bodyDecoder) schema(desc protoreflect.MessageDescriptor) (*hcl.BodySchema, error) { var schema hcl.BodySchema fields := desc.Fields() for i := 0; i < fields.Len(); i++ { f := fields.Get(i) kind := f.Kind() // maps are special and whether they can use block syntax depends on the value type if f.IsMap() { valueDesc := f.MapValue() valueKind := valueDesc.Kind() wktHint := wellKnownTypeSchemaHint(valueDesc) // Maps with values that are Messages can generally be decoded using the block syntax. // The exception are some of the Well-Known-Types that appear as scalar values with // either string or numeric encoding but get parsed into message types. It is still // fine to also decode these from the attribute syntax. if valueKind == protoreflect.MessageKind && wktHint != wellKnownAttribute { schema.Blocks = append(schema.Blocks, hcl.BlockHeaderSchema{ Type: bd.namer.NameField(f), LabelNames: []string{"key"}, }) } // non-message types or Well Known Message types that need attribute encoding // get decoded as attributes schema.Attributes = append(schema.Attributes, hcl.AttributeSchema{ Name: bd.namer.NameField(f), }) continue } wktHint := wellKnownTypeSchemaHint(f) // message types generally will use block syntax unless its a well known // message type that requires attribute syntax specifically. if kind == protoreflect.MessageKind && wktHint != wellKnownAttribute { schema.Blocks = append(schema.Blocks, hcl.BlockHeaderSchema{ Type: bd.namer.NameField(f), }) } // by default use attribute encoding // - primitives // - repeated primitives // - Well Known Types requiring attribute syntax // - repeated Well Known Types requiring attribute syntax schema.Attributes = append(schema.Attributes, hcl.AttributeSchema{ Name: bd.namer.NameField(f), }) continue } // Add skipped fields to the schema so HCL doesn't throw an error when it finds them. for field := range bd.skipFields { schema.Attributes = append(schema.Attributes, hcl.AttributeSchema{Name: field}) schema.Blocks = append(schema.Blocks, hcl.BlockHeaderSchema{Type: field}) } return &schema, nil } func newObjectDecoder(object cty.Value, namer FieldNamer, rng hcl.Range) MessageDecoder { return objectDecoder{ object: object, namer: namer, rng: rng, skipFields: make(map[string]struct{}), } } type objectDecoder struct { object cty.Value namer FieldNamer rng hcl.Range skipFields map[string]struct{} } func (od objectDecoder) EachField(iter FieldIterator) error { for attr := range od.object.Type().AttributeTypes() { if _, ok := od.skipFields[attr]; ok { continue } desc := od.namer.GetField(iter.Desc.Fields(), attr) if desc == nil { if iter.IgnoreUnknown { continue } else { return fmt.Errorf("%s: Unsupported argument; An argument named %q is not expected here.", od.rng, attr) } } val := od.object.GetAttr(attr) if err := iter.Func(&IterField{ Name: attr, Desc: desc, Val: &val, }); err != nil { return err } } return nil } func (od objectDecoder) SkipFields(fields ...string) MessageDecoder { skip := make(map[string]struct{}, len(fields)+len(od.skipFields)) for k, v := range od.skipFields { skip[k] = v } for _, field := range fields { skip[field] = struct{}{} } // Note: we rely on the fact od isn't a pointer to copy the struct here. od.skipFields = skip return od }