Skip to content

Commit

Permalink
Updates for module add upgrade and restore CLI commands microsoft#2550
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Sep 24, 2024
1 parent 56a2136 commit 6c21b04
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 100 deletions.
9 changes: 9 additions & 0 deletions docs/CHANGELOG-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

What's changed since pre-release v3.0.0-B0267:

- New features:
- Allow CLI upgrade command to upgrade a single module by @BernieWhite.
[#2551](https://github.com/microsoft/PSRule/issues/2551)
- A single or specific modules can be upgraded by name when using `module upgrade`.
- By default, all modules are upgraded.
- Allow CLI to install pre-release modules by @BernieWhite.
[#2550](https://github.com/microsoft/PSRule/issues/2550)
- Add and upgrade pre-release modules with `--prerelease`.
- Pre-release modules will be restored from the lock file with `module restore`.
- General improvements:
- **Breaking change**: Empty version comparison only accepts stable versions by default by @BernieWhite.
[#2557](https://github.com/microsoft/PSRule/issues/2557)
Expand Down
23 changes: 22 additions & 1 deletion docs/concepts/cli/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ Optional parameters:
- `--version` - Specifies a specific version of the module to add.
By default, the latest stable version of the module is added.
Any required version constraints set by the `Requires` option are taken into consideration.
- `--prerelease` - Accept pre-release versions in addition to stable module versions.
By default, pre-release versions are not included.
A pre-release version may also be accepted when `Requires` includes pre-releases.

For example:

Expand All @@ -80,6 +83,12 @@ For example, a specific version of the module is added:
ps-rule module add PSRule.Rules.Azure --version 1.32.1
```

For example, include pre-release versions added:

```bash title="PSRule CLI command-line"
ps-rule module add PSRule.Rules.Azure --prerelease
```

## `module remove`

Remove one or more modules from the lock file.
Expand Down Expand Up @@ -112,14 +121,26 @@ ps-rule module restore --force

## `module upgrade`

Upgrade to the latest versions any modules within the lock file.
Upgrade to the latest versions for all or a specific module within the lock file.

Optional parameters:

- `--prerelease` - Accept pre-release versions in addition to stable module versions.
By default, pre-release versions are not included.
A pre-release version may also be accepted when `Requires` includes pre-releases.

For example:

```bash title="PSRule CLI command-line"
ps-rule module upgrade
```

For example, upgrade a specific module and include pre-release versions:

```bash title="PSRule CLI command-line"
ps-rule module upgrade PSRule.Rules.Azure --prerelease
```

## Next steps

For more information on the module lock file, see [Lock file](../lockfile.md).
Expand Down
5 changes: 5 additions & 0 deletions schemas/PSRule-lock.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
"type": "string",
"title": "Module version",
"description": "The version of the module to use."
},
"includePrerelease": {
"type": "boolean",
"title": "Include prerelease",
"description": "Accept pre-release versions in addition to stable module versions."
}
},
"required": [
Expand Down
62 changes: 40 additions & 22 deletions src/PSRule.CommandLine/Commands/ModuleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
using SemanticVersion = PSRule.Data.SemanticVersion;
using NuGet.Packaging;
using NuGet.Common;
using PSRule.Pipeline;

namespace PSRule.CommandLine.Commands;

Expand Down Expand Up @@ -51,7 +50,7 @@ public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions
var requires = clientContext.Option.Requires.ToDictionary();
var file = LockFile.Read(null);

using var pwsh = PowerShell.Create();
using var pwsh = CreatePowerShell();

// Restore from the lock file.
foreach (var kv in file.Modules)
Expand All @@ -70,9 +69,11 @@ public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions

var idealVersion = await FindVersionAsync(module, null, targetVersion, null, cancellationToken);
if (idealVersion != null)
await InstallVersionAsync(clientContext, module, idealVersion.ToString(), cancellationToken);
{
installedVersion = await InstallVersionAsync(clientContext, module, idealVersion, cancellationToken);
}

if (pwsh.HadErrors || (idealVersion == null && installedVersion == null))
if (pwsh.HadErrors || idealVersion == null || installedVersion == null)
{
exitCode = ERROR_MODULE_FAILED_TO_INSTALL;
clientContext.LogError(Messages.Error_501, module, targetVersion);
Expand Down Expand Up @@ -109,7 +110,7 @@ public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions
var idealVersion = await FindVersionAsync(includeModule, moduleConstraint, null, null, cancellationToken);
if (idealVersion != null)
{
await InstallVersionAsync(clientContext, includeModule, idealVersion.ToString(), cancellationToken);
await InstallVersionAsync(clientContext, includeModule, idealVersion, cancellationToken);
}
else if (idealVersion == null)
{
Expand Down Expand Up @@ -143,8 +144,7 @@ public static async Task<int> ModuleInitAsync(ModuleOptions operationOptions, Cl
var exitCode = 0;
var requires = clientContext.Option.Requires.ToDictionary();
var file = !operationOptions.Force ? LockFile.Read(null) : new LockFile();

using var pwsh = PowerShell.Create();
using var pwsh = CreatePowerShell();

// Add for any included modules.
if (clientContext.Option?.Include?.Module != null && clientContext.Option.Include.Module.Length > 0)
Expand Down Expand Up @@ -197,8 +197,7 @@ public static async Task<int> ModuleListAsync(ModuleOptions operationOptions, Cl
var exitCode = 0;
var requires = clientContext.Option.Requires.ToDictionary();
var file = LockFile.Read(null);

var pwsh = PowerShell.Create();
var pwsh = CreatePowerShell();

if (exitCode == 0)
{
Expand All @@ -218,13 +217,13 @@ public static async Task<int> ModuleAddAsync(ModuleOptions operationOptions, Cli
var requires = clientContext.Option.Requires.ToDictionary();
var file = LockFile.Read(null);

using var pwsh = PowerShell.Create();
using var pwsh = CreatePowerShell();
foreach (var module in operationOptions.Module)
{
if (!file.Modules.TryGetValue(module, out var item) || operationOptions.Force)
{
// Get a constraint if set from options.
var moduleConstraint = requires.TryGetValue(module, out var c) ? c : ModuleConstraint.Any(module, includePrerelease: false);
var moduleConstraint = requires.TryGetValue(module, out var c) ? c : ModuleConstraint.Any(module, includePrerelease: operationOptions.Prerelease);

// Get target version if specified in command-line.
var targetVersion = !string.IsNullOrEmpty(operationOptions.Version) && SemanticVersion.TryParseVersion(operationOptions.Version, out var v) && v != null ? v : null;
Expand All @@ -250,7 +249,8 @@ public static async Task<int> ModuleAddAsync(ModuleOptions operationOptions, Cli
clientContext.LogVerbose(Messages.UsingModule, module, idealVersion.ToString());
item = new LockEntry
{
Version = idealVersion
Version = idealVersion,
IncludePrerelease = operationOptions.Prerelease && !idealVersion.Stable ? true : null,
};
file.Modules[module] = item;
}
Expand Down Expand Up @@ -281,7 +281,7 @@ public static async Task<int> ModuleRemoveAsync(ModuleOptions operationOptions,

var file = LockFile.Read(null);

using var pwsh = PowerShell.Create();
using var pwsh = CreatePowerShell();
foreach (var module in operationOptions.Module)
{
if (file.Modules.TryGetValue(module, out var constraint))
Expand Down Expand Up @@ -311,12 +311,13 @@ public static async Task<int> ModuleUpgradeAsync(ModuleOptions operationOptions,
var exitCode = 0;
var requires = clientContext.Option.Requires.ToDictionary();
var file = LockFile.Read(null);
var filteredModules = operationOptions.Module != null && operationOptions.Module.Length > 0 ? new HashSet<string>(operationOptions.Module, StringComparer.OrdinalIgnoreCase) : null;

using var pwsh = PowerShell.Create();
foreach (var kv in file.Modules)
using var pwsh = CreatePowerShell();
foreach (var kv in file.Modules.Where(m => filteredModules == null || filteredModules.Contains(m.Key)))
{
// Get a constraint if set from options.
var moduleConstraint = requires.TryGetValue(kv.Key, out var c) ? c : ModuleConstraint.Any(kv.Key, includePrerelease: false);
var moduleConstraint = requires.TryGetValue(kv.Key, out var c) ? c : ModuleConstraint.Any(kv.Key, includePrerelease: kv.Value.IncludePrerelease ?? operationOptions.Prerelease);

// Find the ideal version.
var idealVersion = await FindVersionAsync(kv.Key, moduleConstraint, null, null, cancellationToken);
Expand All @@ -332,6 +333,7 @@ public static async Task<int> ModuleUpgradeAsync(ModuleOptions operationOptions,
clientContext.LogVerbose(Messages.UsingModule, kv.Key, idealVersion.ToString());

kv.Value.Version = idealVersion;
kv.Value.IncludePrerelease = (kv.Value.IncludePrerelease.GetValueOrDefault(false) || operationOptions.Prerelease) && !idealVersion.Stable ? true : null;
file.Modules[kv.Key] = kv.Value;
}

Expand Down Expand Up @@ -461,24 +463,26 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H
return result;
}

private static async Task InstallVersionAsync([DisallowNull] ClientContext context, [DisallowNull] string name, [DisallowNull] string version, CancellationToken cancellationToken)
private static async Task<SemanticVersion.Version?> InstallVersionAsync([DisallowNull] ClientContext context, [DisallowNull] string name, [DisallowNull] SemanticVersion.Version version, CancellationToken cancellationToken)
{
context.LogVerbose(Messages.RestoringModule, name, version);

var cache = new SourceCacheContext();
var logger = new NullLogger();
var resource = await GetSourceRepositoryAsync();
var stringVersion = version.ToString();

var packageVersion = new NuGetVersion(version);
var packageVersion = new NuGetVersion(stringVersion);
using var packageStream = new MemoryStream();

await resource.CopyNupkgToStreamAsync(
if (!await resource.CopyNupkgToStreamAsync(
name,
packageVersion,
packageStream,
cache,
logger,
cancellationToken);
cancellationToken))
return null;

using var packageReader = new PackageArchiveReader(packageStream);
var nuspecReader = await packageReader.GetNuspecReaderAsync(cancellationToken);
Expand All @@ -489,17 +493,26 @@ await resource.CopyNupkgToStreamAsync(
if (Directory.Exists(modulePath))
Directory.Delete(modulePath, true);

var count = 0;
var files = packageReader.GetFiles();
packageReader.CopyFiles(modulePath, files, (name, targetPath, s) =>
{
if (ShouldIgnorePackageFile(name))
return null;

s.CopyToFile(targetPath);
count++;

return targetPath;

}, logger, cancellationToken);

// Check module path exists.
if (!Directory.Exists(modulePath))
return null;

context.LogVerbose("Module saved to: {0} -- {1}", name, modulePath);
return count > 0 ? version : null;
}

private static async Task<FindPackageByIdResource> GetSourceRepositoryAsync()
Expand All @@ -509,9 +522,9 @@ private static async Task<FindPackageByIdResource> GetSourceRepositoryAsync()
return await repository.GetResourceAsync<FindPackageByIdResource>();
}

private static string GetModulePath(ClientContext context, string name, string version)
private static string GetModulePath(ClientContext context, string name, [DisallowNull] SemanticVersion.Version version)
{
return Path.Combine(context.CachePath, MODULES_PATH, name, version);
return Path.Combine(context.CachePath, MODULES_PATH, name, version.ToShortString());
}

private static bool ShouldIgnorePackageFile(string name)
Expand All @@ -520,5 +533,10 @@ private static bool ShouldIgnorePackageFile(string name)
string.Equals(name, "_rels/.rels", StringComparison.OrdinalIgnoreCase);
}

private static PowerShell CreatePowerShell()
{
return PowerShell.Create();
}

#endregion Helper methods
}
17 changes: 11 additions & 6 deletions src/PSRule.CommandLine/Models/ModuleOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,37 @@
namespace PSRule.CommandLine.Models;

/// <summary>
///
/// Options for the module command.
/// </summary>
public sealed class ModuleOptions
{
/// <summary>
///
/// A specific path to use for the operation.
/// </summary>
public string[]? Path { get; set; }

/// <summary>
///
/// The name of any specified modules.
/// </summary>
public string[]? Module { get; set; }

/// <summary>
///
/// Determines if the module is overridden if it already exists.
/// </summary>
public bool Force { get; set; }

/// <summary>
///
/// The target module version.
/// </summary>
public string? Version { get; set; }

/// <summary>
///
/// Determines if verification that the module exists is skipped.
/// </summary>
public bool SkipVerification { get; set; }

/// <summary>
/// Accept pre-release versions in addition to stable module versions.
/// </summary>
public bool Prerelease { get; set; }
}
Loading

0 comments on commit 6c21b04

Please sign in to comment.