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  }