Files
steampipe/pkg/steampipeconfig/modconfig/require.go

212 lines
6.7 KiB
Go

package modconfig
import (
"fmt"
"sort"
"github.com/Masterminds/semver/v3"
"github.com/hashicorp/hcl/v2"
"github.com/turbot/steampipe/pkg/error_helpers"
"github.com/turbot/steampipe/pkg/ociinstaller"
"github.com/turbot/steampipe/pkg/steampipeconfig/hclhelpers"
"github.com/turbot/steampipe/pkg/version"
"github.com/turbot/steampipe/sperr"
)
// Require is a struct representing mod dependencies
type Require struct {
Plugins []*PluginVersion `hcl:"plugin,block"`
DeprecatedSteampipeVersionString string `hcl:"steampipe,optional"`
Steampipe *SteampipeRequire `hcl:"steampipe,block"`
Mods []*ModVersionConstraint `hcl:"mod,block"`
DeclRange hcl.Range
// map keyed by name [and alias]
modMap map[string]*ModVersionConstraint
}
func NewRequire() *Require {
return &Require{
modMap: make(map[string]*ModVersionConstraint),
}
}
func (r *Require) initialise(modBlock *hcl.Block) hcl.Diagnostics {
// This will actually be called twice - once when we load the mod definition,
// and again when we load the mod resources (and set the mod metadata, references etc)
// If we have already initialised, return (we can tell by checking the DeclRange)
if !r.DeclRange.Empty() {
return nil
}
var diags hcl.Diagnostics
// handle deprecated properties
moreDiags := r.handleDeprecations()
diags = append(diags, moreDiags...)
// find the require block
requireBlock := hclhelpers.FindFirstChildBlock(modBlock, BlockTypeRequire)
if requireBlock == nil {
// if none was specified, fall back to parent block
requireBlock = modBlock
}
// build maps of plugin and mod blocks
pluginBlockMap := hclhelpers.BlocksToMap(hclhelpers.FindChildBlocks(requireBlock, BlockTypePlugin))
modBlockMap := hclhelpers.BlocksToMap(hclhelpers.FindChildBlocks(requireBlock, BlockTypeMod))
// set our DecRange
r.DeclRange = requireBlock.DefRange
r.modMap = make(map[string]*ModVersionConstraint)
if r.Steampipe != nil {
moreDiags := r.Steampipe.initialise(requireBlock)
diags = append(diags, moreDiags...)
}
for _, p := range r.Plugins {
moreDiags := p.Initialise(pluginBlockMap[p.RawName])
diags = append(diags, moreDiags...)
}
for _, m := range r.Mods {
moreDiags := m.Initialise(modBlockMap[m.Name])
diags = append(diags, moreDiags...)
if !diags.HasErrors() {
// key map entry by name [and alias]
r.modMap[m.Name] = m
}
}
return diags
}
func (r *Require) handleDeprecations() hcl.Diagnostics {
var diags hcl.Diagnostics
// the 'steampipe' property is deprecated and replace with a steampipe block
if r.DeprecatedSteampipeVersionString != "" {
// if there is both a steampipe block and property, fail
if r.Steampipe != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Both 'steampipe' block and deprecated 'steampipe' property are set",
Subject: &r.DeclRange,
})
} else {
r.Steampipe = &SteampipeRequire{MinVersionString: r.DeprecatedSteampipeVersionString}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Property 'steampipe' is deprecated for mod require block - use a steampipe block instead",
Subject: &r.DeclRange,
},
)
}
}
return diags
}
func (r *Require) ValidateSteampipeVersion(modName string) error {
if steampipeVersionConstraint := r.SteampipeVersionConstraint(); steampipeVersionConstraint != nil {
if !steampipeVersionConstraint.Check(version.SteampipeVersion) {
return fmt.Errorf("steampipe version %s does not satisfy %s which requires version %s", version.SteampipeVersion.String(), modName, r.Steampipe.MinVersionString)
}
}
return nil
}
// ValidatePluginVersions validates that for every plugin requirement there's at least one plugin installed
func (r *Require) ValidatePluginVersions(modName string, plugins map[string]*semver.Version) error {
if len(r.Plugins) == 0 {
return nil
}
errors := []error{}
for _, requiredPlugin := range r.Plugins {
if err := r.searchInstalledPluginForRequirement(modName, requiredPlugin, plugins); err != nil {
errors = append(errors, err)
}
}
return error_helpers.CombineErrors(errors...)
}
func (r *Require) searchInstalledPluginForRequirement(modName string, requirement *PluginVersion, plugins map[string]*semver.Version) error {
for installedName, installed := range plugins {
org, name, _ := ociinstaller.NewSteampipeImageRef(installedName).GetOrgNameAndStream()
if org != requirement.Org || name != requirement.Name {
// no point check - different plugin
continue
}
if requirement.Constraint.Check(installed) {
// constraint is satisfied
return nil
}
}
return sperr.New("could not find plugin which satisfies requirement '%s@%s' - required by '%s'", requirement.RawName, requirement.MinVersionString, modName)
}
// AddModDependencies adds all the mod in newModVersions to our list of mods, using the following logic
// - if a mod with same name, [alias] and constraint exists, it is not added
// - if a mod with same name [and alias] and different constraint exist, it is replaced
func (r *Require) AddModDependencies(newModVersions map[string]*ModVersionConstraint) {
// rebuild the Mods array
// first rebuild the mod map
for name, newVersion := range newModVersions {
r.modMap[name] = newVersion
}
// now update the mod array from the map
var newMods = make([]*ModVersionConstraint, len(r.modMap))
idx := 0
for _, requiredVersion := range r.modMap {
newMods[idx] = requiredVersion
idx++
}
// sort by name
sort.Sort(ModVersionConstraintCollection(newMods))
// write back
r.Mods = newMods
}
func (r *Require) RemoveModDependencies(versions map[string]*ModVersionConstraint) {
// first rebuild the mod map
for name := range versions {
delete(r.modMap, name)
}
// now update the mod array from the map
var newMods = make([]*ModVersionConstraint, len(r.modMap))
idx := 0
for _, requiredVersion := range r.modMap {
newMods[idx] = requiredVersion
idx++
}
// sort by name
sort.Sort(ModVersionConstraintCollection(newMods))
// write back
r.Mods = newMods
}
func (r *Require) RemoveAllModDependencies() {
r.Mods = nil
}
func (r *Require) GetModDependency(name string /*,alias string*/) *ModVersionConstraint {
return r.modMap[name]
}
func (r *Require) ContainsMod(requiredModVersion *ModVersionConstraint) bool {
if c := r.GetModDependency(requiredModVersion.Name); c != nil {
return c.Equals(requiredModVersion)
}
return false
}
func (r *Require) Empty() bool {
return r.SteampipeVersionConstraint() == nil && len(r.Mods) == 0 && len(r.Plugins) == 0
}
func (r *Require) SteampipeVersionConstraint() *semver.Constraints {
if r.Steampipe == nil {
return nil
}
return r.Steampipe.Constraint
}