hcl/parse.go (371 lines of code) (raw):

package hcl import ( "fmt" "os" "regexp" "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclparse" "github.com/sirupsen/logrus" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/function/stdlib" ) var ResourceBlockSchema = hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ { Type: "resource", LabelNames: []string{"type", "name"}, }, }, } var VarBlockSchema = hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ { Type: "variable", LabelNames: []string{"name"}, }, }, } var ProviderBlockSchema = hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ { Type: "provider", LabelNames: []string{"type"}, }, }, } var evalContext = &hcl.EvalContext{ Functions: map[string]function.Function{ "abs": stdlib.AbsoluteFunc, "coalesce": stdlib.CoalesceFunc, "concat": stdlib.ConcatFunc, "hasindex": stdlib.HasIndexFunc, "int": stdlib.IntFunc, "jsondecode": stdlib.JSONDecodeFunc, "jsonencode": stdlib.JSONEncodeFunc, "length": stdlib.LengthFunc, "lower": stdlib.LowerFunc, "max": stdlib.MaxFunc, "min": stdlib.MinFunc, "reverse": stdlib.ReverseFunc, "strlen": stdlib.StrlenFunc, "substr": stdlib.SubstrFunc, "upper": stdlib.UpperFunc, }, } // could be azurerm or azapi type AzureProvider struct { Type string Alias string SubscriptionId string TenantId string AuxiliaryTenantIds []string AuxiliaryTenantIdsString string ClientId string ClientCertificate string ClientCertificatePassword string ClientSecret string OidcRequestToken string OidcToken string FileName string LineNumber int } func (p AzureProvider) Name() string { if p.Alias != "" { return fmt.Sprintf("%q.%q", p.Type, p.Alias) } return p.Type } type AzapiResource struct { Name string Type string Body string FileName string LineNumber int } type Variable struct { Name string HasDefault bool FileName string LineNumber int IsSensitive bool } // mockVariables returns a map of variables that are used in the given traversals. // The mocked variables are prefixed with the given prefix and of type string. func mockVariables(traversals []hcl.Traversal) map[string]cty.Value { const variablePrefix = "$" result := make(map[string]cty.Value) for _, traversal := range traversals { mockedVariable := mockVariable(traversal, 0, variablePrefix) result = mergeKeys(result, mockedVariable) } return result } func mergeKeys(left, right map[string]cty.Value) map[string]cty.Value { for key, rightVal := range right { if leftVal, present := left[key]; present { if leftVal.Type().IsObjectType() && rightVal.Type().IsObjectType() { left[key] = cty.ObjectVal(mergeKeys(leftVal.AsValueMap(), rightVal.AsValueMap())) } else { left[key] = rightVal } } else { left[key] = rightVal } } return left } // one hcl.Traversal corresponds to one reference // e.g.,[{{} azapi_resource testdata/test.tf:121,18-32} {{} networkInterface testdata/test.tf:121,32-49} {{} id testdata/test.tf:121,49-52}] func mockVariable(steps hcl.Traversal, index int, placeholder string) map[string]cty.Value { if index >= len(steps) { return map[string]cty.Value{} } step := steps[index] result := map[string]cty.Value{} switch stepValue := step.(type) { case hcl.TraverseRoot: placeholder += stepValue.Name result[stepValue.Name] = cty.ObjectVal(mockVariable(steps, index+1, placeholder)) case hcl.TraverseAttr: placeholder += "." + stepValue.Name if index < len(steps)-1 { result[stepValue.Name] = cty.ObjectVal(mockVariable(steps, index+1, placeholder)) } else { result[stepValue.Name] = cty.StringVal(placeholder) } } return result } // mockExpression evaluates the given expression with variables replaced by their mock values. func mockExpression(expr hcl.Expression) (*cty.Value, []error) { evalContext.Variables = mockVariables(expr.Variables()) v, diags := expr.Value(evalContext) if diags.HasErrors() { return nil, diags.Errs() } return &v, nil } func FindTfFiles(path string) (*[]string, error) { entries, err := os.ReadDir(path) if err != nil { return nil, err } tfFiles := make([]string, 0) for _, entry := range entries { if entry.IsDir() { // We only care about terraform configuration files. continue } name := entry.Name() if strings.HasSuffix(name, ".tf") { tfFiles = append(tfFiles, path+"/"+name) } } return &tfFiles, nil } func ParseHclFile(path string) (*hcl.File, []error) { parser := hclparse.NewParser() f, diags := parser.ParseHCLFile(path) if diags.HasErrors() { return nil, diags.Errs() } return f, nil } func ParseAzapiResource(f hcl.File) (*[]AzapiResource, []error) { content, _, diags := f.Body.PartialContent(&ResourceBlockSchema) if diags.HasErrors() { logrus.Error(diags) } results := make([]AzapiResource, 0) for _, block := range content.Blocks { if block.Type == "resource" && len(block.Labels) > 1 && block.Labels[0] == "azapi_resource" { attrs, diags := block.Body.JustAttributes() if diags.HasErrors() { diags := skipJustAttributesDiags(diags) if diags.HasErrors() { return nil, diags.Errs() } } resourceTypeRaw, ok := attrs["type"] if !ok { return nil, []error{fmt.Errorf("resource type is not specified for azapi_resource.%s", block.Labels[1])} } resourceType, errs := mockExpression(resourceTypeRaw.Expr) if errs != nil { return nil, errs } r := AzapiResource{ Name: block.Labels[1], Type: resourceType.AsString(), FileName: block.DefRange.Filename, LineNumber: block.DefRange.Start.Line, } if p := attrs["body"]; p != nil { body, errs := mockExpression(p.Expr) if errs != nil { return nil, errs } if body.Type() == cty.String { r.Body = body.AsString() } else { logrus.Debugf("jsonencode for azapi_resource %s with dynamic schema body: %+v", r.Name, body) // azapi dynamic schema is used v, err := stdlib.JSONEncode(*body) if err != nil { errs = append(errs, fmt.Errorf("jsonencode for azapi dynamic schema: %+v", err)) return nil, errs } r.Body = v.AsString() } } results = append(results, r) } } return &results, nil } func ParseVariables(f hcl.File) (*map[string]Variable, []error) { content, _, diags := f.Body.PartialContent(&VarBlockSchema) if diags.HasErrors() { logrus.Error(diags) } results := make(map[string]Variable, 0) for _, block := range content.Blocks { attrs, diags := block.Body.JustAttributes() if diags.HasErrors() { diags := skipJustAttributesDiags(diags) if diags.HasErrors() { return nil, diags.Errs() } } var hasDefault bool if p := attrs["default"]; p != nil { hasDefault = true } var isSensitive bool if p := attrs["sensitive"]; p != nil { value, diags := p.Expr.Value(nil) if diags.HasErrors() { return nil, diags.Errs() } isSensitive = value.True() } results[block.Labels[0]] = Variable{ Name: block.Labels[0], FileName: block.DefRange.Filename, LineNumber: block.DefRange.Start.Line, IsSensitive: isSensitive, HasDefault: hasDefault, } } return &results, nil } func ParseAzureProvider(f hcl.File) (*[]AzureProvider, []error) { content, _, diags := f.Body.PartialContent(&ProviderBlockSchema) if diags.HasErrors() { logrus.Error(diags) } results := make([]AzureProvider, 0) for _, block := range content.Blocks { if block.Type == "provider" && len(block.Labels) > 0 && (block.Labels[0] == "azapi" || block.Labels[0] == "azurerm") { attrs, diags := block.Body.JustAttributes() if diags.HasErrors() { diags := skipJustAttributesDiags(diags) if diags.HasErrors() { return nil, diags.Errs() } } p := AzureProvider{ Type: block.Labels[0], FileName: block.DefRange.Filename, LineNumber: block.DefRange.Start.Line, } if raw, ok := attrs["alias"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.Alias = v.AsString() } if raw, ok := attrs["subscription_id"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.SubscriptionId = v.AsString() } if raw, ok := attrs["tenant_id"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.TenantId = v.AsString() } if raw, ok := attrs["auxiliary_tenant_ids"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } // mocked variable is string type, but this field could be list or string if v.Type().IsPrimitiveType() { p.AuxiliaryTenantIdsString = v.AsString() } if v.CanIterateElements() { slice := v.AsValueSlice() for _, v := range slice { p.AuxiliaryTenantIds = append(p.AuxiliaryTenantIds, v.AsString()) } } } if raw, ok := attrs["client_id"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.ClientId = v.AsString() } if raw, ok := attrs["client_certificate"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.ClientCertificate = v.AsString() } if raw, ok := attrs["client_certificate_password"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.ClientCertificatePassword = v.AsString() } if raw, ok := attrs["client_secret"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.ClientSecret = v.AsString() } if raw, ok := attrs["oidc_request_token"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.OidcRequestToken = v.AsString() } if raw, ok := attrs["oidc_token"]; ok { v, errs := mockExpression(raw.Expr) if errs != nil { return nil, errs } p.OidcToken = v.AsString() } results = append(results, p) } } return &results, nil } // skip the diags when blocks are found func skipJustAttributesDiags(diags hcl.Diagnostics) hcl.Diagnostics { if diags == nil { return nil } const BlockNotAllowed = "Blocks are not allowed here" result := hcl.Diagnostics{} for _, diag := range diags { if !regexp.MustCompile(BlockNotAllowed).MatchString(diag.Error()) { result = result.Append(diag) } } return result }