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 }