github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/versionmap/workspace_lock.go (about) 1 package versionmap 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log" 8 "os" 9 "path" 10 "path/filepath" 11 "strings" 12 13 "github.com/Masterminds/semver/v3" 14 filehelpers "github.com/turbot/go-kit/files" 15 "github.com/turbot/steampipe/pkg/error_helpers" 16 "github.com/turbot/steampipe/pkg/filepaths" 17 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 18 "github.com/turbot/steampipe/pkg/versionhelpers" 19 ) 20 21 const WorkspaceLockStructVersion = 20220411 22 23 // WorkspaceLock is a map of ModVersionMaps items keyed by the parent mod whose dependencies are installed 24 type WorkspaceLock struct { 25 WorkspacePath string 26 InstallCache DependencyVersionMap 27 MissingVersions DependencyVersionMap 28 29 ModInstallationPath string 30 installedMods VersionListMap 31 } 32 33 // EmptyWorkspaceLock creates a new empty workspace lock based, 34 // sharing workspace path and installedMods with 'existingLock' 35 func EmptyWorkspaceLock(existingLock *WorkspaceLock) *WorkspaceLock { 36 return &WorkspaceLock{ 37 WorkspacePath: existingLock.WorkspacePath, 38 ModInstallationPath: filepaths.WorkspaceModPath(existingLock.WorkspacePath), 39 InstallCache: make(DependencyVersionMap), 40 MissingVersions: make(DependencyVersionMap), 41 installedMods: existingLock.installedMods, 42 } 43 } 44 45 func LoadWorkspaceLock(ctx context.Context, workspacePath string) (*WorkspaceLock, error) { 46 var installCache = make(DependencyVersionMap) 47 lockPath := filepaths.WorkspaceLockPath(workspacePath) 48 if filehelpers.FileExists(lockPath) { 49 fileContent, err := os.ReadFile(lockPath) 50 if err != nil { 51 log.Printf("[TRACE] error reading %s: %s\n", lockPath, err.Error()) 52 return nil, err 53 } 54 err = json.Unmarshal(fileContent, &installCache) 55 if err != nil { 56 log.Printf("[TRACE] failed to unmarshal %s: %s\n", lockPath, err.Error()) 57 return nil, err 58 } 59 } 60 res := &WorkspaceLock{ 61 WorkspacePath: workspacePath, 62 ModInstallationPath: filepaths.WorkspaceModPath(workspacePath), 63 InstallCache: installCache, 64 MissingVersions: make(DependencyVersionMap), 65 } 66 67 if err := res.getInstalledMods(ctx); err != nil { 68 return nil, err 69 } 70 71 // populate the MissingVersions 72 // (this removes missing items from the install cache) 73 res.setMissing() 74 75 return res, nil 76 } 77 78 // getInstalledMods returns a map installed mods, and the versions installed for each 79 func (l *WorkspaceLock) getInstalledMods(ctx context.Context) error { 80 // recursively search for all the mod.sp files under the .steampipe/mods folder, then build the mod name from the file path 81 modFiles, err := filehelpers.ListFilesWithContext(ctx, l.ModInstallationPath, &filehelpers.ListOptions{ 82 Flags: filehelpers.FilesRecursive, 83 Include: []string{"**/mod.sp"}, 84 }) 85 if err != nil { 86 return err 87 } 88 89 // create result map - a list of version for each mod 90 installedMods := make(VersionListMap, len(modFiles)) 91 // collect errors 92 var errors []error 93 94 for _, modfilePath := range modFiles { 95 if ctx.Err() != nil { 96 return ctx.Err() 97 } 98 // try to parse the mon name and version form the parent folder of the modfile 99 modDependencyName, version, err := l.parseModPath(modfilePath) 100 if err != nil { 101 // if we fail to parse, just ignore this modfile 102 // - it's parent is not a valid mod installation folder so it is probably a child folder of a mod 103 continue 104 } 105 106 // ensure the dependency mod folder is correctly named 107 // - for old versions of steampipe the folder name would omit the patch number 108 if err := l.validateAndFixFolderNamingFormat(modDependencyName, version, modfilePath); err != nil { 109 continue 110 } 111 112 // add this mod version to the map 113 installedMods.Add(modDependencyName, version) 114 } 115 116 if len(errors) > 0 { 117 return error_helpers.CombineErrors(errors...) 118 } 119 l.installedMods = installedMods 120 return nil 121 } 122 123 func (l *WorkspaceLock) validateAndFixFolderNamingFormat(modName string, version *semver.Version, modfilePath string) error { 124 // verify folder name is of correct format (i.e. including patch number) 125 modDir := filepath.Dir(modfilePath) 126 parts := strings.Split(modDir, "@") 127 currentVersionString := parts[1] 128 desiredVersionString := fmt.Sprintf("v%s", version.String()) 129 if desiredVersionString != currentVersionString { 130 desiredDir := fmt.Sprintf("%s@%s", parts[0], desiredVersionString) 131 log.Printf("[TRACE] renaming dependency mod folder %s to %s", modDir, desiredDir) 132 return os.Rename(modDir, desiredDir) 133 } 134 return nil 135 } 136 137 // GetUnreferencedMods returns a map of all installed mods which are not in the lock file 138 func (l *WorkspaceLock) GetUnreferencedMods() VersionListMap { 139 var unreferencedVersions = make(VersionListMap) 140 for name, versions := range l.installedMods { 141 for _, version := range versions { 142 if !l.ContainsModVersion(name, version) { 143 unreferencedVersions.Add(name, version) 144 } 145 } 146 } 147 return unreferencedVersions 148 } 149 150 // identify mods which are in InstallCache but not installed 151 // move them from InstallCache into MissingVersions 152 func (l *WorkspaceLock) setMissing() { 153 // create a map of full modname to bool to allow simple checking 154 flatInstalled := l.installedMods.FlatMap() 155 156 for parent, deps := range l.InstallCache { 157 // deps is a map of dep name to resolved contraint list 158 // flatten and iterate 159 160 for name, resolvedConstraint := range deps { 161 fullName := modconfig.BuildModDependencyPath(name, resolvedConstraint.Version) 162 163 if !flatInstalled[fullName] { 164 // get the mod name from the constraint (fullName includes the version) 165 name := resolvedConstraint.Name 166 // remove this item from the install cache and add into missing 167 l.MissingVersions.Add(name, resolvedConstraint.Alias, resolvedConstraint.Version, resolvedConstraint.Constraint, parent) 168 l.InstallCache[parent].Remove(name) 169 } 170 } 171 } 172 } 173 174 // extract the mod name and version from the modfile path 175 func (l *WorkspaceLock) parseModPath(modfilePath string) (modDependencyName string, modVersion *semver.Version, err error) { 176 modFullName, err := filepath.Rel(l.ModInstallationPath, filepath.Dir(modfilePath)) 177 if err != nil { 178 return 179 } 180 return modconfig.ParseModDependencyPath(modFullName) 181 } 182 183 func (l *WorkspaceLock) Save() error { 184 if len(l.InstallCache) == 0 { 185 // ignore error 186 l.Delete() 187 return nil 188 } 189 content, err := json.MarshalIndent(l.InstallCache, "", " ") 190 if err != nil { 191 return err 192 } 193 return os.WriteFile(filepaths.WorkspaceLockPath(l.WorkspacePath), content, 0644) 194 } 195 196 // Delete deletes the lock file 197 func (l *WorkspaceLock) Delete() error { 198 if filehelpers.FileExists(filepaths.WorkspaceLockPath(l.WorkspacePath)) { 199 return os.Remove(filepaths.WorkspaceLockPath(l.WorkspacePath)) 200 } 201 return nil 202 } 203 204 // DeleteMods removes mods from the lock file then, if it is empty, deletes the file 205 func (l *WorkspaceLock) DeleteMods(mods VersionConstraintMap, parent *modconfig.Mod) { 206 for modName := range mods { 207 if parentDependencies := l.InstallCache[parent.GetInstallCacheKey()]; parentDependencies != nil { 208 parentDependencies.Remove(modName) 209 } 210 } 211 } 212 213 // GetMod looks for a lock file entry matching the given mod dependency name 214 // (e.g.github.com/turbot/steampipe-mod-azure-thrifty 215 func (l *WorkspaceLock) GetMod(modDependencyName string, parent *modconfig.Mod) *ResolvedVersionConstraint { 216 parentKey := parent.GetInstallCacheKey() 217 218 if parentDependencies := l.InstallCache[parentKey]; parentDependencies != nil { 219 // look for this mod in the lock file entries for this parent 220 return parentDependencies[modDependencyName] 221 } 222 return nil 223 } 224 225 // GetLockedModVersions builds a ResolvedVersionListMap with the resolved versions 226 // for each item of the given VersionConstraintMap found in the lock file 227 func (l *WorkspaceLock) GetLockedModVersions(mods VersionConstraintMap, parent *modconfig.Mod) (ResolvedVersionListMap, error) { 228 var res = make(ResolvedVersionListMap) 229 for name, constraint := range mods { 230 resolvedConstraint, err := l.GetLockedModVersion(constraint, parent) 231 if err != nil { 232 return nil, err 233 } 234 if resolvedConstraint != nil { 235 res.Add(name, resolvedConstraint) 236 } 237 } 238 return res, nil 239 } 240 241 // GetLockedModVersion looks for a lock file entry matching the required constraint and returns nil if not found 242 func (l *WorkspaceLock) GetLockedModVersion(requiredModVersion *modconfig.ModVersionConstraint, parent *modconfig.Mod) (*ResolvedVersionConstraint, error) { 243 lockedVersion := l.GetMod(requiredModVersion.Name, parent) 244 if lockedVersion == nil { 245 return nil, nil 246 } 247 248 // verify the locked version satisfies the version constraint 249 if !requiredModVersion.Constraint.Check(lockedVersion.Version) { 250 return nil, nil 251 } 252 253 return lockedVersion, nil 254 } 255 256 // EnsureLockedModVersion looks for a lock file entry matching the required mod name 257 func (l *WorkspaceLock) EnsureLockedModVersion(requiredModVersion *modconfig.ModVersionConstraint, parent *modconfig.Mod) (*ResolvedVersionConstraint, error) { 258 lockedVersion := l.GetMod(requiredModVersion.Name, parent) 259 if lockedVersion == nil { 260 return nil, nil 261 } 262 263 // verify the locked version satisfies the version constraint 264 if !requiredModVersion.Constraint.Check(lockedVersion.Version) { 265 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) 266 } 267 268 return lockedVersion, nil 269 } 270 271 // GetLockedModVersionConstraint looks for a lock file entry matching the required mod version and if found, 272 // returns it in the form of a ModVersionConstraint 273 func (l *WorkspaceLock) GetLockedModVersionConstraint(requiredModVersion *modconfig.ModVersionConstraint, parent *modconfig.Mod) (*modconfig.ModVersionConstraint, error) { 274 lockedVersion, err := l.EnsureLockedModVersion(requiredModVersion, parent) 275 if err != nil { 276 // EnsureLockedModVersion returns an error if the locked version does not satisfy the requirement 277 return nil, err 278 } 279 if lockedVersion == nil { 280 // EnsureLockedModVersion returns nil if no locked version is found 281 return nil, nil 282 } 283 // create a new ModVersionConstraint using the locked version 284 lockedVersionFullName := modconfig.BuildModDependencyPath(requiredModVersion.Name, lockedVersion.Version) 285 return modconfig.NewModVersionConstraint(lockedVersionFullName) 286 } 287 288 // ContainsModVersion returns whether the lockfile contains the given mod version 289 func (l *WorkspaceLock) ContainsModVersion(modName string, modVersion *semver.Version) bool { 290 for _, modVersionMap := range l.InstallCache { 291 for lockName, lockVersion := range modVersionMap { 292 // TODO consider handling of metadata 293 if lockName == modName && lockVersion.Version.Equal(modVersion) && lockVersion.Version.Metadata() == modVersion.Metadata() { 294 return true 295 } 296 } 297 } 298 return false 299 } 300 301 func (l *WorkspaceLock) ContainsModConstraint(modName string, constraint *versionhelpers.Constraints) bool { 302 for _, modVersionMap := range l.InstallCache { 303 for lockName, lockVersion := range modVersionMap { 304 if lockName == modName && lockVersion.Constraint == constraint.Original { 305 return true 306 } 307 } 308 } 309 return false 310 } 311 312 // Incomplete returned whether there are any missing dependencies 313 // (i.e. they exist in the lock file but ate not installed) 314 func (l *WorkspaceLock) Incomplete() bool { 315 return len(l.MissingVersions) > 0 316 } 317 318 // Empty returns whether the install cache is empty 319 func (l *WorkspaceLock) Empty() bool { 320 return l == nil || len(l.InstallCache) == 0 321 } 322 323 // StructVersion returns the struct version of the workspace lock 324 // because only the InstallCache is serialised, read the StructVersion from the first install cache entry 325 func (l *WorkspaceLock) StructVersion() int { 326 for _, depVersionMap := range l.InstallCache { 327 for _, depVersion := range depVersionMap { 328 return depVersion.StructVersion 329 } 330 } 331 // we have no deps - just return the new struct version 332 return WorkspaceLockStructVersion 333 334 } 335 336 func (l *WorkspaceLock) FindInstalledDependency(modDependency *ResolvedVersionConstraint) (string, error) { 337 dependencyFilepath := path.Join(l.ModInstallationPath, modDependency.DependencyPath()) 338 339 if filehelpers.DirectoryExists(dependencyFilepath) { 340 return dependencyFilepath, nil 341 } 342 343 return "", fmt.Errorf("dependency mod '%s' is not installed - run 'steampipe mod install'", modDependency.DependencyPath()) 344 }