Modules/M365DSCModuleMgmt.psm1
|
$Script:IsPowerShellCore = $PSVersionTable.PSEdition -eq 'Core' $Script:IsPsResourceGetAvailable = $null -ne (Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable) $Script:M365DSCDependenciesValidated = $false $Script:M365DSCGraphShimLoaded = $false <# .DESCRIPTION This function performs the one-time initialization of the M365DSC dependency and resource-settings metadata used throughout this module. It is wrapped in a function (rather than run as top-level module code) so that the scratch/intermediate variables it uses are released once it returns, instead of being pinned for the lifetime of the session as module-scope variables. .FUNCTIONALITY Internal #> function Initialize-M365DSCModuleMgmt { [CmdletBinding()] param() if ($null -eq $Script:M365DSCDependencies) { $Script:M365DSCDependencies = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new([System.StringComparer]::OrdinalIgnoreCase) $Script:M365DSCDevDependencies = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new([System.StringComparer]::OrdinalIgnoreCase) $dependencies = (Import-PowerShellDataFile "$PSScriptRoot/../Dependencies/Manifest.psd1").Dependencies $devDependencies = (Import-PowerShellDataFile "$PSScriptRoot/../Dependencies/DevManifest.psd1").Dependencies foreach ($dependency in $dependencies) { # TODO: Review again once ModuleFast can work with additional properties # https://github.com/microsoft/Microsoft365DSC/pull/6726 # https://github.com/ykuijs/M365DSC_CICD/issues/53 if ($dependency.ModuleName -eq 'PnP.PowerShell') { $dependency.DependsOn = @('Microsoft.Graph.Authentication') } $Script:M365DSCDependencies.Add($dependency.ModuleName, $dependency) } foreach ($devDependency in $devDependencies) { $Script:M365DSCDevDependencies.Add($devDependency.ModuleName, $devDependency) } $commandToModuleMap = @{} $Script:M365DSCResourceSettings = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($file in (Get-ChildItem -Path "$PSScriptRoot/../DscResources" -Filter 'settings.json' -Recurse -File)) { Write-Verbose -Message "Processing settings.json file at path: $($file.FullName)" $jsonContent = [System.IO.File]::ReadAllText($file.FullName) | ConvertFrom-Json foreach ($commandMap in ($jsonContent.commands | Where-Object -Property module -NotIn $Script:M365DSCDevDependencies.Keys)) { $commandToModuleMap[$commandMap.module] += @($commandMap.cmdlets) } $directoryName = (Split-Path -Path $file.DirectoryName -Leaf).Replace('MSFT_', '') $Script:M365DSCResourceSettings.Add($directoryName, @{ requiredModules = $jsonContent.requiredModules | Where-Object { $_ -notin $Script:M365DSCDevDependencies.Keys } mode = $jsonContent.mode }) } Write-Verbose -Message "Loading current configuration from config.json" $Script:M365DSCValidatedDependencies = [System.Collections.Generic.List[System.String]]::new($Script:M365DSCDependencies.Count + $Script:M365DSCDevDependencies.Count) $configAsPsCustomObject = Get-Content -Path "$PSScriptRoot/../config.json" | ConvertFrom-Json $configAsHashtable = @{} foreach ($property in $configAsPsCustomObject.PSObject.Properties) { $configAsHashtable.Add($property.Name, $property.Value) } $Script:CurrentConfiguration = $configAsHashtable $globalRequiredModules = $Script:CurrentConfiguration.requiredModules foreach ($entry in $commandToModuleMap.GetEnumerator()) { $sortedFunctions = @($globalRequiredModules.$($entry.Key)) + @($entry.Value) | Sort-Object -Unique $Script:M365DSCDependencies[$entry.Key].Commands = $sortedFunctions } $Script:M365DSCRequiredModules = @($globalRequiredModules.psobject.Properties.Name) $Script:M365DSCRequiredModulesLoaded = $false } } Initialize-M365DSCModuleMgmt <# .SYNOPSIS Returns resource settings metadata loaded by module management. .DESCRIPTION Returns the in-memory dictionary of resource settings built from each resource settings.json file. #> function Get-M365DSCResourceSettings { [CmdletBinding()] param() return $Script:M365DSCResourceSettings } <# .SYNOPSIS Returns globally required module names for Microsoft365DSC. .DESCRIPTION Returns the module names defined as required in the module configuration. #> function Get-M365DSCRequiredModules { [CmdletBinding()] param() return $Script:M365DSCRequiredModules } <# .SYNOPSIS Sets the required-modules-loaded state flag. .DESCRIPTION Updates the module-scope flag used to track whether required modules were loaded in the current session. .PARAMETER Value Specifies the new loaded state value. #> function Set-M365DSCRequiredModulesLoaded { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.Boolean]$Value ) $Script:M365DSCRequiredModulesLoaded = $Value } <# .SYNOPSIS Returns whether required modules are marked as loaded. .DESCRIPTION Returns the module-scope boolean state indicating whether required modules were loaded. #> function Test-IsM365DSCRequiredModulesLoaded { [CmdletBinding()] param() return $Script:M365DSCRequiredModulesLoaded } <# .SYNOPSIS Returns the current Microsoft365DSC module configuration. .DESCRIPTION Returns a cloned hashtable of the loaded module configuration values. .OUTPUTS System.Collections.Hashtable #> function Get-M365DSCModuleConfiguration { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param() return $Script:CurrentConfiguration.Clone() } <# .SYNOPSIS Updates a single Microsoft365DSC module configuration value. .DESCRIPTION Sets a configuration entry in the current module configuration hashtable. .PARAMETER Key Specifies the configuration key to update. .PARAMETER Value Specifies the value to assign to the configuration key. #> function Set-M365DSCModuleConfiguration { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Key, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [AllowEmptyString()] [AllowNull()] [System.Object] $Value ) $Script:CurrentConfiguration.$Key = $Value } <# .SYNOPSIS Validates that required Microsoft365DSC dependencies are installed. .DESCRIPTION Checks dependency health and throws when required module versions are missing. Validation can be skipped by session flags already used by Microsoft365DSC. .FUNCTIONALITY Internal #> function Confirm-M365DSCDependencies { [CmdletBinding()] param() if (-not $Script:M365DSCDependenciesValidated -and ($null -eq $Global:M365DSCSkipDependenciesValidation -or -not $Global:M365DSCSkipDependenciesValidation)) { Write-Verbose -Message 'Dependencies were not already validated.' Test-CodePage $result = Update-M365DSCDependencies -ValidateOnly if ($result.Length -gt 0) { $ErrorMessage = "The following dependencies need updating:`r`n" foreach ($invalidDependency in $result) { $ErrorMessage += ' * ' + $invalidDependency.ModuleName + "`r`n" } $ErrorMessage += 'Please run Update-M365DSCDependencies as Administrator. ' $Script:M365DSCDependenciesValidated = $false Add-M365DSCEvent -Message $ErrorMessage -EntryType 'Error' ` -EventID 1 -Source $($MyInvocation.MyCommand.Source) ` -TenantId $tenantIdValue throw $ErrorMessage } else { Write-Verbose -Message 'Dependencies were all successfully validated.' $Script:M365DSCDependenciesValidated = $true } } else { Write-Verbose -Message 'Dependencies were already successfully validated.' } } <# .SYNOPSIS Ensures a dependency module is loaded at the required version. .DESCRIPTION Loads dependency modules on demand, validates versions, and recursively validates dependency chains. For Graph typed modules, it applies the Microsoft365DSC Graph shim behavior. .PARAMETER ModuleName Specifies the dependency module name to validate and load. .EXAMPLE PS> Confirm-M365DSCLoadedModule -ModuleName 'Microsoft.Graph.Authentication' .FUNCTIONALITY Internal #> function Confirm-M365DSCLoadedModule { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.String] $ModuleName ) if ($Script:M365DSCValidatedDependencies.Contains($ModuleName)) { Write-Verbose -Message "Module '$ModuleName' has already been validated." return } # Graph Shim intercept: Replace typed Graph SDK # sub-modules with the lightweight M365DSCGraphShim module that wraps # Invoke-MgGraphRequest. Microsoft.Graph.Authentication is always loaded # natively because it provides Connect-MgGraph and the underlying HTTP client. if ($ModuleName -like 'Microsoft.Graph.*' -and $ModuleName -ne 'Microsoft.Graph.Authentication') { if (-not $Script:M365DSCGraphShimLoaded) { Write-Verbose -Message "Graph Shim enabled: importing M365DSCGraphShim instead of '$ModuleName'." # Ensure Microsoft.Graph.Authentication is loaded first Confirm-M365DSCLoadedModule -ModuleName 'Microsoft.Graph.Authentication' Import-Module -Name "$PSScriptRoot/M365DSCGraphShim.psd1" -Global -Force -DisableNameChecking -Function * -Cmdlet @() -Variable @() -Alias @() $Script:M365DSCGraphShimLoaded = $true } else { Write-Verbose -Message "Graph Shim already loaded, skipping import for '$ModuleName'." } $Script:M365DSCValidatedDependencies.Add($ModuleName) return } $manifestModule = $Script:M365DSCDependencies[$ModuleName] if ($null -ne $manifestModule.DependsOn -and $manifestModule.DependsOn.Count -gt 0) { foreach ($dependency in $manifestModule.DependsOn) { Write-Verbose -Message "Validating dependency '$dependency' for module '$ModuleName'." Confirm-M365DSCLoadedModule -ModuleName $dependency } } $loadedModule = Get-Module -Name $ModuleName if ($null -eq $loadedModule) { Write-Verbose -Message "Module '$ModuleName' is not loaded. Importing it now." $importModuleSplat = @{ Name = $ModuleName RequiredVersion = $manifestModule.RequiredVersion Global = $true Alias = @() Cmdlet = @() Variable = @() DisableNameChecking = $true } if ($manifestModule.Commands.Count -gt 0) { $importModuleSplat.Add('Function', $manifestModule.Commands) $importModuleSplat.Cmdlet = $manifestModule.Commands } if ($ModuleName -eq 'PnP.PowerShell' -and $manifestModule.RequiredVersion -eq '1.12.0' -and $Script:IsPowerShellCore) { $importModuleSplat.Add('UseWindowsPowerShell', $true) if ($importModuleSplat.ContainsKey('Function')) { $importModuleSplat.Remove('Function') } if ($importModuleSplat.ContainsKey('Cmdlet')) { $importModuleSplat.Remove('Cmdlet') } if ($importModuleSplat.ContainsKey('Alias')) { $importModuleSplat.Remove('Alias') } if ($importModuleSplat.ContainsKey('Variable')) { $importModuleSplat.Remove('Variable') } } Import-Module @importModuleSplat Write-Verbose -Message "Module '$ModuleName' with version '$($manifestModule.RequiredVersion)' has been imported." } elseif ($loadedModule.Version -ne $manifestModule.RequiredVersion) { Write-Verbose -Message "Module '$ModuleName' is loaded but the version '$($loadedModule.Version)' does not match the required version '$($manifestModule.RequiredVersion)'." Remove-Module -Name $ModuleName -Force -ErrorAction SilentlyContinue Write-Verbose -Message "Unloaded module '$ModuleName' with version '$($loadedModule.Version)'." Import-Module -Name $ModuleName -RequiredVersion $manifestModule.RequiredVersion -Global -Alias @() -Cmdlet @() -Variable @() -DisableNameChecking Write-Verbose -Message "Re-imported module '$ModuleName' with version '$($manifestModule.RequiredVersion)'." } else { Write-Verbose -Message "Module '$ModuleName' is already loaded." } if (-not $Script:M365DSCValidatedDependencies.Contains($ModuleName)) { $Script:M365DSCValidatedDependencies.Add($ModuleName) } } <# .SYNOPSIS Validates dependencies required by a DSC resource module. .DESCRIPTION Resolves required modules from resource settings and validates each dependency before resource execution. .PARAMETER ModuleName Specifies the DSC resource module name whose dependencies should be validated. .EXAMPLE PS> Confirm-M365DSCModuleDependency -ModuleName 'MSFT_AADApplication' .FUNCTIONALITY Internal #> function Confirm-M365DSCModuleDependency { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.String] $ModuleName ) $Global:MaximumFunctionCount = 32767 if ($Global:IsTestEnvironment -or (Get-M365DSCModuleConfiguration).skipModuleDependencyValidation) { Write-Verbose -Message "Skipping module dependency validation in test environment for module '$ModuleName'." return } $modulesToCheck = $Script:M365DSCResourceSettings[$ModuleName.Replace('MSFT_', '')].requiredModules foreach ($module in $modulesToCheck) { Write-Verbose -Message "Validating module dependency: $($module)" Confirm-M365DSCLoadedModule -ModuleName $module } Write-Verbose -Message "All dependencies for module '$ModuleName' have been validated." } <# .SYNOPSIS Checks whether newer versions exist for configured dependencies. .DESCRIPTION Queries the gallery for each configured dependency and reports modules with newer available versions. .EXAMPLE PS> Test-M365DSCDependenciesForNewVersions .FUNCTIONALITY Public #> function Test-M365DSCDependenciesForNewVersions { [CmdletBinding()] param () $i = 1 Import-Module PowerShellGet -Force foreach ($dependency in $Script:M365DSCDependencies.Values.GetEnumerator()) { Write-Progress -Activity 'Scanning Dependencies' -PercentComplete ($i / $Script:M365DSCDependencies.Count * 100) try { $moduleInGallery = Find-Module $dependency.ModuleName [array]$moduleInstalled = Get-Module $dependency.ModuleName -ListAvailable | Select-Object Version if ($moduleInstalled) { $modules = $moduleInstalled | Sort-Object Version -Descending } $moduleInstalled = $modules[0] if (-not $modules -or [Version]($moduleInGallery.Version) -gt [Version]($moduleInstalled[0].Version)) { Write-Host "New version of {$($dependency.ModuleName)} is available {$($moduleInGallery.Version)}" } } catch { Write-Host $_ Write-Host "New version of {$($dependency.ModuleName)} is available" } $i++ } # The progress bar seems to hang sometimes. Make sure it is no longer displayed. Write-Progress -Activity 'Scanning Dependencies' -Completed } <# .SYNOPSIS Validates the installed Microsoft365DSC module version. .DESCRIPTION Compares local and gallery module versions and reports when a newer Microsoft365DSC module is available. .EXAMPLE PS> Test-M365DSCModuleValidity .FUNCTIONALITY Public #> function Test-M365DSCModuleValidity { [CmdletBinding()] param() if ($Script:IsM365DSCModuleValidated) { Write-Verbose -Message 'The Microsoft365DSC module has already been validated in this session.' Write-Verbose -Message 'If you have updated the module, please restart your PowerShell session to re-validate.' return } if ($env:AZUREPS_HOST_ENVIRONMENT -like 'AzureAutomation*') { $message = 'Skipping check for newer version of Microsoft365DSC due to Azure Automation Environment restrictions.' Write-Verbose -Message $message return } # Validate if only one installation of the module is present and that it's the latest version available if ($Script:IsPsResourceGetAvailable) { $latestVersion = (Find-PSResource -Name 'Microsoft365DSC' -Repository 'PSGallery').Version | Sort-Object -Descending | Select-Object -First 1 } else { $latestVersion = (Find-Module -Name 'Microsoft365DSC' -Includes 'DSCResource').Version } $localVersion = (Get-Module -Name 'Microsoft365DSC').Version if ($latestVersion -gt $localVersion) { Write-Host "There is a newer version of the 'Microsoft365DSC' module available on the gallery." Write-Host "To update the module and it's dependencies, run the following command:" Write-Host 'Update-M365DSCModule' -ForegroundColor Blue } $Script:IsM365DSCModuleValidated = $true } <# .SYNOPSIS Removes outdated Microsoft365DSC module and dependency versions. .DESCRIPTION Scans installed module versions and removes outdated Microsoft365DSC and dependency versions while preserving required versions. .EXAMPLE PS> Uninstall-M365DSCOutdatedDependencies .FUNCTIONALITY Public #> function Uninstall-M365DSCOutdatedDependencies { [CmdletBinding()] param() try { $InformationPreference = 'Continue' [array]$microsoft365DscModules = Get-Module Microsoft365DSC -ListAvailable $outdatedMicrosoft365DscModules = $microsoft365DscModules | Sort-Object -Property Version | Select-Object -SkipLast 1 foreach ($module in $outdatedMicrosoft365DscModules) { try { Write-Information -MessageData "Uninstalling $($module.Name) Version {$($module.Version)}" if (Test-Path -Path $($module.Path)) { Remove-Item $($module.ModuleBase) -Force -Recurse -ErrorAction Stop } } catch { $message = "Could not uninstall $($module.Name) Version $($module.Version)" if ($_.Exception.Message -like "*Access to the path* is denied*" -and ($Scope -eq "AllUsers") -and ($PSEdition -eq 'Desktop' -or $IsWindows) -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { $message += ' You need to run this command as a local administrator.' } New-M365DSCLogEntry -Message $message ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) Write-Error -Message $message -ErrorAction Continue } } $allDependenciesExceptAuth = $Script:M365DSCDependencies.Values.GetEnumerator().Where({ $_.ModuleName -ne 'Microsoft.Graph.Authentication' }) $i = 1 foreach ($dependency in $allDependenciesExceptAuth) { Write-Progress -Activity 'Scanning Dependencies' -PercentComplete ($i / $allDependenciesExceptAuth.Count * 100) try { if ($dependency.PowerShellCore -and -not $Script:IsPowerShellCore) { Write-Verbose -Message "Skipping module {$($dependency.ModuleName)} as it is managed by PowerShell Core." continue } elseif ($dependency.PowerShellCore -eq $false -and $Script:IsPowerShellCore) { Write-Verbose -Message "Skipping module {$($dependency.ModuleName)} as it is managed by Windows PowerShell." continue } $found = Get-Module $dependency.ModuleName -ListAvailable | Where-Object -Property Version -NE $dependency.RequiredVersion foreach ($foundModule in $found) { try { Write-Information -MessageData "Uninstalling $($foundModule.Name) Version {$($foundModule.Version)}" if (Test-Path -Path $($foundModule.Path)) { Remove-Item $($foundModule.ModuleBase) -Force -Recurse -ErrorAction Stop } } catch { $message = "Could not uninstall $($foundModule.Name) Version $($foundModule.Version)" if ($_.Exception.Message -like "*Access to the path* is denied*" -and ($PSEdition -eq 'Desktop' -or $IsWindows) -and ($Scope -eq "AllUsers") -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { $message += ' You need to run this command as a local administrator.' } New-M365DSCLogEntry -Message $message ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) Write-Error -Message $message -ErrorAction Continue } } } catch { Write-Error -Message "Could not uninstall {$($dependency.ModuleName)}" -ErrorAction Continue } $i++ } } catch { New-M365DSCLogEntry -Message 'Error uninstalling outdated dependencies:' ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) Write-Error $_ } $authModule = $Script:M365DSCDependencies['Microsoft.Graph.Authentication'] try { Write-Information -MessageData 'Checking Microsoft.Graph.Authentication' $found = Get-Module $authModule.ModuleName -ListAvailable | Where-Object -Property Version -NE $authModule.RequiredVersion foreach ($foundModule in $found) { try { Write-Information -MessageData "Uninstalling $($foundModule.Name) version {$($foundModule.Version)}" if (Test-Path -Path $($foundModule.Path)) { Remove-Item $($foundModule.ModuleBase) -Force -Recurse -ErrorAction Stop } } catch { $message = "Could not uninstall $($foundModule.Name) Version $($foundModule.Version)" if ($_.Exception.Message -like "*Access to the path* is denied*" -and ($PSEdition -eq 'Desktop' -or $IsWindows) -and ($Scope -eq "AllUsers") -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { $message += ' You need to run this command as a local administrator.' } New-M365DSCLogEntry -Message $message ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) Write-Error -Message $message -ErrorAction Continue } } } catch { Write-Error -Message "Could not uninstall {$($dependency.ModuleName)}" -ErrorAction Continue } } <# .SYNOPSIS Installs or validates Microsoft365DSC dependencies. .DESCRIPTION Validates, installs, or force-refreshes dependency modules according to manifest requirements and selected installation options. .PARAMETER Force Indicates that dependencies should be reinstalled even when required versions are present. .PARAMETER ValidateOnly Indicates that only validation should run and missing dependencies should be returned. .PARAMETER Scope Specifies the installation scope for dependency modules. .PARAMETER Proxy Specifies the proxy server used for module installation requests. .PARAMETER Repository Specifies the repository used to install dependencies. .PARAMETER UsePowerShellGet Indicates that Install-Module should be used instead of Install-PSResource. .PARAMETER Development Indicates that development dependencies should also be processed. .EXAMPLE PS> Update-M365DSCDependencies .EXAMPLE PS> Update-M365DSCDependencies -Force .EXAMPLE PS> Update-M365DSCDependencies -Scope CurrentUser .FUNCTIONALITY Public #> function Update-M365DSCDependencies { [CmdletBinding()] param ( [Parameter()] [Switch] $Force, [Parameter()] [Switch] $ValidateOnly, [Parameter()] [ValidateSet("CurrentUser", "AllUsers")] $Scope = "AllUsers", [Parameter()] [System.String] $Proxy, [Parameter()] [System.String] $Repository = 'PSGallery', [Parameter()] [switch] $UsePowerShellGet, [Parameter()] [Switch] $Development ) try { $InformationPreference = 'Continue' $i = 1 $returnValue = @() $params = @{} if (-not [System.String]::IsNullOrEmpty($Proxy)) { $params.Add('Proxy', $Proxy) } # Check if PSResourceGet is installed or not if (-not $Script:IsPsResourceGetAvailable) { Write-Warning -Message 'Microsoft.PowerShell.PSResourceGet is not installed, installing it now...' try { Install-Module -Name Microsoft.PowerShell.PSResourceGet -Scope $Scope -AllowClobber @params -Force -ErrorAction Stop -Repository PSGallery $Script:IsPsResourceGetAvailable = $true } catch { Write-Warning -Message "Failed to install Microsoft.PowerShell.PSResourceGet, continuing without it..." } } $scopedIsPsResourceGetAvailable = $Script:IsPsResourceGetAvailable if ($params.ContainsKey('Proxy')) { Write-Information -MessageData "Falling back to Install-Module because Install-PSResource does not support a proxy" $scopedIsPsResourceGetAvailable = $false } $dependencies = [System.Object[]]::new($Script:M365DSCDependencies.Count + $Script:M365DSCDevDependencies.Count) $Script:M365DSCDependencies.Values.CopyTo($dependencies, 0) if ($Development) { $Script:M365DSCDevDependencies.Values.CopyTo($dependencies, $Script:M365DSCDependencies.Count) } # $null comparison is correct in that way because the left-hand side is always an array and the right-hand side is a single value foreach ($dependency in ($dependencies -ne $null)) { Write-Progress -Activity 'Scanning dependencies' -PercentComplete ($i / $dependencies.Count * 100) try { if (-not $Force) { if ($dependency.PowerShellCore -and -not $Script:IsPowerShellCore) { Write-Verbose -Message "The dependency {$($dependency.ModuleName)} requires PowerShell Core. Skipping." continue } elseif ($dependency.PowerShellCore -eq $false -and $Script:IsPowerShellCore -and $IsWindows) { Write-Verbose -Message "The dependency {$($dependency.ModuleName)} requires Windows PowerShell. Skipping." continue } $found = Get-Module $dependency.ModuleName -ListAvailable | Where-Object -Property Version -EQ $dependency.RequiredVersion } if ((-not $found -or $Force) -and -not $ValidateOnly) { $errorFound = $false try { if (($PSEdition -eq 'Desktop' -or $IsWindows) -and (-not(([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) -and ($Scope -eq "AllUsers")) { Write-Error 'Cannot update the dependencies for Microsoft365DSC. You need to run this command as a local administrator.' $errorFound = $true } } catch { Write-Verbose -Message "Couldn't retrieve Windows Principal. One possible cause is that the current environment is not a Windows OS." } if (-not $errorFound) { if (-not $dependency.PowerShellCore -and $Script:IsPowerShellCore -and $IsWindows) { Write-Warning "The dependency {$($dependency.ModuleName)} does not support PowerShell Core. Please run Update-M365DSCDependencies in Windows PowerShell." continue } elseif ($dependency.PowerShellCore -and -not $Script:IsPowerShellCore) { Write-Warning "The dependency {$($dependency.ModuleName)} requires PowerShell Core. Please run Update-M365DSCDependencies in PowerShell Core." continue } Remove-Module $dependency.ModuleName -Force -ErrorAction SilentlyContinue if ($dependency.ModuleName -like 'Microsoft.Graph*') { Remove-Module 'Microsoft.Graph.Authentication' -Force -ErrorAction SilentlyContinue } Remove-Module $dependency.ModuleName -Force -ErrorAction SilentlyContinue if ($scopedIsPsResourceGetAvailable -and -not $UsePowerShellGet) { Write-Information -MessageData "Using Install-PSResource to install $($dependency.ModuleName) with version {$($dependency.RequiredVersion)}" Install-PSResource -Name $dependency.ModuleName -Version $dependency.RequiredVersion -Scope $Scope -AcceptLicense -SkipDependencyCheck -TrustRepository -Repository $Repository } else { Write-Information -MessageData "Using Install-Module to install $($dependency.ModuleName) with version {$($dependency.RequiredVersion)}" Install-Module $dependency.ModuleName -RequiredVersion $dependency.RequiredVersion -AllowClobber -Force -Scope "$Scope" @Params -Repository $Repository } } } if ($dependency.ExplicitLoading) { Remove-Module $dependency.ModuleName -Force -ErrorAction SilentlyContinue if ($dependency.Prefix) { Import-Module $dependency.ModuleName -Global -Prefix $dependency.Prefix -Force -DisableNameChecking } else { Import-Module $dependency.ModuleName -Global -Force -Alias @() -Cmdlet @() -Variable @() -DisableNameChecking } } if (-not $found -and $validateOnly) { $returnValue += $dependency } } catch { Write-Error -Message "Could not update or import {$($dependency.ModuleName)}: $($_.Exception.Message)" -ErrorAction Continue } $i++ } # The progress bar seems to hang sometimes. Make sure it is no longer displayed. Write-Progress -Activity 'Scanning dependencies' -Completed if ($ValidateOnly) { return $returnValue } } catch { New-M365DSCLogEntry -Message 'Error updating dependencies:' ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) Write-Error $_ -ErrorAction Continue } } <# .SYNOPSIS Updates Microsoft365DSC and refreshes dependency state. .DESCRIPTION Updates the Microsoft365DSC module, reloads the latest installed version, updates dependencies, and optionally removes outdated versions. .PARAMETER Scope Specifies the installation scope used for update operations. .PARAMETER Proxy Specifies the proxy server used for update operations. .PARAMETER BaseRepository Specifies the repository used to update the Microsoft365DSC module. .PARAMETER DependencyRepository Specifies the repository used to update dependencies. .PARAMETER NoUninstall Indicates that outdated module and dependency versions should not be removed. .EXAMPLE PS> Update-M365DSCModule .EXAMPLE PS> Update-M365DSCModule -Scope CurrentUser .EXAMPLE PS> Update-M365DSCModule -Scope AllUsers .FUNCTIONALITY Public #> function Update-M365DSCModule { [CmdletBinding()] param( [Parameter()] [ValidateSet("CurrentUser", "AllUsers")] $Scope = "AllUsers", [Parameter()] [System.String] $Proxy, [Parameter()] [System.String] $BaseRepository = 'PSGallery', [Parameter()] [System.String] $DependencyRepository = 'PSGallery', [Parameter()] [switch] $NoUninstall ) $params = @{} $unloadModule = $true if (-not [System.String]::IsNullOrEmpty($proxy)) { $params.Add('Proxy', $Proxy) } try { Update-Module -Name 'Microsoft365DSC' @Params -ErrorAction Stop } catch { if ($_.Exception.Message -like "*Module 'Microsoft365DSC' was not installed by using Install-Module*") { Write-Verbose -Message "The Microsoft365DSC module might have been installed with Install-PSResource" if ($Script:IsPsResourceGetAvailable) { Write-Verbose -Message "Updating the Microsoft365DSC module using Update-PSResource..." try { Update-PSResource -Name 'Microsoft365DSC' -Scope $Scope ` -TrustRepository -AcceptLicense -SkipDependencyCheck ` -Repository $BaseRepository -ErrorAction Stop } catch { if ($_.Exception.Message -like "*No installed packages*") { Write-Verbose -Message "Microsoft365DSC was neither installed using Install-Module nor Install-PSResource. Trying to install it now..." Install-PSResource -Name 'Microsoft365DSC' -Scope $Scope ` -TrustRepository -AcceptLicense -SkipDependencyCheck ` -Repository $BaseRepository -ErrorAction Stop } else { New-M365DSCLogEntry -Message 'Error Updating Module:' ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) throw } } } } elseif ($_.Exception.Message -like "*was not updated because no valid module was found*") { Write-Verbose -Message "No valid module was found to update." $unloadModule = $false } } try { if ($unloadModule) { Write-Verbose -Message "Unloading all instances of the Microsoft365DSC module from the current PowerShell session." Remove-Module Microsoft365DSC -Force } Write-Verbose -Message "Retrieving all versions of the Microsoft365DSC installed on the machine." [Array]$instances = Get-Module Microsoft365DSC -ListAvailable | Sort-Object -Property Version -Descending if ($instances.Length -gt 0) { Write-Verbose -Message "Loading version {$($instances[0].Version.ToString())} of the Microsoft365DSC module from {$($instances[0].ModuleBase)}" Import-Module Microsoft365DSC -RequiredVersion $instances[0].Version.ToString() -Force } } catch { New-M365DSCLogEntry -Message 'Error Updating Module:' ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) throw $_ } Update-M365DSCDependencies -Scope $Scope -Proxy $Proxy -Repository $DependencyRepository if (-not $NoUninstall) { Uninstall-M365DSCOutdatedDependencies } } Export-ModuleMember -Function @( 'Confirm-M365DSCDependencies', 'Confirm-M365DSCLoadedModule', 'Confirm-M365DSCModuleDependency', 'Get-M365DSCModuleConfiguration', 'Get-M365DSCRequiredModules', 'Get-M365DSCResourceSettings', 'Set-M365DSCModuleConfiguration', 'Set-M365DSCRequiredModulesLoaded', 'Test-IsM365DSCRequiredModulesLoaded', 'Test-M365DSCDependenciesForNewVersions', 'Test-M365DSCModuleValidity', 'Uninstall-M365DSCOutdatedDependencies', 'Update-M365DSCDependencies', 'Update-M365DSCModule' ) |