github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/parser.go (about)

     1  package parse
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/turbot/steampipe/pkg/filepaths"
     6  	"io"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"sort"
    11  
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/hashicorp/hcl/v2/hclparse"
    14  	"github.com/hashicorp/hcl/v2/json"
    15  	"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
    16  	"github.com/turbot/steampipe/pkg/constants"
    17  	"github.com/turbot/steampipe/pkg/error_helpers"
    18  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    19  	"sigs.k8s.io/yaml"
    20  )
    21  
    22  // LoadFileData builds a map of filepath to file data
    23  func LoadFileData(paths ...string) (map[string][]byte, hcl.Diagnostics) {
    24  	var diags hcl.Diagnostics
    25  	var fileData = map[string][]byte{}
    26  
    27  	for _, configPath := range paths {
    28  		data, err := os.ReadFile(configPath)
    29  
    30  		if err != nil {
    31  			diags = append(diags, &hcl.Diagnostic{
    32  				Severity: hcl.DiagError,
    33  				Summary:  fmt.Sprintf("failed to read config file %s", configPath),
    34  				Detail:   err.Error()})
    35  			continue
    36  		}
    37  		fileData[configPath] = data
    38  	}
    39  	return fileData, diags
    40  }
    41  
    42  // ParseHclFiles parses hcl file data and returns the hcl body object
    43  func ParseHclFiles(fileData map[string][]byte) (hcl.Body, hcl.Diagnostics) {
    44  	var parsedConfigFiles []*hcl.File
    45  	var diags hcl.Diagnostics
    46  	parser := hclparse.NewParser()
    47  
    48  	// build ordered list of files so that we parse in a repeatable order
    49  	filePaths := buildOrderedFileNameList(fileData)
    50  
    51  	for _, filePath := range filePaths {
    52  		var file *hcl.File
    53  		var moreDiags hcl.Diagnostics
    54  		ext := filepath.Ext(filePath)
    55  		if ext == constants.JsonExtension {
    56  			file, moreDiags = json.ParseFile(filePath)
    57  		} else if constants.IsYamlExtension(ext) {
    58  			file, moreDiags = parseYamlFile(filePath)
    59  		} else {
    60  			data := fileData[filePath]
    61  			file, moreDiags = parser.ParseHCL(data, filePath)
    62  		}
    63  
    64  		if moreDiags.HasErrors() {
    65  			diags = append(diags, moreDiags...)
    66  			continue
    67  		}
    68  		parsedConfigFiles = append(parsedConfigFiles, file)
    69  	}
    70  
    71  	return hcl.MergeFiles(parsedConfigFiles), diags
    72  }
    73  
    74  func buildOrderedFileNameList(fileData map[string][]byte) []string {
    75  	filePaths := make([]string, len(fileData))
    76  	idx := 0
    77  	for filePath := range fileData {
    78  		filePaths[idx] = filePath
    79  		idx++
    80  	}
    81  	sort.Strings(filePaths)
    82  	return filePaths
    83  }
    84  
    85  // ModfileExists returns whether a mod file exists at the specified path and if so returns the filepath
    86  func ModfileExists(modPath string) (string, bool) {
    87  	for _, modFilePath := range filepaths.ModFilePaths(modPath) {
    88  		if _, err := os.Stat(modFilePath); err == nil {
    89  			return modFilePath, true
    90  		}
    91  	}
    92  	return "", false
    93  }
    94  
    95  // parse a yaml file into a hcl.File object
    96  func parseYamlFile(filename string) (*hcl.File, hcl.Diagnostics) {
    97  	f, err := os.Open(filename)
    98  	if err != nil {
    99  		return nil, hcl.Diagnostics{
   100  			{
   101  				Severity: hcl.DiagError,
   102  				Summary:  "Failed to open file",
   103  				Detail:   fmt.Sprintf("The file %q could not be opened.", filename),
   104  			},
   105  		}
   106  	}
   107  	defer f.Close()
   108  
   109  	src, err := io.ReadAll(f)
   110  	if err != nil {
   111  		return nil, hcl.Diagnostics{
   112  			{
   113  				Severity: hcl.DiagError,
   114  				Summary:  "Failed to read file",
   115  				Detail:   fmt.Sprintf("The file %q was opened, but an error occured while reading it.", filename),
   116  			},
   117  		}
   118  	}
   119  	jsonData, err := yaml.YAMLToJSON(src)
   120  	if err != nil {
   121  		return nil, hcl.Diagnostics{
   122  			{
   123  				Severity: hcl.DiagError,
   124  				Summary:  "Failed to read convert YAML to JSON",
   125  				Detail:   fmt.Sprintf("The file %q was opened, but an error occured while converting it to JSON.", filename),
   126  			},
   127  		}
   128  	}
   129  	return json.Parse(jsonData, filename)
   130  }
   131  
   132  func addPseudoResourcesToMod(pseudoResources []modconfig.MappableResource, hclResources map[string]bool, mod *modconfig.Mod) error_helpers.ErrorAndWarnings {
   133  	res := error_helpers.EmptyErrorsAndWarning()
   134  	for _, r := range pseudoResources {
   135  		// is there a hcl resource with the same name as this pseudo resource - it takes precedence
   136  		name := r.GetUnqualifiedName()
   137  		if _, ok := hclResources[name]; ok {
   138  			res.AddWarning(fmt.Sprintf("%s ignored as hcl resources of same name is already defined", r.GetDeclRange().Filename))
   139  			log.Printf("[TRACE] %s ignored as hcl resources of same name is already defined", r.GetDeclRange().Filename)
   140  			continue
   141  		}
   142  		// add pseudo resource to mod
   143  		mod.AddResource(r.(modconfig.HclResource))
   144  		// add to map of existing resources
   145  		hclResources[name] = true
   146  	}
   147  	return res
   148  }
   149  
   150  // get names of all resources defined in hcl which may also be created as pseudo resources
   151  // if we find a mod block, build a shell mod
   152  func loadMappableResourceNames(content *hcl.BodyContent) (map[string]bool, error) {
   153  	hclResources := make(map[string]bool)
   154  
   155  	for _, block := range content.Blocks {
   156  		// if this is a mod, build a shell mod struct (with just the name populated)
   157  		switch block.Type {
   158  		case modconfig.BlockTypeQuery:
   159  			// for any mappable resource, store the resource name
   160  			name := modconfig.BuildModResourceName(block.Type, block.Labels[0])
   161  			hclResources[name] = true
   162  		}
   163  	}
   164  	return hclResources, nil
   165  }
   166  
   167  // ParseModResourceNames parses all source hcl files for the mod path and associated resources,
   168  // and returns the resource names
   169  func ParseModResourceNames(fileData map[string][]byte) (*modconfig.WorkspaceResources, error) {
   170  	var resources = modconfig.NewWorkspaceResources()
   171  	body, diags := ParseHclFiles(fileData)
   172  	if diags.HasErrors() {
   173  		return nil, plugin.DiagsToError("Failed to load all mod source files", diags)
   174  	}
   175  
   176  	content, moreDiags := body.Content(WorkspaceBlockSchema)
   177  	if moreDiags.HasErrors() {
   178  		diags = append(diags, moreDiags...)
   179  		return nil, plugin.DiagsToError("Failed to load mod", diags)
   180  	}
   181  
   182  	for _, block := range content.Blocks {
   183  		// if this is a mod, build a shell mod struct (with just the name populated)
   184  		switch block.Type {
   185  
   186  		case modconfig.BlockTypeQuery:
   187  			// for any mappable resource, store the resource name
   188  			name := modconfig.BuildModResourceName(block.Type, block.Labels[0])
   189  			resources.Query[name] = true
   190  		case modconfig.BlockTypeControl:
   191  			// for any mappable resource, store the resource name
   192  			name := modconfig.BuildModResourceName(block.Type, block.Labels[0])
   193  			resources.Control[name] = true
   194  		case modconfig.BlockTypeBenchmark:
   195  			// for any mappable resource, store the resource name
   196  			name := modconfig.BuildModResourceName(block.Type, block.Labels[0])
   197  			resources.Benchmark[name] = true
   198  		}
   199  	}
   200  	return resources, nil
   201  }