Files
steampipe/pkg/steampipeconfig/versionmap/workspace_lock.go
kaidaguerre 91436fafba Support resolution of variables for transitive dependencies using parent mod 'args' property
`steampipe mod update` now updates transitive mods
It is now be possible to set values for variables in the current mod using fully qualified variable names. 
Only variables for root mod and top level dependency mods can be set by user
Closes #3533. Closes #3547. Closes #3548. Closes #3549
2023-06-09 16:22:09 +01:00

341 lines
12 KiB
Go

package versionmap
import (
"encoding/json"
"fmt"
"log"
"os"
"path"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
filehelpers "github.com/turbot/go-kit/files"
"github.com/turbot/steampipe/pkg/error_helpers"
"github.com/turbot/steampipe/pkg/filepaths"
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
"github.com/turbot/steampipe/pkg/versionhelpers"
)
const WorkspaceLockStructVersion = 20220411
// WorkspaceLock is a map of ModVersionMaps items keyed by the parent mod whose dependencies are installed
type WorkspaceLock struct {
WorkspacePath string
InstallCache DependencyVersionMap
MissingVersions DependencyVersionMap
ModInstallationPath string
installedMods VersionListMap
}
// EmptyWorkspaceLock creates a new empty workspace lock based,
// sharing workspace path and installedMods with 'existingLock'
func EmptyWorkspaceLock(existingLock *WorkspaceLock) *WorkspaceLock {
return &WorkspaceLock{
WorkspacePath: existingLock.WorkspacePath,
ModInstallationPath: filepaths.WorkspaceModPath(existingLock.WorkspacePath),
InstallCache: make(DependencyVersionMap),
MissingVersions: make(DependencyVersionMap),
installedMods: existingLock.installedMods,
}
}
func LoadWorkspaceLock(workspacePath string) (*WorkspaceLock, error) {
var installCache = make(DependencyVersionMap)
lockPath := filepaths.WorkspaceLockPath(workspacePath)
if filehelpers.FileExists(lockPath) {
fileContent, err := os.ReadFile(lockPath)
if err != nil {
log.Printf("[TRACE] error reading %s: %s\n", lockPath, err.Error())
return nil, err
}
err = json.Unmarshal(fileContent, &installCache)
if err != nil {
log.Printf("[TRACE] failed to unmarshal %s: %s\n", lockPath, err.Error())
return nil, err
}
}
res := &WorkspaceLock{
WorkspacePath: workspacePath,
ModInstallationPath: filepaths.WorkspaceModPath(workspacePath),
InstallCache: installCache,
MissingVersions: make(DependencyVersionMap),
}
if err := res.getInstalledMods(); err != nil {
return nil, err
}
// populate the MissingVersions
// (this removes missing items from the install cache)
res.setMissing()
return res, nil
}
// getInstalledMods returns a map installed mods, and the versions installed for each
func (l *WorkspaceLock) getInstalledMods() error {
// recursively search for all the mod.sp files under the .steampipe/mods folder, then build the mod name from the file path
modFiles, err := filehelpers.ListFiles(l.ModInstallationPath, &filehelpers.ListOptions{
Flags: filehelpers.FilesRecursive,
Include: []string{"**/mod.sp"},
})
if err != nil {
return err
}
// create result map - a list of version for each mod
installedMods := make(VersionListMap, len(modFiles))
// collect errors
var errors []error
for _, modfilePath := range modFiles {
// try to parse the mon name and version form the parent folder of the modfile
modDependencyName, version, err := l.parseModPath(modfilePath)
if err != nil {
// if we fail to parse, just ignore this modfile
// - it's parent is not a valid mod installation folder so it is probably a child folder of a mod
continue
}
// ensure the dependency mod folder is correctly named
// - for old versions of steampipe the folder name would omit the patch number
if err := l.validateAndFixFolderNamingFormat(modDependencyName, version, modfilePath); err != nil {
continue
}
// add this mod version to the map
installedMods.Add(modDependencyName, version)
}
if len(errors) > 0 {
return error_helpers.CombineErrors(errors...)
}
l.installedMods = installedMods
return nil
}
func (l *WorkspaceLock) validateAndFixFolderNamingFormat(modName string, version *semver.Version, modfilePath string) error {
// verify folder name is of correct format (i.e. including patch number)
modDir := filepath.Dir(modfilePath)
parts := strings.Split(modDir, "@")
currentVersionString := parts[1]
desiredVersionString := fmt.Sprintf("v%s", version.String())
if desiredVersionString != currentVersionString {
desiredDir := fmt.Sprintf("%s@%s", parts[0], desiredVersionString)
log.Printf("[TRACE] renaming dependency mod folder %s to %s", modDir, desiredDir)
return os.Rename(modDir, desiredDir)
}
return nil
}
// GetUnreferencedMods returns a map of all installed mods which are not in the lock file
func (l *WorkspaceLock) GetUnreferencedMods() VersionListMap {
var unreferencedVersions = make(VersionListMap)
for name, versions := range l.installedMods {
for _, version := range versions {
if !l.ContainsModVersion(name, version) {
unreferencedVersions.Add(name, version)
}
}
}
return unreferencedVersions
}
// identify mods which are in InstallCache but not installed
// move them from InstallCache into MissingVersions
func (l *WorkspaceLock) setMissing() {
// create a map of full modname to bool to allow simple checking
flatInstalled := l.installedMods.FlatMap()
for parent, deps := range l.InstallCache {
// deps is a map of dep name to resolved contraint list
// flatten and iterate
for name, resolvedConstraint := range deps {
fullName := modconfig.BuildModDependencyPath(name, resolvedConstraint.Version)
if !flatInstalled[fullName] {
// get the mod name from the constraint (fullName includes the version)
name := resolvedConstraint.Name
// remove this item from the install cache and add into missing
l.MissingVersions.Add(name, resolvedConstraint.Alias, resolvedConstraint.Version, resolvedConstraint.Constraint, parent)
l.InstallCache[parent].Remove(name)
}
}
}
}
// extract the mod name and version from the modfile path
func (l *WorkspaceLock) parseModPath(modfilePath string) (modDependencyName string, modVersion *semver.Version, err error) {
modFullName, err := filepath.Rel(l.ModInstallationPath, filepath.Dir(modfilePath))
if err != nil {
return
}
return modconfig.ParseModDependencyPath(modFullName)
}
func (l *WorkspaceLock) Save() error {
if len(l.InstallCache) == 0 {
// ignore error
l.Delete()
return nil
}
content, err := json.MarshalIndent(l.InstallCache, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepaths.WorkspaceLockPath(l.WorkspacePath), content, 0644)
}
// Delete deletes the lock file
func (l *WorkspaceLock) Delete() error {
if filehelpers.FileExists(filepaths.WorkspaceLockPath(l.WorkspacePath)) {
return os.Remove(filepaths.WorkspaceLockPath(l.WorkspacePath))
}
return nil
}
// DeleteMods removes mods from the lock file then, if it is empty, deletes the file
func (l *WorkspaceLock) DeleteMods(mods VersionConstraintMap, parent *modconfig.Mod) {
for modName := range mods {
if parentDependencies := l.InstallCache[parent.GetInstallCacheKey()]; parentDependencies != nil {
parentDependencies.Remove(modName)
}
}
}
// GetMod looks for a lock file entry matching the given mod dependency name
// (e.g.github.com/turbot/steampipe-mod-azure-thrifty
func (l *WorkspaceLock) GetMod(modDependencyName string, parent *modconfig.Mod) *ResolvedVersionConstraint {
parentKey := parent.GetInstallCacheKey()
if parentDependencies := l.InstallCache[parentKey]; parentDependencies != nil {
// look for this mod in the lock file entries for this parent
return parentDependencies[modDependencyName]
}
return nil
}
// GetLockedModVersions builds a ResolvedVersionListMap with the resolved versions
// for each item of the given VersionConstraintMap found in the lock file
func (l *WorkspaceLock) GetLockedModVersions(mods VersionConstraintMap, parent *modconfig.Mod) (ResolvedVersionListMap, error) {
var res = make(ResolvedVersionListMap)
for name, constraint := range mods {
resolvedConstraint, err := l.GetLockedModVersion(constraint, parent)
if err != nil {
return nil, err
}
if resolvedConstraint != nil {
res.Add(name, resolvedConstraint)
}
}
return res, nil
}
// GetLockedModVersion looks for a lock file entry matching the required constraint and returns nil if not found
func (l *WorkspaceLock) GetLockedModVersion(requiredModVersion *modconfig.ModVersionConstraint, parent *modconfig.Mod) (*ResolvedVersionConstraint, error) {
lockedVersion := l.GetMod(requiredModVersion.Name, parent)
if lockedVersion == nil {
return nil, nil
}
// verify the locked version satisfies the version constraint
if !requiredModVersion.Constraint.Check(lockedVersion.Version) {
return nil, nil
}
return lockedVersion, nil
}
// EnsureLockedModVersion looks for a lock file entry matching the required mod name
func (l *WorkspaceLock) EnsureLockedModVersion(requiredModVersion *modconfig.ModVersionConstraint, parent *modconfig.Mod) (*ResolvedVersionConstraint, error) {
lockedVersion := l.GetMod(requiredModVersion.Name, parent)
if lockedVersion == nil {
return nil, nil
}
// verify the locked version satisfies the version constraint
if !requiredModVersion.Constraint.Check(lockedVersion.Version) {
return nil, fmt.Errorf("failed to resolve dependencies for %s - locked version %s does not meet the constraint %s", parent.GetInstallCacheKey(), modconfig.BuildModDependencyPath(requiredModVersion.Name, lockedVersion.Version), requiredModVersion.Constraint.Original)
}
return lockedVersion, nil
}
// GetLockedModVersionConstraint looks for a lock file entry matching the required mod version and if found,
// returns it in the form of a ModVersionConstraint
func (l *WorkspaceLock) GetLockedModVersionConstraint(requiredModVersion *modconfig.ModVersionConstraint, parent *modconfig.Mod) (*modconfig.ModVersionConstraint, error) {
lockedVersion, err := l.EnsureLockedModVersion(requiredModVersion, parent)
if err != nil {
// EnsureLockedModVersion returns an error if the locked version does not satisfy the requirement
return nil, err
}
if lockedVersion == nil {
// EnsureLockedModVersion returns nil if no locked version is found
return nil, nil
}
// create a new ModVersionConstraint using the locked version
lockedVersionFullName := modconfig.BuildModDependencyPath(requiredModVersion.Name, lockedVersion.Version)
return modconfig.NewModVersionConstraint(lockedVersionFullName)
}
// ContainsModVersion returns whether the lockfile contains the given mod version
func (l *WorkspaceLock) ContainsModVersion(modName string, modVersion *semver.Version) bool {
for _, modVersionMap := range l.InstallCache {
for lockName, lockVersion := range modVersionMap {
// TODO consider handling of metadata
if lockName == modName && lockVersion.Version.Equal(modVersion) && lockVersion.Version.Metadata() == modVersion.Metadata() {
return true
}
}
}
return false
}
func (l *WorkspaceLock) ContainsModConstraint(modName string, constraint *versionhelpers.Constraints) bool {
for _, modVersionMap := range l.InstallCache {
for lockName, lockVersion := range modVersionMap {
if lockName == modName && lockVersion.Constraint == constraint.Original {
return true
}
}
}
return false
}
// Incomplete returned whether there are any missing dependencies
// (i.e. they exist in the lock file but ate not installed)
func (l *WorkspaceLock) Incomplete() bool {
return len(l.MissingVersions) > 0
}
// Empty returns whether the install cache is empty
func (l *WorkspaceLock) Empty() bool {
return l == nil || len(l.InstallCache) == 0
}
// StructVersion returns the struct version of the workspace lock
// because only the InstallCache is serialised, read the StructVersion from the first install cache entry
func (l *WorkspaceLock) StructVersion() int {
for _, depVersionMap := range l.InstallCache {
for _, depVersion := range depVersionMap {
return depVersion.StructVersion
}
}
// we have no deps - just return the new struct version
return WorkspaceLockStructVersion
}
func (l *WorkspaceLock) FindInstalledDependency(modDependency *ResolvedVersionConstraint) (string, error) {
dependencyFilepath := path.Join(l.ModInstallationPath, modDependency.DependencyPath())
if filehelpers.DirectoryExists(dependencyFilepath) {
return dependencyFilepath, nil
}
return "", fmt.Errorf("dependency mod '%s' is not installed - run 'steampipe mod install'", modDependency.DependencyPath())
}