mirror of
https://github.com/turbot/steampipe.git
synced 2026-05-11 00:02:37 -04:00
412 lines
12 KiB
Go
412 lines
12 KiB
Go
package modconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/hashicorp/hcl/v2/hclwrite"
|
|
filehelpers "github.com/turbot/go-kit/files"
|
|
typehelpers "github.com/turbot/go-kit/types"
|
|
"github.com/turbot/steampipe/pkg/filepaths"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// mod name used if a default mod is created for a workspace which does not define one explicitly
|
|
const defaultModName = "local"
|
|
|
|
// Mod is a struct representing a Mod resource
|
|
type Mod struct {
|
|
ResourceWithMetadataImpl
|
|
ModTreeItemImpl
|
|
|
|
// required to allow partial decoding
|
|
Remain hcl.Body `hcl:",remain" json:"-"`
|
|
|
|
// attributes
|
|
Categories []string `cty:"categories" hcl:"categories,optional" column:"categories,jsonb"`
|
|
Color *string `cty:"color" hcl:"color" column:"color,text"`
|
|
Icon *string `cty:"icon" hcl:"icon" column:"icon,text"`
|
|
|
|
// blocks
|
|
Require *Require `hcl:"require,block"`
|
|
LegacyRequire *Require `hcl:"requires,block"`
|
|
OpenGraph *OpenGraph `hcl:"opengraph,block" column:"open_graph,jsonb"`
|
|
|
|
// Depency attributes - set if this mod is loaded as a dependency
|
|
|
|
// the mod version
|
|
Version *semver.Version
|
|
// DependencyPath is the fully qualified mod name including version,
|
|
// which will by the map key in the workspace lock file
|
|
// NOTE: this is the relative path to th emod location from the depdemncy install dir (.steampipe/mods)
|
|
// e.g. github.com/turbot/steampipe-mod-azure-thrifty@v1.0.0
|
|
// (NOTE: pointer so it is nil in introspection tables if unpopulated)
|
|
DependencyPath *string `column:"dependency_path,text"`
|
|
// DependencyName return the name of the mod as a dependency, i.e. the mod dependency path, _without_ the version
|
|
// e.g. github.com/turbot/steampipe-mod-azure-thrifty
|
|
DependencyName string
|
|
|
|
// ModPath is the installation location of the mod
|
|
ModPath string
|
|
|
|
// convenient aggregation of all resources
|
|
ResourceMaps *ResourceMaps
|
|
|
|
// the filepath of the mod.sp file (will be empty for default mod)
|
|
modFilePath string
|
|
}
|
|
|
|
func NewMod(shortName, modPath string, defRange hcl.Range) *Mod {
|
|
name := fmt.Sprintf("mod.%s", shortName)
|
|
mod := &Mod{
|
|
ModTreeItemImpl: ModTreeItemImpl{
|
|
HclResourceImpl: HclResourceImpl{
|
|
ShortName: shortName,
|
|
FullName: name,
|
|
UnqualifiedName: name,
|
|
DeclRange: defRange,
|
|
blockType: BlockTypeMod,
|
|
},
|
|
},
|
|
ModPath: modPath,
|
|
Require: NewRequire(),
|
|
}
|
|
mod.ResourceMaps = NewModResources(mod)
|
|
|
|
return mod
|
|
}
|
|
|
|
func (m *Mod) Equals(other *Mod) bool {
|
|
res := m.ShortName == other.ShortName &&
|
|
m.FullName == other.FullName &&
|
|
typehelpers.SafeString(m.Color) == typehelpers.SafeString(other.Color) &&
|
|
typehelpers.SafeString(m.Description) == typehelpers.SafeString(other.Description) &&
|
|
typehelpers.SafeString(m.Documentation) == typehelpers.SafeString(other.Documentation) &&
|
|
typehelpers.SafeString(m.Icon) == typehelpers.SafeString(other.Icon) &&
|
|
typehelpers.SafeString(m.Title) == typehelpers.SafeString(other.Title)
|
|
if !res {
|
|
return res
|
|
}
|
|
// categories
|
|
if m.Categories == nil {
|
|
if other.Categories != nil {
|
|
return false
|
|
}
|
|
} else {
|
|
// we have categories
|
|
if other.Categories == nil {
|
|
return false
|
|
}
|
|
|
|
if len(m.Categories) != len(other.Categories) {
|
|
return false
|
|
}
|
|
for i, c := range m.Categories {
|
|
if (other.Categories)[i] != c {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// tags
|
|
if len(m.Tags) != len(other.Tags) {
|
|
return false
|
|
}
|
|
for k, v := range m.Tags {
|
|
if otherVal := other.Tags[k]; v != otherVal {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// now check the child resources
|
|
return m.ResourceMaps.Equals(other.ResourceMaps)
|
|
}
|
|
|
|
// CreateDefaultMod creates a default mod created for a workspace with no mod definition
|
|
func CreateDefaultMod(modPath string) *Mod {
|
|
m := NewMod(defaultModName, modPath, hcl.Range{})
|
|
folderName := filepath.Base(modPath)
|
|
m.Title = &folderName
|
|
return m
|
|
}
|
|
|
|
// IsDefaultMod returns whether this mod is a default mod created for a workspace with no mod definition
|
|
func (m *Mod) IsDefaultMod() bool {
|
|
return m.modFilePath == ""
|
|
}
|
|
|
|
// GetPaths implements ModTreeItem (override base functionality)
|
|
func (m *Mod) GetPaths() []NodePath {
|
|
return []NodePath{{m.Name()}}
|
|
}
|
|
|
|
// SetPaths implements ModTreeItem (override base functionality)
|
|
func (m *Mod) SetPaths() {}
|
|
|
|
// OnDecoded implements HclResource
|
|
func (m *Mod) OnDecoded(block *hcl.Block, _ ResourceMapsProvider) hcl.Diagnostics {
|
|
// handle legacy requires block
|
|
if m.LegacyRequire != nil && !m.LegacyRequire.Empty() {
|
|
// ensure that both 'require' and 'requires' were not set
|
|
for _, b := range block.Body.(*hclsyntax.Body).Blocks {
|
|
if b.Type == BlockTypeRequire {
|
|
return hcl.Diagnostics{&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Both 'require' and legacy 'requires' blocks are defined",
|
|
Subject: &block.DefRange,
|
|
}}
|
|
}
|
|
}
|
|
m.Require = m.LegacyRequire
|
|
}
|
|
|
|
// initialise our Require
|
|
if m.Require == nil {
|
|
return nil
|
|
}
|
|
|
|
return m.Require.initialise(block)
|
|
}
|
|
|
|
// AddReference implements ResourceWithMetadata (overridden from ResourceWithMetadataImpl)
|
|
func (m *Mod) AddReference(ref *ResourceReference) {
|
|
m.ResourceMaps.References[ref.Name()] = ref
|
|
}
|
|
|
|
// GetReferences implements ResourceWithMetadata (overridden from ResourceWithMetadataImpl)
|
|
func (m *Mod) GetReferences() []*ResourceReference {
|
|
var res = make([]*ResourceReference, len(m.ResourceMaps.References))
|
|
// convert from map to array
|
|
idx := 0
|
|
for _, ref := range m.ResourceMaps.References {
|
|
res[idx] = ref
|
|
idx++
|
|
}
|
|
return res
|
|
}
|
|
|
|
// GetResourceMaps implements ResourceMapsProvider
|
|
func (m *Mod) GetResourceMaps() *ResourceMaps {
|
|
return m.ResourceMaps
|
|
}
|
|
|
|
func (m *Mod) GetResource(parsedName *ParsedResourceName) (resource HclResource, found bool) {
|
|
return m.ResourceMaps.GetResource(parsedName)
|
|
}
|
|
|
|
func (m *Mod) AddModDependencies(modVersions map[string]*ModVersionConstraint) {
|
|
m.Require.AddModDependencies(modVersions)
|
|
}
|
|
|
|
func (m *Mod) RemoveModDependencies(modVersions map[string]*ModVersionConstraint) {
|
|
m.Require.RemoveModDependencies(modVersions)
|
|
}
|
|
|
|
func (m *Mod) RemoveAllModDependencies() {
|
|
m.Require.RemoveAllModDependencies()
|
|
}
|
|
|
|
func (m *Mod) Save() error {
|
|
f := hclwrite.NewEmptyFile()
|
|
rootBody := f.Body()
|
|
|
|
modBody := rootBody.AppendNewBlock("mod", []string{m.ShortName}).Body()
|
|
if m.Title != nil {
|
|
modBody.SetAttributeValue("title", cty.StringVal(*m.Title))
|
|
}
|
|
if m.Description != nil {
|
|
modBody.SetAttributeValue("description", cty.StringVal(*m.Description))
|
|
}
|
|
if m.Color != nil {
|
|
modBody.SetAttributeValue("color", cty.StringVal(*m.Color))
|
|
}
|
|
if m.Documentation != nil {
|
|
modBody.SetAttributeValue("documentation", cty.StringVal(*m.Documentation))
|
|
}
|
|
if m.Icon != nil {
|
|
modBody.SetAttributeValue("icon", cty.StringVal(*m.Icon))
|
|
}
|
|
if len(m.Categories) > 0 {
|
|
categoryValues := make([]cty.Value, len(m.Categories))
|
|
for i, c := range m.Categories {
|
|
categoryValues[i] = cty.StringVal(typehelpers.SafeString(c))
|
|
}
|
|
modBody.SetAttributeValue("categories", cty.ListVal(categoryValues))
|
|
}
|
|
|
|
if len(m.Tags) > 0 {
|
|
tagMap := make(map[string]cty.Value, len(m.Tags))
|
|
for k, v := range m.Tags {
|
|
tagMap[k] = cty.StringVal(v)
|
|
}
|
|
modBody.SetAttributeValue("tags", cty.MapVal(tagMap))
|
|
}
|
|
|
|
// opengraph
|
|
if opengraph := m.OpenGraph; opengraph != nil {
|
|
opengraphBody := modBody.AppendNewBlock("opengraph", nil).Body()
|
|
if opengraph.Title != nil {
|
|
opengraphBody.SetAttributeValue("title", cty.StringVal(*opengraph.Title))
|
|
}
|
|
if opengraph.Description != nil {
|
|
opengraphBody.SetAttributeValue("description", cty.StringVal(*opengraph.Description))
|
|
}
|
|
if opengraph.Image != nil {
|
|
opengraphBody.SetAttributeValue("image", cty.StringVal(*opengraph.Image))
|
|
}
|
|
|
|
}
|
|
|
|
// require
|
|
if require := m.Require; require != nil && !require.Empty() {
|
|
requiresBody := modBody.AppendNewBlock("require", nil).Body()
|
|
|
|
if require.Steampipe != nil && require.Steampipe.MinVersionString != "" {
|
|
steampipeRequiresBody := requiresBody.AppendNewBlock("steampipe", nil).Body()
|
|
steampipeRequiresBody.SetAttributeValue("min_version", cty.StringVal(require.Steampipe.MinVersionString))
|
|
}
|
|
if len(require.Plugins) > 0 {
|
|
pluginValues := make([]cty.Value, len(require.Plugins))
|
|
for i, p := range require.Plugins {
|
|
pluginValues[i] = cty.StringVal(typehelpers.SafeString(p))
|
|
}
|
|
requiresBody.SetAttributeValue("plugins", cty.ListVal(pluginValues))
|
|
}
|
|
if len(require.Mods) > 0 {
|
|
for _, m := range require.Mods {
|
|
modBody := requiresBody.AppendNewBlock("mod", []string{m.Name}).Body()
|
|
modBody.SetAttributeValue("version", cty.StringVal(m.VersionString))
|
|
}
|
|
}
|
|
}
|
|
|
|
// load existing mod data and remove the mod definitions from it
|
|
nonModData, err := m.loadNonModDataInModFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
modData := append(f.Bytes(), nonModData...)
|
|
return os.WriteFile(filepaths.ModFilePath(m.ModPath), modData, 0644)
|
|
}
|
|
|
|
func (m *Mod) HasDependentMods() bool {
|
|
return m.Require != nil && len(m.Require.Mods) > 0
|
|
}
|
|
|
|
func (m *Mod) GetModDependency(modName string) *ModVersionConstraint {
|
|
if m.Require == nil {
|
|
return nil
|
|
}
|
|
return m.Require.GetModDependency(modName)
|
|
}
|
|
|
|
func (m *Mod) loadNonModDataInModFile() ([]byte, error) {
|
|
modFilePath := filepaths.ModFilePath(m.ModPath)
|
|
if !filehelpers.FileExists(modFilePath) {
|
|
return nil, nil
|
|
}
|
|
|
|
fileData, err := os.ReadFile(modFilePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fileLines := strings.Split(string(fileData), "\n")
|
|
decl := m.DeclRange
|
|
// just use line positions
|
|
start := decl.Start.Line - 1
|
|
end := decl.End.Line - 1
|
|
|
|
var resLines []string
|
|
for i, line := range fileLines {
|
|
if (i < start || i > end) && line != "" {
|
|
resLines = append(resLines, line)
|
|
}
|
|
}
|
|
return []byte(strings.Join(resLines, "\n")), nil
|
|
}
|
|
|
|
func (m *Mod) WalkResources(resourceFunc func(item HclResource) (bool, error)) error {
|
|
return m.ResourceMaps.WalkResources(resourceFunc)
|
|
}
|
|
|
|
func (m *Mod) SetFilePath(modFilePath string) {
|
|
m.modFilePath = modFilePath
|
|
}
|
|
|
|
// ValidateRequirements validates that the current steampipe CLI and the installed plugins is compatible with the mod
|
|
func (m *Mod) ValidateRequirements(pluginVersionMap map[string]*PluginVersionString) []error {
|
|
validationErrors := []error{}
|
|
if err := m.validateSteampipeVersion(); err != nil {
|
|
validationErrors = append(validationErrors, err)
|
|
}
|
|
pluginErr := m.validatePluginVersions(pluginVersionMap)
|
|
validationErrors = append(validationErrors, pluginErr...)
|
|
return validationErrors
|
|
}
|
|
|
|
func (m *Mod) validateSteampipeVersion() error {
|
|
if m.Require == nil {
|
|
return nil
|
|
}
|
|
return m.Require.validateSteampipeVersion(m.Name())
|
|
}
|
|
|
|
func (m *Mod) validatePluginVersions(availablePlugins map[string]*PluginVersionString) []error {
|
|
if m.Require == nil {
|
|
return nil
|
|
}
|
|
|
|
return m.Require.validatePluginVersions(m.GetInstallCacheKey(), availablePlugins)
|
|
}
|
|
|
|
// CtyValue implements CtyValueProvider
|
|
func (m *Mod) CtyValue() (cty.Value, error) {
|
|
return GetCtyValue(m)
|
|
}
|
|
|
|
// GetInstallCacheKey returns the key used to find this mod in a workspace lock InstallCache
|
|
func (m *Mod) GetInstallCacheKey() string {
|
|
// if the ModDependencyPath is set, this is a dependency mod - use that
|
|
if m.DependencyPath != nil {
|
|
return *m.DependencyPath
|
|
}
|
|
// otherwise use the short name
|
|
return m.ShortName
|
|
}
|
|
|
|
// SetDependencyConfig sets DependencyPath, DependencyName and Version
|
|
func (m *Mod) SetDependencyConfig(dependencyPath string) error {
|
|
// parse the dependency path to get the dependency name and version
|
|
dependencyName, version, err := ParseModDependencyPath(dependencyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.DependencyPath = &dependencyPath
|
|
m.DependencyName = dependencyName
|
|
m.Version = version
|
|
return nil
|
|
}
|
|
|
|
// RequireHasUnresolvedArgs returns whether the mod has any mod requirements which have unresolved args
|
|
// (this could be because the arg refers to a variable, meanin gwe need an additional parse phase
|
|
// to resolve the arg values)
|
|
func (m *Mod) RequireHasUnresolvedArgs() bool {
|
|
if m.Require == nil {
|
|
return false
|
|
}
|
|
for _, m := range m.Require.Mods {
|
|
for _, a := range m.Args {
|
|
if !a.IsKnown() {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|