github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/mod.go (about) 1 package parse 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "os" 8 "path" 9 10 "github.com/hashicorp/hcl/v2" 11 "github.com/turbot/pipe-fittings/hclhelpers" 12 "github.com/turbot/steampipe-plugin-sdk/v5/plugin" 13 "github.com/turbot/steampipe/pkg/error_helpers" 14 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 func LoadModfile(modPath string) (*modconfig.Mod, error) { 19 modFilePath, exists := ModfileExists(modPath) 20 if !exists { 21 return nil, nil 22 } 23 24 // build an eval context just containing functions 25 evalCtx := &hcl.EvalContext{ 26 Functions: ContextFunctions(modPath), 27 Variables: make(map[string]cty.Value), 28 } 29 30 mod, res := ParseModDefinition(modFilePath, evalCtx) 31 if res.Diags.HasErrors() { 32 return nil, plugin.DiagsToError("Failed to load mod", res.Diags) 33 } 34 35 return mod, nil 36 } 37 38 // ParseModDefinition parses the modfile only 39 // it is expected the calling code will have verified the existence of the modfile by calling ModfileExists 40 // this is called before parsing the workspace to, for example, identify dependency mods 41 func ParseModDefinition(modFilePath string, evalCtx *hcl.EvalContext) (*modconfig.Mod, *DecodeResult) { 42 res := newDecodeResult() 43 44 // if there is no mod at this location, return error 45 if _, err := os.Stat(modFilePath); os.IsNotExist(err) { 46 res.Diags = append(res.Diags, &hcl.Diagnostic{ 47 Severity: hcl.DiagError, 48 Summary: fmt.Sprintf("modfile %s does not exist", modFilePath), 49 }) 50 return nil, res 51 } 52 53 fileData, diags := LoadFileData(modFilePath) 54 res.addDiags(diags) 55 if diags.HasErrors() { 56 return nil, res 57 } 58 59 body, diags := ParseHclFiles(fileData) 60 res.addDiags(diags) 61 if diags.HasErrors() { 62 return nil, res 63 } 64 65 workspaceContent, diags := body.Content(WorkspaceBlockSchema) 66 res.addDiags(diags) 67 if diags.HasErrors() { 68 return nil, res 69 } 70 71 block := hclhelpers.GetFirstBlockOfType(workspaceContent.Blocks, modconfig.BlockTypeMod) 72 if block == nil { 73 res.Diags = append(res.Diags, &hcl.Diagnostic{ 74 Severity: hcl.DiagError, 75 Summary: fmt.Sprintf("failed to parse mod definition file: no mod definition found in %s", modFilePath), 76 }) 77 return nil, res 78 } 79 var defRange = hclhelpers.BlockRange(block) 80 mod := modconfig.NewMod(block.Labels[0], path.Dir(modFilePath), defRange) 81 // set modFilePath 82 mod.SetFilePath(modFilePath) 83 84 mod, res = decodeMod(block, evalCtx, mod) 85 if res.Diags.HasErrors() { 86 return nil, res 87 } 88 89 // NOTE: IGNORE DEPENDENCY ERRORS 90 91 // call decode callback 92 diags = mod.OnDecoded(block, nil) 93 res.addDiags(diags) 94 95 return mod, res 96 } 97 98 // ParseMod parses all source hcl files for the mod path and associated resources, and returns the mod object 99 // NOTE: the mod definition has already been parsed (or a default created) and is in opts.RunCtx.RootMod 100 func ParseMod(ctx context.Context, fileData map[string][]byte, pseudoResources []modconfig.MappableResource, parseCtx *ModParseContext) (*modconfig.Mod, error_helpers.ErrorAndWarnings) { 101 body, diags := ParseHclFiles(fileData) 102 if diags.HasErrors() { 103 return nil, error_helpers.NewErrorsAndWarning(plugin.DiagsToError("Failed to load all mod source files", diags)) 104 } 105 106 content, moreDiags := body.Content(WorkspaceBlockSchema) 107 if moreDiags.HasErrors() { 108 diags = append(diags, moreDiags...) 109 return nil, error_helpers.NewErrorsAndWarning(plugin.DiagsToError("Failed to load mod", diags)) 110 } 111 112 mod := parseCtx.CurrentMod 113 if mod == nil { 114 return nil, error_helpers.NewErrorsAndWarning(fmt.Errorf("ParseMod called with no Current Mod set in ModParseContext")) 115 } 116 // get names of all resources defined in hcl which may also be created as pseudo resources 117 hclResources, err := loadMappableResourceNames(content) 118 if err != nil { 119 return nil, error_helpers.NewErrorsAndWarning(err) 120 } 121 122 // if variables were passed in parsecontext, add to the mod 123 if parseCtx.Variables != nil { 124 for _, v := range parseCtx.Variables.RootVariables { 125 if diags = mod.AddResource(v); diags.HasErrors() { 126 return nil, error_helpers.NewErrorsAndWarning(plugin.DiagsToError("Failed to add resource to mod", diags)) 127 } 128 } 129 } 130 131 // collect warnings as we parse 132 var res = error_helpers.ErrorAndWarnings{} 133 134 // add pseudo resources to the mod 135 errorsAndWarnings := addPseudoResourcesToMod(pseudoResources, hclResources, mod) 136 137 // merge the warnings generated while adding pseudoresources 138 res.Merge(errorsAndWarnings) 139 140 // add the parsed content to the run context 141 parseCtx.SetDecodeContent(content, fileData) 142 143 // add the mod to the run context 144 // - this it to ensure all pseudo resources get added and build the eval context with the variables we just added 145 // - it also adds the top level resources of the any dependency mods 146 if diags = parseCtx.AddModResources(mod); diags.HasErrors() { 147 return nil, error_helpers.NewErrorsAndWarning(plugin.DiagsToError("Failed to add mod to run context", diags)) 148 } 149 150 // we may need to decode more than once as we gather dependencies as we go 151 // continue decoding as long as the number of unresolved blocks decreases 152 prevUnresolvedBlocks := 0 153 for attempts := 0; ; attempts++ { 154 diags = decode(parseCtx) 155 if diags.HasErrors() { 156 return nil, error_helpers.NewErrorsAndWarning(plugin.DiagsToError("Failed to decode all mod hcl files", diags)) 157 } 158 // now retrieve the warning strings 159 res.AddWarning(plugin.DiagsToWarnings(diags)...) 160 161 // if there are no unresolved blocks, we are done 162 unresolvedBlocks := len(parseCtx.UnresolvedBlocks) 163 if unresolvedBlocks == 0 { 164 log.Printf("[TRACE] parse complete after %d decode passes", attempts+1) 165 break 166 } 167 // if the number of unresolved blocks has NOT reduced, fail 168 if prevUnresolvedBlocks != 0 && unresolvedBlocks >= prevUnresolvedBlocks { 169 str := parseCtx.FormatDependencies() 170 return nil, error_helpers.NewErrorsAndWarning(fmt.Errorf("failed to resolve dependencies for mod '%s' after %d attempts\nDependencies:\n%s", mod.FullName, attempts+1, str)) 171 } 172 // update prevUnresolvedBlocks 173 prevUnresolvedBlocks = unresolvedBlocks 174 } 175 176 // now tell mod to build tree of resources 177 res.Error = mod.BuildResourceTree(parseCtx.GetTopLevelDependencyMods()) 178 179 return mod, res 180 }