github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/modinstaller/mod_installer.go (about) 1 package modinstaller 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "os" 8 "path" 9 "path/filepath" 10 11 "github.com/Masterminds/semver/v3" 12 git "github.com/go-git/go-git/v5" 13 "github.com/otiai10/copy" 14 "github.com/spf13/viper" 15 "github.com/turbot/steampipe-plugin-sdk/v5/sperr" 16 "github.com/turbot/steampipe/pkg/constants" 17 "github.com/turbot/steampipe/pkg/error_helpers" 18 "github.com/turbot/steampipe/pkg/filepaths" 19 "github.com/turbot/steampipe/pkg/plugin" 20 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 21 "github.com/turbot/steampipe/pkg/steampipeconfig/parse" 22 "github.com/turbot/steampipe/pkg/steampipeconfig/versionmap" 23 "github.com/turbot/steampipe/pkg/utils" 24 ) 25 26 type ModInstaller struct { 27 installData *InstallData 28 29 // this will be updated as changes are made to dependencies 30 workspaceMod *modconfig.Mod 31 32 // since changes are made to workspaceMod, we need a copy of the Require as is on disk 33 // to be able to calculate changes 34 oldRequire *modconfig.Require 35 36 // installed plugins 37 installedPlugins map[string]*modconfig.PluginVersionString 38 39 mods versionmap.VersionConstraintMap 40 41 // the final resting place of all dependency mods 42 modsPath string 43 // a shadow directory for installing mods 44 // this is necessary to make mod installation transactional 45 shadowDirPath string 46 47 workspacePath string 48 49 // what command is being run 50 command string 51 // are dependencies being added to the workspace 52 dryRun bool 53 // do we force install even if there are require errors 54 force bool 55 } 56 57 func NewModInstaller(ctx context.Context, opts *InstallOpts) (*ModInstaller, error) { 58 if opts.WorkspaceMod == nil { 59 return nil, sperr.New("no workspace mod passed to mod installer") 60 } 61 i := &ModInstaller{ 62 workspacePath: opts.WorkspaceMod.ModPath, 63 workspaceMod: opts.WorkspaceMod, 64 command: opts.Command, 65 dryRun: opts.DryRun, 66 force: opts.Force, 67 } 68 69 if opts.WorkspaceMod.Require != nil { 70 i.oldRequire = opts.WorkspaceMod.Require.Clone() 71 } 72 73 if err := i.setModsPath(); err != nil { 74 return nil, err 75 } 76 77 installedPlugins, err := plugin.GetInstalledPlugins(ctx) 78 if err != nil { 79 return nil, err 80 } 81 i.installedPlugins = installedPlugins 82 83 // load lock file 84 workspaceLock, err := versionmap.LoadWorkspaceLock(ctx, i.workspacePath) 85 if err != nil { 86 return nil, err 87 } 88 89 // create install data 90 i.installData = NewInstallData(workspaceLock, i.workspaceMod) 91 92 // parse args to get the required mod versions 93 requiredMods, err := i.GetRequiredModVersionsFromArgs(opts.ModArgs) 94 if err != nil { 95 return nil, err 96 } 97 i.mods = requiredMods 98 99 return i, nil 100 } 101 102 func (i *ModInstaller) removeOldShadowDirectories() error { 103 removeErrors := []error{} 104 // get the parent of the 'mods' directory - all shadow directories are siblings of this 105 parent := filepath.Base(i.modsPath) 106 entries, err := os.ReadDir(parent) 107 if err != nil { 108 return err 109 } 110 for _, dir := range entries { 111 if dir.IsDir() && filepaths.IsModInstallShadowPath(dir.Name()) { 112 err := os.RemoveAll(filepath.Join(parent, dir.Name())) 113 if err != nil { 114 removeErrors = append(removeErrors, err) 115 } 116 } 117 } 118 return error_helpers.CombineErrors(removeErrors...) 119 } 120 121 func (i *ModInstaller) setModsPath() error { 122 i.modsPath = filepaths.WorkspaceModPath(i.workspacePath) 123 _ = i.removeOldShadowDirectories() 124 i.shadowDirPath = filepaths.WorkspaceModShadowPath(i.workspacePath) 125 return nil 126 } 127 128 func (i *ModInstaller) UninstallWorkspaceDependencies(ctx context.Context) error { 129 workspaceMod := i.workspaceMod 130 131 // remove required dependencies from the mod file 132 if len(i.mods) == 0 { 133 workspaceMod.RemoveAllModDependencies() 134 135 } else { 136 // verify all the mods specifed in the args exist in the modfile 137 workspaceMod.RemoveModDependencies(i.mods) 138 } 139 140 // uninstall by calling Install 141 if err := i.installMods(ctx, workspaceMod.Require.Mods, workspaceMod); err != nil { 142 return err 143 } 144 145 if workspaceMod.Require.Empty() { 146 workspaceMod.Require = nil 147 } 148 149 // if this is a dry run, return now 150 if i.dryRun { 151 log.Printf("[TRACE] UninstallWorkspaceDependencies - dry-run=true, returning before saving mod file and cache\n") 152 return nil 153 } 154 155 // write the lock file 156 if err := i.installData.Lock.Save(); err != nil { 157 return err 158 } 159 160 // now safe to save the mod file 161 if err := i.updateModFile(); err != nil { 162 return err 163 } 164 165 // tidy unused mods 166 if viper.GetBool(constants.ArgPrune) { 167 if _, err := i.Prune(); err != nil { 168 return err 169 } 170 } 171 return nil 172 } 173 174 // InstallWorkspaceDependencies installs all dependencies of the workspace mod 175 func (i *ModInstaller) InstallWorkspaceDependencies(ctx context.Context) (err error) { 176 workspaceMod := i.workspaceMod 177 defer func() { 178 if err != nil && i.force { 179 // suppress the error since this is a forced install 180 log.Println("[TRACE] suppressing error in InstallWorkspaceDependencies because force is enabled", err) 181 err = nil 182 } 183 // tidy unused mods 184 // (put in defer so it still gets called in case of errors) 185 if viper.GetBool(constants.ArgPrune) && !i.dryRun { 186 // be sure not to overwrite an existing return error 187 _, pruneErr := i.Prune() 188 if pruneErr != nil && err == nil { 189 err = pruneErr 190 } 191 } 192 }() 193 194 if validationErrors := workspaceMod.ValidateRequirements(i.installedPlugins); len(validationErrors) > 0 { 195 if !i.force { 196 // if this is not a force install, return errors in validation 197 return error_helpers.CombineErrors(validationErrors...) 198 } 199 // ignore if this is a force install 200 // TODO: raise warnings for errors getting suppressed [https://github.com/turbot/steampipe/issues/3364] 201 log.Println("[TRACE] suppressing mod validation error", validationErrors) 202 } 203 204 // if mod args have been provided, add them to the workspace mod requires 205 // (this will replace any existing dependencies of same name) 206 if len(i.mods) > 0 { 207 workspaceMod.AddModDependencies(i.mods) 208 } 209 210 if err := i.installMods(ctx, workspaceMod.Require.Mods, workspaceMod); err != nil { 211 return err 212 } 213 214 // if this is a dry run, return now 215 if i.dryRun { 216 log.Printf("[TRACE] InstallWorkspaceDependencies - dry-run=true, returning before saving mod file and cache\n") 217 return nil 218 } 219 220 // write the lock file 221 if err := i.installData.Lock.Save(); err != nil { 222 return err 223 } 224 225 // now safe to save the mod file 226 if err := i.updateModFile(); err != nil { 227 return err 228 } 229 230 if !workspaceMod.HasDependentMods() { 231 // there are no dependencies - delete the cache 232 i.installData.Lock.Delete() 233 } 234 return nil 235 } 236 237 func (i *ModInstaller) GetModList() string { 238 return i.installData.Lock.GetModList(i.workspaceMod.GetInstallCacheKey()) 239 } 240 241 // commitShadow recursively copies over the contents of the shadow directory 242 // to the mods directory, replacing conflicts as it goes 243 // (uses `os.Create(dest)` under the hood - which truncates the target) 244 func (i *ModInstaller) commitShadow(ctx context.Context) error { 245 if error_helpers.IsContextCanceled(ctx) { 246 return ctx.Err() 247 } 248 if _, err := os.Stat(i.shadowDirPath); os.IsNotExist(err) { 249 // nothing to do here 250 // there's no shadow directory to commit 251 // this is not an error and may happen when install does not make any changes 252 return nil 253 } 254 entries, err := os.ReadDir(i.shadowDirPath) 255 if err != nil { 256 return sperr.WrapWithRootMessage(err, "could not read shadow directory") 257 } 258 for _, entry := range entries { 259 if !entry.IsDir() { 260 continue 261 } 262 source := filepath.Join(i.shadowDirPath, entry.Name()) 263 destination := filepath.Join(i.modsPath, entry.Name()) 264 log.Println("[TRACE] copying", source, destination) 265 if err := copy.Copy(source, destination); err != nil { 266 return sperr.WrapWithRootMessage(err, "could not commit shadow directory '%s'", entry.Name()) 267 } 268 } 269 return nil 270 } 271 272 func (i *ModInstaller) shouldCommitShadow(ctx context.Context, installError error) bool { 273 // no commit if this is a dry run 274 if i.dryRun { 275 return false 276 } 277 // commit if this is forced - even if there's errors 278 return installError == nil || i.force 279 } 280 281 func (i *ModInstaller) installMods(ctx context.Context, mods []*modconfig.ModVersionConstraint, parent *modconfig.Mod) (err error) { 282 defer func() { 283 var commitErr error 284 if i.shouldCommitShadow(ctx, err) { 285 commitErr = i.commitShadow(ctx) 286 } 287 288 // if this was forced, we need to suppress the install error 289 // otherwise the calling code will fail 290 if i.force { 291 err = nil 292 } 293 294 // ensure we return any commit error 295 if commitErr != nil { 296 err = commitErr 297 } 298 299 // force remove the shadow directory - we can ignore any error here, since 300 // these directories get cleaned up before any install session 301 os.RemoveAll(i.shadowDirPath) 302 }() 303 304 var errors []error 305 for _, requiredModVersion := range mods { 306 modToUse, err := i.getCurrentlyInstalledVersionToUse(ctx, requiredModVersion, parent, i.updating()) 307 if err != nil { 308 errors = append(errors, err) 309 continue 310 } 311 312 // if the mod is not installed or needs updating, OR if this is an update command, 313 // pass shouldUpdate=true into installModDependencesRecursively 314 // this ensures that we update any dependencies which have updates available 315 shouldUpdate := modToUse == nil || i.updating() 316 if err := i.installModDependencesRecursively(ctx, requiredModVersion, modToUse, parent, shouldUpdate); err != nil { 317 errors = append(errors, err) 318 } 319 } 320 321 // update the lock to be the new lock, and record any uninstalled mods 322 i.installData.onInstallComplete() 323 324 return i.buildInstallError(errors) 325 } 326 327 func (i *ModInstaller) buildInstallError(errors []error) error { 328 if len(errors) == 0 { 329 return nil 330 } 331 verb := "install" 332 if i.updating() { 333 verb = "update" 334 } 335 prefix := fmt.Sprintf("%d %s failed to %s", len(errors), utils.Pluralize("dependency", len(errors)), verb) 336 err := error_helpers.CombineErrorsWithPrefix(prefix, errors...) 337 return err 338 } 339 340 func (i *ModInstaller) installModDependencesRecursively(ctx context.Context, requiredModVersion *modconfig.ModVersionConstraint, dependencyMod *modconfig.Mod, parent *modconfig.Mod, shouldUpdate bool) error { 341 if error_helpers.IsContextCanceled(ctx) { 342 // short circuit if the execution context has been cancelled 343 return ctx.Err() 344 } 345 // get available versions for this mod 346 includePrerelease := requiredModVersion.Constraint.IsPrerelease() 347 availableVersions, err := i.installData.getAvailableModVersions(requiredModVersion.Name, includePrerelease) 348 349 if err != nil { 350 return err 351 } 352 353 var errors []error 354 355 if dependencyMod == nil { 356 // get a resolved mod ref that satisfies the version constraints 357 resolvedRef, err := i.getModRefSatisfyingConstraints(requiredModVersion, availableVersions) 358 if err != nil { 359 return err 360 } 361 362 // install the mod 363 dependencyMod, err = i.install(ctx, resolvedRef, parent) 364 if err != nil { 365 return err 366 } 367 368 validationErrors := dependencyMod.ValidateRequirements(i.installedPlugins) 369 errors = append(errors, validationErrors...) 370 } else { 371 // update the install data 372 i.installData.addExisting(requiredModVersion.Name, dependencyMod, requiredModVersion.Constraint, parent) 373 log.Printf("[TRACE] not installing %s with version constraint %s as version %s is already installed", requiredModVersion.Name, requiredModVersion.Constraint.Original, dependencyMod.Version) 374 } 375 376 // to get here we have the dependency mod - either we installed it or it was already installed 377 // recursively install its dependencies 378 for _, childDependency := range dependencyMod.Require.Mods { 379 childDependencyMod, err := i.getCurrentlyInstalledVersionToUse(ctx, childDependency, dependencyMod, shouldUpdate) 380 if err != nil { 381 errors = append(errors, err) 382 continue 383 } 384 if err := i.installModDependencesRecursively(ctx, childDependency, childDependencyMod, dependencyMod, shouldUpdate); err != nil { 385 errors = append(errors, err) 386 continue 387 } 388 } 389 390 return error_helpers.CombineErrorsWithPrefix(fmt.Sprintf("%d child %s failed to install", len(errors), utils.Pluralize("dependency", len(errors))), errors...) 391 } 392 393 func (i *ModInstaller) getCurrentlyInstalledVersionToUse(ctx context.Context, requiredModVersion *modconfig.ModVersionConstraint, parent *modconfig.Mod, forceUpdate bool) (*modconfig.Mod, error) { 394 // do we have an installed version of this mod matching the required mod constraint 395 installedVersion, err := i.installData.Lock.GetLockedModVersion(requiredModVersion, parent) 396 if err != nil { 397 return nil, err 398 } 399 if installedVersion == nil { 400 return nil, nil 401 } 402 403 // can we update this 404 canUpdate, err := i.canUpdateMod(installedVersion, requiredModVersion, forceUpdate) 405 if err != nil { 406 return nil, err 407 408 } 409 if canUpdate { 410 // return nil mod to indicate we should update 411 return nil, nil 412 } 413 414 // load the existing mod and return 415 return i.loadDependencyMod(ctx, installedVersion) 416 } 417 418 // loadDependencyMod tries to load the mod definition from the shadow directory 419 // and falls back to the 'mods' directory of the root mod 420 func (i *ModInstaller) loadDependencyMod(ctx context.Context, modVersion *versionmap.ResolvedVersionConstraint) (*modconfig.Mod, error) { 421 // construct the dependency path - this is the relative path of the dependency we are installing 422 dependencyPath := modVersion.DependencyPath() 423 424 // first try loading from the shadow dir 425 modDefinition, err := i.loadDependencyModFromRoot(ctx, i.shadowDirPath, dependencyPath) 426 if err != nil { 427 return nil, err 428 } 429 430 // failed to load from shadow dir, try mods dir 431 if modDefinition == nil { 432 modDefinition, err = i.loadDependencyModFromRoot(ctx, i.modsPath, dependencyPath) 433 if err != nil { 434 return nil, err 435 } 436 } 437 438 // if we still failed, give up 439 if modDefinition == nil { 440 return nil, fmt.Errorf("could not find dependency mod '%s'", dependencyPath) 441 } 442 443 // set the DependencyName, DependencyPath and Version properties on the mod 444 if err := i.setModDependencyConfig(modDefinition, dependencyPath); err != nil { 445 return nil, err 446 } 447 448 return modDefinition, nil 449 } 450 451 func (i *ModInstaller) loadDependencyModFromRoot(ctx context.Context, modInstallRoot string, dependencyPath string) (*modconfig.Mod, error) { 452 log.Printf("[TRACE] loadDependencyModFromRoot: trying to load %s from root %s", dependencyPath, modInstallRoot) 453 454 modPath := path.Join(modInstallRoot, dependencyPath) 455 modDefinition, err := parse.LoadModfile(modPath) 456 if err != nil { 457 return nil, sperr.WrapWithMessage(err, "failed to load mod definition for %s from %s", dependencyPath, modInstallRoot) 458 } 459 return modDefinition, nil 460 } 461 462 // determine if we should update this mod, and if so whether there is an update available 463 func (i *ModInstaller) canUpdateMod(installedVersion *versionmap.ResolvedVersionConstraint, requiredModVersion *modconfig.ModVersionConstraint, forceUpdate bool) (bool, error) { 464 // so should we update? 465 // if forceUpdate is set or if the required version constraint is different to the locked version constraint, update 466 isSatisfied, errs := requiredModVersion.Constraint.Validate(installedVersion.Version) 467 if len(errs) > 0 { 468 return false, error_helpers.CombineErrors(errs...) 469 } 470 if forceUpdate || !isSatisfied { 471 // get available versions for this mod 472 includePrerelease := requiredModVersion.Constraint.IsPrerelease() 473 availableVersions, err := i.installData.getAvailableModVersions(requiredModVersion.Name, includePrerelease) 474 if err != nil { 475 return false, err 476 } 477 478 return i.updateAvailable(requiredModVersion, installedVersion.Version, availableVersions) 479 } 480 return false, nil 481 482 } 483 484 // determine whether there is a newer mod version avoilable which satisfies the dependency version constraint 485 func (i *ModInstaller) updateAvailable(requiredVersion *modconfig.ModVersionConstraint, currentVersion *semver.Version, availableVersions []*semver.Version) (bool, error) { 486 latestVersion, err := i.getModRefSatisfyingConstraints(requiredVersion, availableVersions) 487 if err != nil { 488 return false, err 489 } 490 if latestVersion.Version.GreaterThan(currentVersion) { 491 return true, nil 492 } 493 return false, nil 494 } 495 496 // get the most recent available mod version which satisfies the version constraint 497 func (i *ModInstaller) getModRefSatisfyingConstraints(modVersion *modconfig.ModVersionConstraint, availableVersions []*semver.Version) (*ResolvedModRef, error) { 498 // find a version which satisfies the version constraint 499 var version = getVersionSatisfyingConstraint(modVersion.Constraint, availableVersions) 500 if version == nil { 501 return nil, fmt.Errorf("no version of %s found satisfying version constraint: %s", modVersion.Name, modVersion.Constraint.Original) 502 } 503 504 return NewResolvedModRef(modVersion, version) 505 } 506 507 // install a mod 508 func (i *ModInstaller) install(ctx context.Context, dependency *ResolvedModRef, parent *modconfig.Mod) (_ *modconfig.Mod, err error) { 509 var modDef *modconfig.Mod 510 // get the temp location to install the mod to 511 dependencyPath := dependency.DependencyPath() 512 destPath := i.getDependencyShadowPath(dependencyPath) 513 514 defer func() { 515 if err == nil { 516 i.installData.onModInstalled(dependency, modDef, parent) 517 } 518 }() 519 // if the target path exists, use the exiting file 520 // if it does not exist (the usual case), install it 521 if _, err := os.Stat(destPath); os.IsNotExist(err) { 522 log.Println("[TRACE] installing", dependencyPath, "in", destPath) 523 if err := i.installFromGit(dependency, destPath); err != nil { 524 return nil, err 525 } 526 } 527 528 // now load the installed mod and return it 529 modDef, err = parse.LoadModfile(destPath) 530 if err != nil { 531 return nil, err 532 } 533 if modDef == nil { 534 return nil, fmt.Errorf("'%s' has no mod definition file", dependencyPath) 535 } 536 537 if !i.dryRun { 538 // now the mod is installed in its final location, set mod dependency path 539 if err := i.setModDependencyConfig(modDef, dependencyPath); err != nil { 540 return nil, err 541 } 542 } 543 544 return modDef, nil 545 } 546 547 func (i *ModInstaller) installFromGit(dependency *ResolvedModRef, installPath string) error { 548 // get the mod from git 549 gitUrl := getGitUrl(dependency.Name) 550 log.Println("[TRACE] >>> cloning", gitUrl, dependency.GitReference) 551 _, err := git.PlainClone(installPath, 552 false, 553 &git.CloneOptions{ 554 URL: gitUrl, 555 ReferenceName: dependency.GitReference, 556 Depth: 1, 557 SingleBranch: true, 558 }) 559 if err != nil { 560 return sperr.WrapWithMessage(err, "failed to clone mod '%s' from git", dependency.Name) 561 } 562 // verify the cloned repo contains a valid modfile 563 return i.verifyModFile(dependency, installPath) 564 } 565 566 // build the path of the temp location to copy this depednency to 567 func (i *ModInstaller) getDependencyDestPath(dependencyFullName string) string { 568 return filepath.Join(i.modsPath, dependencyFullName) 569 } 570 571 // build the path of the temp location to copy this depednency to 572 func (i *ModInstaller) getDependencyShadowPath(dependencyFullName string) string { 573 return filepath.Join(i.shadowDirPath, dependencyFullName) 574 } 575 576 // set the mod dependency path 577 func (i *ModInstaller) setModDependencyConfig(mod *modconfig.Mod, dependencyPath string) error { 578 return mod.SetDependencyConfig(dependencyPath) 579 } 580 581 func (i *ModInstaller) updating() bool { 582 return i.command == "update" 583 } 584 585 func (i *ModInstaller) uninstalling() bool { 586 return i.command == "uninstall" 587 } 588 589 func (i *ModInstaller) verifyModFile(dependency *ResolvedModRef, installPath string) error { 590 for _, modFilePath := range filepaths.ModFilePaths(installPath) { 591 _, err := os.Stat(modFilePath) 592 if err == nil { 593 // found the modfile 594 return nil 595 } 596 } 597 return sperr.New("mod '%s' does not contain a valid mod file", dependency.Name) 598 }