From be2f5626e0be3a772bb7bb4a95ac112bf6df2e3d Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Wed, 26 Apr 2023 12:51:58 +0200 Subject: [PATCH] `Import-SqlDscPreferredModule`: Better handle preferred module (#1921) - `Import-SqlDscPreferredModule` - Better handle preferred module and re-uses logic in `Get-SqlDscPreferredModule`. --- CHANGELOG.md | 2 + azure-pipelines.yml | 3 +- source/Public/Get-SqlDscPreferredModule.ps1 | 10 +- .../Public/Import-SqlDscPreferredModule.ps1 | 104 ++++++++++-------- source/en-US/SqlServerDsc.strings.psd1 | 2 +- .../Get-SqlDscPreferredModule.Tests.ps1 | 20 ++++ .../Import-SqlDscPreferredModule.Tests.ps1 | 69 ++++++++++-- 7 files changed, 151 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14408c278..3c90c19f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([issue #1918](https://github.com/dsccommunity/SqlServerDsc/issues/1918)). - Correctly outputs query in verbose message when parameter `RedactText` is not passed. +- `Import-SqlDscPreferredModule` + - Better handle preferred module and re-uses logic in `Get-SqlDscPreferredModule`. ## [16.2.0] - 2023-04-10 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4de8e375a..ade6ce3e9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -172,8 +172,6 @@ stages: pool: vmImage: $(JOB_VMIMAGE) timeoutInMinutes: 0 - variables: - SMODefaultModuleName: 'SqlServer' steps: - task: DownloadPipelineArtifact@2 displayName: 'Download Build Artifact' @@ -363,6 +361,7 @@ stages: SKIP_DATABASE_ENGINE_DEFAULT_INSTANCE: true SKIP_ANALYSIS_MULTI_INSTANCE: true SKIP_ANALYSIS_TABULAR_INSTANCE: true + SMODefaultModuleName: 'SqlServer' pool: vmImage: $(JOB_VMIMAGE) timeoutInMinutes: 0 diff --git a/source/Public/Get-SqlDscPreferredModule.ps1 b/source/Public/Get-SqlDscPreferredModule.ps1 index bf8b013a6..d2e7ec860 100644 --- a/source/Public/Get-SqlDscPreferredModule.ps1 +++ b/source/Public/Get-SqlDscPreferredModule.ps1 @@ -5,6 +5,10 @@ .DESCRIPTION Get the first available (preferred) module that is installed. + If the environment variable `SMODefaultModuleName` is set to a module name + that name will be used as the preferred module name instead of the default + module 'SqlServer'. + .PARAMETER Name Specifies the list of the (preferred) modules to search for, in order. Defaults to 'SqlServer' and then 'SQLPS'. @@ -60,13 +64,13 @@ function Get-SqlDscPreferredModule if (-not $PSBoundParameters.ContainsKey('Name')) { - $Name = if ($env:SMODefaultModuleName) + if ($env:SMODefaultModuleName) { - @($env:SMODefaultModuleName, 'SQLPS') + $Name = @($env:SMODefaultModuleName, 'SQLPS') } else { - @('SqlServer', 'SQLPS') + $Name = @('SqlServer', 'SQLPS') } } diff --git a/source/Public/Import-SqlDscPreferredModule.ps1 b/source/Public/Import-SqlDscPreferredModule.ps1 index 93dc97c12..0b422f8ad 100644 --- a/source/Public/Import-SqlDscPreferredModule.ps1 +++ b/source/Public/Import-SqlDscPreferredModule.ps1 @@ -1,18 +1,25 @@ <# .SYNOPSIS - Imports the module SqlServer (preferred) or SQLPS in a standardized way. + Imports a (preferred) module in a standardized way. .DESCRIPTION - Imports the module SqlServer (preferred) or SQLPS in a standardized way. + Imports a (preferred) module in a standardized way. If the parameter `Name` + is not specified the command will imports the default module SqlServer + if it exist, otherwise SQLPS. + + If the environment variable `SMODefaultModuleName` is set to a module name + that name will be used as the preferred module name instead of the default + module 'SqlServer'. + The module is always imported globally. - .PARAMETER PreferredModule - Specifies the name of the preferred module. Defaults to 'SqlServer'. + .PARAMETER Name + Specifies the name of a preferred module. .PARAMETER Force - Forces the removal of the previous SQL module, to load the same or newer - version fresh. This is meant to make sure the newest version is used, with - the latest assemblies. + Forces the removal of the previous module, to load the same or newer version + fresh. This is meant to make sure the newest version is used, with the latest + assemblies. .EXAMPLE Import-SqlDscPreferredModule @@ -23,12 +30,12 @@ .EXAMPLE Import-SqlDscPreferredModule -Force - Removes any already loaded module of the default preferred module (SqlServer) - and the module SQLPS, then it will forcibly import the default preferred - module if it exist, otherwise it will try to import the module SQLPS. + Will forcibly import the default preferred module if it exist, otherwise + it will try to import the module SQLPS. Prior to importing it will remove + an already loaded module. .EXAMPLE - Import-SqlDscPreferredModule -PreferredModule 'OtherSqlModule' + Import-SqlDscPreferredModule -Name 'OtherSqlModule' Imports the specified preferred module OtherSqlModule if it exist, otherwise it will try to import the module SQLPS. @@ -39,58 +46,67 @@ function Import-SqlDscPreferredModule param ( [Parameter()] + [Alias('PreferredModule')] + [ValidateNotNullOrEmpty()] [System.String] - $PreferredModule, + $Name, [Parameter()] [System.Management.Automation.SwitchParameter] $Force ) - if (-not $PSBoundParameters.ContainsKey('PreferredModule')) + $getSqlDscPreferredModuleParameters = @{ + Refresh = $true + } + + if ($PSBoundParameters.ContainsKey('Name')) { - $PreferredModule = if ($env:SMODefaultModuleName) - { - $env:SMODefaultModuleName - } - else - { - 'SqlServer' - } + $getSqlDscPreferredModuleParameters.Name = @($Name, 'SQLPS') } + $availableModuleName = Get-SqlDscPreferredModule @getSqlDscPreferredModuleParameters + if ($Force.IsPresent) { Write-Verbose -Message $script:localizedData.PreferredModule_ForceRemoval - Remove-Module -Name @( - $PreferredModule, - 'SQLPS', - 'SQLASCmdlets' # cSpell: disable-line - ) -Force -ErrorAction 'SilentlyContinue' - } - else - { - <# - Check if either of the modules are already loaded into the session. - Prefer to use the first one (in order found). - NOTE: There should actually only be either SqlServer or SQLPS loaded, - otherwise there can be problems with wrong assemblies being loaded. - #> - $loadedModuleName = (Get-Module -Name @($PreferredModule, 'SQLPS') | Select-Object -First 1).Name - - if ($loadedModuleName) + $removeModule = @() + + if ($PSBoundParameters.ContainsKey('Name')) { - Write-Verbose -Message ($script:localizedData.PreferredModule_AlreadyImported -f $loadedModuleName) + $removeModule += $Name + } - return + # Available module could be + if ($availableModuleName) + { + $removeModule += $availableModuleName } - } - $availableModuleName = Get-SqlDscPreferredModule -Name @($PreferredModule, 'SQLPS') -Refresh + if ($removeModule -contains 'SQLPS') + { + $removeModule += 'SQLASCmdlets' # cSpell: disable-line + } + + Remove-Module -Name $removeModule -Force -ErrorAction 'SilentlyContinue' + } if ($availableModuleName) { + if (-not $Force.IsPresent) + { + # Check if the preferred module is already loaded into the session. + $loadedModuleName = (Get-Module -Name $availableModuleName | Select-Object -First 1).Name + + if ($loadedModuleName) + { + Write-Verbose -Message ($script:localizedData.PreferredModule_AlreadyImported -f $loadedModuleName) + + return + } + } + try { Write-Debug -Message ($script:localizedData.PreferredModule_PushingLocation) @@ -108,7 +124,7 @@ function Import-SqlDscPreferredModule Only return the object with module type 'Manifest'. SqlServer only returns one object (of module type 'Script'), so no need to do anything for SqlServer module. #> - if ($availableModuleName -ne $PreferredModule) + if ($availableModuleName -eq 'SQLPS') { $importedModule = $importedModule | Where-Object -Property 'ModuleType' -EQ -Value 'Manifest' } @@ -126,7 +142,7 @@ function Import-SqlDscPreferredModule { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.PreferredModule_FailedFinding -f $PreferredModule), + ($script:localizedData.PreferredModule_FailedFinding), 'ISDPM0001', # cspell: disable-line [System.Management.Automation.ErrorCategory]::ObjectNotFound, 'PreferredModule' diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 299b0fff3..b7a4fe10a 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -157,7 +157,7 @@ ConvertFrom-StringData @' PreferredModule_ForceRemoval = Forcibly removed the SQL PowerShell module from the session to import it fresh again. PreferredModule_PushingLocation = SQLPS module changes CWD to SQLServer:\ when loading, pushing location to pop it when module is loaded. PreferredModule_PoppingLocation = Popping location back to what it was before importing SQLPS module. - PreferredModule_FailedFinding = Failed to find a dependent module. Unable to run SQL Server commands or use SQL Server types. Please install the {0} or SQLPS then try to import SqlServerDsc again. + PreferredModule_FailedFinding = Failed to find a dependent module. Unable to run SQL Server commands or use SQL Server types. Please install one of the preferred SMO modules or the SQLPS module, then try to import SqlServerDsc again. ## Invoke-SqlDscQuery Query_Invoke_ShouldProcessVerboseDescription = Executing a Transact-SQL query on the instance '{0}'. diff --git a/tests/Unit/Public/Get-SqlDscPreferredModule.Tests.ps1 b/tests/Unit/Public/Get-SqlDscPreferredModule.Tests.ps1 index d25405309..844687f46 100644 --- a/tests/Unit/Public/Get-SqlDscPreferredModule.Tests.ps1 +++ b/tests/Unit/Public/Get-SqlDscPreferredModule.Tests.ps1 @@ -423,4 +423,24 @@ Describe 'Get-SqlDscPreferredModule' -Tag 'Public' { Should -Invoke -CommandName Set-PSModulePath -Exactly -Times 1 -Scope It } } + + Context 'When the environment variable SMODefaultModuleName is assigned a module name' { + BeforeAll { + $env:SMODefaultModuleName = 'OtherModule' + + Mock -CommandName Get-Module -MockWith { + return @{ + Name = $env:SMODefaultModuleName + } + } + } + + AfterAll { + Remove-Item -Path 'env:SMODefaultModuleName' + } + + It 'Should return the correct module name' { + Get-SqlDscPreferredModule | Should -Be $env:SMODefaultModuleName + } + } } diff --git a/tests/Unit/Public/Import-SqlDscPreferredModule.Tests.ps1 b/tests/Unit/Public/Import-SqlDscPreferredModule.Tests.ps1 index 1d8210322..35b68b4cc 100644 --- a/tests/Unit/Public/Import-SqlDscPreferredModule.Tests.ps1 +++ b/tests/Unit/Public/Import-SqlDscPreferredModule.Tests.ps1 @@ -52,7 +52,7 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { @{ MockParameterSetName = '__AllParameterSets' # cSpell: disable-next - MockExpectedParameters = '[[-PreferredModule] ] [-Force] []' + MockExpectedParameters = '[[-Name] ] [-Force] []' } ) { $result = (Get-Command -Name 'Import-SqlDscPreferredModule').ParameterSets | @@ -140,7 +140,11 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { Context 'When module SqlServer is already loaded into the session' { BeforeAll { - Mock -CommandName Import-Module -MockWith $mockImportModule + Mock -CommandName Import-Module + Mock -CommandName Get-SqlDscPreferredModule -MockWith { + return 'SqlServer' + } + Mock -CommandName Get-Module -MockWith { return @{ Name = 'SqlServer' @@ -157,7 +161,11 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { Context 'When module SQLPS is already loaded into the session' { BeforeAll { - Mock -CommandName Import-Module -MockWith $mockImportModule + Mock -CommandName Import-Module + Mock -CommandName Get-SqlDscPreferredModule -MockWith { + return 'SQLPS' + } + Mock -CommandName Get-Module -MockWith { return @{ Name = 'SQLPS' @@ -193,6 +201,27 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { } } + Context 'When the specific module exists, but not loaded into the session' { + BeforeAll { + Mock -CommandName Import-Module -MockWith $mockImportModule + Mock -CommandName Get-Module + Mock -CommandName Get-SqlDscPreferredModule -MockWith { + return 'OtherModule' + } + + $mockExpectedModuleNameToImport = 'OtherModule' + } + + It 'Should import the SqlServer module without throwing' { + { Import-SqlDscPreferredModule -Name 'OtherModule' } | Should -Not -Throw + + Should -Invoke -CommandName Get-SqlDscPreferredModule -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Push-Location -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Pop-Location -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Import-Module -Exactly -Times 1 -Scope It + } + } + Context 'When only module SQLPS exists, but not loaded into the session, and using -Force' { BeforeAll { Mock -CommandName Import-Module -MockWith $mockImportModule @@ -220,22 +249,44 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { Mock -CommandName Import-Module Mock -CommandName Get-Module Mock -CommandName Get-SqlDscPreferredModule - - $mockExpectedModuleNameToImport = $sqlPsExpectedModulePath } It 'Should throw the correct error message' { - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.PowerShellSqlModuleNotFound -f 'SqlServer' + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.PreferredModule_FailedFinding } - { Import-SqlDscPreferredModule } | Should -Throw -ExpectedMessage $mockLocalizedString + { Import-SqlDscPreferredModule } | Should -Throw -ExpectedMessage $mockErrorMessage - Should -Invoke -CommandName Get-Module -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Get-Module -Exactly -Times 0 -Scope It Should -Invoke -CommandName Get-SqlDscPreferredModule -Exactly -Times 1 -Scope It Should -Invoke -CommandName Push-Location -Exactly -Times 0 -Scope It Should -Invoke -CommandName Pop-Location -Exactly -Times 0 -Scope It Should -Invoke -CommandName Import-Module -Exactly -Times 0 -Scope It } } + + Context 'When forcibly importing a specific preferred module but only SQLPS is available' { + BeforeAll { + Mock -CommandName Import-Module -MockWith $mockImportModule + Mock -CommandName Get-Module + Mock -CommandName Remove-Module + Mock -CommandName Get-SqlDscPreferredModule -MockWith { + return 'SQLPS' + } + + $mockExpectedModuleNameToImport = 'SQLPS' + } + + It 'Should import the SqlServer module without throwing' { + { Import-SqlDscPreferredModule -Name 'OtherModule' -Force } | Should -Not -Throw + + Should -Invoke -CommandName Get-SqlDscPreferredModule -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Get-Module -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Remove-Module -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Push-Location -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Pop-Location -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Import-Module -Exactly -Times 1 -Scope It + } + } }