mirror of
https://github.com/turbot/steampipe.git
synced 2026-02-22 14:00:14 -05:00
396 lines
12 KiB
Go
396 lines
12 KiB
Go
package steampipeconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/turbot/steampipe/pkg/utils"
|
|
|
|
"github.com/Masterminds/semver"
|
|
|
|
filehelpers "github.com/turbot/go-kit/files"
|
|
"github.com/turbot/go-kit/helpers"
|
|
"github.com/turbot/steampipe-plugin-sdk/v4/plugin"
|
|
"github.com/turbot/steampipe/pkg/constants"
|
|
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
|
|
"github.com/turbot/steampipe/pkg/steampipeconfig/parse"
|
|
)
|
|
|
|
// LoadMod parses all hcl files in modPath and returns a single mod
|
|
// if CreatePseudoResources flag is set, construct hcl resources for files with specific extensions
|
|
// NOTE: it is an error if there is more than 1 mod defined, however zero mods is acceptable
|
|
// - a default mod will be created assuming there are any resource files
|
|
func LoadMod(modPath string, runCtx *parse.RunContext) (mod *modconfig.Mod, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = helpers.ToError(r)
|
|
}
|
|
}()
|
|
|
|
mod, err = loadModDefinition(modPath, runCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// load the mod dependencies
|
|
if err := loadModDependencies(mod, runCtx); err != nil {
|
|
return nil, err
|
|
}
|
|
// now we have loaded dependencies, set the current mod on the run context
|
|
runCtx.CurrentMod = mod
|
|
// populate the resource maps of the current mod using the dependency mods
|
|
mod.ResourceMaps = runCtx.GetResourceMaps()
|
|
// now load the mod resource hcl
|
|
return loadModResources(modPath, runCtx, mod)
|
|
}
|
|
|
|
func loadModDefinition(modPath string, runCtx *parse.RunContext) (*modconfig.Mod, error) {
|
|
var mod *modconfig.Mod
|
|
// verify the mod folder exists
|
|
_, err := os.Stat(modPath)
|
|
if os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("mod folder %s does not exist", modPath)
|
|
}
|
|
|
|
if parse.ModfileExists(modPath) {
|
|
// load the mod definition to get the dependencies
|
|
mod, err = parse.ParseModDefinition(modPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// now we have loaded the mod, if this is a dependency mod, add in any variables we have loaded
|
|
if runCtx.ParentRunCtx != nil {
|
|
runCtx.Variables = runCtx.ParentRunCtx.DependencyVariables[mod.ShortName]
|
|
runCtx.SetVariablesForDependencyMod(mod, runCtx.ParentRunCtx.DependencyVariables)
|
|
}
|
|
|
|
} else {
|
|
// so there is no mod file - should we create a default?
|
|
if !runCtx.ShouldCreateDefaultMod() {
|
|
// ShouldCreateDefaultMod flag NOT set - fail
|
|
return nil, fmt.Errorf("mod folder %s does not contain a mod resource definition", modPath)
|
|
}
|
|
// just create a default mod
|
|
mod = modconfig.CreateDefaultMod(modPath)
|
|
|
|
}
|
|
return mod, nil
|
|
}
|
|
|
|
func loadModDependencies(mod *modconfig.Mod, runCtx *parse.RunContext) error {
|
|
var errors []error
|
|
|
|
if mod.Require != nil {
|
|
// now ensure there is a lock file - if we have any mod dependnecies there MUST be a lock file -
|
|
// otherwise 'steampipe install' must be run
|
|
if err := runCtx.EnsureWorkspaceLock(mod); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, requiredModVersion := range mod.Require.Mods {
|
|
// if we have a locked version, update the required version to reflect this
|
|
lockedVersion, err := runCtx.WorkspaceLock.GetLockedModVersionConstraint(requiredModVersion, mod)
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
continue
|
|
}
|
|
if lockedVersion != nil {
|
|
requiredModVersion = lockedVersion
|
|
}
|
|
|
|
// have we already loaded a mod which satisfied this
|
|
if loadedMod, ok := runCtx.LoadedDependencyMods[requiredModVersion.Name]; ok {
|
|
if requiredModVersion.Constraint.Check(loadedMod.Version) {
|
|
continue
|
|
}
|
|
}
|
|
if err := loadModDependency(requiredModVersion, runCtx); err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return utils.CombineErrors(errors...)
|
|
}
|
|
|
|
func loadModDependency(modDependency *modconfig.ModVersionConstraint, runCtx *parse.RunContext) error {
|
|
// dependency mods are installed to <mod path>/<mod nam>@version
|
|
// for example workspace_folder/.steampipe/mods/github.com/turbot/steampipe-mod-aws-compliance@v1.0
|
|
|
|
// we need to list all mod folder in the parent folder: workspace_folder/.steampipe/mods/github.com/turbot/
|
|
// for each folder we parse the mod name and version and determine whether it meets the version constraint
|
|
|
|
// we need to iterate through all mods in the parent folder and find one that satisfies requirements
|
|
parentFolder := filepath.Dir(filepath.Join(runCtx.WorkspaceLock.ModInstallationPath, modDependency.Name))
|
|
|
|
// search the parent folder for a mod installation which satisfied the given mod dependency
|
|
dependencyPath, version, err := findInstalledDependency(modDependency, parentFolder)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// we need to modify the ListOptions to ensure we include hidden files - these are excluded by default
|
|
prevExclusions := runCtx.ListOptions.Exclude
|
|
runCtx.ListOptions.Exclude = nil
|
|
defer func() { runCtx.ListOptions.Exclude = prevExclusions }()
|
|
|
|
// create a child run context
|
|
childRunCtx := parse.NewRunContext(
|
|
runCtx.WorkspaceLock,
|
|
dependencyPath,
|
|
parse.CreatePseudoResources,
|
|
&filehelpers.ListOptions{
|
|
// listFlag specifies whether to load files recursively
|
|
Flags: filehelpers.FilesRecursive,
|
|
// only load .sp files
|
|
Include: filehelpers.InclusionsFromExtensions([]string{constants.ModDataExtension}),
|
|
})
|
|
childRunCtx.BlockTypes = runCtx.BlockTypes
|
|
childRunCtx.ParentRunCtx = runCtx
|
|
|
|
mod, err := LoadMod(dependencyPath, childRunCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// set the version and dependency path of the mod
|
|
mod.Version = version
|
|
mod.ModDependencyPath = modDependency.Name
|
|
|
|
// update loaded dependency mods
|
|
runCtx.LoadedDependencyMods[modDependency.Name] = mod
|
|
if runCtx.ParentRunCtx != nil {
|
|
runCtx.ParentRunCtx.LoadedDependencyMods[modDependency.Name] = mod
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
func loadModResources(modPath string, runCtx *parse.RunContext, mod *modconfig.Mod) (*modconfig.Mod, error) {
|
|
// if flag is set, create pseudo resources by mapping files
|
|
var pseudoResources []modconfig.MappableResource
|
|
var err error
|
|
if runCtx.CreatePseudoResources() {
|
|
// now execute any pseudo-resource creations based on file mappings
|
|
pseudoResources, err = createPseudoResources(modPath, runCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// get the source files
|
|
sourcePaths, err := getSourcePaths(modPath, runCtx.ListOptions)
|
|
if err != nil {
|
|
log.Printf("[WARN] LoadMod: failed to get mod file paths: %v\n", err)
|
|
return nil, err
|
|
}
|
|
|
|
// load the raw file data
|
|
fileData, diags := parse.LoadFileData(sourcePaths...)
|
|
if diags.HasErrors() {
|
|
return nil, plugin.DiagsToError("Failed to load all mod files", diags)
|
|
}
|
|
|
|
// parse all hcl files.
|
|
mod, err = parse.ParseMod(modPath, fileData, pseudoResources, runCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// now add fully populated mod to the parent run context
|
|
if runCtx.ParentRunCtx != nil {
|
|
runCtx.ParentRunCtx.CurrentMod = mod
|
|
runCtx.ParentRunCtx.AddMod(mod)
|
|
}
|
|
|
|
return mod, err
|
|
}
|
|
|
|
// search the parent folder for a mod installatio which satisfied the given mod dependency
|
|
func findInstalledDependency(modDependency *modconfig.ModVersionConstraint, parentFolder string) (string, *semver.Version, error) {
|
|
shortDepName := filepath.Base(modDependency.Name)
|
|
entries, err := os.ReadDir(parentFolder)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("mod satisfying '%s' is not installed", modDependency)
|
|
}
|
|
|
|
// results vars
|
|
var dependencyPath string
|
|
var dependencyVersion *semver.Version
|
|
|
|
for _, entry := range entries {
|
|
split := strings.Split(entry.Name(), "@")
|
|
if len(split) != 2 {
|
|
// invalid format - ignore
|
|
continue
|
|
}
|
|
modName := split[0]
|
|
versionString := strings.TrimPrefix(split[1], "v")
|
|
if modName == shortDepName {
|
|
v, err := semver.NewVersion(versionString)
|
|
if err != nil {
|
|
// invalid format - ignore
|
|
continue
|
|
}
|
|
if modDependency.Constraint.Check(v) {
|
|
// if there is more than 1 mod which satisfied the dependency, fail (for now)
|
|
if dependencyVersion != nil {
|
|
return "", nil, fmt.Errorf("more than one mod found which satisfies dependency %s@%s", modDependency.Name, modDependency.VersionString)
|
|
}
|
|
dependencyPath = filepath.Join(parentFolder, entry.Name())
|
|
dependencyVersion = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// did we find a result?
|
|
if dependencyVersion != nil {
|
|
return dependencyPath, dependencyVersion, nil
|
|
}
|
|
|
|
return "", nil, fmt.Errorf("mod satisfying '%s' is not installed", modDependency)
|
|
}
|
|
|
|
// LoadModResourceNames parses all hcl files in modPath and returns the names of all resources
|
|
func LoadModResourceNames(modPath string, runCtx *parse.RunContext) (resources *modconfig.WorkspaceResources, err error) {
|
|
// TODO support dependencies
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = helpers.ToError(r)
|
|
}
|
|
}()
|
|
|
|
resources = modconfig.NewWorkspaceResources()
|
|
if runCtx == nil {
|
|
runCtx = &parse.RunContext{}
|
|
}
|
|
// verify the mod folder exists
|
|
if _, err := os.Stat(modPath); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("mod folder %s does not exist", modPath)
|
|
}
|
|
|
|
// now execute any pseudo-resource creations based on file mappings
|
|
pseudoResources, err := createPseudoResources(modPath, runCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// add pseudo resources to result
|
|
for _, r := range pseudoResources {
|
|
if strings.HasPrefix(r.Name(), "query.") || strings.HasPrefix(r.Name(), "local.query.") {
|
|
resources.Query[r.Name()] = true
|
|
}
|
|
}
|
|
|
|
sourcePaths, err := getSourcePaths(modPath, runCtx.ListOptions)
|
|
if err != nil {
|
|
log.Printf("[WARN] LoadModResourceNames: failed to get mod file paths: %v\n", err)
|
|
return nil, err
|
|
}
|
|
|
|
fileData, diags := parse.LoadFileData(sourcePaths...)
|
|
if diags.HasErrors() {
|
|
return nil, plugin.DiagsToError("Failed to load all mod files", diags)
|
|
}
|
|
|
|
parsedResourceNames, err := parse.ParseModResourceNames(fileData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resources.Merge(parsedResourceNames), nil
|
|
}
|
|
|
|
// GetModFileExtensions :: return list of all file extensions we care about
|
|
// this will be the mod data extension, plus any registered extensions registered in fileToResourceMap
|
|
func GetModFileExtensions() []string {
|
|
return append(modconfig.RegisteredFileExtensions(), constants.ModDataExtension, constants.VariablesExtension)
|
|
}
|
|
|
|
// build list of all filepaths we need to parse/load the mod
|
|
// this will include hcl files (with .sp extension)
|
|
// as well as any other files with extensions that have been registered for pseudo resource creation
|
|
// (see steampipeconfig/modconfig/resource_type_map.go)
|
|
func getSourcePaths(modPath string, listOpts *filehelpers.ListOptions) ([]string, error) {
|
|
sourcePaths, err := filehelpers.ListFiles(modPath, listOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sourcePaths, nil
|
|
}
|
|
|
|
// create pseudo-resources for any files whose extensions are registered
|
|
func createPseudoResources(modPath string, runCtx *parse.RunContext) ([]modconfig.MappableResource, error) {
|
|
// create list options to find pseudo resources
|
|
listOpts := &filehelpers.ListOptions{
|
|
Flags: runCtx.ListOptions.Flags,
|
|
Include: filehelpers.InclusionsFromExtensions(modconfig.RegisteredFileExtensions()),
|
|
Exclude: runCtx.ListOptions.Exclude,
|
|
}
|
|
// list all registered files
|
|
sourcePaths, err := getSourcePaths(modPath, listOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var errors []error
|
|
var res []modconfig.MappableResource
|
|
|
|
// for every source path:
|
|
// - if it is NOT a registered type, skip
|
|
// [- if an existing resource has already referred directly to this file, skip] *not yet*
|
|
for _, path := range sourcePaths {
|
|
factory, ok := modconfig.ResourceTypeMap[filepath.Ext(path)]
|
|
if !ok {
|
|
continue
|
|
}
|
|
resource, fileData, err := factory(modPath, path, runCtx.CurrentMod)
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
continue
|
|
}
|
|
if resource != nil {
|
|
metadata, err := getPseudoResourceMetadata(resource.Name(), path, fileData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resource.SetMetadata(metadata)
|
|
res = append(res, resource)
|
|
}
|
|
}
|
|
|
|
// show errors as trace logging
|
|
if len(errors) > 0 {
|
|
for _, err := range errors {
|
|
log.Printf("[TRACE] failed to convert local file into resource: %v", err)
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func getPseudoResourceMetadata(resourceName string, path string, fileData []byte) (*modconfig.ResourceMetadata, error) {
|
|
sourceDefinition := string(fileData)
|
|
split := strings.Split(sourceDefinition, "\n")
|
|
lineCount := len(split)
|
|
|
|
// convert the name into a short name
|
|
parsedName, err := modconfig.ParseResourceName(resourceName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := &modconfig.ResourceMetadata{
|
|
ResourceName: parsedName.Name,
|
|
FileName: path,
|
|
StartLineNumber: 1,
|
|
EndLineNumber: lineCount,
|
|
IsAutoGenerated: true,
|
|
SourceDefinition: sourceDefinition,
|
|
}
|
|
|
|
return m, nil
|
|
}
|