// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package hcl2template import ( "fmt" "os" "path/filepath" "strings" "github.com/gobwas/glob" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/packer/hcl2template/repl" hcl2shim "github.com/hashicorp/packer/hcl2template/shim" "github.com/zclconf/go-cty/cty" ) func warningErrorsToDiags(block *hcl.Block, warnings []string, err error) hcl.Diagnostics { var diags hcl.Diagnostics for _, warning := range warnings { diags = append(diags, &hcl.Diagnostic{ Summary: warning, Subject: &block.DefRange, Severity: hcl.DiagWarning, }) } if err != nil { diags = append(diags, &hcl.Diagnostic{ Summary: err.Error(), Subject: &block.DefRange, Severity: hcl.DiagError, }) } return diags } func isDir(name string) (bool, error) { s, err := os.Stat(name) if err != nil { return false, err } return s.IsDir(), nil } // GetHCL2Files returns two slices of json formatted and hcl formatted files, // hclSuffix and jsonSuffix tell which file is what. Filename can be a folder // or a file. // // When filename is a folder all files of folder matching the suffixes will be // returned. Otherwise if filename references a file and filename matches one // of the suffixes it is returned in the according slice. func GetHCL2Files(filename, hclSuffix, jsonSuffix string) (hclFiles, jsonFiles []string, diags hcl.Diagnostics) { if filename == "" { return } isDir, err := isDir(filename) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Detail: err.Error(), }) return nil, nil, diags } if !isDir { if strings.HasSuffix(filename, jsonSuffix) { return nil, []string{filename}, diags } if strings.HasSuffix(filename, hclSuffix) { return []string{filename}, nil, diags } return nil, nil, diags } fileInfos, err := os.ReadDir(filename) if err != nil { diag := &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Cannot read hcl directory", Detail: err.Error(), } diags = append(diags, diag) return nil, nil, diags } for _, fileInfo := range fileInfos { if fileInfo.IsDir() { continue } filename := filepath.Join(filename, fileInfo.Name()) if strings.HasSuffix(filename, hclSuffix) { hclFiles = append(hclFiles, filename) } else if strings.HasSuffix(filename, jsonSuffix) { jsonFiles = append(jsonFiles, filename) } } return hclFiles, jsonFiles, diags } // Convert -only and -except globs to glob.Glob instances. func convertFilterOption(patterns []string, optionName string) ([]glob.Glob, hcl.Diagnostics) { var globs []glob.Glob var diags hcl.Diagnostics for _, pattern := range patterns { g, err := glob.Compile(pattern) if err != nil { diags = append(diags, &hcl.Diagnostic{ Summary: fmt.Sprintf("Invalid -%s pattern %s: %s", optionName, pattern, err), Severity: hcl.DiagError, }) } globs = append(globs, g) } return globs, diags } func PrintableCtyValue(v cty.Value) string { if !v.IsWhollyKnown() { return "" } gval := hcl2shim.ConfigValueFromHCL2(v) str := repl.FormatResult(gval) return str } func ConvertPluginConfigValueToHCLValue(v interface{}) (cty.Value, error) { var buildValue cty.Value switch v := v.(type) { case bool: buildValue = cty.BoolVal(v) case string: buildValue = cty.StringVal(v) case uint8: buildValue = cty.NumberUIntVal(uint64(v)) case float64: buildValue = cty.NumberFloatVal(v) case int64: buildValue = cty.NumberIntVal(v) case uint64: buildValue = cty.NumberUIntVal(v) case []string: vals := make([]cty.Value, len(v)) for i, ev := range v { vals[i] = cty.StringVal(ev) } if len(vals) == 0 { buildValue = cty.ListValEmpty(cty.String) } else { buildValue = cty.ListVal(vals) } case []uint8: vals := make([]cty.Value, len(v)) for i, ev := range v { vals[i] = cty.NumberUIntVal(uint64(ev)) } if len(vals) == 0 { buildValue = cty.ListValEmpty(cty.Number) } else { buildValue = cty.ListVal(vals) } case []int64: vals := make([]cty.Value, len(v)) for i, ev := range v { vals[i] = cty.NumberIntVal(ev) } if len(vals) == 0 { buildValue = cty.ListValEmpty(cty.Number) } else { buildValue = cty.ListVal(vals) } case []uint64: vals := make([]cty.Value, len(v)) for i, ev := range v { vals[i] = cty.NumberUIntVal(ev) } if len(vals) == 0 { buildValue = cty.ListValEmpty(cty.Number) } else { buildValue = cty.ListVal(vals) } default: return cty.Value{}, fmt.Errorf("unhandled buildvar type: %T", v) } return buildValue, nil } // GetVarsByType walks through a hcl body, and gathers all the Traversals that // have a root type matching one of the specified top-level labels. // // This will only work on finite, expanded, HCL bodies. func GetVarsByType(block *hcl.Block, topLevelLabels ...string) []hcl.Traversal { var travs []hcl.Traversal switch body := block.Body.(type) { case *hclsyntax.Body: travs = getVarsByTypeForHCLSyntaxBody(body) default: attrs, _ := body.JustAttributes() for _, attr := range attrs { travs = append(travs, attr.Expr.Variables()...) } } return FilterTraversalsByType(travs, topLevelLabels...) } // FilterTraversalsByType lets the caller filter the traversals per top-level type. // // This can then be used to detect dependencies between block types. func FilterTraversalsByType(travs []hcl.Traversal, topLevelLabels ...string) []hcl.Traversal { var rets []hcl.Traversal for _, t := range travs { varRootname := t.RootName() for _, lbl := range topLevelLabels { if varRootname == lbl { rets = append(rets, t) break } } } return rets } func getVarsByTypeForHCLSyntaxBody(body *hclsyntax.Body) []hcl.Traversal { var rets []hcl.Traversal for _, attr := range body.Attributes { rets = append(rets, attr.Expr.Variables()...) } for _, block := range body.Blocks { rets = append(rets, getVarsByTypeForHCLSyntaxBody(block.Body)...) } return rets }