mirror of
https://github.com/turbot/steampipe.git
synced 2025-12-19 18:12:43 -05:00
330 lines
12 KiB
Go
330 lines
12 KiB
Go
package versionmap
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"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 resolvedependencies 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
|
|
|
|
}
|