From 3dbb6c7327bdf5c45a79b72ed18175d7c30227d3 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:07:08 -0800 Subject: [PATCH 01/89] Add azartifacts credential provider integration --- src/code/CredentialProvider.cs | 273 +++++++++++++++++++++++ src/code/FindHelper.cs | 35 ++- src/code/InstallHelper.cs | 64 +----- src/code/PSRepositoryInfo.cs | 40 ++-- src/code/PublishHelper.cs | 10 +- src/code/RegisterPSResourceRepository.cs | 34 ++- src/code/RepositorySettings.cs | 129 ++++++----- src/code/SetPSResourceRepository.cs | 15 +- src/code/Utils.cs | 43 +++- 9 files changed, 504 insertions(+), 139 deletions(-) create mode 100644 src/code/CredentialProvider.cs diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs new file mode 100644 index 000000000..45720380f --- /dev/null +++ b/src/code/CredentialProvider.cs @@ -0,0 +1,273 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security; +using System.Management.Automation; +using System.Text.Json; + +namespace Microsoft.PowerShell.PSResourceGet.UtilClasses +{ + internal static class CredentialProvider + { + private static string FindCredProviderFromPluginsPath() + { + // Get environment variable "NUGET_PLUGIN_PATHS" + // The environment variable NUGET_PLUGIN_PATHS should have the value of the .exe or .dll of the credential provider found in plugins\netfx\CredentialProvider.Microsoft\ + // For example, $env:NUGET_PLUGIN_PATHS="my-alternative-location\CredentialProvider.Microsoft.exe". + // OR $env:NUGET_PLUGIN_PATHS="my-alternative-location\CredentialProvider.Microsoft.dll" + + return Environment.GetEnvironmentVariable("NUGET_PLUGIN_PATHS", EnvironmentVariableTarget.User) ?? Environment.GetEnvironmentVariable("NUGET_PLUGIN_PATHS", EnvironmentVariableTarget.Machine); + } + + private static string FindCredProviderFromDefaultLocation() + { + // Default locations are either: + // $env:UserProfile\.nuget\plugins\netfx\CredentialProvider\CredentialProvider.Microsoft.exe + // OR $env:UserProfile\.nuget\plugins\netcore\CredentialProvider\CredentialProvider.Microsoft.exe (or) CredentialProvider.Microsoft.dll + var credProviderDefaultLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "plugins"); + + var netCorePath = Path.Combine(credProviderDefaultLocation, "netcore", "CredentialProvider.Microsoft"); + var netFxPath = Path.Combine(credProviderDefaultLocation, "netfx", "CredentialProvider.Microsoft"); + var credProviderPath = string.Empty; + if (Directory.Exists(netCorePath)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.exe"); + } + else + { + credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.dll"); + } + } + else if (Directory.Exists(netFxPath) && Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = Path.Combine(netFxPath, "CredentialProvider.Microsoft.exe"); + } + + return credProviderPath; + } + + private static string FindCredProviderFromVSLocation(out ErrorRecord error) + { + error = null; + + // C:\Program Files\Microsoft Visual Studio\ + var visualStudioPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft Visual Studio"); + // "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.exe" + // "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.dll" + + var credProviderPath = string.Empty; + if (Directory.Exists(visualStudioPath)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.exe", out error); + } + else if (string.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.dll", out error); + } + } + + return credProviderPath; + } + + private static string VSCredentialProviderFile(string visualStudioPath, string credProviderFile, out ErrorRecord error) + { + error = null; + try + { + // Search for the file in the directory and subdirectories + string[] exeFile = Directory.GetFiles(visualStudioPath, credProviderFile, SearchOption.AllDirectories); + + if (exeFile.Length > 0) + { + return exeFile[0]; + } + } + catch (UnauthorizedAccessException e) + { + error = new ErrorRecord( + e, + "AccessToCredentialProviderFileDenied", + ErrorCategory.PermissionDenied, + null); + } + catch (Exception ex) + { + error = new ErrorRecord( + ex, + "ErrorRetrievingCredentialProvider", + ErrorCategory.NotSpecified, + null); + } + + return string.Empty; + } + + internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn) + { + string credProviderPath = string.Empty; + + // Find credential provider + // Option 1. Use env var 'NUGET_PLUGIN_PATHS' to find credential provider. + // See: https://docs.microsoft.com/en-us/nuget/reference/extensibility/nuget-cross-platform-plugins#plugin-installation-and-discovery + // Nuget prioritizes credential providers stored in the NUGET_PLUGIN_PATHS env var + credProviderPath = FindCredProviderFromPluginsPath(); + + // Option 2. Check default locations ($env:UserProfile\.nuget\plugins) + // .NET Core based plugins should be installed in: + // %UserProfile%/.nuget/plugins/netcore + // .NET Framework based plugins should be installed in: + // %UserProfile%/.nuget/plugins/netfx + if (String.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = FindCredProviderFromDefaultLocation(); + } + + // Option 3. Check Visual Studio installation paths + if (String.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = FindCredProviderFromVSLocation(out ErrorRecord error); + if (error != null) + { + cmdletPassedIn.WriteError(error); + return null; + } + } + + if (string.IsNullOrEmpty(credProviderPath)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentNullException("Path to the Azure Artifacts Credential Provider is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), + "CredentialProviderPathIsNullOrEmpty", + ErrorCategory.InvalidArgument, + null)); + return null; + } + + // Check case sensitivity here + if (!File.Exists(credProviderPath)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), + "CredentialProviderFileNotFound", + ErrorCategory.ObjectNotFound, + null)); + return null; + } + + cmdletPassedIn.WriteVerbose($"Credential Provider path found at: '{credProviderPath}'"); + + string fileName = credProviderPath; + // If running on unix machines, the Credential Provider needs to be called with dotnet cli. + if (credProviderPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + fileName = "dotnet"; + } + + string arguments = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ? + $"{credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json" : + $"-Uri {uri} -NonInteractive -IsRetry -F Json"; + string fullCallingCmd = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ? + $"dotnet {credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json" : + $"{credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json"; + cmdletPassedIn.WriteVerbose($"Calling Credential Provider with the following: '{fullCallingCmd}'"); + using (Process process = new Process()) + { + // Windows call should look like: "CredentialProvider.Microsoft.exe -Uri -NonInteractive -IsRetry -F Json" + // Unix call should look like: "dotnet CredentialProvider.Microsoft.dll -Uri -NonInteractive -IsRetry -F Json" + process.StartInfo.FileName = fileName; + process.StartInfo.Arguments = arguments; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + var stdError = process.StandardError.ReadToEnd(); + + // Timeout in milliseconds (e.g., 5000 ms = 5 seconds) + process.WaitForExit(5000); + + if (process.ExitCode != 0) + { + if (!string.IsNullOrEmpty(stdError)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Standard error: {stdError}"), + "ProcessStandardError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + + cmdletPassedIn.WriteError(new ErrorRecord( + new Exception($"Process exited with code {process.ExitCode}"), + "ProcessExitCodeError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + else if (string.IsNullOrEmpty(output)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Standard output is empty."), + "ProcessStandardOutputError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + + string username = string.Empty; + SecureString passwordSecure = new SecureString(); + try + { + using (JsonDocument doc = JsonDocument.Parse(output)) + { + JsonElement root = doc.RootElement; + if (root.TryGetProperty("Username", out JsonElement usernameToken)) + { + username = usernameToken.GetString(); + cmdletPassedIn.WriteVerbose("Username retrieved from Credential Provider."); + } + if (String.IsNullOrEmpty(username)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentNullException("Credential Provider username is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."), + "CredentialProviderUserNameIsNullOrEmpty", + ErrorCategory.InvalidArgument, + null)); + return null; + } + + if (root.TryGetProperty("Password", out JsonElement passwordToken)) + { + string password = passwordToken.GetString(); + if (String.IsNullOrEmpty(password)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentNullException("Credential Provider password is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."), + "CredentialProviderUserNameIsNullOrEmpty", + ErrorCategory.InvalidArgument, + null)); + return null; + } + + passwordSecure = Utils.ConvertToSecureString(password); + cmdletPassedIn.WriteVerbose("Password retrieved from Credential Provider."); + } + } + } + catch (Exception e) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new Exception("Error retrieving credentials from Credential Provider. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info.", e), + "InvalidCredentialProviderResponse", + ErrorCategory.InvalidResult, + null)); + return null; + } + + return new PSCredential(username, passwordSecure); + } + } + } +} diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 327d0e024..afb2a1b76 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -202,7 +202,16 @@ public IEnumerable FindByResourceName( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { @@ -386,7 +395,17 @@ public IEnumerable FindByCommandOrDscResource( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { @@ -590,7 +609,17 @@ public IEnumerable FindByTag( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index e31c2b86c..1cf014b98 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -286,7 +286,16 @@ private List ProcessRepositories( string repoName = currentRepository.Name; sourceTrusted = currentRepository.Trusted || trustRepository; - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) @@ -357,59 +366,6 @@ private List ProcessRepositories( return allPkgsInstalled; } - /// - /// Checks if any of the package versions are already installed and if they are removes them from the list of packages to install. - /// - private List FilterByInstalledPkgs(List packages) - { - // Package install paths. - // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable). - // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations. - // e.g.: - // ./InstallPackagePath1/PackageA - // ./InstallPackagePath1/PackageB - // ./InstallPackagePath2/PackageC - // ./InstallPackagePath3/PackageD - - _cmdletPassedIn.WriteDebug("In InstallHelper::FilterByInstalledPkgs()"); - // Get currently installed packages. - var getHelper = new GetHelper(_cmdletPassedIn); - var installedPackageNames = new HashSet(StringComparer.CurrentCultureIgnoreCase); - foreach (var installedPkg in getHelper.GetInstalledPackages( - pkgs: packages, - pathsToSearch: _pathsToSearch)) - { - installedPackageNames.Add(installedPkg.Name); - } - - if (installedPackageNames.Count is 0) - { - return packages; - } - - // Return only packages that are not already installed. - var filteredPackages = new List(); - foreach (var pkg in packages) - { - if (!installedPackageNames.Contains(pkg.Name)) - { - // Add packages that still need to be installed. - filteredPackages.Add(pkg); - } - else - { - // Remove from tracking list of packages to install. - pkg.AdditionalMetadata.TryGetValue("NormalizedVersion", out string normalizedVersion); - _cmdletPassedIn.WriteWarning($"Resource '{pkg.Name}' with version '{normalizedVersion}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter"); - - // Remove from tracking list of packages to install. - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } - } - - return filteredPackages; - } - /// /// Deletes temp directory and is called at end of install process. /// diff --git a/src/code/PSRepositoryInfo.cs b/src/code/PSRepositoryInfo.cs index b74d52cff..1901356c5 100644 --- a/src/code/PSRepositoryInfo.cs +++ b/src/code/PSRepositoryInfo.cs @@ -27,74 +27,70 @@ public enum APIVersion ContainerRegistry } + public enum CredentialProviderType + { + None, + AzArtifacts + } + #endregion #region Constructor - public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, APIVersion apiVersion, bool allowed) + public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, CredentialProviderType credentialProvider, APIVersion apiVersion, bool allowed) { Name = name; Uri = uri; Priority = priority; Trusted = trusted; CredentialInfo = credentialInfo; + CredentialProvider = credentialProvider; ApiVersion = apiVersion; IsAllowedByPolicy = allowed; } #endregion - #region Enum - - public enum RepositoryProviderType - { - None, - ACR, - AzureDevOps - } - - #endregion - #region Properties /// - /// the Name of the repository + /// The Name of the repository. /// public string Name { get; } /// - /// the Uri for the repository + /// The Uri for the repository. /// public Uri Uri { get; } /// - /// whether the repository is trusted + /// Whether the repository is trusted. /// public bool Trusted { get; } /// - /// the priority of the repository + /// The priority of the repository. /// [ValidateRange(0, 100)] public int Priority { get; } /// - /// the type of repository provider (eg, AzureDevOps, ContainerRegistry, etc.) + /// The credential information for repository authentication. /// - public RepositoryProviderType RepositoryProvider { get; } + public PSCredentialInfo CredentialInfo { get; set; } /// - /// the credential information for repository authentication + /// Specifies which credential provider to use. /// - public PSCredentialInfo CredentialInfo { get; } + public CredentialProviderType CredentialProvider { get; set; } /// - /// the API protocol version for the repository + /// The API protocol version for the repository. /// public APIVersion ApiVersion { get; } // - /// is it allowed by policy + /// Specifies whether the repository is allowed by policy. /// public bool IsAllowedByPolicy { get; set; } diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 0eec8e0d9..5d5716a2b 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -379,7 +379,15 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe return; } - _networkCredential = Utils.SetNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (repository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + } // Check if dependencies already exist within the repo if: // 1) the resource to publish has dependencies and diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 1a86db210..943e967c2 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -97,6 +97,12 @@ class RegisterPSResourceRepository : PSCmdlet [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } + /// + /// Specifies which credential provider to use. + /// + [Parameter(ParameterSetName = NameParameterSet)] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + /// /// When specified, displays the succcessfully registered repository and its information. /// @@ -127,6 +133,13 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + PSRepositoryInfo.CredentialProviderType? repoCredentialProvider = null; + if (MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) + { + repoCredentialProvider = CredentialProvider; + } + + switch (ParameterSetName) { case NameParameterSet: @@ -140,7 +153,7 @@ protected override void ProcessRecord() try { - items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, Force, this, out string errorMsg)); + items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, repoCredentialProvider, Force, this, out string errorMsg)); if (!string.IsNullOrEmpty(errorMsg)) { @@ -217,7 +230,8 @@ private PSRepositoryInfo PSGalleryParameterSetHelper(int repoPriority, bool repo repoPriority, repoTrusted, apiVersion: null, - repoCredentialInfo: null, + repoCredentialInfo: null, + credentialProvider: null, Force, this, out string errorMsg); @@ -352,6 +366,21 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + if (repo.ContainsKey("CredentialProvider") && + (repo["CredentialProvider"] == null || String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || + !(repo["CredentialProvider"].ToString().Equals("None", StringComparison.OrdinalIgnoreCase) || + repo["CredentialProvider"].ToString().Equals("AzArtifacts", StringComparison.OrdinalIgnoreCase)))) + { + WriteError(new ErrorRecord( + new PSInvalidOperationException("Repository 'CredentialProvider' must be set to either 'None' or 'AzArtifacts'"), + "InvalidCredentialProviderForRepositoriesParameterSetRegistration", + ErrorCategory.InvalidArgument, + this)); + + return null; + } + + try { WriteDebug($"Registering repository '{repo["Name"]}' with uri '{repoUri}'"); @@ -361,6 +390,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) repo.ContainsKey("Trusted") ? Convert.ToBoolean(repo["Trusted"].ToString()) : DefaultTrusted, apiVersion: repo.ContainsKey("Trusted") ? (PSRepositoryInfo.APIVersion?) repo["ApiVersion"] : null, repoCredentialInfo, + repo.ContainsKey("CredentialProvider") ? (PSRepositoryInfo.CredentialProviderType?)repo["CredentialProvider"] : null, Force, this, out string errorMsg); diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index e9f2693e2..f3299fb49 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -62,7 +62,7 @@ public static void CheckRepositoryStore() // Add PSGallery to the newly created store Uri psGalleryUri = new Uri(PSGalleryRepoUri); - Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, PSRepositoryInfo.APIVersion.V2, force: false); + Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.V2, force: false); } // Open file (which should exist now), if cannot/is corrupted then throw error @@ -76,7 +76,7 @@ public static void CheckRepositoryStore() } } - public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? repoCredentialProvider, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = String.Empty; if (repoName.Equals("PSGallery", StringComparison.OrdinalIgnoreCase)) @@ -85,11 +85,11 @@ public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int r return null; } - return AddToRepositoryStore(repoName, repoUri, repoPriority, repoTrusted, apiVersion, repoCredentialInfo, force, cmdletPassedIn, out errorMsg); + return AddToRepositoryStore(repoName, repoUri, repoPriority, repoTrusted, apiVersion, repoCredentialInfo, repoCredentialProvider, force, cmdletPassedIn, out errorMsg); } - public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; // remove trailing and leading whitespaces, and if Name is just whitespace Name should become null now and be caught by following condition @@ -106,7 +106,7 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri return null; } - PSRepositoryInfo.APIVersion resolvedAPIVersion = apiVersion ?? GetRepoAPIVersion(repoUri); + APIVersion resolvedAPIVersion = apiVersion ?? GetRepoAPIVersion(repoUri); if (repoCredentialInfo != null) { @@ -131,6 +131,13 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri } } + CredentialProviderType resolvedCredentialProvider = credentialProvider ?? CredentialProviderType.None; + // If it's an ADO feed with an ADO designated URL (eg: msazure.pkgs.) then add the 'CredentialProvider' attribute to the repository and by default set it to AzArtifacts + if ((repoUri.AbsoluteUri.Contains("pkgs.dev.azure.com") || repoUri.AbsoluteUri.Contains("pkgs.visualstudio.com")) && credentialProvider == null) + { + resolvedCredentialProvider = CredentialProviderType.AzArtifacts; + } + if (!cmdletPassedIn.ShouldProcess(repoName, "Register repository to repository store")) { return null; @@ -141,13 +148,13 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri return null; } - var repo = RepositorySettings.Add(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, resolvedAPIVersion, force); + var repo = Add(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, resolvedCredentialProvider, resolvedAPIVersion, force); return repo; } - public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, bool isSet, int defaultPriority, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, bool isSet, int defaultPriority, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; // repositories with Uri Scheme "temp" may have PSPath Uri's like: "Temp:\repo" @@ -182,7 +189,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr // determine trusted value to pass in (true/false if set, null otherwise, hence the nullable bool variable) bool? _trustedNullable = isSet ? new bool?(repoTrusted) : new bool?(); - + if (repoCredentialInfo != null) { bool isSecretManagementModuleAvailable = Utils.IsSecretManagementModuleAvailable(repoName, cmdletPassedIn); @@ -216,7 +223,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr return null; } - return Update(repoName, repoUri, repoPriority, _trustedNullable, apiVersion, repoCredentialInfo, cmdletPassedIn, out errorMsg); + return Update(repoName, repoUri, repoPriority, _trustedNullable, apiVersion, repoCredentialInfo, credentialProvider, cmdletPassedIn, out errorMsg); } /// @@ -224,7 +231,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr /// Returns: PSRepositoryInfo containing information about the repository just added to the repository store /// /// - public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSCredentialInfo repoCredentialInfo, PSRepositoryInfo.APIVersion apiVersion, bool force) + public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSCredentialInfo repoCredentialInfo, CredentialProviderType repoCredentialProvider, APIVersion apiVersion, bool force) { try { @@ -238,7 +245,7 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit } // Delete the existing repository before overwriting it (otherwire multiple repos with the same name will be added) - List removedRepositories = RepositorySettings.Remove(new string[] { repoName }, out string[] errorList); + List removedRepositories = Remove(new string[] { repoName }, out string[] errorList); // Need to load the document again because of changes after removing doc = LoadXDocument(FullRepositoryPath); @@ -261,7 +268,8 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit new XAttribute("Url", repoUri), new XAttribute("APIVersion", apiVersion), new XAttribute("Priority", repoPriority), - new XAttribute("Trusted", repoTrusted) + new XAttribute("Trusted", repoTrusted), + new XAttribute("CredentialProvider", repoCredentialProvider) ); if (repoCredentialInfo != null) @@ -282,14 +290,14 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repoUri) : true; - return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, apiVersion, isAllowed); + return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, repoCredentialProvider, apiVersion, isAllowed); } /// /// Updates a repository name, Uri, priority, installation policy, or credential information /// Returns: void /// - public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPriority, bool? repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPriority, bool? repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; PSRepositoryInfo updatedRepo; @@ -303,7 +311,7 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio bool repoIsTrusted = !(repoTrusted == null || repoTrusted == false); repoPriority = repoPriority < 0 ? DefaultPriority : repoPriority; - return AddToRepositoryStore(repoName, repoUri, repoPriority, repoIsTrusted, apiVersion, repoCredentialInfo, force:true, cmdletPassedIn, out errorMsg); + return AddToRepositoryStore(repoName, repoUri, repoPriority, repoIsTrusted, apiVersion, repoCredentialInfo, credentialProvider, force:true, cmdletPassedIn, out errorMsg); } // Check that repository node we are attempting to update has all required attributes: Name, Url (or Uri), Priority, Trusted. @@ -417,15 +425,15 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio } // Update APIVersion if necessary - PSRepositoryInfo.APIVersion resolvedAPIVersion = PSRepositoryInfo.APIVersion.Unknown; + APIVersion resolvedAPIVersion = APIVersion.Unknown; if (apiVersion != null) { - resolvedAPIVersion = (PSRepositoryInfo.APIVersion)apiVersion; + resolvedAPIVersion = (APIVersion)apiVersion; node.Attribute("APIVersion").Value = resolvedAPIVersion.ToString(); } else { - resolvedAPIVersion = (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true); + resolvedAPIVersion = (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true); } @@ -445,14 +453,29 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio } + // Update CredentialProvider if necessary + CredentialProviderType resolvedCredentialProvider = credentialProvider ?? CredentialProviderType.None; + if (credentialProvider != null) + { + resolvedCredentialProvider = (CredentialProviderType)credentialProvider; + if (node.Attribute("CredentialProvider") == null) + { + node.Add(new XAttribute("CredentialProvider", resolvedCredentialProvider.ToString())); + } + else + { + node.Attribute("CredentialProvider").Value = resolvedCredentialProvider.ToString(); + } + } + bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); updatedRepo = new PSRepositoryInfo(repoName, thisUrl, Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), thisCredentialInfo, + resolvedCredentialProvider, resolvedAPIVersion, isAllowed); @@ -523,6 +546,12 @@ public static List Remove(string[] repoNames, out string[] err continue; } + CredentialProviderType resolvedCredentialProvider = CredentialProviderType.None; + if (node.Attribute("CredentialProvider") != null) + { + resolvedCredentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), node.Attribute("CredentialProvider").Value, ignoreCase: true); + } + // determine if repo had Url or Uri (less likely) attribute bool urlAttributeExists = node.Attribute("Url") != null; bool uriAttributeExists = node.Attribute("Uri") != null; @@ -537,14 +566,14 @@ public static List Remove(string[] repoNames, out string[] err bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repoUri) : true; - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(repoUri); removedRepos.Add( new PSRepositoryInfo(repo, new Uri(node.Attribute(attributeUrlUriName).Value), Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), repoCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), + resolvedCredentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), isAllowed)); // Remove item from file @@ -630,13 +659,19 @@ public static List Read(string[] repoNames, out string[] error if (repo.Attribute("APIVersion") == null) { - PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(thisUrl); + APIVersion apiVersion = GetRepoAPIVersion(thisUrl); XElement repoXElem = FindRepositoryElement(doc, repo.Attribute("Name").Value); repoXElem.SetAttributeValue("APIVersion", apiVersion.ToString()); doc.Save(FullRepositoryPath); } + CredentialProviderType credentialProvider = CredentialProviderType.None; + if (repo.Attribute("CredentialProvider") != null) + { + credentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), repo.Attribute("CredentialProvider").Value, ignoreCase: true); + } + PSCredentialInfo thisCredentialInfo; string credentialInfoErrorMessage = $"Repository {repo.Attribute("Name").Value} has invalid CredentialInfo. {PSCredentialInfo.VaultNameAttribute} and {PSCredentialInfo.SecretNameAttribute} should both be present and non-empty"; // both keys are present @@ -669,8 +704,6 @@ public static List Read(string[] repoNames, out string[] error continue; } - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); - bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; PSRepositoryInfo currentRepoItem = new PSRepositoryInfo(repo.Attribute("Name").Value, @@ -678,7 +711,8 @@ public static List Read(string[] repoNames, out string[] error Int32.Parse(repo.Attribute("Priority").Value), Boolean.Parse(repo.Attribute("Trusted").Value), thisCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), repo.Attribute("APIVersion").Value, ignoreCase: true), + credentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), repo.Attribute("APIVersion").Value, ignoreCase: true), isAllowed); foundRepos.Add(currentRepoItem); @@ -738,13 +772,19 @@ public static List Read(string[] repoNames, out string[] error if (node.Attribute("APIVersion") == null) { - PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(thisUrl); + APIVersion apiVersion = GetRepoAPIVersion(thisUrl); XElement repoXElem = FindRepositoryElement(doc, node.Attribute("Name").Value); repoXElem.SetAttributeValue("APIVersion", apiVersion.ToString()); doc.Save(FullRepositoryPath); } + CredentialProviderType credentialProvider = CredentialProviderType.None; + if (node.Attribute("CredentialProvider") != null) + { + credentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), node.Attribute("CredentialProvider").Value, ignoreCase: true); + } + PSCredentialInfo thisCredentialInfo; string credentialInfoErrorMessage = $"Repository {node.Attribute("Name").Value} has invalid CredentialInfo. {PSCredentialInfo.VaultNameAttribute} and {PSCredentialInfo.SecretNameAttribute} should both be present and non-empty"; // both keys are present @@ -777,8 +817,6 @@ public static List Read(string[] repoNames, out string[] error continue; } - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); - bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; PSRepositoryInfo currentRepoItem = new PSRepositoryInfo(node.Attribute("Name").Value, @@ -786,7 +824,8 @@ public static List Read(string[] repoNames, out string[] error Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), thisCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), + credentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), isAllowed); foundRepos.Add(currentRepoItem); @@ -840,54 +879,38 @@ private static XDocument LoadXDocument(string filePath) return XDocument.Load(xmlReader); } - private static PSRepositoryInfo.APIVersion GetRepoAPIVersion(Uri repoUri) + private static APIVersion GetRepoAPIVersion(Uri repoUri) { if (repoUri.AbsoluteUri.EndsWith("/v2", StringComparison.OrdinalIgnoreCase)) { // Scenario: V2 server protocol repositories (i.e PSGallery) - return PSRepositoryInfo.APIVersion.V2; + return APIVersion.V2; } else if (repoUri.AbsoluteUri.EndsWith("/index.json", StringComparison.OrdinalIgnoreCase)) { // Scenario: V3 server protocol repositories (i.e NuGet.org, Azure Artifacts (ADO), Artifactory, Github Packages, MyGet.org) - return PSRepositoryInfo.APIVersion.V3; + return APIVersion.V3; } else if (repoUri.AbsoluteUri.EndsWith("/nuget", StringComparison.OrdinalIgnoreCase)) { // Scenario: ASP.Net application feed created with NuGet.Server to host packages - return PSRepositoryInfo.APIVersion.NugetServer; + return APIVersion.NugetServer; } else if (repoUri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase) || repoUri.Scheme.Equals("temp", StringComparison.OrdinalIgnoreCase)) { // repositories with Uri Scheme "temp" may have PSPath Uri's like: "Temp:\repo" and we should consider them as local repositories. - return PSRepositoryInfo.APIVersion.Local; + return APIVersion.Local; } else if (repoUri.AbsoluteUri.EndsWith(".azurecr.io") || repoUri.AbsoluteUri.EndsWith(".azurecr.io/") || repoUri.AbsoluteUri.Contains("mcr.microsoft.com")) { - return PSRepositoryInfo.APIVersion.ContainerRegistry; + return APIVersion.ContainerRegistry; } else { - return PSRepositoryInfo.APIVersion.Unknown; + return APIVersion.Unknown; } } - private static RepositoryProviderType GetRepositoryProviderType(Uri repoUri) - { - string absoluteUri = repoUri.AbsoluteUri; - // We want to use contains instead of EndsWith to accomodate for trailing '/' - if (absoluteUri.Contains("azurecr.io") || absoluteUri.Contains("mcr.microsoft.com")){ - return RepositoryProviderType.ACR; - } - // TODO: add a regex for this match - // eg: *pkgs.*/_packaging/* - else if (absoluteUri.Contains("pkgs.")){ - return RepositoryProviderType.AzureDevOps; - } - else { - return RepositoryProviderType.None; - } - } #endregion } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index f90646fa2..aea53adbf 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -94,6 +94,12 @@ public SwitchParameter Trusted [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } + /// + /// Specifies which credential provider to use. + /// + [Parameter(ParameterSetName = NameParameterSet)] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + /// /// When specified, displays the successfully registered repository and its information. /// @@ -118,10 +124,11 @@ protected override void ProcessRecord() !MyInvocation.BoundParameters.ContainsKey(nameof(Priority)) && !MyInvocation.BoundParameters.ContainsKey(nameof(Trusted)) && !MyInvocation.BoundParameters.ContainsKey(nameof(ApiVersion)) && - !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialInfo))) + !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialInfo)) && + !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) { ThrowTerminatingError(new ErrorRecord( - new ArgumentException("Must set Uri, Priority, Trusted, ApiVersion, or CredentialInfo parameter"), + new ArgumentException("Must set Uri, Priority, Trusted, ApiVersion, CredentialInfo, or CredentialProvider parameter"), "SetRepositoryParameterBindingFailure", ErrorCategory.InvalidArgument, this)); @@ -151,11 +158,12 @@ protected override void ProcessRecord() items.Add(RepositorySettings.UpdateRepositoryStore(Name, _uri, Priority, - Trusted, + Trusted, isSet, DefaultPriority, repoApiVersion, CredentialInfo, + CredentialProvider, this, out string errorMsg)); @@ -293,6 +301,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) DefaultPriority, ApiVersion, repoCredentialInfo, + CredentialProvider, this, out string errorMsg); diff --git a/src/code/Utils.cs b/src/code/Utils.cs index da80d3f42..5b83ad362 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -927,7 +927,7 @@ public static bool IsSecretManagementVaultAccessible( } } - public static NetworkCredential SetNetworkCredential( + public static NetworkCredential SetSecretManagementNetworkCredential( PSRepositoryInfo repository, NetworkCredential networkCredential, PSCmdlet cmdletPassedIn) @@ -948,6 +948,30 @@ public static NetworkCredential SetNetworkCredential( return networkCredential; } + public static NetworkCredential SetCredentialProviderNetworkCredential( + PSRepositoryInfo repository, + NetworkCredential networkCredential, + PSCmdlet cmdletPassedIn) + { + // Explicitly passed in Credential takes precedence over repository credential provider. + if (networkCredential == null) + { + cmdletPassedIn.WriteVerbose("Attempting to retrieve credentials from Azure Artifacts Credential Provider."); + PSCredential repoCredential = CredentialProvider.GetCredentialsFromProvider(repository.Uri, cmdletPassedIn); + if (repoCredential == null) + { + cmdletPassedIn.WriteVerbose("Unable to retrieve credentials from Azure Artifacts Credential Provider. Network credentials are null."); + } + else + { + networkCredential = new NetworkCredential(repoCredential.UserName, repoCredential.Password); + cmdletPassedIn.WriteVerbose("Credential successfully read from Azure Artifacts Credential Provider for repository: " + repository.Name); + } + } + + return networkCredential; + } + #endregion #region Path methods @@ -1522,6 +1546,23 @@ public static bool TryCreateModuleSpecification( return moduleSpecCreatedSuccessfully; } + public static SecureString ConvertToSecureString(string input) + { + if (input == null) { + throw new ArgumentNullException(nameof(input)); + } + + SecureString secureString = new SecureString(); + foreach (char c in input) + { + secureString.AppendChar(c); + } + + secureString.MakeReadOnly(); + + return secureString; + } + #endregion #region Directory and File From 6d5251409cdbae8ef24f11d60c11dfad63b928bd Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:49:05 -0800 Subject: [PATCH 02/89] Add dynamic parameters to Register-PSResourceRepository and Set-PSResourceRepository --- src/code/RegisterPSResourceRepository.cs | 45 ++++++++++++++++-------- src/code/SetPSResourceRepository.cs | 37 +++++++++++++------ 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 943e967c2..d6186fdef 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Management.Automation; using Dbg = System.Diagnostics.Debug; @@ -23,7 +24,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)] public sealed - class RegisterPSResourceRepository : PSCmdlet + class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters { #region Members @@ -35,6 +36,7 @@ class RegisterPSResourceRepository : PSCmdlet private const string PSGalleryParameterSet = "PSGalleryParameterSet"; private const string RepositoriesParameterSet = "RepositoriesParameterSet"; private Uri _uri; + private CredentialProviderDynamicParameters _credentialProvider; #endregion @@ -97,12 +99,6 @@ class RegisterPSResourceRepository : PSCmdlet [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } - /// - /// Specifies which credential provider to use. - /// - [Parameter(ParameterSetName = NameParameterSet)] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } - /// /// When specified, displays the succcessfully registered repository and its information. /// @@ -117,6 +113,21 @@ class RegisterPSResourceRepository : PSCmdlet #endregion + #region DynamicParameters + + public object GetDynamicParameters() + { + if (Uri.Contains("pkgs.dev.azure.com") || Uri.Contains("pkgs.visualstudio.com")) + { + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; + } + + return null; + } + + #endregion + #region Methods protected override void BeginProcessing() @@ -133,12 +144,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType? repoCredentialProvider = null; - if (MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) - { - repoCredentialProvider = CredentialProvider; - } - + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; switch (ParameterSetName) { @@ -153,7 +159,7 @@ protected override void ProcessRecord() try { - items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, repoCredentialProvider, Force, this, out string errorMsg)); + items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, credentialProvider, Force, this, out string errorMsg)); if (!string.IsNullOrEmpty(errorMsg)) { @@ -367,7 +373,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) } if (repo.ContainsKey("CredentialProvider") && - (repo["CredentialProvider"] == null || String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || + (String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || !(repo["CredentialProvider"].ToString().Equals("None", StringComparison.OrdinalIgnoreCase) || repo["CredentialProvider"].ToString().Equals("AzArtifacts", StringComparison.OrdinalIgnoreCase)))) { @@ -429,4 +435,13 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) #endregion } + + public class CredentialProviderDynamicParameters + { + /// + /// Specifies which credential provider to use. + /// + [Parameter] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index aea53adbf..8f35a323b 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Management.Automation; using Dbg = System.Diagnostics.Debug; @@ -18,7 +19,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets "PSResourceRepository", DefaultParameterSetName = NameParameterSet, SupportsShouldProcess = true)] - public sealed class SetPSResourceRepository : PSCmdlet + public sealed class SetPSResourceRepository : PSCmdlet, IDynamicParameters { #region Members @@ -26,6 +27,7 @@ public sealed class SetPSResourceRepository : PSCmdlet private const string RepositoriesParameterSet = "RepositoriesParameterSet"; private const int DefaultPriority = -1; private Uri _uri; + private CredentialProviderDynamicParameters _credentialProvider; #endregion @@ -92,13 +94,7 @@ public SwitchParameter Trusted /// Specifies vault and secret names as PSCredentialInfo for the repository. /// [Parameter(ParameterSetName = NameParameterSet)] - public PSCredentialInfo CredentialInfo { get; set; } - - /// - /// Specifies which credential provider to use. - /// - [Parameter(ParameterSetName = NameParameterSet)] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + public PSCredentialInfo CredentialInfo { get; set; } /// /// When specified, displays the successfully registered repository and its information. @@ -108,6 +104,23 @@ public SwitchParameter Trusted #endregion + #region DynamicParameters + + public object GetDynamicParameters() + { + PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); + if (repository is not null && + (repository.Uri.AbsoluteUri.Contains("pkgs.dev.azure.com") || repository.Uri.AbsoluteUri.Contains("pkgs.visualstudio.com"))) + { + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; + } + + return null; + } + + #endregion + #region Private methods protected override void BeginProcessing() @@ -148,6 +161,8 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + List items = new List(); switch(ParameterSetName) @@ -163,7 +178,7 @@ protected override void ProcessRecord() DefaultPriority, repoApiVersion, CredentialInfo, - CredentialProvider, + credentialProvider, this, out string errorMsg)); @@ -291,6 +306,8 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + try { var updatedRepo = RepositorySettings.UpdateRepositoryStore(repo["Name"].ToString(), @@ -301,7 +318,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) DefaultPriority, ApiVersion, repoCredentialInfo, - CredentialProvider, + credentialProvider, this, out string errorMsg); From fe270a1fc5e68ced2757bc0e82417c115f7f1c7a Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 18 Dec 2024 22:04:10 -0800 Subject: [PATCH 03/89] Bug fixes and clean up --- src/code/CredentialProvider.cs | 62 +++++++++++++++++------- src/code/RegisterPSResourceRepository.cs | 24 ++++++--- src/code/SetPSResourceRepository.cs | 14 +++--- src/code/Utils.cs | 16 ++++++ 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs index 45720380f..3cd1ee738 100644 --- a/src/code/CredentialProvider.cs +++ b/src/code/CredentialProvider.cs @@ -4,11 +4,16 @@ using System.Security; using System.Management.Automation; using System.Text.Json; +using System.Net.Http; +using System.Net; namespace Microsoft.PowerShell.PSResourceGet.UtilClasses { internal static class CredentialProvider { + private static readonly string _credProviderExe = "CredentialProvider.Microsoft.exe"; + private static readonly string _credProviderDll = "CredentialProvider.Microsoft.dll"; + private static string FindCredProviderFromPluginsPath() { // Get environment variable "NUGET_PLUGIN_PATHS" @@ -33,16 +38,16 @@ private static string FindCredProviderFromDefaultLocation() { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.exe"); + credProviderPath = Path.Combine(netCorePath, _credProviderExe); } else { - credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.dll"); + credProviderPath = Path.Combine(netCorePath, _credProviderDll); } } else if (Directory.Exists(netFxPath) && Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = Path.Combine(netFxPath, "CredentialProvider.Microsoft.exe"); + credProviderPath = Path.Combine(netFxPath, _credProviderExe); } return credProviderPath; @@ -62,11 +67,11 @@ private static string FindCredProviderFromVSLocation(out ErrorRecord error) { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.exe", out error); + credProviderPath = VSCredentialProviderFile(visualStudioPath, _credProviderExe, out error); } else if (string.IsNullOrEmpty(credProviderPath)) { - credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.dll", out error); + credProviderPath = VSCredentialProviderFile(visualStudioPath, _credProviderDll, out error); } } @@ -108,8 +113,9 @@ private static string VSCredentialProviderFile(string visualStudioPath, string c internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn) { + cmdletPassedIn.WriteDebug("Enterting CredentialProvider::GetCredentialsFromProvider"); string credProviderPath = string.Empty; - + // Find credential provider // Option 1. Use env var 'NUGET_PLUGIN_PATHS' to find credential provider. // See: https://docs.microsoft.com/en-us/nuget/reference/extensibility/nuget-cross-platform-plugins#plugin-installation-and-discovery @@ -137,25 +143,47 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet } } + cmdletPassedIn.WriteDebug($"Credential provider path is '{credProviderPath}'"); if (string.IsNullOrEmpty(credProviderPath)) { cmdletPassedIn.WriteError(new ErrorRecord( new ArgumentNullException("Path to the Azure Artifacts Credential Provider is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), "CredentialProviderPathIsNullOrEmpty", ErrorCategory.InvalidArgument, - null)); + credProviderPath)); return null; } - // Check case sensitivity here if (!File.Exists(credProviderPath)) { - cmdletPassedIn.WriteError(new ErrorRecord( - new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), - "CredentialProviderFileNotFound", - ErrorCategory.ObjectNotFound, - null)); - return null; + // If the Credential Provider is not found on a Unix machine, try looking for a case insensitive file. + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + FileInfo fileInfo = new FileInfo(credProviderPath); + string resolvedFilePath = Utils.GetCaseInsensitiveFilePath(fileInfo.Directory.FullName, _credProviderDll); + if (resolvedFilePath != null) + { + credProviderPath = resolvedFilePath; + } + else + { + cmdletPassedIn.WriteError(new ErrorRecord( + new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), + "CredentialProviderFileNotFound", + ErrorCategory.ObjectNotFound, + credProviderPath)); + } + } + else + { + cmdletPassedIn.WriteError(new ErrorRecord( + new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), + "CredentialProviderFileNotFound", + ErrorCategory.ObjectNotFound, + credProviderPath)); + + return null; + } } cmdletPassedIn.WriteVerbose($"Credential Provider path found at: '{credProviderPath}'"); @@ -234,7 +262,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet new ArgumentNullException("Credential Provider username is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."), "CredentialProviderUserNameIsNullOrEmpty", ErrorCategory.InvalidArgument, - null)); + credProviderPath)); return null; } @@ -247,7 +275,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet new ArgumentNullException("Credential Provider password is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."), "CredentialProviderUserNameIsNullOrEmpty", ErrorCategory.InvalidArgument, - null)); + credProviderPath)); return null; } @@ -262,7 +290,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet new Exception("Error retrieving credentials from Credential Provider. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info.", e), "InvalidCredentialProviderResponse", ErrorCategory.InvalidResult, - null)); + credProviderPath)); return null; } diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index d6186fdef..8177d0e7a 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -117,13 +117,13 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { - if (Uri.Contains("pkgs.dev.azure.com") || Uri.Contains("pkgs.visualstudio.com")) + if(Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { - _credentialProvider = new CredentialProviderDynamicParameters(); - return _credentialProvider; + return null; } - return null; + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; } #endregion @@ -144,7 +144,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; switch (ParameterSetName) { @@ -438,10 +438,22 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) public class CredentialProviderDynamicParameters { + PSRepositoryInfo.CredentialProviderType? _credProvider = null; + /// /// Specifies which credential provider to use. /// [Parameter] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + public PSRepositoryInfo.CredentialProviderType? CredentialProvider { + get + { + return _credProvider; + } + + set + { + _credProvider = value; + } + } } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index 8f35a323b..d3d341181 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -109,14 +109,14 @@ public SwitchParameter Trusted public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); - if (repository is not null && - (repository.Uri.AbsoluteUri.Contains("pkgs.dev.azure.com") || repository.Uri.AbsoluteUri.Contains("pkgs.visualstudio.com"))) + if (repository is not null && + (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { - _credentialProvider = new CredentialProviderDynamicParameters(); - return _credentialProvider; + return null; } - return null; + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; } #endregion @@ -161,7 +161,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; List items = new List(); @@ -306,7 +306,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; try { diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 5b83ad362..bcd964de1 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1238,6 +1238,22 @@ internal static void GetMetadataFilesFromPath(string dirPath, string packageName } } } + + internal static string GetCaseInsensitiveFilePath(string directory, string fileName) + { + var files = Directory.GetFiles(directory); + foreach (var file in files) + { + if (string.Equals(Path.GetFileName(file), fileName, StringComparison.OrdinalIgnoreCase)) + { + return file; + } + } + + // File not found + return null; + } + #endregion #region PSDataFile parsing From a99b2b293640fda8fb495b8621fd8662cb1040f3 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:44:43 -0800 Subject: [PATCH 04/89] Add tests --- test/CredentialProvider.Tests.ps1 | 74 +++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/CredentialProvider.Tests.ps1 diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 new file mode 100644 index 000000000..7a651be1f --- /dev/null +++ b/test/CredentialProvider.Tests.ps1 @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force + +Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { + + BeforeAll{ + $TestModuleName = "PackageManagement" + $ADORepoName = "ADORepository" + $ADORepoUri = "https://pkgs.dev.azure.com/mscodehub/PowerShellCore/_packaging/PowerShellCore_PublicPackages/nuget/v2" + $LocalRepoName = "LocalRepository" + $LocalRepoUri = Join-Path -Path $TestDrive -ChildPath "testdir" + $null = New-Item $LocalRepoUri -ItemType Directory -Force + + Get-NewPSResourceRepositoryFile + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Trusted + } + + AfterAll { + Uninstall-PSResource $TestModuleName -SkipDependencyCheck -ErrorAction SilentlyContinue + + Get-RevertPSResourceRepositoryFile + } + + It "Find resource given specific Name and Repository" { + $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName + $res.Name | Should -Be $TestModuleName + } + + It "Install resource given specific Name and Repository" { + Install-PSResource -Name $TestModuleName -Repository $ADORepoName + + Get-InstalledPSResource -Name $TestModuleName | Should -Not -BeNullOrEmpty + } + + It "Register repository with local path (CredentialProvider should be set to 'None')" { + Register-PSResourceRepository -Name $LocalRepoName -Uri $LocalRepoUri -Force + $repo = Get-PSResourceRepository -Name $LocalRepoName + $repo.CredentialProvider | Should -Be "None" + } + + It "Set CredentialProvider for local path repository" { + Register-PSResourceRepository -Name $LocalRepoName -Uri $LocalRepoUri -Trusted -Force + $repo = Get-PSResourceRepository -Name $LocalRepoName + $repo.CredentialProvider | Should -Be "None" + + Set-PSResourceRepository -Name $LocalRepoName -CredentialProvider AzArtifacts + $repo2 = Get-PSResourceRepository -Name $LocalRepoName + $repo2.CredentialProvider | Should -Be "AzArtifacts" + } + + It "Register repository with ADO Uri (CredentialProvider should be set to 'AzArtifacts')" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "AzArtifacts" + } + + It "Set CredentialProvider for ADO repository" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Trusted -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "AzArtifacts" + + Set-PSResourceRepository -Name $ADORepoName -CredentialProvider None + $repo2 = Get-PSResourceRepository -Name $ADORepoName + $repo2.CredentialProvider | Should -Be "None" + } + + It "Register repository with ADO Uri (CredentialProvider should be set to 'AzArtifacts')" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -CredentialProvider None -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "None" + } +} From fc81545a8c03b991bd5bc073c005d9d4d6aba15e Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:26:07 -0800 Subject: [PATCH 05/89] Update AdoV2 server tests --- test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 index 253dad68e..a4548a8c8 100644 --- a/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 @@ -14,7 +14,7 @@ Describe 'Test HTTP Find-PSResource for ADO V2 Server Protocol' -tags 'CI' { $ADOV2RepoName = "PSGetTestingPublicFeed" $ADOV2RepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/psresourceget-public-test-ci/nuget/v2" Get-NewPSResourceRepositoryFile - Register-PSResourceRepository -Name $ADOV2RepoName -Uri $ADOV2RepoUri + Register-PSResourceRepository -Name $ADOV2RepoName -Uri $ADOV2RepoUri -CredentialProvider "None" } AfterAll { From 7f5f0ae92f5cef82f5775eba50ae61e6b9712f7a Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:40:54 -0800 Subject: [PATCH 06/89] Add logging for GH pkgs tests --- test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 index 0d1dc4557..7bc2e09ea 100644 --- a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 @@ -24,6 +24,8 @@ Describe 'Test HTTP Find-PSResource for Github Packages Server' -tags 'CI' { } It "find resource given specific Name, Version null" { + $repo = Get-PSResourceRepository $GithubPackagesRepoName + Write-Error "Repo: $repo; repo credential provider: $($repo.CredentialProvider)" # FindName() $res = Find-PSResource -Name $testModuleName -Repository $GithubPackagesRepoName -Credential $credential $res.Name | Should -Be $testModuleName From 01ab7603ef0a664aaa3af04888a232cc9b8e1ad8 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:35:22 -0800 Subject: [PATCH 07/89] Remove GH pkgs logging --- test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 index 7bc2e09ea..0d1dc4557 100644 --- a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 @@ -24,8 +24,6 @@ Describe 'Test HTTP Find-PSResource for Github Packages Server' -tags 'CI' { } It "find resource given specific Name, Version null" { - $repo = Get-PSResourceRepository $GithubPackagesRepoName - Write-Error "Repo: $repo; repo credential provider: $($repo.CredentialProvider)" # FindName() $res = Find-PSResource -Name $testModuleName -Repository $GithubPackagesRepoName -Credential $credential $res.Name | Should -Be $testModuleName From 43a69e7867c857c283602fc7252a64be6f6185d3 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:58:54 -0800 Subject: [PATCH 08/89] Rename tests --- .../InstallPSResourceADOServer.Tests.ps1 | 4 ++-- .../InstallPSResourceADOV2Server.Tests.ps1 | 4 ++-- .../InstallPSResourceContainerRegistryServer.Tests.ps1 | 2 +- .../InstallPSResourceGithubPackages.Tests.ps1 | 2 +- .../InstallPSResourceV3Server.Tests.ps1 | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 index fbf32c59e..834ea8690 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 @@ -5,7 +5,7 @@ $ProgressPreference = "SilentlyContinue" $modPath = "$psscriptroot/../PSGetTestUtils.psm1" Import-Module $modPath -Force -Verbose -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { +Describe 'Test Install-PSResource for ADO V3Server scenarios' -tags 'CI' { BeforeAll { $testModuleName = "test_local_mod" @@ -216,7 +216,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for ADO V3Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 index 589bff92b..124642f6d 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 @@ -5,7 +5,7 @@ $ProgressPreference = "SilentlyContinue" $modPath = "$psscriptroot/../PSGetTestUtils.psm1" Import-Module $modPath -Force -Verbose -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { +Describe 'Test Install-PSResource for ADO V2Server scenarios' -tags 'CI' { BeforeAll { $testModuleName = "test_local_mod" @@ -217,7 +217,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for ADO V2Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index 2ade007f2..3fa1ca304 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -261,7 +261,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for Container Registry scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 index 7c4e68d6a..62d92069a 100644 --- a/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 @@ -222,7 +222,7 @@ Describe 'Test Install-PSResource for GitHub packages' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for GitHub Packages scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 index 3e4f82823..2eef494b5 100644 --- a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 @@ -377,7 +377,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for V3Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" From bfeb5e053ed4c360f63da4d20a583c39b9381ba9 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:54:34 -0800 Subject: [PATCH 09/89] Add Verbose messaging --- src/code/CredentialProvider.cs | 4 ++-- test/CredentialProvider.Tests.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs index 3cd1ee738..52c731b3c 100644 --- a/src/code/CredentialProvider.cs +++ b/src/code/CredentialProvider.cs @@ -113,7 +113,7 @@ private static string VSCredentialProviderFile(string visualStudioPath, string c internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn) { - cmdletPassedIn.WriteDebug("Enterting CredentialProvider::GetCredentialsFromProvider"); + cmdletPassedIn.WriteVerbose("Enterting CredentialProvider::GetCredentialsFromProvider"); string credProviderPath = string.Empty; // Find credential provider @@ -143,7 +143,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet } } - cmdletPassedIn.WriteDebug($"Credential provider path is '{credProviderPath}'"); + cmdletPassedIn.WriteVerbose($"Credential provider path is '{credProviderPath}'"); if (string.IsNullOrEmpty(credProviderPath)) { cmdletPassedIn.WriteError(new ErrorRecord( diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 7a651be1f..50c26646d 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -24,7 +24,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { } It "Find resource given specific Name and Repository" { - $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName + $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName } From 26441834492ee4575a470ef05c63c1cde410c2fc Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:49:56 -0800 Subject: [PATCH 10/89] Update dynamic parameters for Set and Register repositories --- src/code/RegisterPSResourceRepository.cs | 3 ++- src/code/SetPSResourceRepository.cs | 3 ++- .../ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 8177d0e7a..6a3d69194 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -117,7 +117,8 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { - if(Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) + // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. + if (ParameterSetName.Equals("PSGalleryParameterSet") || Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { return null; } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index d3d341181..39b285f15 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -109,7 +109,8 @@ public SwitchParameter Trusted public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); - if (repository is not null && + // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. + if (repository is not null && repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { return null; diff --git a/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 index 4f1279b87..fa9120dfe 100644 --- a/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 +++ b/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 @@ -154,7 +154,7 @@ Describe "Test Set-PSResourceRepository" -tags 'CI' { $hashtable4 = @{Name = $PSGalleryName; Trusted = $True}; $arrayOfHashtables = $hashtable1, $hashtable2, $hashtable3, $hashtable4 - Set-PSResourceRepository -Repository $arrayOfHashtables + Set-PSResourceRepository -Repository $arrayOfHashtables -Verbose $res = Get-PSResourceRepository -Name $TestRepoName1 $res.Name | Should -Be $TestRepoName1 $Res.Uri.LocalPath | Should -Contain $tmpDir2Path @@ -189,7 +189,7 @@ Describe "Test Set-PSResourceRepository" -tags 'CI' { It "not set and throw error for trying to set PSGallery Uri (NameParameterSet)" { Unregister-PSResourceRepository -Name $PSGalleryName Register-PSResourceRepository -PSGallery - {Set-PSResourceRepository -Name $PSGalleryName -Uri $tmpDir1Path -ErrorAction Stop} | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" + {Set-PSResourceRepository -Name $PSGalleryName -Uri $tmpDir1Path -Verbose -ErrorAction Stop} | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" } It "not set and throw error for trying to set PSGallery CredentialInfo (NameParameterSet)" { From b11eb71c5632c20f13fd1c78b2a28c676599af67 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:20:44 -0800 Subject: [PATCH 11/89] Dynamic param should not appear when using Repositories param set, comment out cred provider tests --- global.json | 2 +- src/code/RegisterPSResourceRepository.cs | 5 ++++- test/CredentialProvider.Tests.ps1 | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/global.json b/global.json index 120c43985..b832c3a01 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.403" + "version": "8.0.404" } } diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 6a3d69194..d1b0adf24 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -118,7 +118,10 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. - if (ParameterSetName.Equals("PSGalleryParameterSet") || Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) + // It should also not appear when using the 'Repositories' parameter set. + if (ParameterSetName.Equals(PSGalleryParameterSet) || + ParameterSetName.Equals(RepositoriesParameterSet) || + Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { return null; } diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 50c26646d..4be26cfe9 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -22,7 +22,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { Get-RevertPSResourceRepositoryFile } - +<# It "Find resource given specific Name and Repository" { $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName @@ -71,4 +71,5 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { $repo = Get-PSResourceRepository -Name $ADORepoName $repo.CredentialProvider | Should -Be "None" } + #> } From d3154edf56750a35697078df5cbfd4fadbab5400 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:20:56 -0800 Subject: [PATCH 12/89] Set dynamic param updates --- src/code/SetPSResourceRepository.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index 39b285f15..3582d571a 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -110,8 +110,11 @@ public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. - if (repository is not null && repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || - (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) + // It should also not appear when using the 'Repositories' parameter set. + if (repository is not null && + (repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || + RepositoriesParameterSet.Equals(RepositoriesParameterSet) || + repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { return null; } From 317463d7ff9d0f67d487fa076a6fbbedabe91160 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 26 Dec 2024 10:47:54 -0800 Subject: [PATCH 13/89] Add -CredentialProvider None to existing ADO tests --- .../InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 | 2 +- .../InstallPSResourceADOV2Server.Tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 index 834ea8690..f141d8e08 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 @@ -14,7 +14,7 @@ Describe 'Test Install-PSResource for ADO V3Server scenarios' -tags 'CI' { $ADORepoName = "PSGetTestingPublicFeed" $ADORepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/psresourceget-public-test-ci/nuget/v3/index.json" Get-NewPSResourceRepositoryFile - Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -CredentialProvider "None" } AfterEach { diff --git a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 index 124642f6d..0a861a180 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 @@ -14,7 +14,7 @@ Describe 'Test Install-PSResource for ADO V2Server scenarios' -tags 'CI' { $ADORepoName = "PSGetTestingPublicFeed" $ADORepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/psresourceget-public-test-ci/nuget/v2" Get-NewPSResourceRepositoryFile - Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -CredentialProvider None } AfterEach { From e063a97e5749cd280a7bf3bc26d2be700f951f52 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 26 Dec 2024 11:28:49 -0800 Subject: [PATCH 14/89] uncomment cred provider tests --- test/CredentialProvider.Tests.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 4be26cfe9..50c26646d 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -22,7 +22,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { Get-RevertPSResourceRepositoryFile } -<# + It "Find resource given specific Name and Repository" { $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName @@ -71,5 +71,4 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { $repo = Get-PSResourceRepository -Name $ADORepoName $repo.CredentialProvider | Should -Be "None" } - #> } From ae5a467173406241ffd25b1fbed6735b26cb7330 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:34:06 -0800 Subject: [PATCH 15/89] Update ADO feed --- test/CredentialProvider.Tests.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 50c26646d..0b09abf3a 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -6,9 +6,10 @@ Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { BeforeAll{ - $TestModuleName = "PackageManagement" + $TestModuleName = "TestModule99" $ADORepoName = "ADORepository" - $ADORepoUri = "https://pkgs.dev.azure.com/mscodehub/PowerShellCore/_packaging/PowerShellCore_PublicPackages/nuget/v2" + $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" + #https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v3/index.json $LocalRepoName = "LocalRepository" $LocalRepoUri = Join-Path -Path $TestDrive -ChildPath "testdir" $null = New-Item $LocalRepoUri -ItemType Directory -Force From f994d148f18fa4515dd2db404b1cedd51f169972 Mon Sep 17 00:00:00 2001 From: Afroz Mohammed Date: Fri, 24 Jan 2025 13:27:23 -0800 Subject: [PATCH 16/89] Fix #1777: Fixes bug with nuspec dependency version range when versions are specified in RequiredModules section Fixes bug with generated nuspec dependency version range when RequiredVersion,MaxiumumVersion and ModuleVersion are specified in RequiredModules section --- src/code/PublishHelper.cs | 57 ++++-- .../CompressPSResource.Tests.ps1 | 177 ++++++++++++++++++ 2 files changed, 220 insertions(+), 14 deletions(-) diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 66daea84e..944a8b7f4 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -1122,16 +1122,38 @@ private string CreateNuspec( if (requiredModules != null) { XmlElement dependenciesElement = doc.CreateElement("dependencies", nameSpaceUri); - foreach (string dependencyName in requiredModules.Keys) { XmlElement element = doc.CreateElement("dependency", nameSpaceUri); - element.SetAttribute("id", dependencyName); - string dependencyVersion = requiredModules[dependencyName].ToString(); - if (!string.IsNullOrEmpty(dependencyVersion)) + + var requiredModulesVersionInfo = (Hashtable)requiredModules[dependencyName]; + string versionRange = ""; + if (requiredModulesVersionInfo.ContainsKey("RequiredVersion")) + { + // For RequiredVersion, use exact version notation [x.x.x] + string requiredModulesVersion = requiredModulesVersionInfo["RequiredVersion"].ToString(); + versionRange = $"[{requiredModulesVersion}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion") && requiredModulesVersionInfo.ContainsKey("MaximumVersion")) + { + // Version range when both min and max specified: [min,max] + versionRange = $"[{requiredModulesVersionInfo["ModuleVersion"]}, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion")) + { + // Only min specified: min (which means ≥ min) + versionRange = requiredModulesVersionInfo["ModuleVersion"].ToString(); + } + else if (requiredModulesVersionInfo.ContainsKey("MaximumVersion")) { - element.SetAttribute("version", requiredModules[dependencyName].ToString()); + // Only max specified: (, max] + versionRange = $"(, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } + + if (!string.IsNullOrEmpty(versionRange)) + { + element.SetAttribute("version", versionRange); } dependenciesElement.AppendChild(element); @@ -1173,19 +1195,26 @@ private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash) if (LanguagePrimitives.TryConvertTo(reqModule, out Hashtable moduleHash)) { string moduleName = moduleHash["ModuleName"] as string; - - if (moduleHash.ContainsKey("ModuleVersion")) + var versionInfo = new Hashtable(); + + // RequiredVersion cannot be used with ModuleVersion or MaximumVersion + if (moduleHash.ContainsKey("RequiredVersion")) { - dependenciesHash.Add(moduleName, moduleHash["ModuleVersion"]); + versionInfo["RequiredVersion"] = moduleHash["RequiredVersion"].ToString(); } - else if (moduleHash.ContainsKey("RequiredVersion")) - { - dependenciesHash.Add(moduleName, moduleHash["RequiredVersion"]); - } - else + else { - dependenciesHash.Add(moduleName, string.Empty); + // ModuleVersion and MaximumVersion can be used together + if (moduleHash.ContainsKey("ModuleVersion")) + { + versionInfo["ModuleVersion"] = moduleHash["ModuleVersion"].ToString(); + } + if (moduleHash.ContainsKey("MaximumVersion")) + { + versionInfo["MaximumVersion"] = moduleHash["MaximumVersion"].ToString(); + } } + dependenciesHash.Add(moduleName, versionInfo); } else if (LanguagePrimitives.TryConvertTo(reqModule, out string moduleName)) { diff --git a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 index 7d9fe9770..2ef0e5b05 100644 --- a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 @@ -42,6 +42,39 @@ function CreateTestModule '@ | Out-File -FilePath $moduleSrc } +function CompressExpandRetrieveNuspec +{ + param( + [string]$PublishModuleBase, + [string]$PublishModuleName, + [string]$ModuleVersion, + [string]$RepositoryPath, + [string]$ModuleBasePath, + [string]$TestDrive, + [object[]]$RequiredModules, + [switch]$SkipModuleManifestValidate + ) + + $testFile = Join-Path -Path "TestSubDirectory" -ChildPath "TestSubDirFile.ps1" + $null = New-ModuleManifest -Path (Join-Path -Path $PublishModuleBase -ChildPath "$PublishModuleName.psd1") -ModuleVersion $version -Description "$PublishModuleName module" -RequiredModules $RequiredModules + $null = New-Item -Path (Join-Path -Path $PublishModuleBase -ChildPath $testFile) -Force + + $null = Compress-PSResource -Path $PublishModuleBase -DestinationPath $repositoryPath -SkipModuleManifestValidate:$SkipModuleManifestValidate + + # Must change .nupkg to .zip so that Expand-Archive can work on Windows PowerShell + $nupkgPath = Join-Path -Path $RepositoryPath -ChildPath "$PublishModuleName.$version.nupkg" + $zipPath = Join-Path -Path $RepositoryPath -ChildPath "$PublishModuleName.$version.zip" + Rename-Item -Path $nupkgPath -NewName $zipPath + $unzippedPath = Join-Path -Path $TestDrive -ChildPath "$PublishModuleName" + $null = New-Item $unzippedPath -Itemtype directory -Force + $null = Expand-Archive -Path $zipPath -DestinationPath $unzippedPath + + $nuspecPath = Join-Path -Path $unzippedPath -ChildPath "$PublishModuleName.nuspec" + $nuspecxml = [xml](Get-Content $nuspecPath) + $null = Remove-Item $unzippedPath -Force -Recurse + return $nuspecxml +} + Describe "Test Compress-PSResource" -tags 'CI' { BeforeAll { Get-NewPSResourceRepositoryFile @@ -218,6 +251,150 @@ Describe "Test Compress-PSResource" -tags 'CI' { $fileInfoObject.Name | Should -Be "$script:PublishModuleName.$version.nupkg" } + It "Compress-PSResource creates nuspec dependecy version range when RequiredVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'RequiredVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + # removing spaces as the nuget packaging is formatting the version range and adding spaces even when the original nuspec file doesn't have spaces. + # e.g (,2.0.0] is being formatted to (, 2.0.0] + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '[2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when ModuleVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '2.0.0' + } + + It "Compress-PSResource creates nuspec dependecy version range when MaximumVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'MaximumVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '(,2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when ModuleVersion and MaximumVersion are in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '1.0.0' + 'MaximumVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '[1.0.0,2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when there are multiple modules in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModuleRequiredVersion' + 'GUID' = (New-Guid).Guid + 'RequiredVersion' = '1.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleModuleVersion' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '2.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleMaximumVersion' + 'GUID' = (New-Guid).Guid + 'MaximumVersion' = '3.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleModuleAndMaximumVersion' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '4.0.0' + 'MaximumVersion' = '5.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + foreach ($dependency in $nuspecxml.package.metadata.dependencies.dependency) { + switch ($dependency.id) { + "PSGetTestRequiredModuleRequiredVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '[1.0.0]' + } + "PSGetTestRequiredModuleModuleVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '2.0.0' + } + "PSGetTestRequiredModuleMaximumVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '(,3.0.0]' + } + "PSGetTestRequiredModuleModuleAndMaximumVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '[4.0.0,5.0.0]' + } + } + } + } + <# Test for Signing the nupkg. Signing doesn't work It "Compressed Module is able to be signed with a certificate" { $version = "1.0.0" From 1cfdf2a32f788bc14482c28059b33b8af4d66ce2 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 4 Mar 2025 17:53:12 -0500 Subject: [PATCH 17/89] only convert Version info to hashtable if its not an empty string --- src/code/PublishHelper.cs | 57 +++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 944a8b7f4..4cbfb0f4a 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -1126,38 +1126,43 @@ private string CreateNuspec( { XmlElement element = doc.CreateElement("dependency", nameSpaceUri); element.SetAttribute("id", dependencyName); - - var requiredModulesVersionInfo = (Hashtable)requiredModules[dependencyName]; - string versionRange = ""; - if (requiredModulesVersionInfo.ContainsKey("RequiredVersion")) + + string dependencyVersion = requiredModules[dependencyName].ToString(); + if (!string.IsNullOrEmpty(dependencyVersion)) { - // For RequiredVersion, use exact version notation [x.x.x] - string requiredModulesVersion = requiredModulesVersionInfo["RequiredVersion"].ToString(); - versionRange = $"[{requiredModulesVersion}]"; - } - else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion") && requiredModulesVersionInfo.ContainsKey("MaximumVersion")) - { - // Version range when both min and max specified: [min,max] - versionRange = $"[{requiredModulesVersionInfo["ModuleVersion"]}, {requiredModulesVersionInfo["MaximumVersion"]}]"; - } - else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion")) - { - // Only min specified: min (which means ≥ min) - versionRange = requiredModulesVersionInfo["ModuleVersion"].ToString(); - } - else if (requiredModulesVersionInfo.ContainsKey("MaximumVersion")) - { - // Only max specified: (, max] - versionRange = $"(, {requiredModulesVersionInfo["MaximumVersion"]}]"; - } + var requiredModulesVersionInfo = (Hashtable)requiredModules[dependencyName]; + string versionRange = String.Empty; + if (requiredModulesVersionInfo.ContainsKey("RequiredVersion")) + { + // For RequiredVersion, use exact version notation [x.x.x] + string requiredModulesVersion = requiredModulesVersionInfo["RequiredVersion"].ToString(); + versionRange = $"[{requiredModulesVersion}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion") && requiredModulesVersionInfo.ContainsKey("MaximumVersion")) + { + // Version range when both min and max specified: [min,max] + versionRange = $"[{requiredModulesVersionInfo["ModuleVersion"]}, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion")) + { + // Only min specified: min (which means ≥ min) + versionRange = requiredModulesVersionInfo["ModuleVersion"].ToString(); + } + else if (requiredModulesVersionInfo.ContainsKey("MaximumVersion")) + { + // Only max specified: (, max] + versionRange = $"(, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } - if (!string.IsNullOrEmpty(versionRange)) - { - element.SetAttribute("version", versionRange); + if (!string.IsNullOrEmpty(versionRange)) + { + element.SetAttribute("version", versionRange); + } } dependenciesElement.AppendChild(element); } + metadataElement.AppendChild(dependenciesElement); } From f3fe35ed74d1cedd47fb6f58537d2297d5ef8055 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 4 Mar 2025 18:51:18 -0500 Subject: [PATCH 18/89] delete unzippedPath in test after it's written to or subsequent Expand-Archive calls to that path will fail --- test/PublishPSResourceTests/CompressPSResource.Tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 index 2ef0e5b05..28f74a742 100644 --- a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 @@ -185,6 +185,7 @@ Describe "Test Compress-PSResource" -tags 'CI' { Expand-Archive -Path $zipPath -DestinationPath $unzippedPath Test-Path -Path (Join-Path -Path $unzippedPath -ChildPath $testFile) | Should -Be $True + $null = Remove-Item $unzippedPath -Force -Recurse } It "Compresses a script" { From 97ef9d7ce82d384518a27209a0cfbc0ae3e87846 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 12 Feb 2025 13:09:10 -0500 Subject: [PATCH 19/89] Update Dependency parsing logic to account for Az packages with Az* naming --- src/code/PSResourceInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 409665edc..0e5aabc85 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -972,7 +972,7 @@ public static bool TryConvertFromContainerRegistryJson( metadata["Dependencies"] = ParseContainerRegistryDependencies(requiredModulesElement, out errorMsg).ToArray(); } - if (string.Equals(packageName, "Az", StringComparison.OrdinalIgnoreCase) || packageName.StartsWith("Az.", StringComparison.OrdinalIgnoreCase)) + if (packageName.StartsWith("Az", StringComparison.OrdinalIgnoreCase)) { if (rootDom.TryGetProperty("ModuleList", out JsonElement moduleListDepsElement)) { From b1ba2b27c2d289a5bad4a66882278a0bfcb81ef0 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 12 Feb 2025 13:58:33 -0500 Subject: [PATCH 20/89] specify Azpreview explicitly in condition --- src/code/PSResourceInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 0e5aabc85..547828393 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -972,7 +972,7 @@ public static bool TryConvertFromContainerRegistryJson( metadata["Dependencies"] = ParseContainerRegistryDependencies(requiredModulesElement, out errorMsg).ToArray(); } - if (packageName.StartsWith("Az", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(packageName, "Az", StringComparison.OrdinalIgnoreCase) || string.Equals(packageName, "Azpreview", StringComparison.OrdinalIgnoreCase) || packageName.StartsWith("Az.", StringComparison.OrdinalIgnoreCase)) { if (rootDom.TryGetProperty("ModuleList", out JsonElement moduleListDepsElement)) { From d18975036841ab150b2223e7d959d45dd4b55a95 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 12 Feb 2025 14:04:01 -0500 Subject: [PATCH 21/89] add test for Azpreview from MAR --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index b7ffdfb8e..43eca7a25 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -255,4 +255,9 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { $res.Dependencies.Length | Should -Be 1 $res.Dependencies[0].Name | Should -Be "Az.Accounts" } + + It "Should find Azpreview resource and it's dependency given specific Name and Version" { + $res = Find-PSResource -Name "Az.Storage" -Version "13.2.0" -Repository "MAR" + $res.Dependencies.Length | Should -Not -Be 0 + } } From 35bce619d307eb04f4c4690b7e4462973121ff49 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 12 Feb 2025 15:42:04 -0500 Subject: [PATCH 22/89] fix test for Azpreview from MAR --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 43eca7a25..69ca8c717 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -257,7 +257,7 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { } It "Should find Azpreview resource and it's dependency given specific Name and Version" { - $res = Find-PSResource -Name "Az.Storage" -Version "13.2.0" -Repository "MAR" + $res = Find-PSResource -Name "Azpreview" -Version "13.2.0" -Repository "MAR" $res.Dependencies.Length | Should -Not -Be 0 } } From 74abeb0d637d00ce3ef2e8b42db0647eca4853f9 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 12 Feb 2025 17:08:56 -0500 Subject: [PATCH 23/89] update outdated az FindName() from MAR test --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 69ca8c717..db2d5f0a2 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -247,7 +247,7 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { It "Should find resource given specific Name, Version null" { $res = Find-PSResource -Name "Az.Accounts" -Repository "MAR" $res.Name | Should -Be "Az.Accounts" - $res.Version | Should -Be "4.0.0" + $res.Version | Should -Be "4.0.2" } It "Should find resource and its dependency given specific Name and Version" { From 79f1e23e42ec42a47dc4ca3be1d501df15344d6a Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 12 Feb 2025 18:56:31 -0500 Subject: [PATCH 24/89] check if version is greaterthan not equal --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index db2d5f0a2..ccc0142cb 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -247,7 +247,7 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { It "Should find resource given specific Name, Version null" { $res = Find-PSResource -Name "Az.Accounts" -Repository "MAR" $res.Name | Should -Be "Az.Accounts" - $res.Version | Should -Be "4.0.2" + $res.Version | Should -BeGreaterThan "4.0.0" } It "Should find resource and its dependency given specific Name and Version" { From 25bee9d7dea088507e4dd805a92250c07e0301b9 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 19 Feb 2025 11:40:36 -0800 Subject: [PATCH 25/89] Allow wildcard for MAR repository for FindAll and FindByName (#1786) --- src/code/ContainerRegistryServerAPICalls.cs | 92 ++++++++++++++++--- ...SResourceContainerRegistryServer.Tests.ps1 | 12 ++- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index d9175c0b0..973525daa 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -46,6 +46,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename) const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest + const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry #endregion @@ -76,13 +77,13 @@ public ContainerRegistryServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmd public override FindResults FindAll(bool includePrerelease, ResourceType type, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindAll()"); - errRecord = new ErrorRecord( - new InvalidOperationException($"Find all is not supported for the ContainerRegistry server protocol repository '{Repository.Name}'"), - "FindAllFailure", - ErrorCategory.InvalidOperation, - this); + var findResult = FindPackages("*", includePrerelease, out errRecord); + if (errRecord != null) + { + return emptyResponseResults; + } - return emptyResponseResults; + return findResult; } /// @@ -161,13 +162,13 @@ public override FindResults FindNameWithTag(string packageName, string[] tags, b public override FindResults FindNameGlobbing(string packageName, bool includePrerelease, ResourceType type, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindNameGlobbing()"); - errRecord = new ErrorRecord( - new InvalidOperationException($"FindNameGlobbing all is not supported for the ContainerRegistry server protocol repository '{Repository.Name}'"), - "FindNameGlobbingFailure", - ErrorCategory.InvalidOperation, - this); + var findResult = FindPackages(packageName, includePrerelease, out errRecord); + if (errRecord != null) + { + return emptyResponseResults; + } - return emptyResponseResults; + return findResult; } /// @@ -591,6 +592,20 @@ internal JObject FindContainerRegistryImageTags(string packageName, string versi return GetHttpResponseJObjectUsingDefaultHeaders(findImageUrl, HttpMethod.Get, defaultHeaders, out errRecord); } + /// + /// Helper method to find all packages on container registry + /// + /// + /// + /// + internal JObject FindAllRepositories(string containerRegistryAccessToken, out ErrorRecord errRecord) + { + _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindAllRepositories()"); + string repositoryListUrl = string.Format(containerRegistryRepositoryListTemplate, Registry); + var defaultHeaders = GetDefaultHeaders(containerRegistryAccessToken); + return GetHttpResponseJObjectUsingDefaultHeaders(repositoryListUrl, HttpMethod.Get, defaultHeaders, out errRecord); + } + /// /// Get metadata for a package version. /// @@ -1705,12 +1720,63 @@ private string PrependMARPrefix(string packageName) // If the repostitory is MAR and its not a wildcard search, we need to prefix the package name with MAR prefix. string updatedPackageName = Repository.IsMARRepository() && packageName.Trim() != "*" - ? string.Concat(prefix, packageName) + ? packageName.StartsWith(prefix) ? packageName : string.Concat(prefix, packageName) : packageName; return updatedPackageName; } + private FindResults FindPackages(string packageName, bool includePrerelease, out ErrorRecord errRecord) + { + _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindPackages()"); + errRecord = null; + string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + if (errRecord != null) + { + return emptyResponseResults; + } + + var pkgResult = FindAllRepositories(containerRegistryAccessToken, out errRecord); + if (errRecord != null) + { + return emptyResponseResults; + } + + List repositoriesList = new List(); + var isMAR = Repository.IsMARRepository(); + + // Convert the list of repositories to a list of hashtables + foreach (var repository in pkgResult["repositories"].ToList()) + { + string repositoryName = repository.ToString(); + + if (isMAR && !repositoryName.StartsWith(PSRepositoryInfo.MARPrefix)) + { + continue; + } + + // This remove the 'psresource/' prefix from the repository name for comparison with wildcard. + string moduleName = repositoryName.Substring(11); + + WildcardPattern wildcardPattern = new WildcardPattern(packageName, WildcardOptions.IgnoreCase); + + if (!wildcardPattern.IsMatch(moduleName)) + { + continue; + } + + _cmdletPassedIn.WriteDebug($"Found repository: {repositoryName}"); + + repositoriesList.AddRange(FindPackagesWithVersionHelper(repositoryName, VersionType.VersionRange, versionRange: VersionRange.All, requiredVersion: null, includePrerelease, getOnlyLatest: true, out errRecord)); + if (errRecord != null) + { + return emptyResponseResults; + } + } + + return new FindResults(stringResponse: new string[] { }, hashtableResponse: repositoriesList.ToArray(), responseType: containerRegistryFindResponseType); + } + #endregion } } diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index ccc0142cb..8a0962631 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -247,7 +247,7 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { It "Should find resource given specific Name, Version null" { $res = Find-PSResource -Name "Az.Accounts" -Repository "MAR" $res.Name | Should -Be "Az.Accounts" - $res.Version | Should -BeGreaterThan "4.0.0" + $res.Version | Should -BeGreaterThan ([Version]"4.0.0") } It "Should find resource and its dependency given specific Name and Version" { @@ -259,5 +259,15 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { It "Should find Azpreview resource and it's dependency given specific Name and Version" { $res = Find-PSResource -Name "Azpreview" -Version "13.2.0" -Repository "MAR" $res.Dependencies.Length | Should -Not -Be 0 + It "Should find resource with wildcard in Name" { + $res = Find-PSResource -Name "Az.App*" -Repository "MAR" + $res | Should -Not -BeNullOrEmpty + $res.Count | Should -BeGreaterThan 1 + } + + It "Should find all resource with wildcard in Name" { + $res = Find-PSResource -Name "*" -Repository "MAR" + $res | Should -Not -BeNullOrEmpty + $res.Count | Should -BeGreaterThan 1 } } From ffa42a95ebf137cd6eaff4b1dc2042231eea5f91 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 21 Feb 2025 10:27:14 -0800 Subject: [PATCH 26/89] Use authentication challenge for unauthenticated container registry repository (#1797) --- src/code/ContainerRegistryServerAPICalls.cs | 82 ++++++++++++++++++- ...SResourceContainerRegistryServer.Tests.ps1 | 36 +++++++- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 973525daa..785c7aeae 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -38,7 +38,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall private static readonly FindResults emptyResponseResults = new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: containerRegistryFindResponseType); const string containerRegistryRefreshTokenTemplate = "grant_type=access_token&service={0}&tenant={1}&access_token={2}"; // 0 - registry, 1 - tenant, 2 - access token - const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&refresh_token={1}"; // 0 - registry, 1 - refresh token + const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&scope=registry:catalog:*&refresh_token={1}"; // 0 - registry, 1 - refresh token const string containerRegistryOAuthExchangeUrlTemplate = "https://{0}/oauth2/exchange"; // 0 - registry const string containerRegistryOAuthTokenUrlTemplate = "https://{0}/oauth2/token"; // 0 - registry const string containerRegistryManifestUrlTemplate = "https://{0}/v2/{1}/manifests/{2}"; // 0 - registry, 1 - repo(modulename), 2 - tag(version) @@ -46,6 +46,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename) const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest + const string defaultScope = "&scope=repository:*:*&scope=registry:catalog:*"; const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry #endregion @@ -392,12 +393,18 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) } else { - bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord); + bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord, out accessToken); if (errRecord != null) { return null; } + if (!string.IsNullOrEmpty(accessToken)) + { + _cmdletPassedIn.WriteVerbose("Anonymous access token retrieved."); + return accessToken; + } + if (!isRepositoryUnauthenticated) { accessToken = Utils.GetAzAccessToken(); @@ -437,15 +444,82 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) /// /// Checks if container registry repository is unauthenticated. /// - internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord) + internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord, out string anonymousAccessToken) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()"); errRecord = null; + anonymousAccessToken = string.Empty; string endpoint = $"{containerRegistyUrl}/v2/"; HttpResponseMessage response; try { response = _sessionClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, endpoint)).Result; + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + // check if there is a auth challenge header + if (response.Headers.WwwAuthenticate.Count() > 0) + { + var authHeader = response.Headers.WwwAuthenticate.First(); + if (authHeader.Scheme == "Bearer") + { + // check if there is a realm + if (authHeader.Parameter.Contains("realm")) + { + // get the realm + var realm = authHeader.Parameter.Split(',')?.Where(x => x.Contains("realm"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"'); + // get the service + var service = authHeader.Parameter.Split(',')?.Where(x => x.Contains("service"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"'); + + if (string.IsNullOrEmpty(realm) || string.IsNullOrEmpty(service)) + { + errRecord = new ErrorRecord( + new InvalidOperationException("Failed to get realm or service from the auth challenge header."), + "RegistryUnauthenticationCheckError", + ErrorCategory.InvalidResult, + this); + + return false; + } + + string content = "grant_type=access_token&service=" + service + defaultScope; + var contentHeaders = new Collection> { new KeyValuePair("Content-Type", "application/x-www-form-urlencoded") }; + + // get the anonymous access token + var url = $"{realm}?service={service}{defaultScope}"; + + // we dont check the errorrecord here because we want to return false if we get a 401 and not throw an error + var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out _); + + if (results == null) + { + _cmdletPassedIn.WriteDebug("Failed to get access token from the realm. results is null."); + return false; + } + + if (results["access_token"] == null) + { + _cmdletPassedIn.WriteDebug($"Failed to get access token from the realm. access_token is null. results: {results}"); + return false; + } + + anonymousAccessToken = results["access_token"].ToString(); + _cmdletPassedIn.WriteDebug("Anonymous access token retrieved"); + return true; + } + } + } + } + } + catch (HttpRequestException hre) + { + errRecord = new ErrorRecord( + hre, + "RegistryAnonymousAcquireError", + ErrorCategory.ConnectionError, + this); + + return false; } catch (Exception e) { @@ -1756,7 +1830,7 @@ private FindResults FindPackages(string packageName, bool includePrerelease, out } // This remove the 'psresource/' prefix from the repository name for comparison with wildcard. - string moduleName = repositoryName.Substring(11); + string moduleName = repositoryName.StartsWith("psresource/") ? repositoryName.Substring(11) : repositoryName; WildcardPattern wildcardPattern = new WildcardPattern(packageName, WildcardOptions.IgnoreCase); diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 8a0962631..c465f87b7 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -151,12 +151,11 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" } - It "Should not find all resources given Name '*'" { + It "Should find all resources given Name '*'" { # FindAll() $res = Find-PSResource -Name "*" -Repository $ACRRepoName -ErrorVariable err -ErrorAction SilentlyContinue - $res | Should -BeNullOrEmpty - $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "FindAllFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $res | Should -Not -BeNullOrEmpty + $res.Count | Should -BeGreaterThan 0 } It "Should find script given Name" { @@ -271,3 +270,32 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { $res.Count | Should -BeGreaterThan 1 } } + +# Skip this test fo +Describe 'Test Find-PSResource for unauthenticated ACR repository' -tags 'CI' { + BeforeAll { + $skipOnWinPS = $PSVersionTable.PSVersion.Major -eq 5 + + if (-not $skipOnWinPS) { + Register-PSResourceRepository -Name "Unauthenticated" -Uri "https://psresourcegetnoauth.azurecr.io/" -ApiVersion "ContainerRegistry" + } + } + + AfterAll { + if (-not $skipOnWinPS) { + Unregister-PSResourceRepository -Name "Unauthenticated" + } + } + + It "Should find resource given specific Name, Version null" { + + if ($skipOnWinPS) { + Set-ItResult -Pending -Because "Skipping test on Windows PowerShell" + return + } + + $res = Find-PSResource -Name "hello-world" -Repository "Unauthenticated" + $res.Name | Should -Be "hello-world" + $res.Version | Should -Be "5.0.0" + } +} From 405a0f75bb75afb8a7888dea1190789527c506d6 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 26 Feb 2025 10:01:27 -0800 Subject: [PATCH 27/89] Use deploybox to release to Gallery (#1800) --- .pipelines/PSResourceGet-Official.yml | 68 ++++++++++++--------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/.pipelines/PSResourceGet-Official.yml b/.pipelines/PSResourceGet-Official.yml index d096cf33e..ae6e2d484 100644 --- a/.pipelines/PSResourceGet-Official.yml +++ b/.pipelines/PSResourceGet-Official.yml @@ -29,7 +29,7 @@ variables: value: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest # Docker image which is used to build the project https://aka.ms/obpipelines/containers resources: - repositories: + repositories: - repository: onebranchTemplates type: git name: OneBranch.Pipelines/GovernedTemplates @@ -41,6 +41,8 @@ extends: featureFlags: WindowsHostVersion: '1ESWindows2022' customTags: 'ES365AIMigrationTooling' + release: + category: NonAzure globalSdl: disableLegacyManifest: true sbom: @@ -58,7 +60,7 @@ extends: binskim: enabled: true apiscan: - enabled: false + enabled: false stages: - stage: stagebuild @@ -125,15 +127,6 @@ extends: AnalyzeInPipeline: true Language: csharp - - pwsh: | - $module = 'Microsoft.PowerShell.PSResourceGet' - Write-Verbose "installing $module..." -verbose - $ProgressPreference = 'SilentlyContinue' - Install-Module $module -AllowClobber -Force - displayName: Install PSResourceGet 0.9.0 or above for build.psm1 - env: - ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - # this is installing .NET - pwsh: | Set-Location "$(repoRoot)" @@ -167,14 +160,14 @@ extends: } } displayName: Find all 3rd party files that need to be signed - + - task: onebranch.pipeline.signing@1 displayName: Sign 3rd Party files inputs: command: 'sign' signing_profile: 135020002 files_to_sign: '*.dll' - search_root: $(signSrcPath)/Microsoft.PowerShell.PSResourceGet/UnsignedDependencies + search_root: $(signSrcPath)/Microsoft.PowerShell.PSResourceGet/UnsignedDependencies - pwsh: | $newlySignedDepsPath = Join-Path -Path $(signSrcPath) -ChildPath "Microsoft.PowerShell.PSResourceGet" -AdditionalChildPath "UnsignedDependencies" @@ -216,7 +209,7 @@ extends: value: $(Build.SourcesDirectory)\PSResourceGet\.config\tsaoptions.json # Disable because SBOM was already built in the previous job - name: ob_sdl_sbom_enabled - value: false + value: true - name: signOutPath value: $(repoRoot)/signed - name: ob_signing_setup_enabled @@ -250,15 +243,12 @@ extends: displayName: Capture artifacts directory structure - pwsh: | - $module = 'Microsoft.PowerShell.PSResourceGet' - Write-Verbose "installing $module..." -verbose - $ProgressPreference = 'SilentlyContinue' - Install-Module $module -AllowClobber -Force - displayName: Install PSResourceGet 0.9.0 or above for build.psm1 + # This need to be done before set-location so the module from PSHome is loaded + Import-Module -Name Microsoft.PowerShell.PSResourceGet -Force - - pwsh: | Set-Location "$(signOutPath)\Microsoft.PowerShell.PSResourceGet" - New-Item -ItemType Directory -Path "$(signOutPath)\PublishedNupkg" -Force + $null = New-Item -ItemType Directory -Path "$(signOutPath)\PublishedNupkg" -Force + Register-PSResourceRepository -Name 'localRepo' -Uri "$(signOutPath)\PublishedNupkg" Publish-PSResource -Path "$(signOutPath)\Microsoft.PowerShell.PSResourceGet" -Repository 'localRepo' -Verbose displayName: Create nupkg for publishing @@ -274,7 +264,7 @@ extends: - pwsh: | Set-Location "$(signOutPath)\PublishedNupkg" Write-Host "Contents of signOutPath:" - Get-ChildItem "$(signOutPath)" -Recurse + Get-ChildItem "$(signOutPath)" -Recurse displayName: Find Nupkg - task: CopyFiles@2 @@ -282,10 +272,10 @@ extends: inputs: Contents: $(signOutPath)\PublishedNupkg\Microsoft.PowerShell.PSResourceGet.*.nupkg TargetFolder: $(ob_outputDirectory) - + - pwsh: | Write-Host "Contents of ob_outputDirectory:" - Get-ChildItem "$(ob_outputDirectory)" -Recurse + Get-ChildItem "$(ob_outputDirectory)" -Recurse displayName: Find Signed Nupkg - stage: release @@ -293,12 +283,14 @@ extends: dependsOn: stagebuild variables: version: $[ stageDependencies.build.main.outputs['package.version'] ] - drop: $(Pipeline.Workspace)/drop_build_main + drop: $(Pipeline.Workspace)/drop_stagebuild_nupkg + ob_release_environment: 'Production' + jobs: - job: validation displayName: Manual validation pool: - type: agentless + type: server timeoutInMinutes: 1440 steps: - task: ManualValidation@0 @@ -306,29 +298,31 @@ extends: inputs: instructions: Please validate the release timeoutInMinutes: 1440 + - job: PSGalleryPublish displayName: Publish to PSGallery dependsOn: validation + templateContext: + inputs: + - input: pipelineArtifact + artifactName: drop_stagebuild_nupkg pool: - type: windows + type: release + os: windows variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' steps: - - download: current - displayName: Download artifact - - - pwsh: | - Get-ChildItem $(Pipeline.Workspace) -Recurse - displayName: Capture environment - - - pwsh: | - Get-ChildItem "$(Pipeline.Workspace)/drop_stagebuild_nupkg" -Recurse + - task: PowerShell@2 + inputs: + targetType: 'inline' + script: | + Get-ChildItem "$(Pipeline.Workspace)/" -Recurse displayName: Find signed Nupkg - task: NuGetCommand@2 displayName: Push PowerShellGet module artifacts to PSGallery feed inputs: command: push - packagesToPush: '$(Pipeline.Workspace)\drop_stagebuild_nupkg\PSResourceGet\signed\PublishedNupkg\Microsoft.PowerShell.PSResourceGet.*.nupkg' + packagesToPush: '$(Pipeline.Workspace)\PSResourceGet\signed\PublishedNupkg\Microsoft.PowerShell.PSResourceGet.*.nupkg' nuGetFeedType: external publishFeedCredentials: PSGet-PSGalleryPush From 8a3c89c47203d49c871f97595afdcdf3aea0ccd1 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 25 Feb 2025 13:09:55 -0500 Subject: [PATCH 28/89] Get metadata properties when finding a PSResource from a ContainerRegistry --- src/code/PSResourceInfo.cs | 53 ++++++++++++++----- ...SResourceContainerRegistryServer.Tests.ps1 | 20 +++++++ 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 547828393..205eac4b4 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -852,9 +852,9 @@ public static bool TryConvertFromContainerRegistryJson( pkgVersion = ParseHttpVersion(versionValue, out string prereleaseLabel); metadata["Version"] = pkgVersion; - if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) + if (rootDom.TryGetProperty("PrivateData", out JsonElement versionPrivateDataElement) && versionPrivateDataElement.TryGetProperty("PSData", out JsonElement versionPSDataElement)) { - if (psDataElement.TryGetProperty("Prerelease", out JsonElement pkgPrereleaseLabelElement) && !String.IsNullOrEmpty(pkgPrereleaseLabelElement.ToString().Trim())) + if (versionPSDataElement.TryGetProperty("Prerelease", out JsonElement pkgPrereleaseLabelElement) && !String.IsNullOrEmpty(pkgPrereleaseLabelElement.ToString().Trim())) { prereleaseLabel = pkgPrereleaseLabelElement.ToString().Trim(); versionValue += $"-{prereleaseLabel}"; @@ -884,19 +884,19 @@ public static bool TryConvertFromContainerRegistryJson( metadata["NormalizedVersion"] = parsedNormalizedVersion.ToNormalizedString(); // License Url - if (rootDom.TryGetProperty("LicenseUrl", out JsonElement licenseUrlElement) || rootDom.TryGetProperty("licenseUrl", out licenseUrlElement)) + if (rootDom.TryGetProperty("LicenseUrl", out JsonElement licenseUrlElement) || rootDom.TryGetProperty("licenseUrl", out licenseUrlElement) || rootDom.TryGetProperty("LicenseUri", out licenseUrlElement)) { metadata["LicenseUrl"] = ParseHttpUrl(licenseUrlElement.ToString()) as Uri; } // Project Url - if (rootDom.TryGetProperty("ProjectUrl", out JsonElement projectUrlElement) || rootDom.TryGetProperty("projectUrl", out projectUrlElement)) + if (rootDom.TryGetProperty("ProjectUrl", out JsonElement projectUrlElement) || rootDom.TryGetProperty("projectUrl", out projectUrlElement) || rootDom.TryGetProperty("ProjectUri", out projectUrlElement)) { metadata["ProjectUrl"] = ParseHttpUrl(projectUrlElement.ToString()) as Uri; } // Icon Url - if (rootDom.TryGetProperty("IconUrl", out JsonElement iconUrlElement) || rootDom.TryGetProperty("iconUrl", out iconUrlElement)) + if (rootDom.TryGetProperty("IconUrl", out JsonElement iconUrlElement) || rootDom.TryGetProperty("iconUrl", out iconUrlElement) || rootDom.TryGetProperty("IconUri", out iconUrlElement)) { metadata["IconUrl"] = ParseHttpUrl(iconUrlElement.ToString()) as Uri; } @@ -938,14 +938,19 @@ public static bool TryConvertFromContainerRegistryJson( } // Author - if (rootDom.TryGetProperty("Authors", out JsonElement authorsElement) || rootDom.TryGetProperty("authors", out authorsElement)) + if (rootDom.TryGetProperty("Authors", out JsonElement authorsElement) || rootDom.TryGetProperty("authors", out authorsElement) || rootDom.TryGetProperty("Author", out authorsElement)) { metadata["Authors"] = authorsElement.ToString(); + } - // CompanyName - // CompanyName is not provided in v3 pkg metadata response, so we've just set it to the author, - // which is often the company - metadata["CompanyName"] = authorsElement.ToString(); + if (rootDom.TryGetProperty("CompanyName", out JsonElement companyNameElement)) + { + metadata["CompanyName"] = companyNameElement.ToString(); + } + else + { + // if CompanyName property is not provided set it to the Author value which is often the same. + metadata["CompanyName"] = metadata["Authors"]; } // Copyright @@ -978,15 +983,39 @@ public static bool TryConvertFromContainerRegistryJson( { metadata["Dependencies"] = ParseContainerRegistryDependencies(moduleListDepsElement, out errorMsg).ToArray(); } - else if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) + else if (rootDom.TryGetProperty("PrivateData", out JsonElement depsPrivateDataElement) && depsPrivateDataElement.TryGetProperty("PSData", out JsonElement depsPSDataElement)) { - if (psDataElement.TryGetProperty("ModuleList", out JsonElement privateDataModuleListDepsElement)) + if (depsPSDataElement.TryGetProperty("ModuleList", out JsonElement privateDataModuleListDepsElement)) { metadata["Dependencies"] = ParseContainerRegistryDependencies(privateDataModuleListDepsElement, out errorMsg).ToArray(); } } } + if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) + { + // some properties that may be in PrivateData.PSData: LicenseUri, ProjectUri, IconUri, ReleaseNotes + if (!(metadata.ContainsKey("LicenseUrl") || metadata.ContainsKey("licenseUrl")) && psDataElement.TryGetProperty("LicenseUri", out JsonElement psDataLicenseUriElement)) + { + metadata["LicenseUrl"] = ParseHttpUrl(psDataLicenseUriElement.ToString()) as Uri; + } + + if (!(metadata.ContainsKey("ProjectUrl") || metadata.ContainsKey("ProjectUrl")) && psDataElement.TryGetProperty("ProjectUri", out JsonElement psDataProjectUriElement)) + { + metadata["ProjectUrl"] = ParseHttpUrl(psDataProjectUriElement.ToString()) as Uri; + } + + if (!(metadata.ContainsKey("IconUrl") || metadata.ContainsKey("IconUrl")) && psDataElement.TryGetProperty("IconUri", out JsonElement psDataIconUriElement)) + { + metadata["IconUrl"] = ParseHttpUrl(psDataIconUriElement.ToString()) as Uri; + } + + if (!metadata.ContainsKey("ReleaseNotes") && psDataElement.TryGetProperty("ReleaseNotes", out JsonElement psDataReleaseNotesElement)) + { + metadata["ReleaseNotes"] = psDataReleaseNotesElement.ToString(); + } + } + var additionalMetadataHashtable = new Dictionary { { "NormalizedVersion", metadata["NormalizedVersion"].ToString() } diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index c465f87b7..4588a5c1d 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -232,6 +232,26 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $res.Dependencies.Length | Should -Be 1 $res.Dependencies[0].Name | Should -Be "Az.Accounts" } + + It "Should find resource and its associated author, licenseUri, projectUri, releaseNotes, etc properties" { + $res = Find-PSResource -Name "Az.Storage" -Version "8.0.0" -Repository $ACRRepoName + $res.Author | Should -Be "Microsoft Corporation" + $res.CompanyName | Should -Be "Microsoft Corporation" + $res.LicenseUri | Should -Be "https://aka.ms/azps-license" + $res.ProjectUri | Should -Be "https://github.com/Azure/azure-powershell" + $res.ReleaseNotes.Length | Should -Not -Be 0 + } + + It "Install script with companyname, copyright, and repository source location and validate" { + Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + + $res = Get-InstalledPSResource "Install-VSCode" -Version "1.4.2" + $res.Name | Should -Be "Install-VSCode" + $res.Version | Should -Be "1.4.2" + $res.CompanyName | Should -Be "Microsoft Corporation" + $res.Copyright | Should -Be "(c) Microsoft Corporation" + $res.RepositorySourceLocation | Should -Be $PSGalleryUri + } } Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { From 4a60d68761ac586fa76960ce177148fe70ecf7b7 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 25 Feb 2025 13:21:54 -0500 Subject: [PATCH 29/89] Retrieve Tags property which can be in PrivateData.PSData but is not as common and add test for it --- src/code/PSResourceInfo.cs | 35 +++++++++++++++---- ...SResourceContainerRegistryServer.Tests.ps1 | 1 + 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 205eac4b4..cbab137e6 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -884,19 +884,19 @@ public static bool TryConvertFromContainerRegistryJson( metadata["NormalizedVersion"] = parsedNormalizedVersion.ToNormalizedString(); // License Url - if (rootDom.TryGetProperty("LicenseUrl", out JsonElement licenseUrlElement) || rootDom.TryGetProperty("licenseUrl", out licenseUrlElement) || rootDom.TryGetProperty("LicenseUri", out licenseUrlElement)) + if (rootDom.TryGetProperty("LicenseUrl", out JsonElement licenseUrlElement) || rootDom.TryGetProperty("licenseUrl", out licenseUrlElement)) { metadata["LicenseUrl"] = ParseHttpUrl(licenseUrlElement.ToString()) as Uri; } // Project Url - if (rootDom.TryGetProperty("ProjectUrl", out JsonElement projectUrlElement) || rootDom.TryGetProperty("projectUrl", out projectUrlElement) || rootDom.TryGetProperty("ProjectUri", out projectUrlElement)) + if (rootDom.TryGetProperty("ProjectUrl", out JsonElement projectUrlElement) || rootDom.TryGetProperty("projectUrl", out projectUrlElement)) { metadata["ProjectUrl"] = ParseHttpUrl(projectUrlElement.ToString()) as Uri; } // Icon Url - if (rootDom.TryGetProperty("IconUrl", out JsonElement iconUrlElement) || rootDom.TryGetProperty("iconUrl", out iconUrlElement) || rootDom.TryGetProperty("IconUri", out iconUrlElement)) + if (rootDom.TryGetProperty("IconUrl", out JsonElement iconUrlElement) || rootDom.TryGetProperty("iconUrl", out iconUrlElement)) { metadata["IconUrl"] = ParseHttpUrl(iconUrlElement.ToString()) as Uri; } @@ -995,17 +995,17 @@ public static bool TryConvertFromContainerRegistryJson( if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) { // some properties that may be in PrivateData.PSData: LicenseUri, ProjectUri, IconUri, ReleaseNotes - if (!(metadata.ContainsKey("LicenseUrl") || metadata.ContainsKey("licenseUrl")) && psDataElement.TryGetProperty("LicenseUri", out JsonElement psDataLicenseUriElement)) + if (!metadata.ContainsKey("LicenseUrl") && psDataElement.TryGetProperty("LicenseUri", out JsonElement psDataLicenseUriElement)) { metadata["LicenseUrl"] = ParseHttpUrl(psDataLicenseUriElement.ToString()) as Uri; } - if (!(metadata.ContainsKey("ProjectUrl") || metadata.ContainsKey("ProjectUrl")) && psDataElement.TryGetProperty("ProjectUri", out JsonElement psDataProjectUriElement)) + if (!metadata.ContainsKey("ProjectUrl") && psDataElement.TryGetProperty("ProjectUri", out JsonElement psDataProjectUriElement)) { metadata["ProjectUrl"] = ParseHttpUrl(psDataProjectUriElement.ToString()) as Uri; } - if (!(metadata.ContainsKey("IconUrl") || metadata.ContainsKey("IconUrl")) && psDataElement.TryGetProperty("IconUri", out JsonElement psDataIconUriElement)) + if (!metadata.ContainsKey("IconUrl") && psDataElement.TryGetProperty("IconUri", out JsonElement psDataIconUriElement)) { metadata["IconUrl"] = ParseHttpUrl(psDataIconUriElement.ToString()) as Uri; } @@ -1014,6 +1014,29 @@ public static bool TryConvertFromContainerRegistryJson( { metadata["ReleaseNotes"] = psDataReleaseNotesElement.ToString(); } + + if (!metadata.ContainsKey("Tags") && psDataElement.TryGetProperty("Tags", out JsonElement psDataTagsElement)) + { + string[] pkgTags = Utils.EmptyStrArray; + if (psDataTagsElement.ValueKind == JsonValueKind.Array) + { + var arrayLength = psDataTagsElement.GetArrayLength(); + List tags = new List(arrayLength); + foreach (var tag in psDataTagsElement.EnumerateArray()) + { + tags.Add(tag.ToString()); + } + + pkgTags = tags.ToArray(); + } + else if (psDataTagsElement.ValueKind == JsonValueKind.String) + { + string tagStr = psDataTagsElement.ToString(); + pkgTags = tagStr.Split(Utils.WhitespaceSeparator, StringSplitOptions.RemoveEmptyEntries); + } + + metadata["Tags"] = pkgTags; + } } var additionalMetadataHashtable = new Dictionary diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 4588a5c1d..6690b8db6 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -240,6 +240,7 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $res.LicenseUri | Should -Be "https://aka.ms/azps-license" $res.ProjectUri | Should -Be "https://github.com/Azure/azure-powershell" $res.ReleaseNotes.Length | Should -Not -Be 0 + $res.Tags.Length | Should -Be 5 } It "Install script with companyname, copyright, and repository source location and validate" { From e27c8c301c44d70bada1de2e7ce1a0375632e7e2 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 25 Feb 2025 13:23:21 -0500 Subject: [PATCH 30/89] clean up code --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 6690b8db6..fc701fc26 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -242,17 +242,6 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $res.ReleaseNotes.Length | Should -Not -Be 0 $res.Tags.Length | Should -Be 5 } - - It "Install script with companyname, copyright, and repository source location and validate" { - Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository - - $res = Get-InstalledPSResource "Install-VSCode" -Version "1.4.2" - $res.Name | Should -Be "Install-VSCode" - $res.Version | Should -Be "1.4.2" - $res.CompanyName | Should -Be "Microsoft Corporation" - $res.Copyright | Should -Be "(c) Microsoft Corporation" - $res.RepositorySourceLocation | Should -Be $PSGalleryUri - } } Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { From 6af54e1ee2bd46185e944ca8a8ed56369026dd7f Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 25 Feb 2025 15:47:36 -0500 Subject: [PATCH 31/89] check privateData JsonElement kind, if Object check for its properties, if string (as in the case of script PSResourceInfo) do not check as will throw error --- src/code/PSResourceInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index cbab137e6..dd840b62d 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -992,7 +992,7 @@ public static bool TryConvertFromContainerRegistryJson( } } - if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) + if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.ValueKind == JsonValueKind.Object && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) { // some properties that may be in PrivateData.PSData: LicenseUri, ProjectUri, IconUri, ReleaseNotes if (!metadata.ContainsKey("LicenseUrl") && psDataElement.TryGetProperty("LicenseUri", out JsonElement psDataLicenseUriElement)) From 805000615f68e8cab56cc6b926e11efd678f1c03 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 18 Feb 2025 01:18:33 -0500 Subject: [PATCH 32/89] fix install for a package from MAR that may have different version than its normalized version --- src/code/InstallHelper.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index e31c2b86c..8ac7fa8fe 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -824,6 +824,14 @@ private Hashtable BeginPackageInstall( pkgVersion += $"-{pkgToInstall.Prerelease}"; } } + + // For most repositories/providers the server will use the normalized version, which pkgVersion originally reflects + // However, for container registries the version must exactly match what was in the artifact manifest and then reflected in PSResourceInfo.Version.ToString() + if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.ContainerRegistry) + { + pkgVersion = pkgToInstall.Version.ToString(); + } + // Check to see if the pkg is already installed (ie the pkg is installed and the version satisfies the version range provided via param) if (!_reinstall) { From 5a3cca2b148f293c92d6c3ec43a21f94227d90f0 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 18 Feb 2025 13:29:02 -0500 Subject: [PATCH 33/89] add find and install tests for 2 digit pkg --- ...SResourceContainerRegistryServer.Tests.ps1 | 20 +++++++++++ ...SResourceContainerRegistryServer.Tests.ps1 | 35 ++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index fc701fc26..c56b04e86 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -8,6 +8,7 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { BeforeAll{ $testModuleName = "test-module" + $testModuleWith2DigitVersion = "test-2DigitPkg" $testModuleParentName = "test_parent_mod" $testModuleDependencyName = "test_dependency_mod" $testScriptName = "test-script" @@ -82,6 +83,25 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $res.Count | Should -BeGreaterOrEqual 1 } + It "Find resource when version contains different number of digits than the normalized version" { + # the resource has version "1.0", but querying with any equivalent version should work + $res1DigitVersion = Find-PSResource -Name $testModuleWith2DigitVersion -Version "1" -Repository $ACRRepoName + $res1DigitVersion | Should -Not -BeNullOrEmpty + $res1DigitVersion.Version | Should -Be "1.0" + + $res2DigitVersion = Find-PSResource -Name $testModuleWith2DigitVersion -Version "1.0" -Repository $ACRRepoName + $res2DigitVersion | Should -Not -BeNullOrEmpty + $res2DigitVersion.Version | Should -Be "1.0" + + $res3DigitVersion = Find-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0" -Repository $ACRRepoName + $res3DigitVersion | Should -Not -BeNullOrEmpty + $res3DigitVersion.Version | Should -Be "1.0" + + $res4DigitVersion = Find-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0.0" -Repository $ACRRepoName + $res4DigitVersion | Should -Not -BeNullOrEmpty + $res4DigitVersion.Version | Should -Be "1.0" + } + It "Find module and dependencies when -IncludeDependencies is specified" { $res = Find-PSResource -Name $testModuleParentName -Repository $ACRRepoName -IncludeDependencies $res | Should -Not -BeNullOrEmpty diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index 2ade007f2..0dbf394f9 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -10,6 +10,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { BeforeAll { $testModuleName = "test-module" $testModuleName2 = "test-module2" + $testModuleWith2DigitVersion = "test-2DigitPkg" $testCamelCaseModuleName = "test-camelCaseModule" $testCamelCaseScriptName = "test-camelCaseScript" $testModuleParentName = "test_parent_mod" @@ -33,7 +34,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { } AfterEach { - Uninstall-PSResource $testModuleName, $testModuleName2, $testCamelCaseModuleName, $testScriptName, $testCamelCaseScriptName -Version "*" -SkipDependencyCheck -ErrorAction SilentlyContinue + Uninstall-PSResource $testModuleName, $testModuleName2, $testCamelCaseModuleName, $testScriptName, $testCamelCaseScriptName, $testModuleWith2DigitVersion -Version "*" -SkipDependencyCheck -ErrorAction SilentlyContinue } AfterAll { @@ -75,6 +76,38 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { $pkg.Version | Should -BeExactly "1.0.0" } + It "Install resource when version contains different number of digits than the normalized version- 1 digit specified" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1" -Repository $ACRRepoName + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.0" + } + + It "Install resource when version contains different number of digits than the normalized version- 2 digits specified" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0" -Repository $ACRRepoName + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.0" + } + + It "Install resource when version contains different number of digits than the normalized version- 3 digits specified" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0" -Repository $ACRRepoName + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.0" + } + + It "Install resource when version contains different number of digits than the normalized version- 4 digits specified" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0.0" -Repository $ACRRepoName + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.0" + } + It "Install multiple resources by name" { $pkgNames = @($testModuleName, $testModuleName2) Install-PSResource -Name $pkgNames -Repository $ACRRepoName -TrustRepository From 0e77d204a1128d83bfaa76ca00c2cf9333f93718 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 18 Feb 2025 14:52:19 -0500 Subject: [PATCH 34/89] add -TrustRepository to install tests --- .../InstallPSResourceContainerRegistryServer.Tests.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index 0dbf394f9..fab2b912d 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -78,7 +78,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { It "Install resource when version contains different number of digits than the normalized version- 1 digit specified" { # the resource has version "1.0", but querying with any equivalent version should work - Install-PSResource -Name $testModuleWith2DigitVersion -Version "1" -Repository $ACRRepoName + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1" -Repository $ACRRepoName -TrustRepository $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion $res | Should -Not -BeNullOrEmpty $res.Version | Should -Be "1.0" @@ -86,7 +86,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { It "Install resource when version contains different number of digits than the normalized version- 2 digits specified" { # the resource has version "1.0", but querying with any equivalent version should work - Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0" -Repository $ACRRepoName + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0" -Repository $ACRRepoName -TrustRepository $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion $res | Should -Not -BeNullOrEmpty $res.Version | Should -Be "1.0" @@ -94,7 +94,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { It "Install resource when version contains different number of digits than the normalized version- 3 digits specified" { # the resource has version "1.0", but querying with any equivalent version should work - Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0" -Repository $ACRRepoName + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0" -Repository $ACRRepoName -TrustRepository $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion $res | Should -Not -BeNullOrEmpty $res.Version | Should -Be "1.0" @@ -102,7 +102,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { It "Install resource when version contains different number of digits than the normalized version- 4 digits specified" { # the resource has version "1.0", but querying with any equivalent version should work - Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0.0" -Repository $ACRRepoName + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0.0" -Repository $ACRRepoName -TrustRepository $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion $res | Should -Not -BeNullOrEmpty $res.Version | Should -Be "1.0" From 96f76e747c19b82c27345c0e9e93516c30d2ec53 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 18 Feb 2025 15:13:00 -0500 Subject: [PATCH 35/89] account for prerelease version when using manifest version --- src/code/InstallHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 8ac7fa8fe..3e0d8ae9a 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -829,7 +829,7 @@ private Hashtable BeginPackageInstall( // However, for container registries the version must exactly match what was in the artifact manifest and then reflected in PSResourceInfo.Version.ToString() if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.ContainerRegistry) { - pkgVersion = pkgToInstall.Version.ToString(); + pkgVersion = String.IsNullOrEmpty(pkgToInstall.Prerelease) ? pkgToInstall.Version.ToString() : $"{pkgToInstall.Version.ToString()}-{pkgToInstall.Prerelease}"; } // Check to see if the pkg is already installed (ie the pkg is installed and the version satisfies the version range provided via param) From dd142a9389704750c452401b7aba1e8371a5f84f Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 18 Feb 2025 15:38:56 -0500 Subject: [PATCH 36/89] add test for preview install --- .../InstallPSResourceContainerRegistryServer.Tests.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index fab2b912d..5e4eb76ea 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -108,6 +108,15 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { $res.Version | Should -Be "1.0" } + It "Install resource when version contains different number of digits than the normalized version- 4 digits specified and prerelease" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.5.0.0" -Repository $ACRRepoName -TrustRepository + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.5" + $res.Prerelease | Should -Be "alpha" + } + It "Install multiple resources by name" { $pkgNames = @($testModuleName, $testModuleName2) Install-PSResource -Name $pkgNames -Repository $ACRRepoName -TrustRepository From ec8d2cc658a904d870848a2ba3ccca14d224f843 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 18 Feb 2025 16:13:12 -0500 Subject: [PATCH 37/89] fix error in test logic --- .../InstallPSResourceContainerRegistryServer.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index 5e4eb76ea..5f80ace08 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -108,9 +108,9 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { $res.Version | Should -Be "1.0" } - It "Install resource when version contains different number of digits than the normalized version- 4 digits specified and prerelease" { + It "Install resource where version specified is a prerelease version" { # the resource has version "1.0", but querying with any equivalent version should work - Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.5.0.0" -Repository $ACRRepoName -TrustRepository + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.5-alpha" -Prerelease -Repository $ACRRepoName -TrustRepository $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion $res | Should -Not -BeNullOrEmpty $res.Version | Should -Be "1.5" From 5645a144811ddb3c0649785eac0bb626b6b2310d Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 25 Feb 2025 00:02:20 -0500 Subject: [PATCH 38/89] account for 2 digit version being concatenated with prerelease when creating version for .xml when installing --- src/code/Utils.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index d51ba0fdb..89de7cd10 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -316,9 +316,11 @@ public static string GetNormalizedVersionString( string versionString, string prerelease) { - // versionString may be like 1.2.0.0 or 1.2.0 + // versionString may be like 1.2.0.0 or 1.2.0 or 1.2 // prerelease may be null or "alpha1" // possible passed in examples: + // versionString: "1.2" <- container registry 2 digit version + // versionString: "1.2" prerelease: "alpha1" <- container registry 2 digit version // versionString: "1.2.0" prerelease: "alpha1" // versionString: "1.2.0" prerelease: "" <- doubtful though // versionString: "1.2.0.0" prerelease: "alpha1" @@ -331,9 +333,10 @@ public static string GetNormalizedVersionString( int numVersionDigits = versionString.Split('.').Count(); - if (numVersionDigits == 3) + if (numVersionDigits == 2 || numVersionDigits == 3) { - // versionString: "1.2.0" prerelease: "alpha1" + // versionString: "1.2.0" prerelease: "alpha1" -> 1.2.0-alpha1 + // versionString: "1.2" prerelease: "alpha1" -> 1.2-alpha1 return versionString + "-" + prerelease; } else if (numVersionDigits == 4) From 5add5e548555c547cba903a6822f9874dec864bf Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:42:05 -0800 Subject: [PATCH 39/89] Update README.md --- README.md | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e94344ae1..a0ca6c443 100644 --- a/README.md +++ b/README.md @@ -52,25 +52,22 @@ Please use the [PowerShell Gallery](https://www.powershellgallery.com) to get th ### Build the project -```powershell -# Build for the net472 framework -PS C:\Repos\PSResourceGet> .\build.ps1 -Clean -Build -BuildConfiguration Debug -BuildFramework net472 - -# Build for the netstandard2.0 framework -PS C:\Repos\PSResourceGet> .\build.ps1 -Clean -Build -BuildConfiguration Debug -BuildFramework netstandard2.0 -``` - -### Publish the module to a local repository -======= -* Run functional tests +Note: Please ensure you have the exact version of the .NET SDK installed. The current version can be found in the [global.json](https://github.com/PowerShell/PSResourceGet/blob/master/global.json) and installed from the [.NET website](https://dotnet.microsoft.com/en-us/download). + ```powershell + # Build for the net472 framework + PS C:\Repos\PSResourceGet> .\build.ps1 -Clean -Build -BuildConfiguration Debug -BuildFramework net472 + ``` -```powershell -PS C:\Repos\PSResourceGet> Invoke-Pester -``` +### Run functional tests -```powershell -PS C:\Repos\PSResourceGet> Invoke-Pester -``` +* Run all tests + ```powershell + PS C:\Repos\PSResourceGet> Invoke-Pester + ``` +* Run an individual test + ```powershell + PS C:\Repos\PSResourceGet> Invoke-Pester + ``` ### Import the built module into a new PowerShell session From 7ac9a55869b7ce26414854b5ac625f5e1fc5a3ef Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Thu, 6 Mar 2025 12:30:00 -0500 Subject: [PATCH 40/89] Update changelog, release notes, version for v1.1.1 release (#1801) --- CHANGELOG/1.1.md | 12 ++++++++++++ src/Microsoft.PowerShell.PSResourceGet.psd1 | 13 ++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG/1.1.md b/CHANGELOG/1.1.md index ba6cce61e..4fb078eda 100644 --- a/CHANGELOG/1.1.md +++ b/CHANGELOG/1.1.md @@ -1,3 +1,15 @@ +# 1.1 Changelog + +## [1.1.1](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0..v1.1.1) - 2025-03-06 + +- Bugfix to retrieve all metadata properties when finding a PSResource from a ContainerRegistry repository (#1799) +- Update README.md (#1798) +- Use authentication challenge for unauthenticated ContainerRegistry repository (#1797) +- Bugfix for Install-PSResource with varying digit version against ContainerRegistry repository (#1796) +- Bugfix for updating ContainerRegistry dependency parsing logic to account for AzPreview package (#1792) +- Add wildcard support for MAR repository for FindAll and FindByName (#1786) +- Bugfix for nuspec dependency version range calculation for RequiredModules (#1784) + ## [1.1.0](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0-rc3...v1.1.0) - 2025-01-09 ### Bug Fixes diff --git a/src/Microsoft.PowerShell.PSResourceGet.psd1 b/src/Microsoft.PowerShell.PSResourceGet.psd1 index 30994ba2e..c9ca1cdd9 100644 --- a/src/Microsoft.PowerShell.PSResourceGet.psd1 +++ b/src/Microsoft.PowerShell.PSResourceGet.psd1 @@ -4,7 +4,7 @@ @{ RootModule = './Microsoft.PowerShell.PSResourceGet.dll' NestedModules = @('./Microsoft.PowerShell.PSResourceGet.psm1') - ModuleVersion = '1.1.0' + ModuleVersion = '1.1.1' CompatiblePSEditions = @('Core', 'Desktop') GUID = 'e4e0bda1-0703-44a5-b70d-8fe704cd0643' Author = 'Microsoft Corporation' @@ -56,6 +56,17 @@ ProjectUri = 'https://go.microsoft.com/fwlink/?LinkId=828955' LicenseUri = 'https://go.microsoft.com/fwlink/?LinkId=829061' ReleaseNotes = @' +## 1.1.1 + +### Bug Fix +- Bugfix to retrieve all metadata properties when finding a PSResource from a ContainerRegistry repository (#1799) +- Update README.md (#1798) +- Use authentication challenge for unauthenticated ContainerRegistry repository (#1797) +- Bugfix for Install-PSResource with varying digit version against ContainerRegistry repository (#1796) +- Bugfix for updating ContainerRegistry dependency parsing logic to account for AzPreview package (#1792) +- Add wildcard support for MAR repository for FindAll and FindByName (#1786) +- Bugfix for nuspec dependency version range calculation for RequiredModules (#1784) + ## 1.1.0 ### Bug Fix From 24047d70e1a29d0c97ab1fd1be02aa222effd738 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 6 Mar 2025 11:50:51 -0800 Subject: [PATCH 41/89] Update .NET to 8.0.406 (#1802) --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index b832c3a01..77d62424a 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.404" + "version": "8.0.406" } } From 3b34f53319dcd72ed1cfca85018e114f4370c3f0 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Thu, 6 Mar 2025 19:39:04 -0500 Subject: [PATCH 42/89] Remove InstallationPath to Install .NET Dependencies task (#1804) --- .pipelines/PSResourceGet-Official.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.pipelines/PSResourceGet-Official.yml b/.pipelines/PSResourceGet-Official.yml index ae6e2d484..cc51e2e78 100644 --- a/.pipelines/PSResourceGet-Official.yml +++ b/.pipelines/PSResourceGet-Official.yml @@ -115,8 +115,6 @@ extends: inputs: packageType: 'sdk' useGlobalJson: true - # this is to ensure that we are installing the dotnet at the same location as container by default install the dotnet sdks - installationPath: 'C:\Program Files\dotnet\' workingDirectory: $(repoRoot) - task: CodeQL3000Init@0 # Add CodeQL Init task right before your 'Build' step. From 1a043eb56969be99d6949b17d960dfa3e22c802f Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 24 Mar 2025 11:43:14 -0700 Subject: [PATCH 43/89] Update NuGet assembly versions (#1807) --- src/code/Microsoft.PowerShell.PSResourceGet.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/code/Microsoft.PowerShell.PSResourceGet.csproj b/src/code/Microsoft.PowerShell.PSResourceGet.csproj index b76dcc9fc..1d657e1a1 100644 --- a/src/code/Microsoft.PowerShell.PSResourceGet.csproj +++ b/src/code/Microsoft.PowerShell.PSResourceGet.csproj @@ -25,6 +25,7 @@ + From 8290e625de5b53a569eb296e5c1af34b5abd1580 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 30 Apr 2025 12:22:31 -0400 Subject: [PATCH 44/89] Add CODEOWNERS file (#1818) Co-authored-by: anamnavi --- .github/CODEOWNERS | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..1b53fe53a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# https://help.github.com/articles/about-codeowners/ + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# the following owners will be requested for +# review when someone opens a pull request. +* @anamnavi @alerickson @adityapatwardhan @SydneyhSmith From 71110bae3a2999cc689a46b991b65e2052624f24 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Wed, 7 May 2025 09:27:52 -0700 Subject: [PATCH 45/89] Add SupportsWildcards attribute to Repository parameter of Install-PSResource (#1808) --- src/code/InstallPSResource.cs | 37 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index a65797268..d8af185f0 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -17,15 +17,15 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets /// The Install-PSResource cmdlet installs a resource. /// It returns nothing. /// - [Cmdlet(VerbsLifecycle.Install, - "PSResource", - DefaultParameterSetName = "NameParameterSet", + [Cmdlet(VerbsLifecycle.Install, + "PSResource", + DefaultParameterSetName = "NameParameterSet", SupportsShouldProcess = true)] [Alias("isres")] public sealed class InstallPSResource : PSCmdlet { - #region Parameters + #region Parameters /// /// Specifies the exact names of resources to install from a repository. @@ -42,7 +42,7 @@ class InstallPSResource : PSCmdlet [Parameter(ParameterSetName = NameParameterSet, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string Version { get; set; } - + /// /// Specifies to allow installation of prerelease versions /// @@ -53,6 +53,7 @@ class InstallPSResource : PSCmdlet /// /// Specifies the repositories from which to search for the resource to be installed. /// + [SupportsWildcards] [Parameter(ParameterSetName = NameParameterSet, ValueFromPipelineByPropertyName = true)] [Parameter(ParameterSetName = InputObjectParameterSet, ValueFromPipelineByPropertyName = true)] [ArgumentCompleter(typeof(RepositoryNameCompleter))] @@ -83,9 +84,9 @@ public string TemporaryPath set { - if (WildcardPattern.ContainsWildcardCharacters(value)) - { - throw new PSArgumentException("Wildcard characters are not allowed in the temporary path."); + if (WildcardPattern.ContainsWildcardCharacters(value)) + { + throw new PSArgumentException("Wildcard characters are not allowed in the temporary path."); } // This will throw if path cannot be resolved @@ -99,7 +100,7 @@ public string TemporaryPath /// [Parameter] public SwitchParameter TrustRepository { get; set; } - + /// /// Overwrites a previously installed resource with the same name and version. /// @@ -130,7 +131,7 @@ public string TemporaryPath /// [Parameter] public SwitchParameter SkipDependencyCheck { get; set; } - + /// /// Check validation for signed and catalog files /// @@ -287,7 +288,7 @@ protected override void ProcessRecord() pkgCredential: Credential, reqResourceParams: null); break; - + case InputObjectParameterSet: foreach (var inputObj in InputObject) { string normalizedVersionString = Utils.GetNormalizedVersionString(inputObj.Version.ToString(), inputObj.Prerelease); @@ -362,7 +363,7 @@ protected override void ProcessRecord() ErrorCategory.InvalidData, this)); } - + RequiredResourceHelper(pkgsInFile); break; @@ -379,7 +380,7 @@ protected override void ProcessRecord() } } */ - + Hashtable pkgsHash = null; try { @@ -441,7 +442,7 @@ private void RequiredResourceHelper(Hashtable reqResourceHash) { var pkgNameEmptyOrWhitespaceError = new ErrorRecord( new ArgumentException($"The package name '{pkgName}' provided cannot be an empty string or whitespace."), - "pkgNameEmptyOrWhitespaceError", + "pkgNameEmptyOrWhitespaceError", ErrorCategory.InvalidArgument, this); @@ -454,7 +455,7 @@ private void RequiredResourceHelper(Hashtable reqResourceHash) { var requiredResourceHashtableInputFormatError = new ErrorRecord( new ArgumentException($"The RequiredResource input with name '{pkgName}' does not have a valid value, the value must be a hashtable."), - "RequiredResourceHashtableInputFormatError", + "RequiredResourceHashtableInputFormatError", ErrorCategory.InvalidArgument, this); @@ -483,7 +484,7 @@ private void RequiredResourceHelper(Hashtable reqResourceHash) ThrowTerminatingError(ParameterParsingError); } } - + if (pkgParams.Scope == ScopeType.AllUsers) { _pathsToInstallPkg = Utils.GetAllInstallationPaths(this, pkgParams.Scope); @@ -513,10 +514,10 @@ private void ProcessInstallHelper(string[] pkgNames, string pkgVersion, bool pkg "NameContainsWildcard", ErrorCategory.InvalidArgument, this)); - + return; } - + foreach (string error in errorMsgs) { WriteError(new ErrorRecord( From 36acf3cbe027ddec38fb9429d066307f23760b1d Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 17 Jun 2025 12:29:50 -0700 Subject: [PATCH 46/89] Fix PSResourceGet CI for ContainerRegistry (#1829) --- .ci/test.yml | 2 ++ global.json | 2 +- src/code/ContainerRegistryServerAPICalls.cs | 6 +++- src/code/FindPSResource.cs | 19 +++++----- .../Microsoft.PowerShell.PSResourceGet.csproj | 2 +- src/code/RegisterPSResourceRepository.cs | 24 ++++++------- src/code/Utils.cs | 36 +++++++++++++------ 7 files changed, 56 insertions(+), 35 deletions(-) diff --git a/.ci/test.yml b/.ci/test.yml index cd8c8c88f..e59b14ad1 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -107,10 +107,12 @@ jobs: inputs: azureSubscription: PSResourceGetACR azurePowerShellVersion: LatestVersion + pwsh: true ScriptType: InlineScript inline: | $modulePath = Join-Path -Path $env:AGENT_TEMPDIRECTORY -ChildPath 'TempModules' $env:PSModulePath = $modulePath + [System.IO.Path]::PathSeparator + $env:PSModulePath + Write-Verbose -Verbose "Importing build utilities (buildtools.psd1)" Import-Module -Name (Join-Path -Path '${{ parameters.buildDirectory }}' -ChildPath 'buildtools.psd1') -Force Invoke-ModuleTestsACR -Type Functional diff --git a/global.json b/global.json index 77d62424a..fb6d6a3df 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.406" + "version": "8.0.411" } } diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 785c7aeae..5dab4c49e 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -394,6 +394,8 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) else { bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord, out accessToken); + _cmdletPassedIn.WriteDebug($"Is repository unauthenticated: {isRepositoryUnauthenticated}"); + if (errRecord != null) { return null; @@ -407,7 +409,7 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) if (!isRepositoryUnauthenticated) { - accessToken = Utils.GetAzAccessToken(); + accessToken = Utils.GetAzAccessToken(_cmdletPassedIn); if (string.IsNullOrEmpty(accessToken)) { errRecord = new ErrorRecord( @@ -488,6 +490,8 @@ internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out // get the anonymous access token var url = $"{realm}?service={service}{defaultScope}"; + _cmdletPassedIn.WriteDebug($"Getting anonymous access token from the realm: {url}"); + // we dont check the errorrecord here because we want to return false if we get a 401 and not throw an error var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out _); diff --git a/src/code/FindPSResource.cs b/src/code/FindPSResource.cs index d2a78b1a6..2709073e7 100644 --- a/src/code/FindPSResource.cs +++ b/src/code/FindPSResource.cs @@ -24,7 +24,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets public sealed class FindPSResource : PSCmdlet { #region Members - + private const string NameParameterSet = "NameParameterSet"; private const string CommandNameParameterSet = "CommandNameParameterSet"; private const string DscResourceNameParameterSet = "DscResourceNameParameterSet"; @@ -39,7 +39,7 @@ public sealed class FindPSResource : PSCmdlet /// Specifies name of a resource or resources to find. Accepts wild card characters. /// [SupportsWildcards] - [Parameter(Position = 0, + [Parameter(Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = NameParameterSet)] @@ -167,7 +167,6 @@ protected override void ProcessRecord() private void ProcessResourceNameParameterSet() { - WriteDebug("In FindPSResource::ProcessResourceNameParameterSet()"); // only cases where Name is allowed to not be specified is if Type or Tag parameters are if (!MyInvocation.BoundParameters.ContainsKey(nameof(Name))) { @@ -178,7 +177,7 @@ private void ProcessResourceNameParameterSet() } else if (MyInvocation.BoundParameters.ContainsKey(nameof(Type))) { - Name = new string[] {"*"}; + Name = new string[] { "*" }; } else { @@ -191,8 +190,8 @@ private void ProcessResourceNameParameterSet() } WriteDebug("Filtering package name(s) on wildcards"); - Name = Utils.ProcessNameWildcards(Name, removeWildcardEntries:false, out string[] errorMsgs, out bool nameContainsWildcard); - + Name = Utils.ProcessNameWildcards(Name, removeWildcardEntries: false, out string[] errorMsgs, out bool nameContainsWildcard); + foreach (string error in errorMsgs) { WriteError(new ErrorRecord( @@ -208,7 +207,7 @@ private void ProcessResourceNameParameterSet() { WriteDebug("Package name(s) could not be resolved"); return; - } + } // determine/parse out Version param VersionType versionType = VersionType.VersionRange; @@ -232,7 +231,7 @@ private void ProcessResourceNameParameterSet() "IncorrectVersionFormat", ErrorCategory.InvalidArgument, this)); - + return; } } @@ -289,7 +288,7 @@ private void ProcessCommandOrDscParameterSet(bool isSearchingForCommands) WriteDebug("Command or DSCResource name(s) could not be resolved"); return; } - + foreach (PSCommandResourceInfo cmdPkg in _findHelper.FindByCommandOrDscResource( isSearchingForCommands: isSearchingForCommands, prerelease: Prerelease, @@ -325,7 +324,7 @@ private void ProcessTags() WriteDebug("Tags(s) could not be resolved"); return; } - + foreach (PSResourceInfo tagPkg in _findHelper.FindByTag( type: Type, prerelease: Prerelease, diff --git a/src/code/Microsoft.PowerShell.PSResourceGet.csproj b/src/code/Microsoft.PowerShell.PSResourceGet.csproj index 1d657e1a1..225c4292e 100644 --- a/src/code/Microsoft.PowerShell.PSResourceGet.csproj +++ b/src/code/Microsoft.PowerShell.PSResourceGet.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 1a86db210..97e18bdc9 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -102,7 +102,7 @@ class RegisterPSResourceRepository : PSCmdlet /// [Parameter] public SwitchParameter PassThru { get; set; } - + /// /// When specified, will overwrite information for any existing repository with the same name. /// @@ -212,14 +212,14 @@ private PSRepositoryInfo PSGalleryParameterSetHelper(int repoPriority, bool repo WriteDebug("In RegisterPSResourceRepository::PSGalleryParameterSetHelper()"); Uri psGalleryUri = new Uri(PSGalleryRepoUri); WriteDebug("Internal name and uri values for PSGallery are hardcoded and validated. Priority and trusted values, if passed in, also validated"); - var addedRepo = RepositorySettings.AddToRepositoryStore(PSGalleryRepoName, - psGalleryUri, - repoPriority, - repoTrusted, + var addedRepo = RepositorySettings.AddToRepositoryStore(PSGalleryRepoName, + psGalleryUri, + repoPriority, + repoTrusted, apiVersion: null, - repoCredentialInfo: null, - Force, - this, + repoCredentialInfo: null, + Force, + this, out string errorMsg); if (!string.IsNullOrEmpty(errorMsg)) @@ -313,7 +313,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) "NullUriForRepositoriesParameterSetRegistration", ErrorCategory.InvalidArgument, this)); - + return null; } @@ -337,10 +337,10 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } - if (repo.ContainsKey("ApiVersion") && + if (repo.ContainsKey("ApiVersion") && (repo["ApiVersion"] == null || String.IsNullOrEmpty(repo["ApiVersion"].ToString()) || - !(repo["ApiVersion"].ToString().Equals("Local", StringComparison.OrdinalIgnoreCase) || repo["ApiVersion"].ToString().Equals("V2", StringComparison.OrdinalIgnoreCase) || - repo["ApiVersion"].ToString().Equals("V3", StringComparison.OrdinalIgnoreCase) || repo["ApiVersion"].ToString().Equals("NugetServer", StringComparison.OrdinalIgnoreCase) || + !(repo["ApiVersion"].ToString().Equals("Local", StringComparison.OrdinalIgnoreCase) || repo["ApiVersion"].ToString().Equals("V2", StringComparison.OrdinalIgnoreCase) || + repo["ApiVersion"].ToString().Equals("V3", StringComparison.OrdinalIgnoreCase) || repo["ApiVersion"].ToString().Equals("NugetServer", StringComparison.OrdinalIgnoreCase) || repo["ApiVersion"].ToString().Equals("Unknown", StringComparison.OrdinalIgnoreCase)))) { WriteError(new ErrorRecord( diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 89de7cd10..5ca14d0e2 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -648,12 +648,11 @@ public static PSCredential GetRepositoryCredentialFromSecretManagement( } } - public static string GetAzAccessToken() + public static string GetAzAccessToken(PSCmdlet cmdletPassedIn) { var credOptions = new DefaultAzureCredentialOptions { ExcludeEnvironmentCredential = true, - ExcludeVisualStudioCodeCredential = true, ExcludeVisualStudioCredential = true, ExcludeWorkloadIdentityCredential = true, ExcludeManagedIdentityCredential = true, // ManagedIdentityCredential makes the experience slow @@ -665,8 +664,25 @@ public static string GetAzAccessToken() var dCred = new DefaultAzureCredential(credOptions); var tokenRequestContext = new TokenRequestContext(new string[] { "https://management.azure.com/.default" }); - var token = dCred.GetTokenAsync(tokenRequestContext).Result; - return token.Token; + + try + { + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30))) + { + var token = dCred.GetTokenAsync(tokenRequestContext, cts.Token).GetAwaiter().GetResult(); + return token.Token; + } + } + catch (OperationCanceledException) + { + cmdletPassedIn.WriteWarning("Timeout occurred while acquiring Azure access token."); + return null; + } + catch (Exception ex) + { + cmdletPassedIn.WriteWarning($"Failed to acquire Azure access token: {ex.Message}"); + return null; + } } public static string GetContainerRegistryAccessTokenFromSecretManagement( @@ -1874,9 +1890,9 @@ public static Hashtable GetMetadataFromNuspec(string nuspecFilePath, PSCmdlet cm catch (Exception e) { errorRecord = new ErrorRecord( - exception: e, - "GetHashtableForNuspecFailure", - ErrorCategory.ReadError, + exception: e, + "GetHashtableForNuspecFailure", + ErrorCategory.ReadError, cmdletPassedIn); } @@ -1895,9 +1911,9 @@ public static XmlDocument LoadXmlDocument(string filePath, PSCmdlet cmdletPassed catch (Exception e) { errRecord = new ErrorRecord( - exception: e, - "LoadXmlDocumentFailure", - ErrorCategory.ReadError, + exception: e, + "LoadXmlDocumentFailure", + ErrorCategory.ReadError, cmdletPassedIn); } From a6e71fa8fdec87809ee864b395dc1844a9c52f00 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 18 Jun 2025 10:14:59 -0700 Subject: [PATCH 47/89] Fix catalog access for Container Registry repositories (#1831) --- src/code/ContainerRegistryServerAPICalls.cs | 75 ++++++++++++--------- src/code/PSRepositoryInfo.cs | 2 +- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 5dab4c49e..5977d4adf 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -47,6 +47,10 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest const string defaultScope = "&scope=repository:*:*&scope=registry:catalog:*"; + const string catalogScope = "&scope=registry:catalog:*"; + const string grantTypeTemplate = "grant_type=access_token&service={0}{1}"; // 0 - registry, 1 - scope + const string authUrlTemplate = "{0}?service={1}{2}"; // 0 - realm, 1 - service, 2 - scope + const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry #endregion @@ -323,7 +327,7 @@ private Stream InstallVersion( return null; } - string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); if (errRecord != null) { return null; @@ -371,7 +375,7 @@ private Stream InstallVersion( /// If no credential provided at registration then, check if the ACR endpoint can be accessed without a token. If not, try using Azure.Identity to get the az access token, then ACR refresh token and then ACR access token. /// Note: Access token can be empty if the repository is unauthenticated /// - internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) + internal string GetContainerRegistryAccessToken(bool needCatalogAccess, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::GetContainerRegistryAccessToken()"); string accessToken = string.Empty; @@ -393,7 +397,7 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) } else { - bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord, out accessToken); + bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), needCatalogAccess, out errRecord, out accessToken); _cmdletPassedIn.WriteDebug($"Is repository unauthenticated: {isRepositoryUnauthenticated}"); if (errRecord != null) @@ -446,7 +450,7 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) /// /// Checks if container registry repository is unauthenticated. /// - internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord, out string anonymousAccessToken) + internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, bool needCatalogAccess, out ErrorRecord errRecord, out string anonymousAccessToken) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()"); errRecord = null; @@ -484,20 +488,24 @@ internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out return false; } - string content = "grant_type=access_token&service=" + service + defaultScope; + string content = needCatalogAccess ? String.Format(grantTypeTemplate, service, catalogScope) : String.Format(grantTypeTemplate, service, defaultScope); + var contentHeaders = new Collection> { new KeyValuePair("Content-Type", "application/x-www-form-urlencoded") }; - // get the anonymous access token - var url = $"{realm}?service={service}{defaultScope}"; + string url = needCatalogAccess ? String.Format(authUrlTemplate, realm, service, catalogScope) : String.Format(authUrlTemplate, realm, service, defaultScope); _cmdletPassedIn.WriteDebug($"Getting anonymous access token from the realm: {url}"); // we dont check the errorrecord here because we want to return false if we get a 401 and not throw an error - var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out _); + _cmdletPassedIn.WriteDebug($"Getting anonymous access token from the realm: {url}"); + ErrorRecord errRecordTemp = null; + + var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out errRecordTemp); if (results == null) { _cmdletPassedIn.WriteDebug("Failed to get access token from the realm. results is null."); + _cmdletPassedIn.WriteDebug($"ErrorRecord: {errRecordTemp}"); return false; } @@ -508,7 +516,6 @@ internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out } anonymousAccessToken = results["access_token"].ToString(); - _cmdletPassedIn.WriteDebug("Anonymous access token retrieved"); return true; } } @@ -761,7 +768,7 @@ internal Hashtable GetContainerRegistryMetadata(string packageName, string exact if (!NuGetVersion.TryParse(pkgVersionString, out NuGetVersion pkgVersion)) { errRecord = new ErrorRecord( - new ArgumentException($"Version {pkgVersionString} to be parsed from metadata is not a valid NuGet version."), + new ArgumentException($"Version {pkgVersionString} to be parsed from metadata is not a valid NuGet version for package '{packageName}'."), "ParseMetadataFailure", ErrorCategory.InvalidArgument, this); @@ -988,24 +995,29 @@ internal JObject GetHttpResponseJObjectUsingContentHeaders(string url, HttpMetho { HttpRequestMessage request = new HttpRequestMessage(method, url); - if (string.IsNullOrEmpty(content)) + // HTTP GET does not expect a body / content. + if (method != HttpMethod.Get) { - errRecord = new ErrorRecord( - exception: new ArgumentNullException($"Content is null or empty and cannot be used to make a request as its content headers."), - "RequestContentHeadersNullOrEmpty", - ErrorCategory.InvalidData, - _cmdletPassedIn); - return null; - } + if (string.IsNullOrEmpty(content)) + { + errRecord = new ErrorRecord( + exception: new ArgumentNullException($"Content is null or empty and cannot be used to make a request as its content headers."), + "RequestContentHeadersNullOrEmpty", + ErrorCategory.InvalidData, + _cmdletPassedIn); - request.Content = new StringContent(content); - request.Content.Headers.Clear(); - if (contentHeaders != null) - { - foreach (var header in contentHeaders) + return null; + } + + request.Content = new StringContent(content); + request.Content.Headers.Clear(); + if (contentHeaders != null) { - request.Content.Headers.Add(header.Key, header.Value); + foreach (var header in contentHeaders) + { + request.Content.Headers.Add(header.Key, header.Value); + } } } @@ -1234,7 +1246,7 @@ internal bool PushNupkgContainerRegistry( // Get access token (includes refresh tokens) _cmdletPassedIn.WriteVerbose($"Get access token for container registry server."); - var containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + var containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); if (errRecord != null) { return false; @@ -1699,7 +1711,7 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp string packageNameLowercase = packageName.ToLower(); string packageNameForFind = PrependMARPrefix(packageNameLowercase); - string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); if (errRecord != null) { return emptyHashResponses; @@ -1715,8 +1727,9 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp List allVersionsList = foundTags["tags"].ToList(); SortedDictionary sortedQualifyingPkgs = GetPackagesWithRequiredVersion(allVersionsList, versionType, versionRange, requiredVersion, packageNameForFind, includePrerelease, out errRecord); - if (errRecord != null) + if (errRecord != null && sortedQualifyingPkgs?.Count == 0) { + _cmdletPassedIn.WriteDebug("No qualifying packages found for the specified criteria."); return emptyHashResponses; } @@ -1760,12 +1773,14 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp if (!NuGetVersion.TryParse(pkgVersionString, out NuGetVersion pkgVersion)) { errRecord = new ErrorRecord( - new ArgumentException($"Version {pkgVersionString} to be parsed from metadata is not a valid NuGet version."), + new ArgumentException($"Version {pkgVersionString} to be parsed from metadata is not a valid NuGet version for package '{packageName}'."), "FindNameFailure", ErrorCategory.InvalidArgument, this); - return null; + _cmdletPassedIn.WriteError(errRecord); + _cmdletPassedIn.WriteDebug($"Skipping package '{packageName}' with version '{pkgVersionString}' as it is not a valid NuGet version."); + continue; // skip this version and continue with the next one } _cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'"); @@ -1808,7 +1823,7 @@ private FindResults FindPackages(string packageName, bool includePrerelease, out { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindPackages()"); errRecord = null; - string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: true, out errRecord); if (errRecord != null) { return emptyResponseResults; diff --git a/src/code/PSRepositoryInfo.cs b/src/code/PSRepositoryInfo.cs index b74d52cff..07dd06762 100644 --- a/src/code/PSRepositoryInfo.cs +++ b/src/code/PSRepositoryInfo.cs @@ -104,7 +104,7 @@ public enum RepositoryProviderType internal bool IsMARRepository() { - return (ApiVersion == APIVersion.ContainerRegistry && Uri.Host.Contains("mcr.microsoft.com")); + return (ApiVersion == APIVersion.ContainerRegistry && Uri.Host.StartsWith("mcr.microsoft") ); } #endregion From d48a2d5750afe2aa8b92b778abe2d90b4b858e6d Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:28:15 -0700 Subject: [PATCH 48/89] Remove netstandard2.0 from build options (#1835) --- build.ps1 | 2 +- src/code/Microsoft.PowerShell.PSResourceGet.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.ps1 b/build.ps1 index 3aca68e50..68de8f552 100644 --- a/build.ps1 +++ b/build.ps1 @@ -23,7 +23,7 @@ param ( [ValidateSet("Debug", "Release")] [string] $BuildConfiguration = "Debug", - [ValidateSet("netstandard2.0", "net472")] + [ValidateSet("net472")] [string] $BuildFramework = "net472" ) diff --git a/src/code/Microsoft.PowerShell.PSResourceGet.csproj b/src/code/Microsoft.PowerShell.PSResourceGet.csproj index 225c4292e..715e420d4 100644 --- a/src/code/Microsoft.PowerShell.PSResourceGet.csproj +++ b/src/code/Microsoft.PowerShell.PSResourceGet.csproj @@ -8,7 +8,7 @@ 1.1.0.1 1.1.0.1 1.1.0.1 - net472;netstandard2.0 + net472 9.0 true From adac2bf026423a1f2355e8bde5ff4c76cbc5e99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olav=20R=C3=B8nnestad=20Birkeland?= <6450056+o-l-a-v@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:00:40 +0200 Subject: [PATCH 49/89] NuGet v3 populate dependencies (#1778) --- src/code/FindHelper.cs | 45 ++- src/code/InstallHelper.cs | 54 ++-- src/code/PSResourceInfo.cs | 162 +++++++---- .../FindPSResourceLocal.Tests.ps1 | 19 +- ...indPSResourceRepositorySearching.Tests.ps1 | 2 +- .../FindPSResourceV3Server.Tests.ps1 | 186 ++++++------ .../InstallPSResourceGithubPackages.Tests.ps1 | 18 +- .../InstallPSResourceV3Server.Tests.ps1 | 272 +++++++++--------- .../SavePSResourceV3Tests.ps1 | 103 +++---- 9 files changed, 455 insertions(+), 406 deletions(-) diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 327d0e024..a3c5c5c35 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -105,7 +105,7 @@ public IEnumerable FindByResourceName( if (repository != null) { // Write error and disregard repository entries containing wildcards. - repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries:false, out string[] errorMsgs, out _repositoryNameContainsWildcard); + repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries: false, out string[] errorMsgs, out _repositoryNameContainsWildcard); foreach (string error in errorMsgs) { _cmdletPassedIn.WriteError(new ErrorRecord( @@ -156,7 +156,7 @@ public IEnumerable FindByResourceName( if (repositoriesToSearch != null && repositoriesToSearch.Count == 0) { _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( - new PSArgumentException ("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), + new PSArgumentException("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), "RepositoryNameIsNotResolved", ErrorCategory.InvalidArgument, this)); @@ -222,7 +222,8 @@ public IEnumerable FindByResourceName( bool shouldReportErrorForEachRepo = !suppressErrors && !_repositoryNameContainsWildcard; foreach (PSResourceInfo currentPkg in SearchByNames(currentServer, currentResponseUtil, currentRepository, shouldReportErrorForEachRepo)) { - if (currentPkg == null) { + if (currentPkg == null) + { _cmdletPassedIn.WriteDebug("No packages returned from server"); continue; } @@ -244,7 +245,7 @@ public IEnumerable FindByResourceName( { // Scenarios: Find-PSResource -Name "pkg" -> write error only if pkg wasn't found in any registered repositories // Scenarios: Find-PSResource -Name "pkg" -Repository *Gallery -> write error if only if pkg wasn't found in any matching repositories. - foreach(string pkgName in pkgsDiscovered) + foreach (string pkgName in pkgsDiscovered) { var msg = repository == null ? $"Package '{pkgName}' could not be found in any registered repositories." : $"Package '{pkgName}' could not be found in registered repositories: '{string.Join(", ", repositoryNamesToSearch)}'."; @@ -256,7 +257,7 @@ public IEnumerable FindByResourceName( this)); } } -} + } public IEnumerable FindByCommandOrDscResource( bool isSearchingForCommands, @@ -283,12 +284,12 @@ public IEnumerable FindByCommandOrDscResource( // Error out if repository array of names to be searched contains wildcards. if (repository != null) { - repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries:false, out string[] errorMsgs, out _repositoryNameContainsWildcard); + repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries: false, out string[] errorMsgs, out _repositoryNameContainsWildcard); if (string.Equals(repository[0], "*")) { _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( - new PSArgumentException ("-Repository parameter does not support entry '*' with -CommandName and -DSCResourceName parameters."), + new PSArgumentException("-Repository parameter does not support entry '*' with -CommandName and -DSCResourceName parameters."), "RepositoryDoesNotSupportWildcardEntryWithCmdOrDSCName", ErrorCategory.InvalidArgument, this)); @@ -337,7 +338,7 @@ public IEnumerable FindByCommandOrDscResource( if (repositoriesToSearch != null && repositoriesToSearch.Count == 0) { _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( - new PSArgumentException ("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), + new PSArgumentException("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), "RepositoryNameIsNotResolved", ErrorCategory.InvalidArgument, this)); @@ -487,12 +488,12 @@ public IEnumerable FindByTag( if (repository != null) { - repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries:false, out string[] errorMsgs, out _repositoryNameContainsWildcard); + repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries: false, out string[] errorMsgs, out _repositoryNameContainsWildcard); if (string.Equals(repository[0], "*")) { _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( - new PSArgumentException ("-Repository parameter does not support entry '*' with -Tag parameter."), + new PSArgumentException("-Repository parameter does not support entry '*' with -Tag parameter."), "RepositoryDoesNotSupportWildcardEntryWithTag", ErrorCategory.InvalidArgument, this)); @@ -541,7 +542,7 @@ public IEnumerable FindByTag( if (repositoriesToSearch != null && repositoriesToSearch.Count == 0) { _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( - new PSArgumentException ("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), + new PSArgumentException("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), "RepositoryNameIsNotResolved", ErrorCategory.InvalidArgument, this)); @@ -635,7 +636,7 @@ public IEnumerable FindByTag( if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty)) { errRecord = new ErrorRecord( - new ResourceNotFoundException($"Tags '{String.Join(", ", _tag)}' could not be found" , currentResult.exception), + new ResourceNotFoundException($"Tags '{String.Join(", ", _tag)}' could not be found", currentResult.exception), "FindTagConvertToPSResourceFailure", ErrorCategory.InvalidResult, this); @@ -729,7 +730,7 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R } } } - else if(pkgName.Contains("*")) + else if (pkgName.Contains("*")) { // Example: Find-PSResource -Name "Az*" // Example: Find-PSResource -Name "Az*" -Tag "Storage" @@ -1000,12 +1001,6 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R // After retrieving all packages find their dependencies if (_includeDependencies) { - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V3) - { - _cmdletPassedIn.WriteWarning("Installing dependencies is not currently supported for V3 server protocol repositories. The package will be installed without installing dependencies."); - yield break; - } - foreach (PSResourceInfo currentPkg in parentPkgs) { _cmdletPassedIn.WriteDebug($"Finding dependency packages for '{currentPkg.Name}'"); @@ -1102,7 +1097,8 @@ internal IEnumerable FindDependencyPackages( { _cmdletPassedIn.WriteVerbose(errRecord.Exception.Message); } - else { + else + { _cmdletPassedIn.WriteError(errRecord); } yield return null; @@ -1164,7 +1160,8 @@ internal IEnumerable FindDependencyPackages( { _cmdletPassedIn.WriteVerbose(errRecord.Exception.Message); } - else { + else + { _cmdletPassedIn.WriteError(errRecord); } yield return null; @@ -1199,7 +1196,8 @@ internal IEnumerable FindDependencyPackages( // Check to see if version falls within version range PSResourceInfo foundDep = currentResult.returnedObject; string depVersionStr = $"{foundDep.Version}"; - if (foundDep.IsPrerelease) { + if (foundDep.IsPrerelease) + { depVersionStr += $"-{foundDep.Prerelease}"; } @@ -1222,7 +1220,8 @@ internal IEnumerable FindDependencyPackages( yield return depRes; } } - else { + else + { List pkgVersions = _packagesFound[depPkg.Name] as List; // _packagesFound has depPkg.name in it, but the version is not the same if (!pkgVersions.Contains(FormatPkgVersionString(depPkg))) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 3e0d8ae9a..10e48b55d 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -187,7 +187,7 @@ private List ProcessRepositories( if (repository != null && repository.Length != 0) { // Write error and disregard repository entries containing wildcards. - repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries:false, out string[] errorMsgs, out _); + repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries: false, out string[] errorMsgs, out _); foreach (string error in errorMsgs) { _cmdletPassedIn.WriteError(new ErrorRecord( @@ -231,7 +231,7 @@ private List ProcessRepositories( if (repositoriesToSearch != null && repositoriesToSearch.Count == 0) { _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( - new PSArgumentException ("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), + new PSArgumentException("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), "RepositoryNameIsNotResolved", ErrorCategory.InvalidArgument, _cmdletPassedIn)); @@ -305,7 +305,8 @@ private List ProcessRepositories( bool installDepsForRepo = skipDependencyCheck; // If no more packages to install, then return - if (_pkgNamesToInstall.Count == 0) { + if (_pkgNamesToInstall.Count == 0) + { return allPkgsInstalled; } @@ -329,14 +330,9 @@ private List ProcessRepositories( } repositoryNamesToSearch.Add(repoName); - if ((currentRepository.ApiVersion == PSRepositoryInfo.APIVersion.V3) && (!installDepsForRepo)) - { - _cmdletPassedIn.WriteWarning("Installing dependencies is not currently supported for V3 server protocol repositories. The package will be installed without installing dependencies."); - installDepsForRepo = true; - } - var installedPkgs = InstallPackages(_pkgNamesToInstall.ToArray(), currentRepository, currentServer, currentResponseUtil, scope, skipDependencyCheck, findHelper); - foreach (var pkg in installedPkgs) + List installedPkgs = InstallPackages(_pkgNamesToInstall.ToArray(), currentRepository, currentServer, currentResponseUtil, scope, skipDependencyCheck, findHelper); + foreach (PSResourceInfo pkg in installedPkgs) { _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); } @@ -535,8 +531,10 @@ private void MoveFilesIntoInstallPath( File.Delete(Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt)); } } - else { - if (_includeXml) { + else + { + if (_includeXml) + { _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, scriptXML))); Utils.MoveFiles(Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, scriptXML)); } @@ -614,11 +612,6 @@ private List InstallPackages( if (!skipDependencyCheck) { - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V3) - { - _cmdletPassedIn.WriteWarning("Installing dependencies is not currently supported for V3 server protocol repositories. The package will be installed without installing dependencies."); - } - // Get the dependencies from the installed package. if (parentPkgObj.Dependencies.Length > 0) { @@ -748,7 +741,7 @@ private Hashtable BeginPackageInstall( return packagesHash; } - break; + break; case VersionType.SpecificVersion: string nugetVersionString = specificVersion.ToNormalizedString(); // 3.0.17-beta @@ -802,7 +795,9 @@ private Hashtable BeginPackageInstall( break; } - } else { + } + else + { pkgToInstall = currentResult.returnedObject; break; @@ -816,11 +811,13 @@ private Hashtable BeginPackageInstall( pkgToInstall.RepositorySourceLocation = repository.Uri.ToString(); pkgToInstall.AdditionalMetadata.TryGetValue("NormalizedVersion", out string pkgVersion); - if (pkgVersion == null) { + if (pkgVersion == null) + { // Not all NuGet providers (e.g. Artifactory, possibly others) send NormalizedVersion in NuGet package responses. // If they don't, we need to manually construct the combined version+prerelease from pkgToInstall.Version and the prerelease string. pkgVersion = pkgToInstall.Version.ToString(); - if (!String.IsNullOrEmpty(pkgToInstall.Prerelease)) { + if (!String.IsNullOrEmpty(pkgToInstall.Prerelease)) + { pkgVersion += $"-{pkgToInstall.Prerelease}"; } } @@ -921,11 +918,11 @@ private string CreateInstallationTempPath() try { var dir = Directory.CreateDirectory(tempInstallPath); // should check it gets created properly - // To delete file attributes from the existing ones get the current file attributes first and use AND (&) operator - // with a mask (bitwise complement of desired attributes combination). - // TODO: check the attributes and if it's read only then set it - // attribute may be inherited from the parent - // TODO: are there Linux accommodations we need to consider here? + // To delete file attributes from the existing ones get the current file attributes first and use AND (&) operator + // with a mask (bitwise complement of desired attributes combination). + // TODO: check the attributes and if it's read only then set it + // attribute may be inherited from the parent + // TODO: are there Linux accommodations we need to consider here? dir.Attributes &= ~FileAttributes.ReadOnly; } catch (Exception e) @@ -1003,7 +1000,8 @@ private bool TryInstallToTempPath( bool isModule = File.Exists(moduleManifest); bool isScript = File.Exists(scriptPath); - if (!isModule && !isScript) { + if (!isModule && !isScript) + { scriptPath = ""; } @@ -1433,7 +1431,7 @@ private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string t "ForceAcceptLicense", ErrorCategory.InvalidArgument, _cmdletPassedIn); - + return false; } } diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index dd840b62d..6ddfe6e57 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -1,17 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Dbg = System.Diagnostics.Debug; +using Microsoft.PowerShell.Commands; using NuGet.Versioning; using System; using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Management.Automation; using System.Text.Json; using System.Xml; -using Microsoft.PowerShell.Commands; - -using Dbg = System.Diagnostics.Debug; namespace Microsoft.PowerShell.PSResourceGet.UtilClasses { @@ -82,7 +82,8 @@ public sealed class ResourceIncludes /// Hashtable of PSGet includes internal ResourceIncludes(Hashtable includes) { - if (includes == null) { return; } + if (includes == null) + { return; } Cmdlet = GetHashTableItem(includes, nameof(Cmdlet)); Command = GetHashTableItem(includes, nameof(Command)); @@ -194,8 +195,8 @@ public sealed class PSCommandResourceInfo /// the parent module resource the command or dsc resource belongs to public PSCommandResourceInfo(string[] names, PSResourceInfo parentResource) { - Names = names; - ParentResource = parentResource; + Names = names; + ParentResource = parentResource; } #endregion @@ -296,7 +297,7 @@ private PSResourceInfo( #region Private fields - private static readonly char[] Delimeter = {' ', ','}; + private static readonly char[] Delimeter = { ' ', ',' }; #endregion @@ -331,7 +332,7 @@ public bool TryWrite( return true; } - catch(Exception ex) + catch (Exception ex) { errorMsg = string.Format( CultureInfo.InvariantCulture, @@ -363,11 +364,11 @@ public static bool TryRead( try { // Read and deserialize information xml file. - var psObjectInfo = (PSObject) PSSerializer.Deserialize( + var psObjectInfo = (PSObject)PSSerializer.Deserialize( System.IO.File.ReadAllText( filePath)); - var additionalMetadata = GetProperty>(nameof(PSResourceInfo.AdditionalMetadata), psObjectInfo); + var additionalMetadata = GetProperty>(nameof(PSResourceInfo.AdditionalMetadata), psObjectInfo); Version version = GetVersionInfo(psObjectInfo, additionalMetadata, out string prerelease); psGetInfo = new PSResourceInfo( @@ -401,7 +402,7 @@ public static bool TryRead( return true; } - catch(Exception ex) + catch (Exception ex) { errorMsg = string.Format( CultureInfo.InvariantCulture, @@ -486,7 +487,7 @@ public static bool TryConvertFromXml( errorMsg = "TryConvertXmlToPSResourceInfo: Invalid XmlNodeList object. Object cannot be null."; return false; } - + try { Hashtable metadata = new Hashtable(StringComparer.InvariantCultureIgnoreCase); @@ -577,7 +578,8 @@ public static bool TryConvertFromXml( var additionalMetadataHashtable = new Dictionary(); // Only add NormalizedVersion to additionalMetadata if server response included it - if (metadata.ContainsKey("NormalizedVersion")) { + if (metadata.ContainsKey("NormalizedVersion")) + { additionalMetadataHashtable.Add("NormalizedVersion", metadata["NormalizedVersion"].ToString()); } @@ -594,10 +596,10 @@ public static bool TryConvertFromXml( includes: includes, installedDate: null, installedLocation: null, - isPrerelease: (bool) metadata["IsPrerelease"], + isPrerelease: (bool)metadata["IsPrerelease"], licenseUri: metadata["LicenseUrl"] as Uri, name: metadata["Id"] as String, - powershellGetFormatVersion: null, + powershellGetFormatVersion: null, prerelease: metadata["Prerelease"] as String, projectUri: metadata["ProjectUrl"] as Uri, publishedDate: metadata["Published"] as DateTime?, @@ -608,7 +610,7 @@ public static bool TryConvertFromXml( type: typeInfo, updatedDate: null, version: metadata["Version"] as Version); - + return true; } catch (Exception ex) @@ -643,7 +645,7 @@ public static bool TryConvertFromJson( try { - Hashtable metadata = new Hashtable(StringComparer.InvariantCultureIgnoreCase); + Hashtable metadata = new(StringComparer.InvariantCultureIgnoreCase); JsonElement rootDom = pkgJson.RootElement; // Version @@ -716,7 +718,45 @@ public static bool TryConvertFromJson( } // Dependencies - // TODO, tracked via: https://github.com/PowerShell/PSResourceGet/issues/1169 + if (rootDom.TryGetProperty("dependencyGroups", out JsonElement dependencyGroupsElement)) + { + List pkgDeps = new(); + + if (dependencyGroupsElement.ValueKind == JsonValueKind.Array) + { + foreach ( + JsonElement dependencyGroup in dependencyGroupsElement.EnumerateArray().Where( + x => !string.IsNullOrWhiteSpace(x.GetProperty("@id").GetString()) + ) + ) + { + if (dependencyGroup.TryGetProperty("dependencies", out JsonElement dependenciesElement)) + { + if (dependenciesElement.ValueKind == JsonValueKind.Array) + { + foreach ( + JsonElement dependency in dependenciesElement.EnumerateArray().Where( + x => !string.IsNullOrWhiteSpace(x.GetProperty("@id").GetString()) + ) + ) + { + pkgDeps.Add( + new Dependency( + dependency.GetProperty("id").GetString(), + ( + VersionRange.TryParse(dependency.GetProperty("range").GetString(), out VersionRange versionRange) ? + versionRange : + VersionRange.All + ) + ) + ); + } + } + } + } + } + metadata["Dependencies"] = pkgDeps.ToArray(); + } // IsPrerelease // NuGet.org repository's response does contain 'isPrerelease' element so it can be accquired and set here. @@ -753,9 +793,10 @@ public static bool TryConvertFromJson( { metadata["Id"] = idElement.ToString(); } - + // ReleaseNotes - if (rootDom.TryGetProperty("releaseNotes", out JsonElement releaseNotesElement)) { + if (rootDom.TryGetProperty("releaseNotes", out JsonElement releaseNotesElement)) + { metadata["ReleaseNotes"] = releaseNotesElement.ToString(); } @@ -789,9 +830,9 @@ public static bool TryConvertFromJson( type: ResourceType.None, updatedDate: null, version: metadata["Version"] as Version); - + return true; - + } catch (Exception ex) { @@ -799,7 +840,7 @@ public static bool TryConvertFromJson( CultureInfo.InvariantCulture, @"TryConvertFromJson: Cannot parse PSResourceInfo from json object with error: {0}", ex.Message); - + return false; } } @@ -809,12 +850,12 @@ public static bool TryConvertFromJson( /// used for ContainerRegistry Server API call find response conversion to PSResourceInfo object /// public static bool TryConvertFromContainerRegistryJson( - string packageName, - JsonDocument packageMetadata, - ResourceType? resourceType, - out PSResourceInfo psGetInfo, - PSRepositoryInfo repository, - out string errorMsg) + string packageName, + JsonDocument packageMetadata, + ResourceType? resourceType, + out PSResourceInfo psGetInfo, + PSRepositoryInfo repository, + out string errorMsg) { psGetInfo = null; errorMsg = String.Empty; @@ -845,7 +886,7 @@ public static bool TryConvertFromContainerRegistryJson( metadata["Prerelease"] = prereleaseLabel; metadata["IsPrerelease"] = !String.IsNullOrEmpty(prereleaseLabel); } - else if(rootDom.TryGetProperty("ModuleVersion", out JsonElement moduleVersionElement)) + else if (rootDom.TryGetProperty("ModuleVersion", out JsonElement moduleVersionElement)) { // For modules (i.e with "ModuleVersion" property) it will just contain the numerical part not prerelease label, so we must find that from PrivateData.PSData.Prerelease entry versionValue = moduleVersionElement.ToString(); @@ -1043,7 +1084,7 @@ public static bool TryConvertFromContainerRegistryJson( { { "NormalizedVersion", metadata["NormalizedVersion"].ToString() } }; - + psGetInfo = new PSResourceInfo( additionalMetadata: additionalMetadataHashtable, author: metadata["Authors"] as String, @@ -1071,7 +1112,7 @@ public static bool TryConvertFromContainerRegistryJson( version: metadata["Version"] as Version); return true; - + } catch (Exception ex) { @@ -1079,7 +1120,7 @@ public static bool TryConvertFromContainerRegistryJson( CultureInfo.InvariantCulture, @"TryConvertFromContainerRegistryJson: Cannot parse PSResourceInfo from json object with error: {0}", ex.Message); - + return false; } } @@ -1113,7 +1154,7 @@ public static bool TryConvertFromHashtableForPsd1( moduleNameHash.Add("ModuleName", modName); requiredModulesHashList.Add(moduleNameHash); } - } + } } } @@ -1127,7 +1168,7 @@ public static bool TryConvertFromHashtableForPsd1( { nameof(PSResourceInfo.Includes.DscResource), new PSObject(dscResourceNames) } }; - string prereleaseLabel = (string) pkgMetadata["Prerelease"]; + string prereleaseLabel = (string)pkgMetadata["Prerelease"]; bool isPrerelease = !String.IsNullOrEmpty(prereleaseLabel); Uri iconUri = pkgMetadata["IconUri"] as Uri; @@ -1157,7 +1198,7 @@ public static bool TryConvertFromHashtableForPsd1( isPrerelease: isPrerelease, licenseUri: licenseUri, name: pkgMetadata["Id"] as String, - powershellGetFormatVersion: null, + powershellGetFormatVersion: null, prerelease: prereleaseLabel, projectUri: projectUri, publishedDate: null, @@ -1171,7 +1212,7 @@ public static bool TryConvertFromHashtableForPsd1( return true; } - catch(Exception ex) + catch (Exception ex) { errorMsg = string.Format( CultureInfo.InvariantCulture, @@ -1206,13 +1247,13 @@ public static bool TryConvertFromHashtableForPs1( NuGetVersion nugetVersion = pkgMetadata["Version"] as NuGetVersion; bool isPrerelease = nugetVersion.IsPrerelease; Version version = nugetVersion.Version; - string prereleaseLabel = isPrerelease ? nugetVersion.ToNormalizedString().Split(new char[]{'-'})[1] : String.Empty; + string prereleaseLabel = isPrerelease ? nugetVersion.ToNormalizedString().Split(new char[] { '-' })[1] : String.Empty; var additionalMetadataHashtable = new Dictionary { { "NormalizedVersion", nugetVersion.ToNormalizedString() } }; - ModuleSpecification[] requiredModules = pkgMetadata.ContainsKey("RequiredModules") ? pkgMetadata["RequiredModules"] as ModuleSpecification[] : new ModuleSpecification[]{}; + ModuleSpecification[] requiredModules = pkgMetadata.ContainsKey("RequiredModules") ? pkgMetadata["RequiredModules"] as ModuleSpecification[] : new ModuleSpecification[] { }; psGetInfo = new PSResourceInfo( additionalMetadata: additionalMetadataHashtable, @@ -1228,7 +1269,7 @@ public static bool TryConvertFromHashtableForPs1( isPrerelease: isPrerelease, licenseUri: pkgMetadata["LicenseUri"] as Uri, name: pkgMetadata["Id"] as String, - powershellGetFormatVersion: null, + powershellGetFormatVersion: null, prerelease: prereleaseLabel, projectUri: pkgMetadata["ProjectUri"] as Uri, publishedDate: null, @@ -1242,7 +1283,7 @@ public static bool TryConvertFromHashtableForPs1( return true; } - catch(Exception ex) + catch (Exception ex) { errorMsg = string.Format( CultureInfo.InvariantCulture, @@ -1302,7 +1343,7 @@ public static bool TryConvertFromHashtableForNuspec( isPrerelease: isPrerelease, licenseUri: licenseUri, name: pkgMetadata["id"] as String, - powershellGetFormatVersion: null, + powershellGetFormatVersion: null, prerelease: prereleaseLabel, projectUri: projectUri, publishedDate: null, @@ -1316,7 +1357,7 @@ public static bool TryConvertFromHashtableForNuspec( return true; } - catch(Exception ex) + catch (Exception ex) { errorMsg = string.Format( CultureInfo.InvariantCulture, @@ -1386,9 +1427,10 @@ private static T GetProperty( private static Dependency[] GetDependencies(ArrayList dependencyInfos) { List dependenciesFound = new List(); - if (dependencyInfos == null) { return dependenciesFound.ToArray(); } + if (dependencyInfos == null) + { return dependenciesFound.ToArray(); } - foreach(PSObject dependencyObj in dependencyInfos) + foreach (PSObject dependencyObj in dependencyInfos) { // The dependency object can be a string or a hashtable // eg: @@ -1541,7 +1583,7 @@ internal static Uri ParseHttpUrl(string uriString) { Uri parsedUri; Uri.TryCreate(uriString, UriKind.Absolute, out parsedUri); - + return parsedUri; } @@ -1555,10 +1597,10 @@ internal static Dependency[] ParseHttpDependencies(string dependencyString) { /* Az.Profile:[0.1.0, ):|Az.Aks:[0.1.0, ):|Az.AnalysisServices:[0.1.0, ): - Post 1st Split: + Post 1st Split: ["Az.Profile:[0.1.0, ):", "Az.Aks:[0.1.0, ):", "Az.AnalysisServices:[0.1.0, ):"] */ - string[] dependencies = dependencyString.Split(new char[]{'|'}, StringSplitOptions.RemoveEmptyEntries); + string[] dependencies = dependencyString.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries); List dependencyList = new List(); foreach (string dependency in dependencies) @@ -1567,14 +1609,14 @@ internal static Dependency[] ParseHttpDependencies(string dependencyString) The Element: "Az.Profile:[0.1.0, ):" Post 2nd Split: ["Az.Profile", "[0.1.0, )"] */ - string[] dependencyParts = dependency.Split(new char[]{':'}, StringSplitOptions.RemoveEmptyEntries); + string[] dependencyParts = dependency.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); VersionRange dependencyVersion; if (dependencyParts.Length == 1) { dependencyVersion = VersionRange.All; } - else + else { if (!Utils.TryParseVersionOrVersionRange(dependencyParts[1], out dependencyVersion)) { @@ -1584,7 +1626,7 @@ internal static Dependency[] ParseHttpDependencies(string dependencyString) dependencyList.Add(new Dependency(dependencyParts[0], dependencyVersion)); } - + return dependencyList.ToArray(); } @@ -1672,7 +1714,7 @@ private static ResourceType ParseHttpMetadataType( ResourceType pkgType = ResourceType.Module; foreach (string tag in tags) { - if(String.Equals(tag, "PSScript", StringComparison.InvariantCultureIgnoreCase)) + if (String.Equals(tag, "PSScript", StringComparison.InvariantCultureIgnoreCase)) { // clear default Module tag, because a Script resource cannot be a Module resource also pkgType = ResourceType.Script; @@ -1830,7 +1872,7 @@ private static Dependency[] GetDependenciesForPs1(ModuleSpecification[] required return deps.ToArray(); } - foreach(ModuleSpecification depModule in requiredModules) + foreach (ModuleSpecification depModule in requiredModules) { // ModuleSpecification has Version, RequiredVersion, MaximumVersion string depName = depModule.Name; @@ -1878,7 +1920,7 @@ private static Dependency[] GetDependenciesForPs1(ModuleSpecification[] required private static Dependency[] GetDependenciesForPsd1(Hashtable[] requiredModules) { List deps = new List(); - foreach(Hashtable depModule in requiredModules) + foreach (Hashtable depModule in requiredModules) { VersionRange depVersionRange = VersionRange.All; @@ -1887,18 +1929,18 @@ private static Dependency[] GetDependenciesForPsd1(Hashtable[] requiredModules) continue; } - String depName = (string) depModule["ModuleName"]; + String depName = (string)depModule["ModuleName"]; if (depModule.ContainsKey("RequiredVersion")) { // = 2.5.0 - Utils.TryParseVersionOrVersionRange((string) depModule["RequiredVersion"], out depVersionRange); + Utils.TryParseVersionOrVersionRange((string)depModule["RequiredVersion"], out depVersionRange); } else if (depModule.ContainsKey("ModuleVersion") || depModule.ContainsKey("MaximumVersion")) { if (depModule.ContainsKey("ModuleVersion") && depModule.ContainsKey("MaximumVersion")) { - NuGetVersion.TryParse((string) depModule["ModuleVersion"], out NuGetVersion minVersion); - NuGetVersion.TryParse((string) depModule["MaximumVersion"], out NuGetVersion maxVersion); + NuGetVersion.TryParse((string)depModule["ModuleVersion"], out NuGetVersion minVersion); + NuGetVersion.TryParse((string)depModule["MaximumVersion"], out NuGetVersion maxVersion); depVersionRange = new VersionRange( minVersion: minVersion, includeMinVersion: true, @@ -1907,7 +1949,7 @@ private static Dependency[] GetDependenciesForPsd1(Hashtable[] requiredModules) } else if (depModule.ContainsKey("ModuleVersion")) { - NuGetVersion.TryParse((string) depModule["ModuleVersion"], out NuGetVersion minVersion); + NuGetVersion.TryParse((string)depModule["ModuleVersion"], out NuGetVersion minVersion); depVersionRange = new VersionRange( minVersion: minVersion, includeMinVersion: true, @@ -1917,7 +1959,7 @@ private static Dependency[] GetDependenciesForPsd1(Hashtable[] requiredModules) else { // depModule has "MaximumVersion" key - NuGetVersion.TryParse((string) depModule["MaximumVersion"], out NuGetVersion maxVersion); + NuGetVersion.TryParse((string)depModule["MaximumVersion"], out NuGetVersion maxVersion); depVersionRange = new VersionRange( minVersion: null, includeMinVersion: true, diff --git a/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 index b89a245ee..5c784fca2 100644 --- a/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 @@ -74,16 +74,15 @@ Describe 'Test Find-PSResource for local repositories' -tags 'CI' { $res.Version | Should -Be "5.0.0" } - It "find resource given Name, Version null (package containing nuspec only)" { - # FindName() - $pkgName = "test_nonpsresource" - $requiredTag = "Tag1" - Save-PSResource -Name $pkgName -Repository "NuGetGallery" -Path $localRepoUriAddress -AsNupkg -TrustRepository - $res = Find-PSResource -Name $pkgName -Repository $localRepo - $res.Name | Should -Be $pkgName - $res.Repository | Should -Be $localRepo - $res.Tags | Should -Contain $requiredTag - } + # TODO: bug with Save-PSResource + # It "find resource given Name, Version null (package containing nuspec only)" { + # # FindName() + # $pkgName = "PowerShell" + # Save-PSResource -Name $pkgName -Repository "NuGetGallery" -Path $localRepoUriAddress -AsNupkg -TrustRepository + # $res = Find-PSResource -Name $pkgName -Repository $localRepo + # $res.Name | Should -Be $pkgName + # $res.Repository | Should -Be $localRepo + # } It "find script without RequiredModules" { # FindName() diff --git a/test/FindPSResourceTests/FindPSResourceRepositorySearching.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceRepositorySearching.Tests.ps1 index 405c93e61..c350d0051 100644 --- a/test/FindPSResourceTests/FindPSResourceRepositorySearching.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceRepositorySearching.Tests.ps1 @@ -128,7 +128,7 @@ Describe 'Test Find-PSResource for searching and looping through repositories' - It "find multiple resources from all repositories where it exists where package Name contains wildcard (without -Repository specified)" { $res = Find-PSResource -Name "test_module*" -ErrorVariable err -ErrorAction SilentlyContinue - $res | Should -HaveCount 10 + $res | Should -HaveCount 11 $err | Should -HaveCount 0 $pkgFoundinLocalRepo = $false diff --git a/test/FindPSResourceTests/FindPSResourceV3Server.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceV3Server.Tests.ps1 index dc1e46262..57064f3e8 100644 --- a/test/FindPSResourceTests/FindPSResourceV3Server.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceV3Server.Tests.ps1 @@ -11,7 +11,7 @@ Describe 'Test HTTP Find-PSResource for V3 Server Protocol' -tags 'CI' { BeforeAll{ $NuGetGalleryName = Get-NuGetGalleryName - $testModuleName = "test_module" + $testModuleName = 'test_module' Get-NewPSResourceRepositoryFile } @@ -19,44 +19,45 @@ Describe 'Test HTTP Find-PSResource for V3 Server Protocol' -tags 'CI' { Get-RevertPSResourceRepositoryFile } - It "find resource given specific Name, Version null" { + It 'find resource given specific Name, Version null' { # FindName() $res = Find-PSResource -Name $testModuleName -Repository $NuGetGalleryName $res.Name | Should -Be $testModuleName - $res.Version | Should -Be "5.0.0" + $res.Version | Should -Be '5.0.0' } - It "should not find resource given nonexistant Name" { + It 'should not find resource given nonexistant Name' { $res = Find-PSResource -Name NonExistantModule -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' $res | Should -BeNullOrEmpty } - It "find resource(s) given wildcard Name" { + It 'find resource(s) given wildcard Name' { # FindNameGlobbing - $wildcardName = "test_module*" + $wildcardName = 'test_module*' $res = Find-PSResource -Name $wildcardName -Repository $NuGetGalleryName $res.Count | Should -BeGreaterThan 1 - foreach ($item in $res) - { + foreach ($item in $res) { $item.Name | Should -BeLike $wildcardName } } - $testCases2 = @{Version="[5.0.0.0]"; ExpectedVersions=@("5.0.0"); Reason="validate version, exact match"}, - @{Version="5.0.0.0"; ExpectedVersions=@("5.0.0"); Reason="validate version, exact match without bracket syntax"}, - @{Version="[1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, exact range inclusive"}, - @{Version="(1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("3.0.0"); Reason="validate version, exact range exclusive"}, - @{Version="(1.0.0.0,)"; ExpectedVersions=@("3.0.0", "5.0.0"); Reason="validate version, minimum version exclusive"}, - @{Version="[1.0.0.0,)"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, minimum version inclusive"}, - @{Version="(,3.0.0.0)"; ExpectedVersions=@("1.0.0"); Reason="validate version, maximum version exclusive"}, - @{Version="(,3.0.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, maximum version inclusive"}, - @{Version="[1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, mixed inclusive minimum and exclusive maximum version"} - @{Version="(1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("3.0.0", "5.0.0"); Reason="validate version, mixed exclusive minimum and inclusive maximum version"} - - It "find resource when given Name to " -TestCases $testCases2{ + $testCases2 = [hashtable[]]( + @{Version='[5.0.0.0]'; ExpectedVersions=@('5.0.0'); Reason='validate version, exact match'}, + @{Version='5.0.0.0'; ExpectedVersions=@('5.0.0'); Reason='validate version, exact match without bracket syntax'}, + @{Version='[1.0.0.0, 5.0.0.0]'; ExpectedVersions=@('1.0.0', '3.0.0', '5.0.0'); Reason='validate version, exact range inclusive'}, + @{Version='(1.0.0.0, 5.0.0.0)'; ExpectedVersions=@('3.0.0'); Reason='validate version, exact range exclusive'}, + @{Version='(1.0.0.0,)'; ExpectedVersions=@('3.0.0', '5.0.0'); Reason='validate version, minimum version exclusive'}, + @{Version='[1.0.0.0,)'; ExpectedVersions=@('1.0.0', '3.0.0', '5.0.0'); Reason='validate version, minimum version inclusive'}, + @{Version='(,3.0.0.0)'; ExpectedVersions=@('1.0.0'); Reason='validate version, maximum version exclusive'}, + @{Version='(,3.0.0.0]'; ExpectedVersions=@('1.0.0', '3.0.0'); Reason='validate version, maximum version inclusive'}, + @{Version='[1.0.0.0, 5.0.0.0)'; ExpectedVersions=@('1.0.0', '3.0.0'); Reason='validate version, mixed inclusive minimum and exclusive maximum version'}, + @{Version='(1.0.0.0, 5.0.0.0]'; ExpectedVersions=@('3.0.0', '5.0.0'); Reason='validate version, mixed exclusive minimum and inclusive maximum version'} + ) + + It 'find resource when given Name to ' -TestCases $testCases2{ # FindVersionGlobbing() param($Version, $ExpectedVersions) $res = Find-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName @@ -69,7 +70,7 @@ Describe 'Test HTTP Find-PSResource for V3 Server Protocol' -tags 'CI' { It "find all versions of resource when given specific Name, Version not null --> '*'" { # FindVersionGlobbing() - $res = Find-PSResource -Name $testModuleName -Version "*" -Repository $NuGetGalleryName + $res = Find-PSResource -Name $testModuleName -Version '*' -Repository $NuGetGalleryName $res | ForEach-Object { $_.Name | Should -Be $testModuleName } @@ -77,29 +78,28 @@ Describe 'Test HTTP Find-PSResource for V3 Server Protocol' -tags 'CI' { $res.Count | Should -BeGreaterOrEqual 1 } - It "find resource with latest (including prerelease) version given Prerelease parameter" { + It 'find resource with latest (including prerelease) version given Prerelease parameter' { # FindName() # test_module resource's latest version is a prerelease version, before that it has a non-prerelease version $res = Find-PSResource -Name $testModuleName -Repository $NuGetGalleryName - $res.Version | Should -Be "5.0.0" + $res.Version | Should -Be '5.0.0' $resPrerelease = Find-PSResource -Name $testModuleName -Prerelease -Repository $NuGetGalleryName - $resPrerelease.Version | Should -Be "5.2.5" - $resPrerelease.Prerelease | Should -Be "alpha001" + $resPrerelease.Version | Should -Be '5.2.5' + $resPrerelease.Prerelease | Should -Be 'alpha001' } - It "find resources, including Prerelease version resources, when given Prerelease parameter" { + It 'find resources, including Prerelease version resources, when given Prerelease parameter' { # FindVersionGlobbing() - $resWithoutPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $NuGetGalleryName - $resWithPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $NuGetGalleryName + $resWithoutPrerelease = Find-PSResource -Name $testModuleName -Version '*' -Repository $NuGetGalleryName + $resWithPrerelease = Find-PSResource -Name $testModuleName -Version '*' -Repository $NuGetGalleryName $resWithPrerelease.Count | Should -BeGreaterOrEqual $resWithoutPrerelease.Count } - It "find resource and its dependency resources with IncludeDependencies parameter" { - # find with dependencies is not yet supported for V3, so this should only install parent package - $pkg = Find-PSResource -Name "TestModuleWithDependencyE" -IncludeDependencies -Repository $NuGetGalleryName - $pkg.Name | Should -Be "TestModuleWithDependencyE" - $pkg | Should -HaveCount 1 + It 'find resource and its dependency resources with IncludeDependencies parameter' { + $pkg = Find-PSResource -Name 'TestModuleWithDependencyE' -IncludeDependencies -Repository $NuGetGalleryName + $pkg.Name | Should -Contain 'TestModuleWithDependencyE' + $pkg.Count | Should -BeGreaterThan 1 } # It "find resources only with Tag parameter" { @@ -110,118 +110,116 @@ Describe 'Test HTTP Find-PSResource for V3 Server Protocol' -tags 'CI' { # } # } - It "find resource that satisfies given Name and Tag property (single tag)" { + It 'find resource that satisfies given Name and Tag property (single tag)' { # FindNameWithTag() - $requiredTag = "test" + $requiredTag = 'test' $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $NuGetGalleryName $res.Name | Should -Be $testModuleName $res.Tags | Should -Contain $requiredTag } - It "should not find resource if Name and Tag are not both satisfied (single tag)" { + It 'should not find resource if Name and Tag are not both satisfied (single tag)' { # FindNameWithTag - $requiredTag = "Windows" # tag "windows" is not present for test_module package + $requiredTag = 'Windows' # tag "windows" is not present for test_module package $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } - It "find resource that satisfies given Name and Tag property (multiple tags)" { + It 'find resource that satisfies given Name and Tag property (multiple tags)' { # FindNameWithTag() - $requiredTags = @("test", "Tag2") + $requiredTags = @('test', 'Tag2') $res = Find-PSResource -Name $testModuleName -Tag $requiredTags -Repository $NuGetGalleryName $res.Name | Should -Be $testModuleName $res.Tags | Should -Contain $requiredTags[0] $res.Tags | Should -Contain $requiredTags[1] } - It "should not find resource if Name and Tag are not both satisfied (multiple tag)" { + It 'should not find resource if Name and Tag are not both satisfied (multiple tag)' { # FindNameWithTag - $requiredTags = @("test", "Windows") # tag "windows" is not present for test_module package + $requiredTags = @('test', 'Windows') # tag "windows" is not present for test_module package $res = Find-PSResource -Name $testModuleName -Tag $requiredTags -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } - It "find all resources that satisfy Name pattern and have specified Tag (single tag)" { + It 'find all resources that satisfy Name pattern and have specified Tag (single tag)' { # FindNameGlobbingWithTag() - $requiredTag = "test" - $nameWithWildcard = "test_module*" + $requiredTag = 'test' + $nameWithWildcard = 'test_module*' $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTag -Repository $NuGetGalleryName $res.Count | Should -BeGreaterThan 1 - foreach ($pkg in $res) - { + foreach ($pkg in $res) { $pkg.Name | Should -BeLike $nameWithWildcard $pkg.Tags | Should -Contain $requiredTag } } - It "should not find resources if both Name pattern and Tags are not satisfied (single tag)" { + It 'should not find resources if both Name pattern and Tags are not satisfied (single tag)' { # FindNameGlobbingWithTag() - $requiredTag = "windows" # tag "windows" is not present for test_module package - $res = Find-PSResource -Name "test_module*" -Tag $requiredTag -Repository $NuGetGalleryName + $requiredTag = 'windows' # tag "windows" is not present for test_module package + $res = Find-PSResource -Name 'test_module*' -Tag $requiredTag -Repository $NuGetGalleryName $res | Should -BeNullOrEmpty } - It "find all resources that satisfy Name pattern and have specified Tag (multiple tags)" { + It 'find all resources that satisfy Name pattern and have specified Tag (multiple tags)' { # FindNameGlobbingWithTag() - $requiredTags = @("test", "Tag2") - $nameWithWildcard = "test_module*" + $requiredTags = @('test', 'Tag2') + $nameWithWildcard = 'test_module*' $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTags -Repository $NuGetGalleryName $res.Count | Should -BeGreaterThan 1 - foreach ($pkg in $res) - { + foreach ($pkg in $res) { $pkg.Name | Should -BeLike $nameWithWildcard $pkg.Tags | Should -Contain $requiredTags[0] $pkg.Tags | Should -Contain $requiredTags[1] } } - It "should not find resources if both Name pattern and Tags are not satisfied (multiple tags)" { + It 'should not find resources if both Name pattern and Tags are not satisfied (multiple tags)' { # FindNameGlobbingWithTag() # tag "windows" is not present for test_module package - $requiredTags = @("test", "windows") - $res = Find-PSResource -Name "test_module*" -Tag $requiredTags -Repository $NuGetGalleryName + $requiredTags = @('test', 'windows') + $res = Find-PSResource -Name 'test_module*' -Tag $requiredTags -Repository $NuGetGalleryName $res | Should -BeNullOrEmpty } - It "find resource that satisfies given Name, Version and Tag property (single tag)" { + It 'find resource that satisfies given Name, Version and Tag property (single tag)' { # FindVersionWithTag() - $requiredTag = "test" - $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $NuGetGalleryName + $requiredTag = 'test' + $res = Find-PSResource -Name $testModuleName -Version '5.0.0.0' -Tag $requiredTag -Repository $NuGetGalleryName $res.Name | Should -Be $testModuleName - $res.Version | Should -Be "5.0.0" + $res.Version | Should -Be '5.0.0' $res.Tags | Should -Contain $requiredTag } - It "should not find resource if Name, Version and Tag property are not all satisfied (single tag)" { + It 'should not find resource if Name, Version and Tag property are not all satisfied (single tag)' { # FindVersionWithTag() - $requiredTag = "windows" # tag "windows" is not present for test_module package - $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $requiredTag = 'windows' # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Version '5.0.0.0' -Tag $requiredTag -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } - It "find resource that satisfies given Name, Version and Tag property (multiple tags)" { + It 'find resource that satisfies given Name, Version and Tag property (multiple tags)' { # FindVersionWithTag() - $requiredTags = @("test", "Tag2") - $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTags -Repository $NuGetGalleryName + $requiredTags = @('test', 'Tag2') + $res = Find-PSResource -Name $testModuleName -Version '5.0.0.0' -Tag $requiredTags -Repository $NuGetGalleryName $res.Name | Should -Be $testModuleName - $res.Version | Should -Be "5.0.0" + $res.Version | Should -Be '5.0.0' $res.Tags | Should -Contain $requiredTags[0] $res.Tags | Should -Contain $requiredTags[1] } - It "should not find resource if Name, Version and Tag property are not all satisfied (multiple tags)" { + It 'should not find resource if Name, Version and Tag property are not all satisfied (multiple tags)' { # FindVersionWithTag() - $requiredTags = @("test", "windows") - $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTags -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $requiredTags = @('test', 'windows') + $res = Find-PSResource -Name $testModuleName -Version '5.0.0.0' -Tag $requiredTags -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } # It "find all resources with specified tag given Tag property" { @@ -248,50 +246,50 @@ Describe 'Test HTTP Find-PSResource for V3 Server Protocol' -tags 'CI' { # $foundTestScript | Should -Be $True # } - It "should not find resource given CommandName" { - $res = Find-PSResource -CommandName "command" -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + It 'should not find resource given CommandName' { + $res = Find-PSResource -CommandName 'command' -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } - It "should not find resource given DscResourceName" { - $res = Find-PSResource -DscResourceName "dscResource" -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + It 'should not find resource given DscResourceName' { + $res = Find-PSResource -DscResourceName 'dscResource' -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } It "should not find all resources given Name '*'" { - $res = Find-PSResource -Name "*" -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res = Find-PSResource -Name '*' -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "FindAllFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'FindAllFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } - It "should not find an unlisted module" { - $res = Find-PSResource -Name "PMTestDependency1" -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + It 'should not find an unlisted module' { + $res = Find-PSResource -Name 'PMTestDependency1' -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'PackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } - + # "carb*" is intentionally chosen as a sequence that will trigger pagination (ie more than 100 results), # but is not too time consuming. # There are currently between 236 packages that should be returned It "Should find more than 100 packages that contain 'carb*' in the name" { # Tests pagination - $res = Find-PSResource -Name "carb*" -Repository $NuGetGalleryName + $res = Find-PSResource -Name 'carb*' -Repository $NuGetGalleryName $res | Should -Not -BeNullOrEmpty $res.Count | Should -BeGreaterOrEqual 236 } - It "should throw unauthorized exception if private repository with no credentials" { - Register-PSResourceRepository -Name "PrivateGallery" -Uri "https://gitlab.com/api/v4/projects/47456554/packages/nuget/index.json" - $res = Find-PSResource -Name $testModuleName -Repository "PrivateGallery" -ErrorVariable err -ErrorAction SilentlyContinue - Unregister-PSResourceRepository -Name "PrivateGallery" + It 'should throw unauthorized exception if private repository with no credentials' { + Register-PSResourceRepository -Name 'PrivateGallery' -Uri 'https://gitlab.com/api/v4/projects/47456554/packages/nuget/index.json' + $res = Find-PSResource -Name $testModuleName -Repository 'PrivateGallery' -ErrorVariable err -ErrorAction SilentlyContinue + Unregister-PSResourceRepository -Name 'PrivateGallery' $res | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "UnauthorizedRequest,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'UnauthorizedRequest,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource' } } diff --git a/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 index 7c4e68d6a..0dafca8dc 100644 --- a/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 @@ -39,7 +39,7 @@ Describe 'Test Install-PSResource for GitHub packages' -tags 'CI' { Install-PSResource -Name $Name -Repository $GithubPackagesRepoName -Credential $credential -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -BeGreaterThan 0 $err[0].FullyQualifiedErrorId | Should -BeExactly "$ErrorId,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" - $res = Get-InstalledPSResource $testModuleName + $res = Get-InstalledPSResource $testModuleName -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty } @@ -66,7 +66,7 @@ Describe 'Test Install-PSResource for GitHub packages' -tags 'CI' { It "Should not install resource given nonexistant name" { Install-PSResource -Name "NonExistantModule" -Repository $GithubPackagesRepoName -Credential $credential -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue - $pkg = Get-InstalledPSResource "NonExistantModule" + $pkg = Get-InstalledPSResource "NonExistantModule" -ErrorAction SilentlyContinue $pkg | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" @@ -111,7 +111,7 @@ Describe 'Test Install-PSResource for GitHub packages' -tags 'CI' { {} $Error[0].FullyQualifiedErrorId | Should -be "IncorrectVersionFormat,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" - $res = Get-InstalledPSResource $testModuleName + $res = Get-InstalledPSResource $testModuleName -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty } @@ -199,13 +199,13 @@ Describe 'Test Install-PSResource for GitHub packages' -tags 'CI' { } It "Reinstall resource that is already installed with -Reinstall parameter" { - Install-PSResource -Name $testModuleName -Repository $GithubPackagesRepoName -Credential $credential -TrustRepository - $pkg = Get-InstalledPSResource $testModuleName - $pkg.Name | Should -Be $testModuleName + Install-PSResource -Name $testModuleName2 -Repository $GithubPackagesRepoName -Credential $credential -TrustRepository + $pkg = Get-InstalledPSResource $testModuleName2 + $pkg.Name | Should -Be $testModuleName2 $pkg.Version | Should -Be "5.0.0" - Install-PSResource -Name $testModuleName -Repository $GithubPackagesRepoName -Credential $credential -Reinstall -TrustRepository - $pkg = Get-InstalledPSResource $testModuleName - $pkg.Name | Should -Be $testModuleName + Install-PSResource -Name $testModuleName2 -Repository $GithubPackagesRepoName -Credential $credential -Reinstall -TrustRepository + $pkg = Get-InstalledPSResource $testModuleName2 + $pkg.Name | Should -Be $testModuleName2 $pkg.Version | Should -Be "5.0.0" } diff --git a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 index 3e4f82823..49397859c 100644 --- a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 @@ -4,7 +4,7 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] Param() -$ProgressPreference = "SilentlyContinue" +$ProgressPreference = 'SilentlyContinue' $modPath = "$psscriptroot/../PSGetTestUtils.psm1" Import-Module $modPath -Force -Verbose @@ -16,185 +16,188 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { BeforeAll { $NuGetGalleryName = Get-NuGetGalleryName $NuGetGalleryUri = Get-NuGetGalleryLocation - $testModuleName = "test_module" - $testModuleName2 = "test_module2" - $testScriptName = "test_script" - $PackageManagement = "PackageManagement" - $RequiredResourceJSONFileName = "TestRequiredResourceFile.json" - $RequiredResourcePSD1FileName = "TestRequiredResourceFile.psd1" + $testModuleName = 'test_module' + $testModuleName2 = 'test_module2' + $testScriptName = 'test_script' + $PackageManagement = 'PackageManagement' + $RequiredResourceJSONFileName = 'TestRequiredResourceFile.json' + $RequiredResourcePSD1FileName = 'TestRequiredResourceFile.psd1' Get-NewPSResourceRepositoryFile Register-LocalRepos } AfterEach { - Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "test_module_with_license", "TestFindModule", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue + Uninstall-PSResource 'test_module', 'test_module2', 'test_script', 'TestModule99', 'test_module_withlicense', 'TestFindModule', 'PackageManagement', ` + 'TestModuleWithDependencyE', 'TestModuleWithDependencyC', 'TestModuleWithDependencyB', 'TestModuleWithDependencyD' -SkipDependencyCheck -ErrorAction SilentlyContinue } AfterAll { Get-RevertPSResourceRepositoryFile } - $testCases = @{Name="*"; ErrorId="NameContainsWildcard"}, - @{Name="Test_Module*"; ErrorId="NameContainsWildcard"}, - @{Name="Test?Module","Test[Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} + $testCases = [hashtable[]]( + @{Name='*'; ErrorId='NameContainsWildcard'}, + @{Name='Test_Module*'; ErrorId='NameContainsWildcard'}, + @{Name='Test?Module','Test[Module'; ErrorId='ErrorFilteringNamesForUnsupportedWildcards'} + ) - It "Should not install resource with wildcard in name" -TestCases $testCases { + It 'Should not install resource with wildcard in name' -TestCases $testCases { param($Name, $ErrorId) Install-PSResource -Name $Name -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -BeGreaterThan 0 $err[0].FullyQualifiedErrorId | Should -BeExactly "$ErrorId,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" - $res = Get-InstalledPSResource $testModuleName + $res = Get-InstalledPSResource $testModuleName -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty } - It "Install specific module resource by name" { + It 'Install specific module resource by name' { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0" + $pkg.Version | Should -Be '5.0.0' } - It "Install specific script resource by name" { + It 'Install specific script resource by name' { Install-PSResource -Name $testScriptName -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testScriptName $pkg.Name | Should -Be $testScriptName - $pkg.Version | Should -Be "3.5.0" + $pkg.Version | Should -Be '3.5.0' } - It "Install multiple resources by name" { + It 'Install multiple resources by name' { $pkgNames = @($testModuleName, $testModuleName2) Install-PSResource -Name $pkgNames -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $pkgNames $pkg.Name | Should -Be $pkgNames } - It "Should not install resource given nonexistant name" { - Install-PSResource -Name "NonExistantModule" -Repository $NuGetGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue - $pkg = Get-InstalledPSResource "NonExistantModule" -ErrorAction SilentlyContinue + It 'Should not install resource given nonexistant name' { + Install-PSResource -Name 'NonExistantModule' -Repository $NuGetGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + $pkg = Get-InstalledPSResource 'NonExistantModule' -ErrorAction SilentlyContinue $pkg.Name | Should -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource' } - It "Install module using -WhatIf, should not install the module" { + It 'Install module using -WhatIf, should not install the module' { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository -WhatIf $? | Should -BeTrue - $res = Get-InstalledPSResource $testModuleName + $res = Get-InstalledPSResource $testModuleName -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty } # Do some version testing, but Find-PSResource should be doing thorough testing - It "Should install resource given name and exact version" { - Install-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -TrustRepository + It 'Should install resource given name and exact version' { + Install-PSResource -Name $testModuleName -Version '1.0.0' -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "1.0.0" + $pkg.Version | Should -Be '1.0.0' } - It "Should install resource given name and exact version with bracket syntax" { - Install-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $NuGetGalleryName -TrustRepository + It 'Should install resource given name and exact version with bracket syntax' { + Install-PSResource -Name $testModuleName -Version '[1.0.0]' -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "1.0.0" + $pkg.Version | Should -Be '1.0.0' } - It "Should install resource given name and exact range inclusive [1.0.0, 5.0.0]" { - Install-PSResource -Name $testModuleName -Version "[1.0.0, 5.0.0]" -Repository $NuGetGalleryName -TrustRepository + It 'Should install resource given name and exact range inclusive [1.0.0, 5.0.0]' { + Install-PSResource -Name $testModuleName -Version '[1.0.0, 5.0.0]' -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0" + $pkg.Version | Should -Be '5.0.0' } - It "Should install resource given name and exact range exclusive (1.0.0, 5.0.0)" { - Install-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $NuGetGalleryName -TrustRepository + It 'Should install resource given name and exact range exclusive (1.0.0, 5.0.0)' { + Install-PSResource -Name $testModuleName -Version '(1.0.0, 5.0.0)' -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "3.0.0" + $pkg.Version | Should -Be '3.0.0' } # TODO: Update this test and others like it that use try/catch blocks instead of Should -Throw - It "Should not install resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { - $Version = "(1.0.0.0)" + It 'Should not install resource with incorrectly formatted version such as exclusive version (1.0.0.0)' { + $Version = '(1.0.0.0)' try { Install-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName -TrustRepository -ErrorAction SilentlyContinue } - catch - {} - $Error[0].FullyQualifiedErrorId | Should -Be "IncorrectVersionFormat,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" + catch { + } + $Error[0].FullyQualifiedErrorId | Should -Be 'IncorrectVersionFormat,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource' $res = Get-InstalledPSResource $testModuleName -ErrorAction SilentlyContinue $res | Should -BeNullOrEmpty } It "Install resource when given Name, Version '*', should install the latest version" { - Install-PSResource -Name $testModuleName -Version "*" -Repository $NuGetGalleryName -TrustRepository + Install-PSResource -Name $testModuleName -Version '*' -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0" + $pkg.Version | Should -Be '5.0.0' } - It "Install resource with latest (including prerelease) version given Prerelease parameter" { + It 'Install resource with latest (including prerelease) version given Prerelease parameter' { Install-PSResource -Name $testModuleName -Prerelease -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.2.5" - $pkg.Prerelease | Should -Be "alpha001" + $pkg.Version | Should -Be '5.2.5' + $pkg.Prerelease | Should -Be 'alpha001' } - It "Install resource via InputObject by piping from Find-PSresource" { + It 'Install resource via InputObject by piping from Find-PSresource' { Find-PSResource -Name $testModuleName -Repository $NuGetGalleryName | Install-PSResource -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0" + $pkg.Version | Should -Be '5.0.0' } - It "Install resource under specified in PSModulePath" { + It 'Install resource under specified in PSModulePath' { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName ($env:PSModulePath).Contains($pkg.InstalledLocation) } - It "Install resource with companyname and repository source location and validate properties" { - Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository $NuGetGalleryName -TrustRepository + It 'Install resource with companyname and repository source location and validate properties' { + Install-PSResource -Name $testModuleName -Version '5.2.5-alpha001' -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName - $pkg.Version | Should -Be "5.2.5" - $pkg.Prerelease | Should -Be "alpha001" + $pkg.Version | Should -Be '5.2.5' + $pkg.Prerelease | Should -Be 'alpha001' - $pkg.CompanyName | Should -Be "Anam Navied" + $pkg.CompanyName | Should -Be 'Anam Navied' # Broken now, tracked in issue # $pkg.Copyright | Should -Be "(c) Anam Navied. All rights reserved." $pkg.RepositorySourceLocation | Should -Be $NuGetGalleryUri } # Windows only - It "Install resource under CurrentUser scope - Windows only" -Skip:(!(Get-IsWindows)) { + It 'Install resource under CurrentUser scope - Windows only' -Skip:(!(Get-IsWindows)) { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true + $pkg.InstalledLocation.ToString().Contains('Documents') | Should -Be $true } # Windows only - It "Install resource under AllUsers scope - Windows only" -Skip:(!((Get-IsWindows) -and (Test-IsAdmin))) { - Install-PSResource -Name "testmodule99" -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers -Verbose - $pkg = Get-Module "testmodule99" -ListAvailable - $pkg.Name | Should -Be "testmodule99" - $pkg.Path.ToString().Contains("Program Files") + It 'Install resource under AllUsers scope - Windows only' -Skip:(!((Get-IsWindows) -and (Test-IsAdmin))) { + Install-PSResource -Name 'testmodule99' -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers -Verbose + $pkg = Get-Module 'testmodule99' -ListAvailable + $pkg.Name | Should -Be 'testmodule99' + $pkg.Path.ToString().Contains('Program Files') } # Windows only - It "Install resource under no specified scope - Windows only" -Skip:(!(Get-IsWindows)) { + It 'Install resource under no specified scope - Windows only' -Skip:(!(Get-IsWindows)) { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true + $pkg.InstalledLocation.ToString().Contains('Documents') | Should -Be $true } # Unix only # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' - It "Install resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { + It 'Install resource under CurrentUser scope - Unix only' -Skip:(Get-IsWindows) { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName @@ -203,14 +206,14 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { # Unix only # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' - It "Install resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { + It 'Install resource under no specified scope - Unix only' -Skip:(Get-IsWindows) { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true } - It "Should not install resource that is already installed" { + It 'Should not install resource that is already installed' { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName @@ -218,15 +221,15 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { $WarningVar | Should -Not -BeNullOrEmpty } - It "Reinstall resource that is already installed with -Reinstall parameter" { + It 'Reinstall resource that is already installed with -Reinstall parameter' { Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0" + $pkg.Version | Should -Be '5.0.0' Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -Reinstall -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0" + $pkg.Version | Should -Be '5.0.0' } # It "Restore resource after reinstall fails" { @@ -255,36 +258,36 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { # (Get-ChildItem -Path $resourcePath -Recurse).Count | Should -BeExactly $resourceFiles.Count # } - It "Install resource that requires accept license with -AcceptLicense flag" { - Install-PSResource -Name "test_module_with_license" -Repository $NuGetGalleryName -AcceptLicense - $pkg = Get-InstalledPSResource "test_module_with_license" - $pkg.Name | Should -Be "test_module_with_license" - $pkg.Version | Should -Be "2.0.0" + It 'Install resource that requires accept license with -AcceptLicense flag' { + Install-PSResource -Name 'test_module_withlicense' -Repository $NuGetGalleryName -AcceptLicense + $pkg = Get-InstalledPSResource 'test_module_withlicense' + $pkg.Name | Should -Be 'test_module_withlicense' + $pkg.Version | Should -Be '1.0.0' } - It "Install PSResourceInfo object piped in" { - Find-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName | Install-PSResource -TrustRepository + It 'Install PSResourceInfo object piped in' { + Find-PSResource -Name $testModuleName -Version '1.0.0.0' -Repository $NuGetGalleryName | Install-PSResource -TrustRepository $res = Get-InstalledPSResource -Name $testModuleName $res.Name | Should -Be $testModuleName - $res.Version | Should -Be "1.0.0" + $res.Version | Should -Be '1.0.0' } - It "Install module using -PassThru" { + It 'Install module using -PassThru' { $res = Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -PassThru -TrustRepository $res.Name | Should -Contain $testModuleName } - It "Install modules using -RequiredResource with hashtable" { + It 'Install modules using -RequiredResource with hashtable' { $rrHash = @{ test_module = @{ - version = "[1.0.0,5.0.0)" + version = '[1.0.0,5.0.0)' repository = $NuGetGalleryName } test_module2 = @{ - version = "[1.0.0,5.0.0]" + version = '[1.0.0,5.0.0]' repository = $NuGetGalleryName - prerelease = "true" + prerelease = 'true' } TestModule99 = @{ @@ -296,92 +299,97 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { $res1 = Get-InstalledPSResource $testModuleName $res1.Name | Should -Be $testModuleName - $res1.Version | Should -Be "3.0.0" + $res1.Version | Should -Be '3.0.0' $res2 = Get-InstalledPSResource $testModuleName2 $res2.Name | Should -Be $testModuleName2 - $res2.Version | Should -Be "5.0.0" + $res2.Version | Should -Be '5.0.0' - $res3 = Get-InstalledPSResource "TestModule99" - $res3.Name | Should -Be "TestModule99" - $res3.Version | Should -Be "0.0.93" + $res3 = Get-InstalledPSResource 'TestModule99' + $res3.Name | Should -Be 'TestModule99' + $res3.Version | Should -Be '0.0.93' } - It "Install modules using -RequiredResource with JSON string" { + It 'Install modules using -RequiredResource with JSON string' { $rrJSON = "{ - 'test_module': { - 'version': '[1.0.0,5.0.0)', - 'repository': 'NuGetGallery' - }, - 'test_module2': { - 'version': '[1.0.0,5.0.0]', - 'repository': 'PSGallery', - 'prerelease': 'true' - }, - 'TestModule99': { - 'repository': 'NuGetGallery' - } - }" + 'test_module': { + 'version': '[1.0.0,5.0.0)', + 'repository': 'NuGetGallery' + }, + 'test_module2': { + 'version': '[1.0.0,5.0.0]', + 'repository': 'PSGallery', + 'prerelease': 'true' + }, + 'TestModule99': { + 'repository': 'NuGetGallery' + } + }" Install-PSResource -RequiredResource $rrJSON -TrustRepository $res1 = Get-InstalledPSResource $testModuleName $res1.Name | Should -Be $testModuleName - $res1.Version | Should -Be "3.0.0" + $res1.Version | Should -Be '3.0.0' $res2 = Get-InstalledPSResource $testModuleName2 $res2.Name | Should -Be $testModuleName2 - $res2.Version | Should -Be "5.0.0.0" + $res2.Version | Should -Be '5.0.0.0' - $res3 = Get-InstalledPSResource "testModule99" - $res3.Name | Should -Be "testModule99" - $res3.Version | Should -Be "0.0.93" + $res3 = Get-InstalledPSResource 'testModule99' + $res3.Name | Should -Be 'testModule99' + $res3.Version | Should -Be '0.0.93' } - It "Install modules using -RequiredResourceFile with PSD1 file" { + It 'Install modules using -RequiredResourceFile with PSD1 file' { $rrFilePSD1 = "$psscriptroot/../$RequiredResourcePSD1FileName" Install-PSResource -RequiredResourceFile $rrFilePSD1 -TrustRepository $res1 = Get-InstalledPSResource $testModuleName $res1.Name | Should -Be $testModuleName - $res1.Version | Should -Be "3.0.0.0" + $res1.Version | Should -Be '3.0.0.0' - $res2 = Get-InstalledPSResource $testModuleName2 -Version "2.5.0-beta" + $res2 = Get-InstalledPSResource $testModuleName2 -Version '2.5.0-beta' $res2.Name | Should -Be $testModuleName2 - $res2.Version | Should -Be "2.5.0" - $res2.Prerelease | Should -Be "beta" + $res2.Version | Should -Be '2.5.0' + $res2.Prerelease | Should -Be 'beta' - $res3 = Get-InstalledPSResource "testModule99" - $res3.Name | Should -Be "testModule99" - $res3.Version | Should -Be "0.0.93" + $res3 = Get-InstalledPSResource 'testModule99' + $res3.Name | Should -Be 'testModule99' + $res3.Version | Should -Be '0.0.93' } - It "Install modules using -RequiredResourceFile with JSON file" { + It 'Install modules using -RequiredResourceFile with JSON file' { $rrFileJSON = "$psscriptroot/../$RequiredResourceJSONFileName" Install-PSResource -RequiredResourceFile $rrFileJSON -TrustRepository $res1 = Get-InstalledPSResource $testModuleName $res1.Name | Should -Be $testModuleName - $res1.Version | Should -Be "3.0.0.0" + $res1.Version | Should -Be '3.0.0.0' - $res2 = Get-InstalledPSResource $testModuleName2 -Version "2.5.0-beta" + $res2 = Get-InstalledPSResource $testModuleName2 -Version '2.5.0-beta' $res2.Name | Should -Be $testModuleName2 - $res2.Version | Should -Be "2.5.0" - $res2.Prerelease | Should -Be "beta" + $res2.Version | Should -Be '2.5.0' + $res2.Prerelease | Should -Be 'beta' - $res3 = Get-InstalledPSResource "testModule99" - $res3.Name | Should -Be "testModule99" - $res3.Version | Should -Be "0.0.93" + $res3 = Get-InstalledPSResource 'testModule99' + $res3.Name | Should -Be 'testModule99' + $res3.Version | Should -Be '0.0.93' + } + + It "Install module and its dependencies" { + $res = Install-PSResource 'TestModuleWithDependencyE' -Repository $NuGetGalleryName -TrustRepository -PassThru + $res.Length | Should -Be 4 } } Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { BeforeAll { - $testModuleName = "TestModule" - $testModuleName2 = "test_module_with_license" + $testModuleName = 'TestModule' + $testModuleName2 = 'test_module_withlicense' Get-NewPSResourceRepositoryFile Register-LocalRepos } @@ -396,19 +404,19 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidatio # Unix only manual test # Expected path should be similar to: '/usr/local/share/powershell/Modules' - It "Install resource under AllUsers scope - Unix only" -Skip:(Get-IsWindows) { + It 'Install resource under AllUsers scope - Unix only' -Skip:(Get-IsWindows) { Install-PSResource -Name $testModuleName -Repository $TestGalleryName -Scope AllUsers $pkg = Get-Module $testModuleName -ListAvailable $pkg.Name | Should -Be $testModuleName - $pkg.Path.Contains("/usr/") | Should -Be $true + $pkg.Path.Contains('/usr/') | Should -Be $true } # This needs to be manually tested due to prompt - It "Install resource that requires accept license without -AcceptLicense flag" { + It 'Install resource that requires accept license without -AcceptLicense flag' { Install-PSResource -Name $testModuleName2 -Repository $TestGalleryName $pkg = Get-InstalledPSResource $testModuleName2 $pkg.Name | Should -Be $testModuleName2 - $pkg.Version | Should -Be "2.0.0" + $pkg.Version | Should -Be '1.0.0' } # This needs to be manually tested due to prompt @@ -424,16 +432,16 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidatio } It "Install package from NuGetGallery that has outer and inner 'items' elements only and has outer items element array length greater than 1" { - $res = Install-PSResource -Name "Microsoft.Extensions.DependencyInjection" -Repository $NuGetGalleryName -TrustRepository -PassThru -ErrorVariable err -ErrorAction SilentlyContinue + $res = Install-PSResource -Name 'Microsoft.Extensions.DependencyInjection' -Repository $NuGetGalleryName -TrustRepository -PassThru -ErrorVariable err -ErrorAction SilentlyContinue $err | Should -HaveCount 0 - $res.Name | Should -Be "Microsoft.Extensions.DependencyInjection" + $res.Name | Should -Be 'Microsoft.Extensions.DependencyInjection' } It "Install package from NuGetGallery that has outer 'items', '@id' and inner 'items' elements" { - $res = Install-PSResource -Name "MsgReader" -Repository $NuGetGalleryName -TrustRepository -PassThru -ErrorVariable err -ErrorAction SilentlyContinue + $res = Install-PSResource -Name 'MsgReader' -Repository $NuGetGalleryName -TrustRepository -PassThru -ErrorVariable err -ErrorAction SilentlyContinue $err | Should -HaveCount 0 - $res.Name | Should -Be "MsgReader" + $res.Name | Should -Be 'MsgReader' } } diff --git a/test/SavePSResourceTests/SavePSResourceV3Tests.ps1 b/test/SavePSResourceTests/SavePSResourceV3Tests.ps1 index cb414ce50..11dbb9688 100644 --- a/test/SavePSResourceTests/SavePSResourceV3Tests.ps1 +++ b/test/SavePSResourceTests/SavePSResourceV3Tests.ps1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -$ProgressPreference = "SilentlyContinue" +$ProgressPreference = 'SilentlyContinue' $modPath = "$psscriptroot/../PSGetTestUtils.psm1" Import-Module $modPath -Force -Verbose @@ -9,8 +9,8 @@ Describe 'Test HTTP Save-PSResource for V3 Server Protocol' -tags 'CI' { BeforeAll { $NuGetGalleryName = Get-NuGetGalleryName - $testModuleName = "test_module" - $testModuleName2 = "test_module2" + $testModuleName = 'test_module' + $testModuleName2 = 'test_module2' Get-NewPSResourceRepositoryFile $SaveDir = Join-Path $TestDrive 'SavedResources' @@ -26,14 +26,14 @@ Describe 'Test HTTP Save-PSResource for V3 Server Protocol' -tags 'CI' { Get-RevertPSResourceRepositoryFile } - It "Save specific module resource by name" { + It 'Save specific module resource by name' { Save-PSResource -Name $testModuleName -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -Not -BeNullOrEmpty (Get-ChildItem $pkgDir.FullName) | Should -HaveCount 1 } - It "Save multiple resources by name" { + It 'Save multiple resources by name' { $pkgNames = @($testModuleName, $testModuleName2) Save-PSResource -Name $pkgNames -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository $pkgDirs = Get-ChildItem -Path $SaveDir | Where-Object { $_.Name -eq $testModuleName -or $_.Name -eq $testModuleName2 } @@ -42,117 +42,122 @@ Describe 'Test HTTP Save-PSResource for V3 Server Protocol' -tags 'CI' { (Get-ChildItem $pkgDirs[1].FullName) | Should -HaveCount 1 } - It "Should not save resource given nonexistant name" { + It 'Should not save resource given nonexistant name' { Save-PSResource -Name NonExistentModule -Repository $NuGetGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository - $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ "NonExistentModule" + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ 'NonExistentModule' $pkgDir.Name | Should -BeNullOrEmpty } - It "Not Save module with Name containing wildcard" { - Save-PSResource -Name "TestModule*" -Repository $NuGetGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + It 'Not Save module with Name containing wildcard' { + Save-PSResource -Name 'TestModule*' -Repository $NuGetGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "NameContainsWildcard,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'NameContainsWildcard,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource' } - It "Should save resource given name and exact version" { - Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + It 'Should save resource given name and exact version' { + Save-PSResource -Name $testModuleName -Version '1.0.0' -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -Not -BeNullOrEmpty $pkgDirVersion = Get-ChildItem $pkgDir.FullName - $pkgDirVersion.Name | Should -Be "1.0.0" + $pkgDirVersion.Name | Should -Be '1.0.0' } - It "Should save resource given name and exact version with bracket syntax" { - Save-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + It 'Should save resource given name and exact version with bracket syntax' { + Save-PSResource -Name $testModuleName -Version '[1.0.0]' -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -Not -BeNullOrEmpty $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName - $pkgDirVersion.Name | Should -Be "1.0.0" + $pkgDirVersion.Name | Should -Be '1.0.0' } - It "Should save resource given name and exact range inclusive [1.0.0, 3.0.0]" { - Save-PSResource -Name $testModuleName -Version "[1.0.0, 3.0.0]" -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + It 'Should save resource given name and exact range inclusive [1.0.0, 3.0.0]' { + Save-PSResource -Name $testModuleName -Version '[1.0.0, 3.0.0]' -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -Not -BeNullOrEmpty $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName - $pkgDirVersion.Name | Should -Be "3.0.0" + $pkgDirVersion.Name | Should -Be '3.0.0' } - It "Should save resource given name and exact range exclusive (1.0.0, 5.0.0)" { - Save-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + It 'Should save resource given name and exact range exclusive (1.0.0, 5.0.0)' { + Save-PSResource -Name $testModuleName -Version '(1.0.0, 5.0.0)' -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -Not -BeNullOrEmpty $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName - $pkgDirVersion.Name | Should -Be "3.0.0" + $pkgDirVersion.Name | Should -Be '3.0.0' } - It "Should not save resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { - $Version="(1.0.0.0)" + It 'Should not save resource with incorrectly formatted version such as exclusive version (1.0.0.0)' { + $Version='(1.0.0.0)' try { Save-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName -Path $SaveDir -ErrorAction SilentlyContinue -TrustRepository } - catch - {} + catch { + } $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -BeNullOrEmpty $Error.Count | Should -BeGreaterThan 0 - $Error[0].FullyQualifiedErrorId | Should -Be "IncorrectVersionFormat,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource" + $Error[0].FullyQualifiedErrorId | Should -Be 'IncorrectVersionFormat,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource' } - It "Save resource with latest (including prerelease) version given Prerelease parameter" { + It 'Save resource with latest (including prerelease) version given Prerelease parameter' { Save-PSResource -Name $testModuleName -Prerelease -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -Not -BeNullOrEmpty $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName - $pkgDirVersion.Name | Should -Be "5.2.5" + $pkgDirVersion.Name | Should -Be '5.2.5' } ### TODO: this is broken because the "Prerelease" parameter is a boolean, but the type from ### the input object is of type string (ie "true"). - It "Save PSResourceInfo object piped in for prerelease version object" -Pending{ - Find-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository $NuGetGalleryName | Save-PSResource -Path $SaveDir -TrustRepository + It 'Save PSResourceInfo object piped in for prerelease version object' -Pending{ + Find-PSResource -Name $testModuleName -Version '5.2.5-alpha001' -Repository $NuGetGalleryName | Save-PSResource -Path $SaveDir -TrustRepository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -Not -BeNullOrEmpty (Get-ChildItem -Path $pkgDir.FullName) | Should -HaveCount 1 } - It "Save module as a nupkg" { - Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -Path $SaveDir -AsNupkg -TrustRepository - $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ "test_module.1.0.0.nupkg" + It 'Save module as a nupkg' { + Save-PSResource -Name $testModuleName -Version '1.0.0' -Repository $NuGetGalleryName -Path $SaveDir -AsNupkg -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ 'test_module.1.0.0.nupkg' $pkgDir | Should -Not -BeNullOrEmpty } - It "Save module and include XML metadata file" { - Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -Path $SaveDir -IncludeXml -TrustRepository + It 'Save module and include XML metadata file' { + Save-PSResource -Name $testModuleName -Version '1.0.0' -Repository $NuGetGalleryName -Path $SaveDir -IncludeXml -TrustRepository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -EQ $testModuleName $pkgDir | Should -Not -BeNullOrEmpty $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName - $pkgDirVersion.Name | Should -Be "1.0.0" - $xmlFile = Get-ChildItem -Path $pkgDirVersion.FullName | Where-Object Name -EQ "PSGetModuleInfo.xml" + $pkgDirVersion.Name | Should -Be '1.0.0' + $xmlFile = Get-ChildItem -Path $pkgDirVersion.FullName | Where-Object Name -EQ 'PSGetModuleInfo.xml' $xmlFile | Should -Not -BeNullOrEmpty } - It "Save module using -PassThru" { - $res = Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -Path $SaveDir -PassThru -TrustRepository + It 'Save module using -PassThru' { + $res = Save-PSResource -Name $testModuleName -Version '1.0.0' -Repository $NuGetGalleryName -Path $SaveDir -PassThru -TrustRepository $res.Name | Should -Be $testModuleName - $res.Version | Should -Be "1.0.0" + $res.Version | Should -Be '1.0.0' } # Save module that is not authenticode signed # Should FAIL to save the module - It "Save module that is not authenticode signed" -Skip:(!(Get-IsWindows)) { - Save-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $NuGetGalleryName -TrustRepository -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue + It 'Save module that is not authenticode signed' -Skip:(!(Get-IsWindows)) { + Save-PSResource -Name $testModuleName -Version '5.0.0' -AuthenticodeCheck -Repository $NuGetGalleryName -TrustRepository -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly 'GetAuthenticodeSignatureError,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource' } # Save resource that requires license - It "Install resource that requires accept license with -AcceptLicense flag" { + It 'Install resource that requires accept license with -AcceptLicense flag' { Save-PSResource -Repository $NuGetGalleryName -TrustRepository -Path $SaveDir ` - -Name "test_module_with_license" -AcceptLicense - $pkg = Get-InstalledPSResource -Path $SaveDir "test_module_with_license" - $pkg.Name | Should -Be "test_module_with_license" - $pkg.Version | Should -Be "2.0.0" + -Name 'test_module_withlicense' -AcceptLicense + $pkg = Get-InstalledPSResource -Path $SaveDir 'test_module_withlicense' + $pkg.Name | Should -Be 'test_module_withlicense' + $pkg.Version | Should -Be '1.0.0' + } + + It "Save module and its dependencies" { + $res = Save-PSResource 'TestModuleWithDependencyE' -Repository $NuGetGalleryName -TrustRepository -PassThru + $res.Length | Should -Be 4 } } From ef6e17d019cd39e0e5f5c3f4ec98e1dedbd2af78 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:52:00 -0700 Subject: [PATCH 50/89] Update changelog, release notes and version (#1838) --- CHANGELOG/1.1.md | 12 ++++++++++++ src/Microsoft.PowerShell.PSResourceGet.psd1 | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG/1.1.md b/CHANGELOG/1.1.md index 4fb078eda..fe2b73472 100644 --- a/CHANGELOG/1.1.md +++ b/CHANGELOG/1.1.md @@ -1,5 +1,17 @@ # 1.1 Changelog +## [1.2.0-preview1](https://github.com/PowerShell/PSResourceGet/compare/v1.1.1..v1.2.0-preview) - 2025-06-26 + +## 1.2.0-preview1 + +### New Features +- Dependency support for PSResources in v3 repositories (#1778 Thanks @o-l-a-v!) + +### Bug Fix +- Updated dependencies and added connection timeout to improve CI tests reliability (#1829) +- Improvements in `ContainerRegistry` repositories in listing repository catalog (#1831) +- Wildcard attribute added to `-Repository` parameter of `Install-PSResource` (#1808) + ## [1.1.1](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0..v1.1.1) - 2025-03-06 - Bugfix to retrieve all metadata properties when finding a PSResource from a ContainerRegistry repository (#1799) diff --git a/src/Microsoft.PowerShell.PSResourceGet.psd1 b/src/Microsoft.PowerShell.PSResourceGet.psd1 index c9ca1cdd9..5ff9f3d13 100644 --- a/src/Microsoft.PowerShell.PSResourceGet.psd1 +++ b/src/Microsoft.PowerShell.PSResourceGet.psd1 @@ -4,7 +4,7 @@ @{ RootModule = './Microsoft.PowerShell.PSResourceGet.dll' NestedModules = @('./Microsoft.PowerShell.PSResourceGet.psm1') - ModuleVersion = '1.1.1' + ModuleVersion = '1.2.0' CompatiblePSEditions = @('Core', 'Desktop') GUID = 'e4e0bda1-0703-44a5-b70d-8fe704cd0643' Author = 'Microsoft Corporation' @@ -46,7 +46,7 @@ 'udres') PrivateData = @{ PSData = @{ - # Prerelease = '' + Prerelease = 'preview1' Tags = @('PackageManagement', 'PSEdition_Desktop', 'PSEdition_Core', @@ -56,6 +56,16 @@ ProjectUri = 'https://go.microsoft.com/fwlink/?LinkId=828955' LicenseUri = 'https://go.microsoft.com/fwlink/?LinkId=829061' ReleaseNotes = @' +## 1.2.0-preview1 + +### New Features +- Dependency support for PSResources in v3 repositories (#1778 Thanks @o-l-a-v!) + +### Bug Fix +- Updated dependencies and added connection timeout to improve CI tests reliability (#1829) +- Improvements in `ContainerRegistry` repositories in listing repository catalog (#1831) +- Wildcard attribute added to `-Repository` parameter of `Install-PSResource` (#1808) + ## 1.1.1 ### Bug Fix From 068493f6a11d7059c13cd117873e123c753040d4 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:07:08 -0800 Subject: [PATCH 51/89] Add azartifacts credential provider integration --- src/code/CredentialProvider.cs | 273 +++++++++++++++++++++++ src/code/FindHelper.cs | 35 ++- src/code/InstallHelper.cs | 64 +----- src/code/PSRepositoryInfo.cs | 40 ++-- src/code/PublishHelper.cs | 10 +- src/code/RegisterPSResourceRepository.cs | 36 ++- src/code/RepositorySettings.cs | 129 ++++++----- src/code/SetPSResourceRepository.cs | 15 +- src/code/Utils.cs | 43 +++- 9 files changed, 505 insertions(+), 140 deletions(-) create mode 100644 src/code/CredentialProvider.cs diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs new file mode 100644 index 000000000..45720380f --- /dev/null +++ b/src/code/CredentialProvider.cs @@ -0,0 +1,273 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security; +using System.Management.Automation; +using System.Text.Json; + +namespace Microsoft.PowerShell.PSResourceGet.UtilClasses +{ + internal static class CredentialProvider + { + private static string FindCredProviderFromPluginsPath() + { + // Get environment variable "NUGET_PLUGIN_PATHS" + // The environment variable NUGET_PLUGIN_PATHS should have the value of the .exe or .dll of the credential provider found in plugins\netfx\CredentialProvider.Microsoft\ + // For example, $env:NUGET_PLUGIN_PATHS="my-alternative-location\CredentialProvider.Microsoft.exe". + // OR $env:NUGET_PLUGIN_PATHS="my-alternative-location\CredentialProvider.Microsoft.dll" + + return Environment.GetEnvironmentVariable("NUGET_PLUGIN_PATHS", EnvironmentVariableTarget.User) ?? Environment.GetEnvironmentVariable("NUGET_PLUGIN_PATHS", EnvironmentVariableTarget.Machine); + } + + private static string FindCredProviderFromDefaultLocation() + { + // Default locations are either: + // $env:UserProfile\.nuget\plugins\netfx\CredentialProvider\CredentialProvider.Microsoft.exe + // OR $env:UserProfile\.nuget\plugins\netcore\CredentialProvider\CredentialProvider.Microsoft.exe (or) CredentialProvider.Microsoft.dll + var credProviderDefaultLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "plugins"); + + var netCorePath = Path.Combine(credProviderDefaultLocation, "netcore", "CredentialProvider.Microsoft"); + var netFxPath = Path.Combine(credProviderDefaultLocation, "netfx", "CredentialProvider.Microsoft"); + var credProviderPath = string.Empty; + if (Directory.Exists(netCorePath)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.exe"); + } + else + { + credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.dll"); + } + } + else if (Directory.Exists(netFxPath) && Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = Path.Combine(netFxPath, "CredentialProvider.Microsoft.exe"); + } + + return credProviderPath; + } + + private static string FindCredProviderFromVSLocation(out ErrorRecord error) + { + error = null; + + // C:\Program Files\Microsoft Visual Studio\ + var visualStudioPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft Visual Studio"); + // "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.exe" + // "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.dll" + + var credProviderPath = string.Empty; + if (Directory.Exists(visualStudioPath)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.exe", out error); + } + else if (string.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.dll", out error); + } + } + + return credProviderPath; + } + + private static string VSCredentialProviderFile(string visualStudioPath, string credProviderFile, out ErrorRecord error) + { + error = null; + try + { + // Search for the file in the directory and subdirectories + string[] exeFile = Directory.GetFiles(visualStudioPath, credProviderFile, SearchOption.AllDirectories); + + if (exeFile.Length > 0) + { + return exeFile[0]; + } + } + catch (UnauthorizedAccessException e) + { + error = new ErrorRecord( + e, + "AccessToCredentialProviderFileDenied", + ErrorCategory.PermissionDenied, + null); + } + catch (Exception ex) + { + error = new ErrorRecord( + ex, + "ErrorRetrievingCredentialProvider", + ErrorCategory.NotSpecified, + null); + } + + return string.Empty; + } + + internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn) + { + string credProviderPath = string.Empty; + + // Find credential provider + // Option 1. Use env var 'NUGET_PLUGIN_PATHS' to find credential provider. + // See: https://docs.microsoft.com/en-us/nuget/reference/extensibility/nuget-cross-platform-plugins#plugin-installation-and-discovery + // Nuget prioritizes credential providers stored in the NUGET_PLUGIN_PATHS env var + credProviderPath = FindCredProviderFromPluginsPath(); + + // Option 2. Check default locations ($env:UserProfile\.nuget\plugins) + // .NET Core based plugins should be installed in: + // %UserProfile%/.nuget/plugins/netcore + // .NET Framework based plugins should be installed in: + // %UserProfile%/.nuget/plugins/netfx + if (String.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = FindCredProviderFromDefaultLocation(); + } + + // Option 3. Check Visual Studio installation paths + if (String.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = FindCredProviderFromVSLocation(out ErrorRecord error); + if (error != null) + { + cmdletPassedIn.WriteError(error); + return null; + } + } + + if (string.IsNullOrEmpty(credProviderPath)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentNullException("Path to the Azure Artifacts Credential Provider is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), + "CredentialProviderPathIsNullOrEmpty", + ErrorCategory.InvalidArgument, + null)); + return null; + } + + // Check case sensitivity here + if (!File.Exists(credProviderPath)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), + "CredentialProviderFileNotFound", + ErrorCategory.ObjectNotFound, + null)); + return null; + } + + cmdletPassedIn.WriteVerbose($"Credential Provider path found at: '{credProviderPath}'"); + + string fileName = credProviderPath; + // If running on unix machines, the Credential Provider needs to be called with dotnet cli. + if (credProviderPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + fileName = "dotnet"; + } + + string arguments = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ? + $"{credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json" : + $"-Uri {uri} -NonInteractive -IsRetry -F Json"; + string fullCallingCmd = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ? + $"dotnet {credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json" : + $"{credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json"; + cmdletPassedIn.WriteVerbose($"Calling Credential Provider with the following: '{fullCallingCmd}'"); + using (Process process = new Process()) + { + // Windows call should look like: "CredentialProvider.Microsoft.exe -Uri -NonInteractive -IsRetry -F Json" + // Unix call should look like: "dotnet CredentialProvider.Microsoft.dll -Uri -NonInteractive -IsRetry -F Json" + process.StartInfo.FileName = fileName; + process.StartInfo.Arguments = arguments; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + var stdError = process.StandardError.ReadToEnd(); + + // Timeout in milliseconds (e.g., 5000 ms = 5 seconds) + process.WaitForExit(5000); + + if (process.ExitCode != 0) + { + if (!string.IsNullOrEmpty(stdError)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Standard error: {stdError}"), + "ProcessStandardError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + + cmdletPassedIn.WriteError(new ErrorRecord( + new Exception($"Process exited with code {process.ExitCode}"), + "ProcessExitCodeError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + else if (string.IsNullOrEmpty(output)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Standard output is empty."), + "ProcessStandardOutputError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + + string username = string.Empty; + SecureString passwordSecure = new SecureString(); + try + { + using (JsonDocument doc = JsonDocument.Parse(output)) + { + JsonElement root = doc.RootElement; + if (root.TryGetProperty("Username", out JsonElement usernameToken)) + { + username = usernameToken.GetString(); + cmdletPassedIn.WriteVerbose("Username retrieved from Credential Provider."); + } + if (String.IsNullOrEmpty(username)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentNullException("Credential Provider username is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."), + "CredentialProviderUserNameIsNullOrEmpty", + ErrorCategory.InvalidArgument, + null)); + return null; + } + + if (root.TryGetProperty("Password", out JsonElement passwordToken)) + { + string password = passwordToken.GetString(); + if (String.IsNullOrEmpty(password)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentNullException("Credential Provider password is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."), + "CredentialProviderUserNameIsNullOrEmpty", + ErrorCategory.InvalidArgument, + null)); + return null; + } + + passwordSecure = Utils.ConvertToSecureString(password); + cmdletPassedIn.WriteVerbose("Password retrieved from Credential Provider."); + } + } + } + catch (Exception e) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new Exception("Error retrieving credentials from Credential Provider. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info.", e), + "InvalidCredentialProviderResponse", + ErrorCategory.InvalidResult, + null)); + return null; + } + + return new PSCredential(username, passwordSecure); + } + } + } +} diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index a3c5c5c35..8082d9237 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -202,7 +202,16 @@ public IEnumerable FindByResourceName( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { @@ -387,7 +396,17 @@ public IEnumerable FindByCommandOrDscResource( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { @@ -591,7 +610,17 @@ public IEnumerable FindByTag( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 10e48b55d..503bd573e 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -286,7 +286,16 @@ private List ProcessRepositories( string repoName = currentRepository.Name; sourceTrusted = currentRepository.Trusted || trustRepository; - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) @@ -353,59 +362,6 @@ private List ProcessRepositories( return allPkgsInstalled; } - /// - /// Checks if any of the package versions are already installed and if they are removes them from the list of packages to install. - /// - private List FilterByInstalledPkgs(List packages) - { - // Package install paths. - // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable). - // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations. - // e.g.: - // ./InstallPackagePath1/PackageA - // ./InstallPackagePath1/PackageB - // ./InstallPackagePath2/PackageC - // ./InstallPackagePath3/PackageD - - _cmdletPassedIn.WriteDebug("In InstallHelper::FilterByInstalledPkgs()"); - // Get currently installed packages. - var getHelper = new GetHelper(_cmdletPassedIn); - var installedPackageNames = new HashSet(StringComparer.CurrentCultureIgnoreCase); - foreach (var installedPkg in getHelper.GetInstalledPackages( - pkgs: packages, - pathsToSearch: _pathsToSearch)) - { - installedPackageNames.Add(installedPkg.Name); - } - - if (installedPackageNames.Count is 0) - { - return packages; - } - - // Return only packages that are not already installed. - var filteredPackages = new List(); - foreach (var pkg in packages) - { - if (!installedPackageNames.Contains(pkg.Name)) - { - // Add packages that still need to be installed. - filteredPackages.Add(pkg); - } - else - { - // Remove from tracking list of packages to install. - pkg.AdditionalMetadata.TryGetValue("NormalizedVersion", out string normalizedVersion); - _cmdletPassedIn.WriteWarning($"Resource '{pkg.Name}' with version '{normalizedVersion}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter"); - - // Remove from tracking list of packages to install. - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } - } - - return filteredPackages; - } - /// /// Deletes temp directory and is called at end of install process. /// diff --git a/src/code/PSRepositoryInfo.cs b/src/code/PSRepositoryInfo.cs index 07dd06762..d84cf10d1 100644 --- a/src/code/PSRepositoryInfo.cs +++ b/src/code/PSRepositoryInfo.cs @@ -27,74 +27,70 @@ public enum APIVersion ContainerRegistry } + public enum CredentialProviderType + { + None, + AzArtifacts + } + #endregion #region Constructor - public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, APIVersion apiVersion, bool allowed) + public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, CredentialProviderType credentialProvider, APIVersion apiVersion, bool allowed) { Name = name; Uri = uri; Priority = priority; Trusted = trusted; CredentialInfo = credentialInfo; + CredentialProvider = credentialProvider; ApiVersion = apiVersion; IsAllowedByPolicy = allowed; } #endregion - #region Enum - - public enum RepositoryProviderType - { - None, - ACR, - AzureDevOps - } - - #endregion - #region Properties /// - /// the Name of the repository + /// The Name of the repository. /// public string Name { get; } /// - /// the Uri for the repository + /// The Uri for the repository. /// public Uri Uri { get; } /// - /// whether the repository is trusted + /// Whether the repository is trusted. /// public bool Trusted { get; } /// - /// the priority of the repository + /// The priority of the repository. /// [ValidateRange(0, 100)] public int Priority { get; } /// - /// the type of repository provider (eg, AzureDevOps, ContainerRegistry, etc.) + /// The credential information for repository authentication. /// - public RepositoryProviderType RepositoryProvider { get; } + public PSCredentialInfo CredentialInfo { get; set; } /// - /// the credential information for repository authentication + /// Specifies which credential provider to use. /// - public PSCredentialInfo CredentialInfo { get; } + public CredentialProviderType CredentialProvider { get; set; } /// - /// the API protocol version for the repository + /// The API protocol version for the repository. /// public APIVersion ApiVersion { get; } // - /// is it allowed by policy + /// Specifies whether the repository is allowed by policy. /// public bool IsAllowedByPolicy { get; set; } diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 4cbfb0f4a..f711de7e6 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -380,7 +380,15 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe return; } - _networkCredential = Utils.SetNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (repository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + } // Check if dependencies already exist within the repo if: // 1) the resource to publish has dependencies and diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 97e18bdc9..889b45017 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -97,6 +97,12 @@ class RegisterPSResourceRepository : PSCmdlet [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } + /// + /// Specifies which credential provider to use. + /// + [Parameter(ParameterSetName = NameParameterSet)] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + /// /// When specified, displays the succcessfully registered repository and its information. /// @@ -127,6 +133,13 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + PSRepositoryInfo.CredentialProviderType? repoCredentialProvider = null; + if (MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) + { + repoCredentialProvider = CredentialProvider; + } + + switch (ParameterSetName) { case NameParameterSet: @@ -140,7 +153,7 @@ protected override void ProcessRecord() try { - items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, Force, this, out string errorMsg)); + items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, repoCredentialProvider, Force, this, out string errorMsg)); if (!string.IsNullOrEmpty(errorMsg)) { @@ -218,8 +231,9 @@ private PSRepositoryInfo PSGalleryParameterSetHelper(int repoPriority, bool repo repoTrusted, apiVersion: null, repoCredentialInfo: null, - Force, - this, + credentialProvider: null, + Force, + this, out string errorMsg); if (!string.IsNullOrEmpty(errorMsg)) @@ -352,6 +366,21 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + if (repo.ContainsKey("CredentialProvider") && + (repo["CredentialProvider"] == null || String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || + !(repo["CredentialProvider"].ToString().Equals("None", StringComparison.OrdinalIgnoreCase) || + repo["CredentialProvider"].ToString().Equals("AzArtifacts", StringComparison.OrdinalIgnoreCase)))) + { + WriteError(new ErrorRecord( + new PSInvalidOperationException("Repository 'CredentialProvider' must be set to either 'None' or 'AzArtifacts'"), + "InvalidCredentialProviderForRepositoriesParameterSetRegistration", + ErrorCategory.InvalidArgument, + this)); + + return null; + } + + try { WriteDebug($"Registering repository '{repo["Name"]}' with uri '{repoUri}'"); @@ -361,6 +390,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) repo.ContainsKey("Trusted") ? Convert.ToBoolean(repo["Trusted"].ToString()) : DefaultTrusted, apiVersion: repo.ContainsKey("Trusted") ? (PSRepositoryInfo.APIVersion?) repo["ApiVersion"] : null, repoCredentialInfo, + repo.ContainsKey("CredentialProvider") ? (PSRepositoryInfo.CredentialProviderType?)repo["CredentialProvider"] : null, Force, this, out string errorMsg); diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index e9f2693e2..f3299fb49 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -62,7 +62,7 @@ public static void CheckRepositoryStore() // Add PSGallery to the newly created store Uri psGalleryUri = new Uri(PSGalleryRepoUri); - Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, PSRepositoryInfo.APIVersion.V2, force: false); + Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.V2, force: false); } // Open file (which should exist now), if cannot/is corrupted then throw error @@ -76,7 +76,7 @@ public static void CheckRepositoryStore() } } - public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? repoCredentialProvider, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = String.Empty; if (repoName.Equals("PSGallery", StringComparison.OrdinalIgnoreCase)) @@ -85,11 +85,11 @@ public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int r return null; } - return AddToRepositoryStore(repoName, repoUri, repoPriority, repoTrusted, apiVersion, repoCredentialInfo, force, cmdletPassedIn, out errorMsg); + return AddToRepositoryStore(repoName, repoUri, repoPriority, repoTrusted, apiVersion, repoCredentialInfo, repoCredentialProvider, force, cmdletPassedIn, out errorMsg); } - public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; // remove trailing and leading whitespaces, and if Name is just whitespace Name should become null now and be caught by following condition @@ -106,7 +106,7 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri return null; } - PSRepositoryInfo.APIVersion resolvedAPIVersion = apiVersion ?? GetRepoAPIVersion(repoUri); + APIVersion resolvedAPIVersion = apiVersion ?? GetRepoAPIVersion(repoUri); if (repoCredentialInfo != null) { @@ -131,6 +131,13 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri } } + CredentialProviderType resolvedCredentialProvider = credentialProvider ?? CredentialProviderType.None; + // If it's an ADO feed with an ADO designated URL (eg: msazure.pkgs.) then add the 'CredentialProvider' attribute to the repository and by default set it to AzArtifacts + if ((repoUri.AbsoluteUri.Contains("pkgs.dev.azure.com") || repoUri.AbsoluteUri.Contains("pkgs.visualstudio.com")) && credentialProvider == null) + { + resolvedCredentialProvider = CredentialProviderType.AzArtifacts; + } + if (!cmdletPassedIn.ShouldProcess(repoName, "Register repository to repository store")) { return null; @@ -141,13 +148,13 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri return null; } - var repo = RepositorySettings.Add(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, resolvedAPIVersion, force); + var repo = Add(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, resolvedCredentialProvider, resolvedAPIVersion, force); return repo; } - public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, bool isSet, int defaultPriority, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, bool isSet, int defaultPriority, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; // repositories with Uri Scheme "temp" may have PSPath Uri's like: "Temp:\repo" @@ -182,7 +189,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr // determine trusted value to pass in (true/false if set, null otherwise, hence the nullable bool variable) bool? _trustedNullable = isSet ? new bool?(repoTrusted) : new bool?(); - + if (repoCredentialInfo != null) { bool isSecretManagementModuleAvailable = Utils.IsSecretManagementModuleAvailable(repoName, cmdletPassedIn); @@ -216,7 +223,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr return null; } - return Update(repoName, repoUri, repoPriority, _trustedNullable, apiVersion, repoCredentialInfo, cmdletPassedIn, out errorMsg); + return Update(repoName, repoUri, repoPriority, _trustedNullable, apiVersion, repoCredentialInfo, credentialProvider, cmdletPassedIn, out errorMsg); } /// @@ -224,7 +231,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr /// Returns: PSRepositoryInfo containing information about the repository just added to the repository store /// /// - public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSCredentialInfo repoCredentialInfo, PSRepositoryInfo.APIVersion apiVersion, bool force) + public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSCredentialInfo repoCredentialInfo, CredentialProviderType repoCredentialProvider, APIVersion apiVersion, bool force) { try { @@ -238,7 +245,7 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit } // Delete the existing repository before overwriting it (otherwire multiple repos with the same name will be added) - List removedRepositories = RepositorySettings.Remove(new string[] { repoName }, out string[] errorList); + List removedRepositories = Remove(new string[] { repoName }, out string[] errorList); // Need to load the document again because of changes after removing doc = LoadXDocument(FullRepositoryPath); @@ -261,7 +268,8 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit new XAttribute("Url", repoUri), new XAttribute("APIVersion", apiVersion), new XAttribute("Priority", repoPriority), - new XAttribute("Trusted", repoTrusted) + new XAttribute("Trusted", repoTrusted), + new XAttribute("CredentialProvider", repoCredentialProvider) ); if (repoCredentialInfo != null) @@ -282,14 +290,14 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repoUri) : true; - return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, apiVersion, isAllowed); + return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, repoCredentialProvider, apiVersion, isAllowed); } /// /// Updates a repository name, Uri, priority, installation policy, or credential information /// Returns: void /// - public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPriority, bool? repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPriority, bool? repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; PSRepositoryInfo updatedRepo; @@ -303,7 +311,7 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio bool repoIsTrusted = !(repoTrusted == null || repoTrusted == false); repoPriority = repoPriority < 0 ? DefaultPriority : repoPriority; - return AddToRepositoryStore(repoName, repoUri, repoPriority, repoIsTrusted, apiVersion, repoCredentialInfo, force:true, cmdletPassedIn, out errorMsg); + return AddToRepositoryStore(repoName, repoUri, repoPriority, repoIsTrusted, apiVersion, repoCredentialInfo, credentialProvider, force:true, cmdletPassedIn, out errorMsg); } // Check that repository node we are attempting to update has all required attributes: Name, Url (or Uri), Priority, Trusted. @@ -417,15 +425,15 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio } // Update APIVersion if necessary - PSRepositoryInfo.APIVersion resolvedAPIVersion = PSRepositoryInfo.APIVersion.Unknown; + APIVersion resolvedAPIVersion = APIVersion.Unknown; if (apiVersion != null) { - resolvedAPIVersion = (PSRepositoryInfo.APIVersion)apiVersion; + resolvedAPIVersion = (APIVersion)apiVersion; node.Attribute("APIVersion").Value = resolvedAPIVersion.ToString(); } else { - resolvedAPIVersion = (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true); + resolvedAPIVersion = (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true); } @@ -445,14 +453,29 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio } + // Update CredentialProvider if necessary + CredentialProviderType resolvedCredentialProvider = credentialProvider ?? CredentialProviderType.None; + if (credentialProvider != null) + { + resolvedCredentialProvider = (CredentialProviderType)credentialProvider; + if (node.Attribute("CredentialProvider") == null) + { + node.Add(new XAttribute("CredentialProvider", resolvedCredentialProvider.ToString())); + } + else + { + node.Attribute("CredentialProvider").Value = resolvedCredentialProvider.ToString(); + } + } + bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); updatedRepo = new PSRepositoryInfo(repoName, thisUrl, Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), thisCredentialInfo, + resolvedCredentialProvider, resolvedAPIVersion, isAllowed); @@ -523,6 +546,12 @@ public static List Remove(string[] repoNames, out string[] err continue; } + CredentialProviderType resolvedCredentialProvider = CredentialProviderType.None; + if (node.Attribute("CredentialProvider") != null) + { + resolvedCredentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), node.Attribute("CredentialProvider").Value, ignoreCase: true); + } + // determine if repo had Url or Uri (less likely) attribute bool urlAttributeExists = node.Attribute("Url") != null; bool uriAttributeExists = node.Attribute("Uri") != null; @@ -537,14 +566,14 @@ public static List Remove(string[] repoNames, out string[] err bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repoUri) : true; - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(repoUri); removedRepos.Add( new PSRepositoryInfo(repo, new Uri(node.Attribute(attributeUrlUriName).Value), Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), repoCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), + resolvedCredentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), isAllowed)); // Remove item from file @@ -630,13 +659,19 @@ public static List Read(string[] repoNames, out string[] error if (repo.Attribute("APIVersion") == null) { - PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(thisUrl); + APIVersion apiVersion = GetRepoAPIVersion(thisUrl); XElement repoXElem = FindRepositoryElement(doc, repo.Attribute("Name").Value); repoXElem.SetAttributeValue("APIVersion", apiVersion.ToString()); doc.Save(FullRepositoryPath); } + CredentialProviderType credentialProvider = CredentialProviderType.None; + if (repo.Attribute("CredentialProvider") != null) + { + credentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), repo.Attribute("CredentialProvider").Value, ignoreCase: true); + } + PSCredentialInfo thisCredentialInfo; string credentialInfoErrorMessage = $"Repository {repo.Attribute("Name").Value} has invalid CredentialInfo. {PSCredentialInfo.VaultNameAttribute} and {PSCredentialInfo.SecretNameAttribute} should both be present and non-empty"; // both keys are present @@ -669,8 +704,6 @@ public static List Read(string[] repoNames, out string[] error continue; } - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); - bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; PSRepositoryInfo currentRepoItem = new PSRepositoryInfo(repo.Attribute("Name").Value, @@ -678,7 +711,8 @@ public static List Read(string[] repoNames, out string[] error Int32.Parse(repo.Attribute("Priority").Value), Boolean.Parse(repo.Attribute("Trusted").Value), thisCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), repo.Attribute("APIVersion").Value, ignoreCase: true), + credentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), repo.Attribute("APIVersion").Value, ignoreCase: true), isAllowed); foundRepos.Add(currentRepoItem); @@ -738,13 +772,19 @@ public static List Read(string[] repoNames, out string[] error if (node.Attribute("APIVersion") == null) { - PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(thisUrl); + APIVersion apiVersion = GetRepoAPIVersion(thisUrl); XElement repoXElem = FindRepositoryElement(doc, node.Attribute("Name").Value); repoXElem.SetAttributeValue("APIVersion", apiVersion.ToString()); doc.Save(FullRepositoryPath); } + CredentialProviderType credentialProvider = CredentialProviderType.None; + if (node.Attribute("CredentialProvider") != null) + { + credentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), node.Attribute("CredentialProvider").Value, ignoreCase: true); + } + PSCredentialInfo thisCredentialInfo; string credentialInfoErrorMessage = $"Repository {node.Attribute("Name").Value} has invalid CredentialInfo. {PSCredentialInfo.VaultNameAttribute} and {PSCredentialInfo.SecretNameAttribute} should both be present and non-empty"; // both keys are present @@ -777,8 +817,6 @@ public static List Read(string[] repoNames, out string[] error continue; } - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); - bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; PSRepositoryInfo currentRepoItem = new PSRepositoryInfo(node.Attribute("Name").Value, @@ -786,7 +824,8 @@ public static List Read(string[] repoNames, out string[] error Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), thisCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), + credentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), isAllowed); foundRepos.Add(currentRepoItem); @@ -840,54 +879,38 @@ private static XDocument LoadXDocument(string filePath) return XDocument.Load(xmlReader); } - private static PSRepositoryInfo.APIVersion GetRepoAPIVersion(Uri repoUri) + private static APIVersion GetRepoAPIVersion(Uri repoUri) { if (repoUri.AbsoluteUri.EndsWith("/v2", StringComparison.OrdinalIgnoreCase)) { // Scenario: V2 server protocol repositories (i.e PSGallery) - return PSRepositoryInfo.APIVersion.V2; + return APIVersion.V2; } else if (repoUri.AbsoluteUri.EndsWith("/index.json", StringComparison.OrdinalIgnoreCase)) { // Scenario: V3 server protocol repositories (i.e NuGet.org, Azure Artifacts (ADO), Artifactory, Github Packages, MyGet.org) - return PSRepositoryInfo.APIVersion.V3; + return APIVersion.V3; } else if (repoUri.AbsoluteUri.EndsWith("/nuget", StringComparison.OrdinalIgnoreCase)) { // Scenario: ASP.Net application feed created with NuGet.Server to host packages - return PSRepositoryInfo.APIVersion.NugetServer; + return APIVersion.NugetServer; } else if (repoUri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase) || repoUri.Scheme.Equals("temp", StringComparison.OrdinalIgnoreCase)) { // repositories with Uri Scheme "temp" may have PSPath Uri's like: "Temp:\repo" and we should consider them as local repositories. - return PSRepositoryInfo.APIVersion.Local; + return APIVersion.Local; } else if (repoUri.AbsoluteUri.EndsWith(".azurecr.io") || repoUri.AbsoluteUri.EndsWith(".azurecr.io/") || repoUri.AbsoluteUri.Contains("mcr.microsoft.com")) { - return PSRepositoryInfo.APIVersion.ContainerRegistry; + return APIVersion.ContainerRegistry; } else { - return PSRepositoryInfo.APIVersion.Unknown; + return APIVersion.Unknown; } } - private static RepositoryProviderType GetRepositoryProviderType(Uri repoUri) - { - string absoluteUri = repoUri.AbsoluteUri; - // We want to use contains instead of EndsWith to accomodate for trailing '/' - if (absoluteUri.Contains("azurecr.io") || absoluteUri.Contains("mcr.microsoft.com")){ - return RepositoryProviderType.ACR; - } - // TODO: add a regex for this match - // eg: *pkgs.*/_packaging/* - else if (absoluteUri.Contains("pkgs.")){ - return RepositoryProviderType.AzureDevOps; - } - else { - return RepositoryProviderType.None; - } - } #endregion } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index f90646fa2..aea53adbf 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -94,6 +94,12 @@ public SwitchParameter Trusted [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } + /// + /// Specifies which credential provider to use. + /// + [Parameter(ParameterSetName = NameParameterSet)] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + /// /// When specified, displays the successfully registered repository and its information. /// @@ -118,10 +124,11 @@ protected override void ProcessRecord() !MyInvocation.BoundParameters.ContainsKey(nameof(Priority)) && !MyInvocation.BoundParameters.ContainsKey(nameof(Trusted)) && !MyInvocation.BoundParameters.ContainsKey(nameof(ApiVersion)) && - !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialInfo))) + !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialInfo)) && + !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) { ThrowTerminatingError(new ErrorRecord( - new ArgumentException("Must set Uri, Priority, Trusted, ApiVersion, or CredentialInfo parameter"), + new ArgumentException("Must set Uri, Priority, Trusted, ApiVersion, CredentialInfo, or CredentialProvider parameter"), "SetRepositoryParameterBindingFailure", ErrorCategory.InvalidArgument, this)); @@ -151,11 +158,12 @@ protected override void ProcessRecord() items.Add(RepositorySettings.UpdateRepositoryStore(Name, _uri, Priority, - Trusted, + Trusted, isSet, DefaultPriority, repoApiVersion, CredentialInfo, + CredentialProvider, this, out string errorMsg)); @@ -293,6 +301,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) DefaultPriority, ApiVersion, repoCredentialInfo, + CredentialProvider, this, out string errorMsg); diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 5ca14d0e2..d14ad8b0b 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -947,7 +947,7 @@ public static bool IsSecretManagementVaultAccessible( } } - public static NetworkCredential SetNetworkCredential( + public static NetworkCredential SetSecretManagementNetworkCredential( PSRepositoryInfo repository, NetworkCredential networkCredential, PSCmdlet cmdletPassedIn) @@ -968,6 +968,30 @@ public static NetworkCredential SetNetworkCredential( return networkCredential; } + public static NetworkCredential SetCredentialProviderNetworkCredential( + PSRepositoryInfo repository, + NetworkCredential networkCredential, + PSCmdlet cmdletPassedIn) + { + // Explicitly passed in Credential takes precedence over repository credential provider. + if (networkCredential == null) + { + cmdletPassedIn.WriteVerbose("Attempting to retrieve credentials from Azure Artifacts Credential Provider."); + PSCredential repoCredential = CredentialProvider.GetCredentialsFromProvider(repository.Uri, cmdletPassedIn); + if (repoCredential == null) + { + cmdletPassedIn.WriteVerbose("Unable to retrieve credentials from Azure Artifacts Credential Provider. Network credentials are null."); + } + else + { + networkCredential = new NetworkCredential(repoCredential.UserName, repoCredential.Password); + cmdletPassedIn.WriteVerbose("Credential successfully read from Azure Artifacts Credential Provider for repository: " + repository.Name); + } + } + + return networkCredential; + } + #endregion #region Path methods @@ -1542,6 +1566,23 @@ public static bool TryCreateModuleSpecification( return moduleSpecCreatedSuccessfully; } + public static SecureString ConvertToSecureString(string input) + { + if (input == null) { + throw new ArgumentNullException(nameof(input)); + } + + SecureString secureString = new SecureString(); + foreach (char c in input) + { + secureString.AppendChar(c); + } + + secureString.MakeReadOnly(); + + return secureString; + } + #endregion #region Directory and File From bc4fbdbe7a40dca3415ad80ba2cc368bba7e4d96 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:49:05 -0800 Subject: [PATCH 52/89] Add dynamic parameters to Register-PSResourceRepository and Set-PSResourceRepository --- src/code/RegisterPSResourceRepository.cs | 45 ++++++++++++++++-------- src/code/SetPSResourceRepository.cs | 37 +++++++++++++------ 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 889b45017..26e514671 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Management.Automation; using Dbg = System.Diagnostics.Debug; @@ -23,7 +24,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)] public sealed - class RegisterPSResourceRepository : PSCmdlet + class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters { #region Members @@ -35,6 +36,7 @@ class RegisterPSResourceRepository : PSCmdlet private const string PSGalleryParameterSet = "PSGalleryParameterSet"; private const string RepositoriesParameterSet = "RepositoriesParameterSet"; private Uri _uri; + private CredentialProviderDynamicParameters _credentialProvider; #endregion @@ -97,12 +99,6 @@ class RegisterPSResourceRepository : PSCmdlet [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } - /// - /// Specifies which credential provider to use. - /// - [Parameter(ParameterSetName = NameParameterSet)] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } - /// /// When specified, displays the succcessfully registered repository and its information. /// @@ -117,6 +113,21 @@ class RegisterPSResourceRepository : PSCmdlet #endregion + #region DynamicParameters + + public object GetDynamicParameters() + { + if (Uri.Contains("pkgs.dev.azure.com") || Uri.Contains("pkgs.visualstudio.com")) + { + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; + } + + return null; + } + + #endregion + #region Methods protected override void BeginProcessing() @@ -133,12 +144,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType? repoCredentialProvider = null; - if (MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) - { - repoCredentialProvider = CredentialProvider; - } - + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; switch (ParameterSetName) { @@ -153,7 +159,7 @@ protected override void ProcessRecord() try { - items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, repoCredentialProvider, Force, this, out string errorMsg)); + items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, credentialProvider, Force, this, out string errorMsg)); if (!string.IsNullOrEmpty(errorMsg)) { @@ -367,7 +373,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) } if (repo.ContainsKey("CredentialProvider") && - (repo["CredentialProvider"] == null || String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || + (String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || !(repo["CredentialProvider"].ToString().Equals("None", StringComparison.OrdinalIgnoreCase) || repo["CredentialProvider"].ToString().Equals("AzArtifacts", StringComparison.OrdinalIgnoreCase)))) { @@ -429,4 +435,13 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) #endregion } + + public class CredentialProviderDynamicParameters + { + /// + /// Specifies which credential provider to use. + /// + [Parameter] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index aea53adbf..8f35a323b 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Management.Automation; using Dbg = System.Diagnostics.Debug; @@ -18,7 +19,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets "PSResourceRepository", DefaultParameterSetName = NameParameterSet, SupportsShouldProcess = true)] - public sealed class SetPSResourceRepository : PSCmdlet + public sealed class SetPSResourceRepository : PSCmdlet, IDynamicParameters { #region Members @@ -26,6 +27,7 @@ public sealed class SetPSResourceRepository : PSCmdlet private const string RepositoriesParameterSet = "RepositoriesParameterSet"; private const int DefaultPriority = -1; private Uri _uri; + private CredentialProviderDynamicParameters _credentialProvider; #endregion @@ -92,13 +94,7 @@ public SwitchParameter Trusted /// Specifies vault and secret names as PSCredentialInfo for the repository. /// [Parameter(ParameterSetName = NameParameterSet)] - public PSCredentialInfo CredentialInfo { get; set; } - - /// - /// Specifies which credential provider to use. - /// - [Parameter(ParameterSetName = NameParameterSet)] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + public PSCredentialInfo CredentialInfo { get; set; } /// /// When specified, displays the successfully registered repository and its information. @@ -108,6 +104,23 @@ public SwitchParameter Trusted #endregion + #region DynamicParameters + + public object GetDynamicParameters() + { + PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); + if (repository is not null && + (repository.Uri.AbsoluteUri.Contains("pkgs.dev.azure.com") || repository.Uri.AbsoluteUri.Contains("pkgs.visualstudio.com"))) + { + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; + } + + return null; + } + + #endregion + #region Private methods protected override void BeginProcessing() @@ -148,6 +161,8 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + List items = new List(); switch(ParameterSetName) @@ -163,7 +178,7 @@ protected override void ProcessRecord() DefaultPriority, repoApiVersion, CredentialInfo, - CredentialProvider, + credentialProvider, this, out string errorMsg)); @@ -291,6 +306,8 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + try { var updatedRepo = RepositorySettings.UpdateRepositoryStore(repo["Name"].ToString(), @@ -301,7 +318,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) DefaultPriority, ApiVersion, repoCredentialInfo, - CredentialProvider, + credentialProvider, this, out string errorMsg); From 1f0c7685e5f0fdc337b1bd669f29be1741df5ebe Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 18 Dec 2024 22:04:10 -0800 Subject: [PATCH 53/89] Bug fixes and clean up --- src/code/CredentialProvider.cs | 62 +++++++++++++++++------- src/code/RegisterPSResourceRepository.cs | 24 ++++++--- src/code/SetPSResourceRepository.cs | 14 +++--- src/code/Utils.cs | 16 ++++++ 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs index 45720380f..3cd1ee738 100644 --- a/src/code/CredentialProvider.cs +++ b/src/code/CredentialProvider.cs @@ -4,11 +4,16 @@ using System.Security; using System.Management.Automation; using System.Text.Json; +using System.Net.Http; +using System.Net; namespace Microsoft.PowerShell.PSResourceGet.UtilClasses { internal static class CredentialProvider { + private static readonly string _credProviderExe = "CredentialProvider.Microsoft.exe"; + private static readonly string _credProviderDll = "CredentialProvider.Microsoft.dll"; + private static string FindCredProviderFromPluginsPath() { // Get environment variable "NUGET_PLUGIN_PATHS" @@ -33,16 +38,16 @@ private static string FindCredProviderFromDefaultLocation() { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.exe"); + credProviderPath = Path.Combine(netCorePath, _credProviderExe); } else { - credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.dll"); + credProviderPath = Path.Combine(netCorePath, _credProviderDll); } } else if (Directory.Exists(netFxPath) && Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = Path.Combine(netFxPath, "CredentialProvider.Microsoft.exe"); + credProviderPath = Path.Combine(netFxPath, _credProviderExe); } return credProviderPath; @@ -62,11 +67,11 @@ private static string FindCredProviderFromVSLocation(out ErrorRecord error) { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.exe", out error); + credProviderPath = VSCredentialProviderFile(visualStudioPath, _credProviderExe, out error); } else if (string.IsNullOrEmpty(credProviderPath)) { - credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.dll", out error); + credProviderPath = VSCredentialProviderFile(visualStudioPath, _credProviderDll, out error); } } @@ -108,8 +113,9 @@ private static string VSCredentialProviderFile(string visualStudioPath, string c internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn) { + cmdletPassedIn.WriteDebug("Enterting CredentialProvider::GetCredentialsFromProvider"); string credProviderPath = string.Empty; - + // Find credential provider // Option 1. Use env var 'NUGET_PLUGIN_PATHS' to find credential provider. // See: https://docs.microsoft.com/en-us/nuget/reference/extensibility/nuget-cross-platform-plugins#plugin-installation-and-discovery @@ -137,25 +143,47 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet } } + cmdletPassedIn.WriteDebug($"Credential provider path is '{credProviderPath}'"); if (string.IsNullOrEmpty(credProviderPath)) { cmdletPassedIn.WriteError(new ErrorRecord( new ArgumentNullException("Path to the Azure Artifacts Credential Provider is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), "CredentialProviderPathIsNullOrEmpty", ErrorCategory.InvalidArgument, - null)); + credProviderPath)); return null; } - // Check case sensitivity here if (!File.Exists(credProviderPath)) { - cmdletPassedIn.WriteError(new ErrorRecord( - new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), - "CredentialProviderFileNotFound", - ErrorCategory.ObjectNotFound, - null)); - return null; + // If the Credential Provider is not found on a Unix machine, try looking for a case insensitive file. + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + FileInfo fileInfo = new FileInfo(credProviderPath); + string resolvedFilePath = Utils.GetCaseInsensitiveFilePath(fileInfo.Directory.FullName, _credProviderDll); + if (resolvedFilePath != null) + { + credProviderPath = resolvedFilePath; + } + else + { + cmdletPassedIn.WriteError(new ErrorRecord( + new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), + "CredentialProviderFileNotFound", + ErrorCategory.ObjectNotFound, + credProviderPath)); + } + } + else + { + cmdletPassedIn.WriteError(new ErrorRecord( + new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."), + "CredentialProviderFileNotFound", + ErrorCategory.ObjectNotFound, + credProviderPath)); + + return null; + } } cmdletPassedIn.WriteVerbose($"Credential Provider path found at: '{credProviderPath}'"); @@ -234,7 +262,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet new ArgumentNullException("Credential Provider username is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."), "CredentialProviderUserNameIsNullOrEmpty", ErrorCategory.InvalidArgument, - null)); + credProviderPath)); return null; } @@ -247,7 +275,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet new ArgumentNullException("Credential Provider password is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."), "CredentialProviderUserNameIsNullOrEmpty", ErrorCategory.InvalidArgument, - null)); + credProviderPath)); return null; } @@ -262,7 +290,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet new Exception("Error retrieving credentials from Credential Provider. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info.", e), "InvalidCredentialProviderResponse", ErrorCategory.InvalidResult, - null)); + credProviderPath)); return null; } diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 26e514671..2914ea5c8 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -117,13 +117,13 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { - if (Uri.Contains("pkgs.dev.azure.com") || Uri.Contains("pkgs.visualstudio.com")) + if(Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { - _credentialProvider = new CredentialProviderDynamicParameters(); - return _credentialProvider; + return null; } - return null; + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; } #endregion @@ -144,7 +144,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; switch (ParameterSetName) { @@ -438,10 +438,22 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) public class CredentialProviderDynamicParameters { + PSRepositoryInfo.CredentialProviderType? _credProvider = null; + /// /// Specifies which credential provider to use. /// [Parameter] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + public PSRepositoryInfo.CredentialProviderType? CredentialProvider { + get + { + return _credProvider; + } + + set + { + _credProvider = value; + } + } } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index 8f35a323b..d3d341181 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -109,14 +109,14 @@ public SwitchParameter Trusted public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); - if (repository is not null && - (repository.Uri.AbsoluteUri.Contains("pkgs.dev.azure.com") || repository.Uri.AbsoluteUri.Contains("pkgs.visualstudio.com"))) + if (repository is not null && + (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { - _credentialProvider = new CredentialProviderDynamicParameters(); - return _credentialProvider; + return null; } - return null; + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; } #endregion @@ -161,7 +161,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; List items = new List(); @@ -306,7 +306,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; try { diff --git a/src/code/Utils.cs b/src/code/Utils.cs index d14ad8b0b..88671d558 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1258,6 +1258,22 @@ internal static void GetMetadataFilesFromPath(string dirPath, string packageName } } } + + internal static string GetCaseInsensitiveFilePath(string directory, string fileName) + { + var files = Directory.GetFiles(directory); + foreach (var file in files) + { + if (string.Equals(Path.GetFileName(file), fileName, StringComparison.OrdinalIgnoreCase)) + { + return file; + } + } + + // File not found + return null; + } + #endregion #region PSDataFile parsing From 2446093b881f1ab9c3f9de336fe8fe2404dd74de Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:44:43 -0800 Subject: [PATCH 54/89] Add tests --- test/CredentialProvider.Tests.ps1 | 74 +++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/CredentialProvider.Tests.ps1 diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 new file mode 100644 index 000000000..7a651be1f --- /dev/null +++ b/test/CredentialProvider.Tests.ps1 @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force + +Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { + + BeforeAll{ + $TestModuleName = "PackageManagement" + $ADORepoName = "ADORepository" + $ADORepoUri = "https://pkgs.dev.azure.com/mscodehub/PowerShellCore/_packaging/PowerShellCore_PublicPackages/nuget/v2" + $LocalRepoName = "LocalRepository" + $LocalRepoUri = Join-Path -Path $TestDrive -ChildPath "testdir" + $null = New-Item $LocalRepoUri -ItemType Directory -Force + + Get-NewPSResourceRepositoryFile + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Trusted + } + + AfterAll { + Uninstall-PSResource $TestModuleName -SkipDependencyCheck -ErrorAction SilentlyContinue + + Get-RevertPSResourceRepositoryFile + } + + It "Find resource given specific Name and Repository" { + $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName + $res.Name | Should -Be $TestModuleName + } + + It "Install resource given specific Name and Repository" { + Install-PSResource -Name $TestModuleName -Repository $ADORepoName + + Get-InstalledPSResource -Name $TestModuleName | Should -Not -BeNullOrEmpty + } + + It "Register repository with local path (CredentialProvider should be set to 'None')" { + Register-PSResourceRepository -Name $LocalRepoName -Uri $LocalRepoUri -Force + $repo = Get-PSResourceRepository -Name $LocalRepoName + $repo.CredentialProvider | Should -Be "None" + } + + It "Set CredentialProvider for local path repository" { + Register-PSResourceRepository -Name $LocalRepoName -Uri $LocalRepoUri -Trusted -Force + $repo = Get-PSResourceRepository -Name $LocalRepoName + $repo.CredentialProvider | Should -Be "None" + + Set-PSResourceRepository -Name $LocalRepoName -CredentialProvider AzArtifacts + $repo2 = Get-PSResourceRepository -Name $LocalRepoName + $repo2.CredentialProvider | Should -Be "AzArtifacts" + } + + It "Register repository with ADO Uri (CredentialProvider should be set to 'AzArtifacts')" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "AzArtifacts" + } + + It "Set CredentialProvider for ADO repository" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Trusted -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "AzArtifacts" + + Set-PSResourceRepository -Name $ADORepoName -CredentialProvider None + $repo2 = Get-PSResourceRepository -Name $ADORepoName + $repo2.CredentialProvider | Should -Be "None" + } + + It "Register repository with ADO Uri (CredentialProvider should be set to 'AzArtifacts')" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -CredentialProvider None -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "None" + } +} From 312853217b940fa9ad5fea9fb55e3de26ddbc87c Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:26:07 -0800 Subject: [PATCH 55/89] Update AdoV2 server tests --- test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 index 253dad68e..a4548a8c8 100644 --- a/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 @@ -14,7 +14,7 @@ Describe 'Test HTTP Find-PSResource for ADO V2 Server Protocol' -tags 'CI' { $ADOV2RepoName = "PSGetTestingPublicFeed" $ADOV2RepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/psresourceget-public-test-ci/nuget/v2" Get-NewPSResourceRepositoryFile - Register-PSResourceRepository -Name $ADOV2RepoName -Uri $ADOV2RepoUri + Register-PSResourceRepository -Name $ADOV2RepoName -Uri $ADOV2RepoUri -CredentialProvider "None" } AfterAll { From adfd8d73b1ab5ff3ab6649fc82efac8d29b9b660 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:40:54 -0800 Subject: [PATCH 56/89] Add logging for GH pkgs tests --- test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 index 0d1dc4557..7bc2e09ea 100644 --- a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 @@ -24,6 +24,8 @@ Describe 'Test HTTP Find-PSResource for Github Packages Server' -tags 'CI' { } It "find resource given specific Name, Version null" { + $repo = Get-PSResourceRepository $GithubPackagesRepoName + Write-Error "Repo: $repo; repo credential provider: $($repo.CredentialProvider)" # FindName() $res = Find-PSResource -Name $testModuleName -Repository $GithubPackagesRepoName -Credential $credential $res.Name | Should -Be $testModuleName From f83e9f818cae835baf005b9e28bb866748786d7a Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:35:22 -0800 Subject: [PATCH 57/89] Remove GH pkgs logging --- test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 index 7bc2e09ea..0d1dc4557 100644 --- a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 @@ -24,8 +24,6 @@ Describe 'Test HTTP Find-PSResource for Github Packages Server' -tags 'CI' { } It "find resource given specific Name, Version null" { - $repo = Get-PSResourceRepository $GithubPackagesRepoName - Write-Error "Repo: $repo; repo credential provider: $($repo.CredentialProvider)" # FindName() $res = Find-PSResource -Name $testModuleName -Repository $GithubPackagesRepoName -Credential $credential $res.Name | Should -Be $testModuleName From 603a04a2bcfa04278bb6a84bc74662219f52cf05 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:58:54 -0800 Subject: [PATCH 58/89] Rename tests --- .../InstallPSResourceADOServer.Tests.ps1 | 4 ++-- .../InstallPSResourceADOV2Server.Tests.ps1 | 4 ++-- .../InstallPSResourceContainerRegistryServer.Tests.ps1 | 2 +- .../InstallPSResourceGithubPackages.Tests.ps1 | 2 +- .../InstallPSResourceV3Server.Tests.ps1 | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 index fbf32c59e..834ea8690 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 @@ -5,7 +5,7 @@ $ProgressPreference = "SilentlyContinue" $modPath = "$psscriptroot/../PSGetTestUtils.psm1" Import-Module $modPath -Force -Verbose -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { +Describe 'Test Install-PSResource for ADO V3Server scenarios' -tags 'CI' { BeforeAll { $testModuleName = "test_local_mod" @@ -216,7 +216,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for ADO V3Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 index 589bff92b..124642f6d 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 @@ -5,7 +5,7 @@ $ProgressPreference = "SilentlyContinue" $modPath = "$psscriptroot/../PSGetTestUtils.psm1" Import-Module $modPath -Force -Verbose -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { +Describe 'Test Install-PSResource for ADO V2Server scenarios' -tags 'CI' { BeforeAll { $testModuleName = "test_local_mod" @@ -217,7 +217,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for ADO V2Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index 5f80ace08..d8a42a7cb 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -303,7 +303,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for Container Registry scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 index 0dafca8dc..2e4b88d37 100644 --- a/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 @@ -222,7 +222,7 @@ Describe 'Test Install-PSResource for GitHub packages' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for GitHub Packages scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 index 49397859c..b599de622 100644 --- a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 @@ -385,7 +385,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for V3Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = 'TestModule' From b93be9b9527f9c9ecf665f54d40a72668bccbcc2 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:54:34 -0800 Subject: [PATCH 59/89] Add Verbose messaging --- src/code/CredentialProvider.cs | 4 ++-- test/CredentialProvider.Tests.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs index 3cd1ee738..52c731b3c 100644 --- a/src/code/CredentialProvider.cs +++ b/src/code/CredentialProvider.cs @@ -113,7 +113,7 @@ private static string VSCredentialProviderFile(string visualStudioPath, string c internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn) { - cmdletPassedIn.WriteDebug("Enterting CredentialProvider::GetCredentialsFromProvider"); + cmdletPassedIn.WriteVerbose("Enterting CredentialProvider::GetCredentialsFromProvider"); string credProviderPath = string.Empty; // Find credential provider @@ -143,7 +143,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet } } - cmdletPassedIn.WriteDebug($"Credential provider path is '{credProviderPath}'"); + cmdletPassedIn.WriteVerbose($"Credential provider path is '{credProviderPath}'"); if (string.IsNullOrEmpty(credProviderPath)) { cmdletPassedIn.WriteError(new ErrorRecord( diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 7a651be1f..50c26646d 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -24,7 +24,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { } It "Find resource given specific Name and Repository" { - $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName + $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName } From 9410ba577a70422c1dc8f90835d256aa56c4b4a1 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:49:56 -0800 Subject: [PATCH 60/89] Update dynamic parameters for Set and Register repositories --- src/code/RegisterPSResourceRepository.cs | 3 ++- src/code/SetPSResourceRepository.cs | 3 ++- .../ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 2914ea5c8..3e591df64 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -117,7 +117,8 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { - if(Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) + // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. + if (ParameterSetName.Equals("PSGalleryParameterSet") || Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { return null; } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index d3d341181..39b285f15 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -109,7 +109,8 @@ public SwitchParameter Trusted public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); - if (repository is not null && + // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. + if (repository is not null && repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { return null; diff --git a/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 index 4f1279b87..fa9120dfe 100644 --- a/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 +++ b/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 @@ -154,7 +154,7 @@ Describe "Test Set-PSResourceRepository" -tags 'CI' { $hashtable4 = @{Name = $PSGalleryName; Trusted = $True}; $arrayOfHashtables = $hashtable1, $hashtable2, $hashtable3, $hashtable4 - Set-PSResourceRepository -Repository $arrayOfHashtables + Set-PSResourceRepository -Repository $arrayOfHashtables -Verbose $res = Get-PSResourceRepository -Name $TestRepoName1 $res.Name | Should -Be $TestRepoName1 $Res.Uri.LocalPath | Should -Contain $tmpDir2Path @@ -189,7 +189,7 @@ Describe "Test Set-PSResourceRepository" -tags 'CI' { It "not set and throw error for trying to set PSGallery Uri (NameParameterSet)" { Unregister-PSResourceRepository -Name $PSGalleryName Register-PSResourceRepository -PSGallery - {Set-PSResourceRepository -Name $PSGalleryName -Uri $tmpDir1Path -ErrorAction Stop} | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" + {Set-PSResourceRepository -Name $PSGalleryName -Uri $tmpDir1Path -Verbose -ErrorAction Stop} | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" } It "not set and throw error for trying to set PSGallery CredentialInfo (NameParameterSet)" { From d2099ec870a1c2d423f78aef1ec99c2208642f29 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:20:44 -0800 Subject: [PATCH 61/89] Dynamic param should not appear when using Repositories param set, comment out cred provider tests --- src/code/RegisterPSResourceRepository.cs | 5 ++++- test/CredentialProvider.Tests.ps1 | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 3e591df64..371835aa3 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -118,7 +118,10 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. - if (ParameterSetName.Equals("PSGalleryParameterSet") || Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) + // It should also not appear when using the 'Repositories' parameter set. + if (ParameterSetName.Equals(PSGalleryParameterSet) || + ParameterSetName.Equals(RepositoriesParameterSet) || + Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { return null; } diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 50c26646d..4be26cfe9 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -22,7 +22,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { Get-RevertPSResourceRepositoryFile } - +<# It "Find resource given specific Name and Repository" { $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName @@ -71,4 +71,5 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { $repo = Get-PSResourceRepository -Name $ADORepoName $repo.CredentialProvider | Should -Be "None" } + #> } From f9e32535bec5dab31b6c894a62a85122e1efd942 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:20:56 -0800 Subject: [PATCH 62/89] Set dynamic param updates --- src/code/SetPSResourceRepository.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index 39b285f15..3582d571a 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -110,8 +110,11 @@ public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. - if (repository is not null && repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || - (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) + // It should also not appear when using the 'Repositories' parameter set. + if (repository is not null && + (repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || + RepositoriesParameterSet.Equals(RepositoriesParameterSet) || + repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { return null; } From ca9e36e49650ef84847d99dd3b368e516c0292c9 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 26 Dec 2024 10:47:54 -0800 Subject: [PATCH 63/89] Add -CredentialProvider None to existing ADO tests --- .../InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 | 2 +- .../InstallPSResourceADOV2Server.Tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 index 834ea8690..f141d8e08 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 @@ -14,7 +14,7 @@ Describe 'Test Install-PSResource for ADO V3Server scenarios' -tags 'CI' { $ADORepoName = "PSGetTestingPublicFeed" $ADORepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/psresourceget-public-test-ci/nuget/v3/index.json" Get-NewPSResourceRepositoryFile - Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -CredentialProvider "None" } AfterEach { diff --git a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 index 124642f6d..0a861a180 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 @@ -14,7 +14,7 @@ Describe 'Test Install-PSResource for ADO V2Server scenarios' -tags 'CI' { $ADORepoName = "PSGetTestingPublicFeed" $ADORepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/psresourceget-public-test-ci/nuget/v2" Get-NewPSResourceRepositoryFile - Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -CredentialProvider None } AfterEach { From 5be8a6d9d2cee72c39908c6a2088f599a5f53e03 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 26 Dec 2024 11:28:49 -0800 Subject: [PATCH 64/89] uncomment cred provider tests --- test/CredentialProvider.Tests.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 4be26cfe9..50c26646d 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -22,7 +22,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { Get-RevertPSResourceRepositoryFile } -<# + It "Find resource given specific Name and Repository" { $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName @@ -71,5 +71,4 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { $repo = Get-PSResourceRepository -Name $ADORepoName $repo.CredentialProvider | Should -Be "None" } - #> } From 0a193733597a50260b79788fc6bc197496ba15d6 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:34:06 -0800 Subject: [PATCH 65/89] Update ADO feed --- test/CredentialProvider.Tests.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 50c26646d..0b09abf3a 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -6,9 +6,10 @@ Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { BeforeAll{ - $TestModuleName = "PackageManagement" + $TestModuleName = "TestModule99" $ADORepoName = "ADORepository" - $ADORepoUri = "https://pkgs.dev.azure.com/mscodehub/PowerShellCore/_packaging/PowerShellCore_PublicPackages/nuget/v2" + $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" + #https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v3/index.json $LocalRepoName = "LocalRepository" $LocalRepoUri = Join-Path -Path $TestDrive -ChildPath "testdir" $null = New-Item $LocalRepoUri -ItemType Directory -Force From 8436ac4d400afb7812b1d95d31de7acb982bb206 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:35:51 -0700 Subject: [PATCH 66/89] Install cred provider and setup secret in test CI --- .ci/test.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.ci/test.yml b/.ci/test.yml index e59b14ad1..40c4a3997 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -21,6 +21,11 @@ jobs: displayName: Install Secret store condition: eq(${{ parameters.useAzAuth }}, false) + - task: NuGetAuthenticate@1 + displayName: Install Azure Artifacts Credential Provider + inputs: + forceReinstallCredentialProvider: true + - task: DownloadBuildArtifacts@0 displayName: 'Download artifacts' inputs: @@ -63,6 +68,27 @@ jobs: displayName: Install module for test from downloaded artifact workingDirectory: ${{ parameters.buildDirectory }} + - task: AzurePowerShell@5 + inputs: + azureSubscription: PSResourceGetACR + azurePowerShellVersion: LatestVersion + ScriptType: InlineScript + pwsh: true + inline: | + Write-Verbose -Verbose "Setting up secret for Azure Artifacts Credential Provider" + $azt = Get-AzAccessToken + + $ADORepoName = "psrg-credprovidertest" + $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" + + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$ADOV2RepoUri, 'password':$azt.Token}]}" + + $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" + Write-Host "sending " + $vstsCommandString + Write-Host "##$vstsCommandString" + displayName: 'Setup Azure Artifacts Credential Provider secret' + condition: eq(${{ parameters.useAzAuth }}, false) + - task: AzurePowerShell@5 inputs: azureSubscription: PSResourceGetACR From 992af99497d6642dbf2d8cd6a3077200494db59f Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:44:59 -0700 Subject: [PATCH 67/89] Add capture environment --- .ci/test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.ci/test.yml b/.ci/test.yml index 40c4a3997..54ecf112b 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -81,7 +81,8 @@ jobs: $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$ADOV2RepoUri, 'password':$azt.Token}]}" + + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$ADOV2RepoUri, 'password':$($azt.Token)}]}" $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString @@ -89,6 +90,10 @@ jobs: displayName: 'Setup Azure Artifacts Credential Provider secret' condition: eq(${{ parameters.useAzAuth }}, false) + - pwsh: | + Get-ChildItem -Path env: | Out-String -width 9999 -Stream | Write-Verbose -Verbose + displayName: Capture environment + - task: AzurePowerShell@5 inputs: azureSubscription: PSResourceGetACR From bfcdc9a02e75d34e125b2e6f34554022ed78f593 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:19:55 -0700 Subject: [PATCH 68/89] Update endpoint variable for setting creds --- .ci/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test.yml b/.ci/test.yml index 54ecf112b..90f50b501 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -82,7 +82,7 @@ jobs: $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$ADOV2RepoUri, 'password':$($azt.Token)}]}" + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$($ADOV2RepoUri), 'password':$($azt.Token)}]}" $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString From 463461707da49f032dc313ef0e4ac343e5c57aa3 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:09:19 -0700 Subject: [PATCH 69/89] Update repo uri variable value --- .ci/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test.yml b/.ci/test.yml index 90f50b501..4970675e1 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -82,7 +82,7 @@ jobs: $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$($ADOV2RepoUri), 'password':$($azt.Token)}]}" + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$($ADORepoUri), 'password':$($azt.Token)}]}" $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString From 324ab1b2bd74842fed85e5a7df8fc328a8079192 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:56:12 -0700 Subject: [PATCH 70/89] add logging --- .ci/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/test.yml b/.ci/test.yml index 4970675e1..eb20aaa93 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -86,6 +86,7 @@ jobs: $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString + Write-Host "sending azt $($azt.Token)" Write-Host "##$vstsCommandString" displayName: 'Setup Azure Artifacts Credential Provider secret' condition: eq(${{ parameters.useAzAuth }}, false) From 1ae81db1f90953f51d46ef3b89cf63c165ce3ae1 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:23:55 -0700 Subject: [PATCH 71/89] Try adding username to endpoints value --- .ci/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.ci/test.yml b/.ci/test.yml index eb20aaa93..a0258402e 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -82,11 +82,10 @@ jobs: $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$($ADORepoUri), 'password':$($azt.Token)}]}" + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$($ADORepoUri), 'username': 'AzureDevOps', 'password':$($azt.Token)}]}" $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString - Write-Host "sending azt $($azt.Token)" Write-Host "##$vstsCommandString" displayName: 'Setup Azure Artifacts Credential Provider secret' condition: eq(${{ parameters.useAzAuth }}, false) From c8d01eefa82ee11255f58ba9d3b8c3dd3c1f7761 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:42:30 -0700 Subject: [PATCH 72/89] add logging --- .ci/test.yml | 2 ++ test/CredentialProvider.Tests.ps1 | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.ci/test.yml b/.ci/test.yml index a0258402e..c8b47558b 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -87,6 +87,8 @@ jobs: $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString Write-Host "##$vstsCommandString" + $plain = ConvertFrom-SecureString $($azt.Token) -AsPlainText + write-host "plain: $plain" displayName: 'Setup Azure Artifacts Credential Provider secret' condition: eq(${{ parameters.useAzAuth }}, false) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 0b09abf3a..815aec08e 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -7,7 +7,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { BeforeAll{ $TestModuleName = "TestModule99" - $ADORepoName = "ADORepository" + $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" #https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v3/index.json $LocalRepoName = "LocalRepository" From dd32cd8d057ad9e0560e9d8ef416a62d516c2847 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:00:41 -0700 Subject: [PATCH 73/89] check if env is set for tests --- .ci/test.yml | 2 -- test/CredentialProvider.Tests.ps1 | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.ci/test.yml b/.ci/test.yml index c8b47558b..a0258402e 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -87,8 +87,6 @@ jobs: $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString Write-Host "##$vstsCommandString" - $plain = ConvertFrom-SecureString $($azt.Token) -AsPlainText - write-host "plain: $plain" displayName: 'Setup Azure Artifacts Credential Provider secret' condition: eq(${{ parameters.useAzAuth }}, false) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 815aec08e..a5432d0eb 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -25,6 +25,12 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { } It "Find resource given specific Name and Repository" { + # Get user-level environment variable + $vssVar = Get-ItemProperty -Path "HKCU:\Environment" -Name "VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" + + Write-Host "$($vssVar.VSS_NUGET_EXTERNAL_FEED_ENDPOINTS)" + + Write-Host "$vssVar" $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName } From 4c71f1d4e4cb4d0f1e930baf648f52a8385fee62 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:20:58 -0700 Subject: [PATCH 74/89] See if new env var is accessible --- test/CredentialProvider.Tests.ps1 | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index a5432d0eb..a929e71a9 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -25,12 +25,8 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { } It "Find resource given specific Name and Repository" { - # Get user-level environment variable - $vssVar = Get-ItemProperty -Path "HKCU:\Environment" -Name "VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" - - Write-Host "$($vssVar.VSS_NUGET_EXTERNAL_FEED_ENDPOINTS)" - - Write-Host "$vssVar" + Write-Host "Var: $env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" + $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName } From d1ff4fa2021847f25818691ec19437f7527737db Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:50:48 -0700 Subject: [PATCH 75/89] Add double quotations --- .ci/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/test.yml b/.ci/test.yml index a0258402e..63df33146 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -83,6 +83,7 @@ jobs: $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$($ADORepoUri), 'username': 'AzureDevOps', 'password':$($azt.Token)}]}" + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{\"endpointCredentials\": [{\"endpoint\":\"$($ADORepoUri)\", \"username\": \"AzureDevOps\", \"password\":\"$($azt.Token)\"}]}" $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString From b82a714e4ff0e488e14af86d2392f2762b245bfb Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:27:23 -0700 Subject: [PATCH 76/89] change var to here string --- .ci/test.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.ci/test.yml b/.ci/test.yml index 63df33146..c9f232c15 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -81,10 +81,18 @@ jobs: $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = @" + { + "endpointCredentials": [ + { + "endpoint": "$ADORepoUri", + "username": "AzureDevOps", + "password": "$($azt.Token)" + } + ] + } + "@ - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{'endpointCredentials': [{'endpoint':$($ADORepoUri), 'username': 'AzureDevOps', 'password':$($azt.Token)}]}" - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{\"endpointCredentials\": [{\"endpoint\":\"$($ADORepoUri)\", \"username\": \"AzureDevOps\", \"password\":\"$($azt.Token)\"}]}" - $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString Write-Host "##$vstsCommandString" From 19f2e593e79ad6ecc19ef0df208001ea1370e574 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:56:16 -0700 Subject: [PATCH 77/89] change variable contents --- .ci/test.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.ci/test.yml b/.ci/test.yml index c9f232c15..810772fc0 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -81,18 +81,8 @@ jobs: $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = @" - { - "endpointCredentials": [ - { - "endpoint": "$ADORepoUri", - "username": "AzureDevOps", - "password": "$($azt.Token)" - } - ] - } - "@ - + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{`"endpointCredentials`":[{`"endpoint`":`"$ADORepoUri`",`"username`":`"AzureDevOps`",`"password`":`"$($azt.Token)`"}]}" + $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString Write-Host "##$vstsCommandString" From a9ac2aa19567ce0e9c84456df169ec459d01991c Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:51:53 -0700 Subject: [PATCH 78/89] remove username --- .ci/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test.yml b/.ci/test.yml index 810772fc0..110c461e9 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -81,7 +81,7 @@ jobs: $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{`"endpointCredentials`":[{`"endpoint`":`"$ADORepoUri`",`"username`":`"AzureDevOps`",`"password`":`"$($azt.Token)`"}]}" + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{`"endpointCredentials`":[{`"endpoint`":`"$ADORepoUri`",`"password`":`"$($azt.Token)`"}]}" $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString From 11657ad59683b66a85f752ac06e3d1c8b82c2bbb Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 10 Jul 2025 11:25:14 -0700 Subject: [PATCH 79/89] Add endpoint obj as json --- .ci/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.ci/test.yml b/.ci/test.yml index 110c461e9..2bca8f742 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -76,12 +76,13 @@ jobs: pwsh: true inline: | Write-Verbose -Verbose "Setting up secret for Azure Artifacts Credential Provider" - $azt = Get-AzAccessToken + $azt = (Get-AzAccessToken).Token $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" - $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = "{`"endpointCredentials`":[{`"endpoint`":`"$ADORepoUri`",`"password`":`"$($azt.Token)`"}]}" + $endpointCredsObj = @{ endpointCredentials = @( @{ endpoint = $ADoRepoURI; password = $azt })} + $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = $endpointCredsObj | ConvertTo-Json -Compress $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString From 1ae884b48b62103da07e29e0e2891129c91e35c5 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 10 Jul 2025 11:27:56 -0700 Subject: [PATCH 80/89] second try --- .ci/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/test.yml b/.ci/test.yml index 2bca8f742..c764203f5 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -87,6 +87,7 @@ jobs: $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString Write-Host "##$vstsCommandString" + displayName: 'Setup Azure Artifacts Credential Provider secret' condition: eq(${{ parameters.useAzAuth }}, false) From 04066df47c71cd0b545dd7436567496f8f9bacaa Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 10 Jul 2025 13:20:44 -0700 Subject: [PATCH 81/89] Add verbose logging --- .ci/test.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.ci/test.yml b/.ci/test.yml index c764203f5..1c4228acc 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -78,22 +78,26 @@ jobs: Write-Verbose -Verbose "Setting up secret for Azure Artifacts Credential Provider" $azt = (Get-AzAccessToken).Token + Write-Verbose -Verbose "Setting up Azure Artifacts Credential Provider secret - token - $azt" + $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" $endpointCredsObj = @{ endpointCredentials = @( @{ endpoint = $ADoRepoURI; password = $azt })} $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = $endpointCredsObj | ConvertTo-Json -Compress + Write-Verbose -Verbose "Setting VSS_NUGET_EXTERNAL_FEED_ENDPOINTS environment variable $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" + $vstsCommandString = "vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" Write-Host "sending " + $vstsCommandString Write-Host "##$vstsCommandString" - + displayName: 'Setup Azure Artifacts Credential Provider secret' condition: eq(${{ parameters.useAzAuth }}, false) - pwsh: | Get-ChildItem -Path env: | Out-String -width 9999 -Stream | Write-Verbose -Verbose - displayName: Capture environment + displayName: Capture environment - task: AzurePowerShell@5 inputs: From f0f5c276a57ca289a28cc0fd4325e98f36e54cda Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 10 Jul 2025 13:31:10 -0700 Subject: [PATCH 82/89] Convert from secure string --- .ci/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test.yml b/.ci/test.yml index 1c4228acc..a241edca2 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -76,7 +76,7 @@ jobs: pwsh: true inline: | Write-Verbose -Verbose "Setting up secret for Azure Artifacts Credential Provider" - $azt = (Get-AzAccessToken).Token + $azt = (Get-AzAccessToken).Token | ConnvertFrom-SecureString -AsPlainText Write-Verbose -Verbose "Setting up Azure Artifacts Credential Provider secret - token - $azt" From bfba721a47cfb7d082d8dbfb42168edd0af66e99 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 10 Jul 2025 13:40:01 -0700 Subject: [PATCH 83/89] Fix typo --- .ci/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test.yml b/.ci/test.yml index a241edca2..0021fe30f 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -76,7 +76,7 @@ jobs: pwsh: true inline: | Write-Verbose -Verbose "Setting up secret for Azure Artifacts Credential Provider" - $azt = (Get-AzAccessToken).Token | ConnvertFrom-SecureString -AsPlainText + $azt = (Get-AzAccessToken).Token | ConvertFrom-SecureString -AsPlainText Write-Verbose -Verbose "Setting up Azure Artifacts Credential Provider secret - token - $azt" From 904bd2815a8427745487c5c4a9cda56355479add Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:13:27 -0700 Subject: [PATCH 84/89] fix tests --- test/CredentialProvider.Tests.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index a929e71a9..cd786b684 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -6,7 +6,7 @@ Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { BeforeAll{ - $TestModuleName = "TestModule99" + $TestModuleName = "testmodule99" $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" #https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v3/index.json @@ -25,8 +25,6 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { } It "Find resource given specific Name and Repository" { - Write-Host "Var: $env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS" - $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName } From b8ff08d76da7151cfc8f3dee1e4381fd15c25f00 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:39:07 -0700 Subject: [PATCH 85/89] test new module --- test/CredentialProvider.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index cd786b684..d74a1fd5d 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -6,7 +6,7 @@ Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { BeforeAll{ - $TestModuleName = "testmodule99" + $TestModuleName = "test_module" $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" #https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v3/index.json From ee6efce6da90df52264dac6800ae3193acaccf16 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:59:00 -0700 Subject: [PATCH 86/89] add debugging --- test/CredentialProvider.Tests.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index d74a1fd5d..27219de59 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -25,6 +25,10 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { } It "Find resource given specific Name and Repository" { + $DebugPreference = "continue" + Find-PSResource -Name "*" -Repository $ADORepoName -verbose -Debug | write-verbose -verbose + + $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName } From 928f7a505ca801495dfe7a1216bdcde86038ee40 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:55:33 -0700 Subject: [PATCH 87/89] revert test changes --- test/CredentialProvider.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 27219de59..4f5cc0b12 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -6,7 +6,7 @@ Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { BeforeAll{ - $TestModuleName = "test_module" + $TestModuleName = "testmodule99" $ADORepoName = "psrg-credprovidertest" $ADORepoUri = "https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v2" #https://pkgs.dev.azure.com/powershell-rel/PSResourceGet/_packaging/psrg-credprovidertest/nuget/v3/index.json From f293c9e5aafd68cd2a560f2cd4f24d26adf43c46 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:44:16 -0700 Subject: [PATCH 88/89] fix dynamic parameter and add test --- src/code/SetPSResourceRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index 3582d571a..acaf0631c 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -113,7 +113,7 @@ public object GetDynamicParameters() // It should also not appear when using the 'Repositories' parameter set. if (repository is not null && (repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || - RepositoriesParameterSet.Equals(RepositoriesParameterSet) || + ParameterSetName.Equals(RepositoriesParameterSet) || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { return null; From b9deb95fe4b4045495418d41bf10ef3f2387cda9 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:44:50 -0700 Subject: [PATCH 89/89] Add test --- test/CredentialProvider.Tests.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 4f5cc0b12..4a1d28d26 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -25,10 +25,6 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { } It "Find resource given specific Name and Repository" { - $DebugPreference = "continue" - Find-PSResource -Name "*" -Repository $ADORepoName -verbose -Debug | write-verbose -verbose - - $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName } @@ -61,6 +57,12 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { $repo.CredentialProvider | Should -Be "AzArtifacts" } + It "Register repository with ADO Uri and CredentialProvider set to 'None')" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -CredentialProvider None -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "None" + } + It "Set CredentialProvider for ADO repository" { Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Trusted -Force $repo = Get-PSResourceRepository -Name $ADORepoName