diff --git a/.vscode/settings.json b/.vscode/settings.json index 95742eed..fc1de5a9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,6 @@ "schemas/2024/04/bundled/config/document.vscode.json": "**.dsc.{yaml,yml,config.yaml,config.yml}", "schemas/2024/04/bundled/resource/manifest.vscode.json": "**.dsc.resource.{yaml,yml}" }, - "sarif-viewer.connectToGithubCodeScanning": "off" + "sarif-viewer.connectToGithubCodeScanning": "off", + "powershell.codeFormatting.preset": "Allman" } \ No newline at end of file diff --git a/operation-methods b/operation-methods new file mode 160000 index 00000000..3f42f5d1 --- /dev/null +++ b/operation-methods @@ -0,0 +1 @@ +Subproject commit 3f42f5d1ccf410d618229a12e185dc74cb6445fb diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/.gitattributes b/powershell-helpers/Microsoft.PowerShell.DSC/.gitattributes new file mode 100644 index 00000000..96c2e0d3 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/.gitattributes @@ -0,0 +1,15 @@ +# Needed for publishing of examples, build worker defaults to core.autocrlf=input. +* text eol=autocrlf + +*.mof text eol=crlf +*.sh text eol=lf +*.svg eol=lf + +# Ensure any exe files are treated as binary +*.exe binary +*.jpg binary +*.xl* binary +*.pfx binary +*.png binary +*.dll binary +*.so binary diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/.gitignore b/powershell-helpers/Microsoft.PowerShell.DSC/.gitignore new file mode 100644 index 00000000..17c483df --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/.gitignore @@ -0,0 +1,17 @@ +output/ + +**.bak +*.local.* +!**/README.md +.kitchen/ + +*.nupkg +*.suo +*.user +*.coverage +.vs +.psproj +.sln +markdownissues.txt +node_modules +package-lock.json diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/.vscode/tasks.json b/powershell-helpers/Microsoft.PowerShell.DSC/.vscode/tasks.json new file mode 100644 index 00000000..29911402 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/.vscode/tasks.json @@ -0,0 +1,125 @@ +{ + "version": "2.0.0", + "_runner": "terminal", + "windows": { + "options": { + "shell": { + "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command" + ] + } + } + }, + "linux": { + "options": { + "shell": { + "executable": "/usr/bin/pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + } + }, + "osx": { + "options": { + "shell": { + "executable": "/usr/local/bin/pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + } + }, + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "&${cwd}/build.ps1", + "args": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "new", + "clear": false + }, + "runOptions": { + "runOn": "default" + }, + "problemMatcher": [ + { + "owner": "powershell", + "fileLocation": [ + "absolute" + ], + "severity": "error", + "pattern": [ + { + "regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$", + "message": 1 + }, + { + "regexp": "(.*)", + "code": 1 + }, + { + "regexp": "" + }, + { + "regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*", + "file": 1, + "line": 2 + } + ] + } + ] + }, + { + "label": "test", + "type": "shell", + "command": "&${cwd}/build.ps1", + "args": ["-AutoRestore","-Tasks","test"], + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher": [ + { + "owner": "powershell", + "fileLocation": [ + "absolute" + ], + "severity": "error", + "pattern": [ + { + "regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$", + "message": 1 + }, + { + "regexp": "(.*)", + "code": 1 + }, + { + "regexp": "" + }, + { + "regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*", + "file": 1, + "line": 2 + } + ] + } + ] + } + ] +} diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/CHANGELOG.md b/powershell-helpers/Microsoft.PowerShell.DSC/CHANGELOG.md new file mode 100644 index 00000000..86aadfc3 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog for Microsoft.PowerShell.DSC + +The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- For new features. + +### Changed + +- For changes in existing functionality. + +### Deprecated + +- For soon-to-be removed features. + +### Removed + +- For now removed features. + +### Fixed + +- For any bug fix. + +### Security + +- In case of vulnerabilities. + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/GitVersion.yml b/powershell-helpers/Microsoft.PowerShell.DSC/GitVersion.yml new file mode 100644 index 00000000..8a1d5ff0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/GitVersion.yml @@ -0,0 +1,40 @@ +mode: ContinuousDelivery +next-version: 0.0.1 +major-version-bump-message: '(breaking\schange|breaking|major)\b' +minor-version-bump-message: '(adds?|features?|minor)\b' +patch-version-bump-message: '\s?(fix|patch)' +no-bump-message: '\+semver:\s?(none|skip)' +assembly-informational-format: '{NuGetVersionV2}+Sha.{Sha}.Date.{CommitDate}' +branches: + master: + tag: preview + regex: ^main$ + pull-request: + tag: PR + feature: + tag: useBranchName + increment: Minor + regex: f(eature(s)?)?[\/-] + source-branches: ['master'] + hotfix: + tag: fix + increment: Patch + regex: (hot)?fix(es)?[\/-] + source-branches: ['master'] + +ignore: + sha: [] +merge-message-formats: {} + + +# feature: +# tag: useBranchName +# increment: Minor +# regex: f(eature(s)?)?[/-] +# source-branches: ['master'] +# hotfix: +# tag: fix +# increment: Patch +# regex: (hot)?fix(es)?[/-] +# source-branches: ['master'] + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/README.md b/powershell-helpers/Microsoft.PowerShell.DSC/README.md new file mode 100644 index 00000000..242d91fe --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/README.md @@ -0,0 +1,13 @@ +# Microsoft.PowerShell.DSC + +PowerShell Desired State Configuration Module for working with `dsc.exe` + +## Make it yours + +--- +Generated with Plaster and the SampleModule template + + +This is a sample Readme + +## Make it yours diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/RequiredModules.psd1 b/powershell-helpers/Microsoft.PowerShell.DSC/RequiredModules.psd1 new file mode 100644 index 00000000..1f6fa0bf --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/RequiredModules.psd1 @@ -0,0 +1,20 @@ +@{ + PSDependOptions = @{ + AddToPath = $true + Target = 'output\RequiredModules' + Parameters = @{ + Repository = 'PSGallery' + } + } + + InvokeBuild = 'latest' + PSScriptAnalyzer = 'latest' + Pester = 'latest' + ModuleBuilder = 'latest' + ChangelogManagement = 'latest' + Sampler = 'latest' + 'Sampler.GitHubTasks' = 'latest' + Yayaml = 'latest' + +} + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/Resolve-Dependency.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/Resolve-Dependency.ps1 new file mode 100644 index 00000000..260b21b1 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/Resolve-Dependency.ps1 @@ -0,0 +1,919 @@ +<# + .DESCRIPTION + Bootstrap script for PSDepend. + + .PARAMETER DependencyFile + Specifies the configuration file for the this script. The default value is + 'RequiredModules.psd1' relative to this script's path. + + .PARAMETER PSDependTarget + Path for PSDepend to be bootstrapped and save other dependencies. + Can also be CurrentUser or AllUsers if you wish to install the modules in + such scope. The default value is 'output/RequiredModules' relative to + this script's path. + + .PARAMETER Proxy + Specifies the URI to use for Proxy when attempting to bootstrap + PackageProvider and PowerShellGet. + + .PARAMETER ProxyCredential + Specifies the credential to contact the Proxy when provided. + + .PARAMETER Scope + Specifies the scope to bootstrap the PackageProvider and PSGet if not available. + THe default value is 'CurrentUser'. + + .PARAMETER Gallery + Specifies the gallery to use when bootstrapping PackageProvider, PSGet and + when calling PSDepend (can be overridden in Dependency files). The default + value is 'PSGallery'. + + .PARAMETER GalleryCredential + Specifies the credentials to use with the Gallery specified above. + + .PARAMETER AllowOldPowerShellGetModule + Allow you to use a locally installed version of PowerShellGet older than + 1.6.0 (not recommended). Default it will install the latest PowerShellGet + if an older version than 2.0 is detected. + + .PARAMETER MinimumPSDependVersion + Allow you to specify a minimum version fo PSDepend, if you're after specific + features. + + .PARAMETER AllowPrerelease + Not yet written. + + .PARAMETER WithYAML + Not yet written. + + .PARAMETER UseModuleFast + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .PARAMETER PSResourceGet + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .NOTES + Load defaults for parameters values from Resolve-Dependency.psd1 if not + provided as parameter. +#> +[CmdletBinding()] +param +( + [Parameter()] + [System.String] + $DependencyFile = 'RequiredModules.psd1', + + [Parameter()] + [System.String] + $PSDependTarget = (Join-Path -Path $PSScriptRoot -ChildPath 'output/RequiredModules'), + + [Parameter()] + [System.Uri] + $Proxy, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ProxyCredential, + + [Parameter()] + [ValidateSet('CurrentUser', 'AllUsers')] + [System.String] + $Scope = 'CurrentUser', + + [Parameter()] + [System.String] + $Gallery = 'PSGallery', + + [Parameter()] + [System.Management.Automation.PSCredential] + $GalleryCredential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AllowOldPowerShellGetModule, + + [Parameter()] + [System.String] + $MinimumPSDependVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AllowPrerelease, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $WithYAML, + + [Parameter()] + [System.Collections.Hashtable] + $RegisterGallery, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseModuleFast, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePSResourceGet, + + [Parameter()] + [System.String] + $PSResourceGetVersion +) + +try +{ + if ($PSVersionTable.PSVersion.Major -le 5) + { + if (-not (Get-Command -Name 'Import-PowerShellDataFile' -ErrorAction 'SilentlyContinue')) + { + Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion '3.1.0.0' + } + } + + Write-Verbose -Message 'Importing Bootstrap default parameters from ''$PSScriptRoot/Resolve-Dependency.psd1''.' + + $resolveDependencyConfigPath = Join-Path -Path $PSScriptRoot -ChildPath '.\Resolve-Dependency.psd1' -Resolve -ErrorAction 'Stop' + + $resolveDependencyDefaults = Import-PowerShellDataFile -Path $resolveDependencyConfigPath + + $parameterToDefault = $MyInvocation.MyCommand.ParameterSets.Where{ $_.Name -eq $PSCmdlet.ParameterSetName }.Parameters.Keys + + if ($parameterToDefault.Count -eq 0) + { + $parameterToDefault = $MyInvocation.MyCommand.Parameters.Keys + } + + # Set the parameters available in the Parameter Set, or it's not possible to choose yet, so all parameters are an option. + foreach ($parameterName in $parameterToDefault) + { + if (-not $PSBoundParameters.Keys.Contains($parameterName) -and $resolveDependencyDefaults.ContainsKey($parameterName)) + { + Write-Verbose -Message "Setting parameter '$parameterName' to value '$($resolveDependencyDefaults[$parameterName])'." + + try + { + $variableValue = $resolveDependencyDefaults[$parameterName] + + if ($variableValue -is [System.String]) + { + $variableValue = $ExecutionContext.InvokeCommand.ExpandString($variableValue) + } + + $PSBoundParameters.Add($parameterName, $variableValue) + + Set-Variable -Name $parameterName -Value $variableValue -Force -ErrorAction 'SilentlyContinue' + } + catch + { + Write-Verbose -Message "Error adding default for $parameterName : $($_.Exception.Message)." + } + } + } +} +catch +{ + Write-Warning -Message "Error attempting to import Bootstrap's default parameters from '$resolveDependencyConfigPath': $($_.Exception.Message)." +} + +# Handles when both ModuleFast and PSResourceGet is configured or/and passed as parameter. +if ($UseModuleFast -and $UsePSResourceGet) +{ + Write-Information -MessageData 'Both ModuleFast and PSResourceGet is configured or/and passed as parameter.' -InformationAction 'Continue' + + if ($PSVersionTable.PSVersion -ge '7.2') + { + $UsePSResourceGet = $false + + Write-Information -MessageData 'PowerShell 7.2 or higher being used, prefer ModuleFast over PSResourceGet.' -InformationAction 'Continue' + } + else + { + $UseModuleFast = $false + + Write-Information -MessageData 'Older PowerShell or Windows PowerShell being used, prefer PSResourceGet since ModuleFast is not supported on this version of PowerShell.' -InformationAction 'Continue' + } +} + +if ($UseModuleFast) +{ + try + { + $invokeWebRequestParameters = @{ + Uri = 'bit.ly/modulefast' # cSpell: disable-line + ErrorAction = 'Stop' + } + + $moduleFastBootstrapScript = Invoke-WebRequest @invokeWebRequestParameters + + <# + Using this method instead of the one mentioned in the instructions from + https://github.com/JustinGrote/ModuleFast to avoid the PSScriptAnalyzer + rule PSAvoidUsingInvokeExpression. + #> + $moduleFastBootstrapScriptBlock = [ScriptBlock]::Create($moduleFastBootstrapScript) + + <# + We could pass parameters to the bootstrap script when calling Invoke(). + But currently the default parameter values works just fine. + #> + $moduleFastBootstrapScriptBlock.Invoke() + } + catch + { + Write-Warning -Message ('ModuleFast could not be bootstrapped. Reverting to PowerShellGet. Error: {0}' -f $_.Exception.Message) + + $UseModuleFast = $false + } +} + +if ($UsePSResourceGet) +{ + $psResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet' + + # If PSResourceGet was used prior it will be locked and we can't replace it. + if ((Test-Path -Path "$PSDependTarget/$psResourceGetModuleName" -PathType 'Container') -and (Get-Module -Name $psResourceGetModuleName)) + { + Write-Information -MessageData ('{0} is already saved and loaded into the session. To refresh the module open a new session and resolve dependencies again.' -f $psResourceGetModuleName) -InformationAction 'Continue' + } + else + { + Write-Debug -Message ('{0} do not exist, saving the module to RequiredModules.' -f $psResourceGetModuleName) + + $psResourceGetDownloaded = $false + + try + { + if (-not $PSResourceGetVersion) + { + # Default version to use if non is specified in parameter or in configuration. + $PSResourceGetVersion = '0.9.0-rc1' + } + + $invokeWebRequestParameters = @{ + # TODO: This should be hardcoded to a stable release in the future. + # TODO: Should support proxy parameters passed to the script. + Uri = "https://www.powershellgallery.com/api/v2/package/$psResourceGetModuleName/$PSResourceGetVersion" + OutFile = "$PSDependTarget/$psResourceGetModuleName.nupkg" # cSpell: ignore nupkg + ErrorAction = 'Stop' + } + + $previousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + # Bootstrapping Microsoft.PowerShell.PSResourceGet. + Invoke-WebRequest @invokeWebRequestParameters + + $ProgressPreference = $previousProgressPreference + + $psResourceGetDownloaded = $true + } + catch + { + Write-Warning -Message ('{0} could not be bootstrapped. Reverting to PowerShellGet. Error: {1}' -f $psResourceGetModuleName, $_.Exception.Message) + } + + $UsePSResourceGet = $false + + if ($psResourceGetDownloaded) + { + # On Windows PowerShell the command Expand-Archive do not like .nupkg as a zip archive extension. + $zipFileName = ((Split-Path -Path $invokeWebRequestParameters.OutFile -Leaf) -replace 'nupkg', 'zip') + + $renameItemParameters = @{ + Path = $invokeWebRequestParameters.OutFile + NewName = $zipFileName + Force = $true + } + + Rename-Item @renameItemParameters + + $psResourceGetZipArchivePath = Join-Path -Path (Split-Path -Path $invokeWebRequestParameters.OutFile -Parent) -ChildPath $zipFileName + + $expandArchiveParameters = @{ + Path = $psResourceGetZipArchivePath + DestinationPath = "$PSDependTarget/$psResourceGetModuleName" + Force = $true + } + + Expand-Archive @expandArchiveParameters + + Remove-Item -Path $psResourceGetZipArchivePath + + Import-Module -Name $expandArchiveParameters.DestinationPath -Force + + # Successfully bootstrapped PSResourceGet, so let's use it. + $UsePSResourceGet = $true + } + } + + if ($UsePSResourceGet) + { + $psResourceGetModule = Get-Module -Name $psResourceGetModuleName + + $psResourceGetModuleVersion = $psResourceGetModule.Version.ToString() + + if ($psResourceGetModule.PrivateData.PSData.Prerelease) + { + $psResourceGetModuleVersion += '-{0}' -f $psResourceGetModule.PrivateData.PSData.Prerelease + } + + Write-Information -MessageData ('Using {0} v{1}.' -f $psResourceGetModuleName, $psResourceGetModuleVersion) -InformationAction 'Continue' + + if ($UsePowerShellGetCompatibilityModule) + { + $savePowerShellGetParameters = @{ + Name = 'PowerShellGet' + Path = $PSDependTarget + Repository = 'PSGallery' + TrustRepository = $true + } + + if ($UsePowerShellGetCompatibilityModuleVersion) + { + $savePowerShellGetParameters.Version = $UsePowerShellGetCompatibilityModuleVersion + + # Check if the version is a prerelease. + if ($UsePowerShellGetCompatibilityModuleVersion -match '\d+\.\d+\.\d+-.*') + { + $savePowerShellGetParameters.Prerelease = $true + } + } + + Save-PSResource @savePowerShellGetParameters + + Import-Module -Name "$PSDependTarget/PowerShellGet" + } + } +} + +if (-not ($UseModuleFast -or $UsePSResourceGet)) +{ + if ($PSVersionTable.PSVersion.Major -le 5) + { + <# + Making sure the imported PackageManagement module is not from PS7 module + path. The VSCode PS extension is changing the $env:PSModulePath and + prioritize the PS7 path. This is an issue with PowerShellGet because + it loads an old version if available (or fail to load latest). + #> + Get-Module -ListAvailable PackageManagement | + Where-Object -Property 'ModuleBase' -NotMatch 'powershell.7' | + Select-Object -First 1 | + Import-Module -Force + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 0 -CurrentOperation 'NuGet Bootstrap' + + $importModuleParameters = @{ + Name = 'PowerShellGet' + MinimumVersion = '2.0' + MaximumVersion = '2.8.999' + ErrorAction = 'SilentlyContinue' + PassThru = $true + } + + if ($AllowOldPowerShellGetModule) + { + $importModuleParameters.Remove('MinimumVersion') + } + + $powerShellGetModule = Import-Module @importModuleParameters + + # Install the package provider if it is not available. + $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable -ErrorAction 'SilentlyContinue' | + Select-Object -First 1 + + if (-not $powerShellGetModule -and -not $nuGetProvider) + { + $providerBootstrapParameters = @{ + Name = 'NuGet' + Force = $true + ForceBootstrap = $true + ErrorAction = 'Stop' + Scope = $Scope + } + + switch ($PSBoundParameters.Keys) + { + 'Proxy' + { + $providerBootstrapParameters.Add('Proxy', $Proxy) + } + + 'ProxyCredential' + { + $providerBootstrapParameters.Add('ProxyCredential', $ProxyCredential) + } + + 'AllowPrerelease' + { + $providerBootstrapParameters.Add('AllowPrerelease', $AllowPrerelease) + } + } + + Write-Information -MessageData 'Bootstrap: Installing NuGet Package Provider from the web (Make sure Microsoft addresses/ranges are allowed).' + + $null = Install-PackageProvider @providerBootstrapParameters + + $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable | Select-Object -First 1 + + $nuGetProviderVersion = $nuGetProvider.Version.ToString() + + Write-Information -MessageData "Bootstrap: Importing NuGet Package Provider version $nuGetProviderVersion to current session." + + $Null = Import-PackageProvider -Name 'NuGet' -RequiredVersion $nuGetProviderVersion -Force + } + + if ($RegisterGallery) + { + if ($RegisterGallery.ContainsKey('Name') -and -not [System.String]::IsNullOrEmpty($RegisterGallery.Name)) + { + $Gallery = $RegisterGallery.Name + } + else + { + $RegisterGallery.Name = $Gallery + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 7 -CurrentOperation "Verifying private package repository '$Gallery'" -Completed + + $previousRegisteredRepository = Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue' + + if ($previousRegisteredRepository.SourceLocation -ne $RegisterGallery.SourceLocation) + { + if ($previousRegisteredRepository) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Re-registrering private package repository '$Gallery'" -Completed + + Unregister-PSRepository -Name $Gallery + + $unregisteredPreviousRepository = $true + } + else + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Registering private package repository '$Gallery'" -Completed + } + + Register-PSRepository @RegisterGallery + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 10 -CurrentOperation "Ensuring Gallery $Gallery is trusted" + + # Fail if the given PSGallery is not registered. + $previousGalleryInstallationPolicy = (Get-PSRepository -Name $Gallery -ErrorAction 'Stop').Trusted + + if ($previousGalleryInstallationPolicy -ne $true) + { + # Only change policy if the repository is not trusted + Set-PSRepository -Name $Gallery -InstallationPolicy 'Trusted' -ErrorAction 'Ignore' + } +} + +try +{ + if (-not ($UseModuleFast -or $UsePSResourceGet)) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 25 -CurrentOperation 'Checking PowerShellGet' + + # Ensure the module is loaded and retrieve the version you have. + $powerShellGetVersion = (Import-Module -Name 'PowerShellGet' -PassThru -ErrorAction 'SilentlyContinue').Version + + Write-Verbose -Message "Bootstrap: The PowerShellGet version is $powerShellGetVersion" + + # Versions below 2.0 are considered old, unreliable & not recommended + if (-not $powerShellGetVersion -or ($powerShellGetVersion -lt [System.Version] '2.0' -and -not $AllowOldPowerShellGetModule)) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 40 -CurrentOperation 'Fetching newer version of PowerShellGet' + + # PowerShellGet module not found, installing or saving it. + if ($PSDependTarget -in 'CurrentUser', 'AllUsers') + { + Write-Debug -Message "PowerShellGet module not found. Attempting to install from Gallery $Gallery." + + Write-Warning -Message "Installing PowerShellGet in $PSDependTarget Scope." + + $installPowerShellGetParameters = @{ + Name = 'PowerShellGet' + Force = $true + SkipPublisherCheck = $true + AllowClobber = $true + Scope = $Scope + Repository = $Gallery + MaximumVersion = '2.8.999' + } + + switch ($PSBoundParameters.Keys) + { + 'Proxy' + { + $installPowerShellGetParameters.Add('Proxy', $Proxy) + } + + 'ProxyCredential' + { + $installPowerShellGetParameters.Add('ProxyCredential', $ProxyCredential) + } + + 'GalleryCredential' + { + $installPowerShellGetParameters.Add('Credential', $GalleryCredential) + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation 'Installing newer version of PowerShellGet' + + Install-Module @installPowerShellGetParameters + } + else + { + Write-Debug -Message "PowerShellGet module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget" + + $saveModuleParameters = @{ + Name = 'PowerShellGet' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + MaximumVersion = '2.8.999' + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation "Saving PowerShellGet from $Gallery to $Scope" + + Save-Module @saveModuleParameters + } + + Write-Debug -Message 'Removing previous versions of PowerShellGet and PackageManagement from session' + + Get-Module -Name 'PowerShellGet' -All | Remove-Module -Force -ErrorAction 'SilentlyContinue' + Get-Module -Name 'PackageManagement' -All | Remove-Module -Force + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 65 -CurrentOperation 'Loading latest version of PowerShellGet' + + Write-Debug -Message 'Importing latest PowerShellGet and PackageManagement versions into session' + + if ($AllowOldPowerShellGetModule) + { + $powerShellGetModule = Import-Module -Name 'PowerShellGet' -Force -PassThru + } + else + { + Import-Module -Name 'PackageManagement' -MinimumVersion '1.4.8.1' -Force + + $powerShellGetModule = Import-Module -Name 'PowerShellGet' -MinimumVersion '2.2.5' -Force -PassThru + } + + $powerShellGetVersion = $powerShellGetModule.Version.ToString() + + Write-Information -MessageData "Bootstrap: PowerShellGet version loaded is $powerShellGetVersion" + } + + # Try to import the PSDepend module from the available modules. + $getModuleParameters = @{ + Name = 'PSDepend' + ListAvailable = $true + } + + $psDependModule = Get-Module @getModuleParameters + + if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + { + try + { + $psDependModule = $psDependModule | Where-Object -FilterScript { $_.Version -ge $MinimumPSDependVersion } + } + catch + { + throw ('There was a problem finding the minimum version of PSDepend. Error: {0}' -f $_) + } + } + + if (-not $psDependModule) + { + Write-Debug -Message 'PSDepend module not found.' + + # PSDepend module not found, installing or saving it. + if ($PSDependTarget -in 'CurrentUser', 'AllUsers') + { + Write-Debug -Message "Attempting to install from Gallery '$Gallery'." + + Write-Warning -Message "Installing PSDepend in $PSDependTarget Scope." + + $installPSDependParameters = @{ + Name = 'PSDepend' + Repository = $Gallery + Force = $true + Scope = $PSDependTarget + SkipPublisherCheck = $true + AllowClobber = $true + } + + if ($MinimumPSDependVersion) + { + $installPSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Installing PSDepend from $Gallery" + + Install-Module @installPSDependParameters + } + else + { + Write-Debug -Message "Attempting to Save from Gallery $Gallery to $PSDependTarget" + + $saveModuleParameters = @{ + Name = 'PSDepend' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + } + + if ($MinimumPSDependVersion) + { + $saveModuleParameters.add('MinimumVersion', $MinimumPSDependVersion) + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Saving PSDepend from $Gallery to $PSDependTarget" + + Save-Module @saveModuleParameters + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 80 -CurrentOperation 'Importing PSDepend' + + $importModulePSDependParameters = @{ + Name = 'PSDepend' + ErrorAction = 'Stop' + Force = $true + } + + if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + { + $importModulePSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) + } + + # We should have successfully bootstrapped PSDepend. Fail if not available. + $null = Import-Module @importModulePSDependParameters + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 81 -CurrentOperation 'Invoke PSDepend' + + if ($WithYAML) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 82 -CurrentOperation 'Verifying PowerShell module PowerShell-Yaml' + + if (-not (Get-Module -ListAvailable -Name 'PowerShell-Yaml')) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 85 -CurrentOperation 'Installing PowerShell module PowerShell-Yaml' + + Write-Verbose -Message "PowerShell-Yaml module not found. Attempting to Save from Gallery '$Gallery' to '$PSDependTarget'." + + $SaveModuleParam = @{ + Name = 'PowerShell-Yaml' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + } + + Save-Module @SaveModuleParam + } + else + { + Write-Verbose -Message 'PowerShell-Yaml is already available' + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 88 -CurrentOperation 'Importing PowerShell module PowerShell-Yaml' + } + } + + if (Test-Path -Path $DependencyFile) + { + if ($UseModuleFast -or $UsePSResourceGet) + { + $requiredModules = Import-PowerShellDataFile -Path $DependencyFile + + $requiredModules = $requiredModules.GetEnumerator() | + Where-Object -FilterScript { $_.Name -ne 'PSDependOptions' } + + $modulesToSave = @( + 'PSDepend' # Always include PSDepend for backward compatibility. + ) + + foreach ($requiredModule in $requiredModules) + { + # If the RequiredModules.psd1 entry is an Hashtable then special handling is needed. + if ($requiredModule.Value -is [System.Collections.Hashtable]) + { + $saveModuleHashtable = @{ + ModuleName = $requiredModule.Name + } + + if ($requiredModule.Value.Version -and $requiredModule.Value.Version -ne 'latest') + { + $saveModuleHashtable.RequiredVersion = $requiredModule.Value.Version + } + + # ModuleFast does no support preview releases yet. + if ($UsePSResourceGet) + { + if ($requiredModule.Value.Parameters.AllowPrerelease -eq $true) + { + $saveModuleHashtable.Prerelease = $true + } + } + + $modulesToSave += $saveModuleHashtable + } + else + { + if ($requiredModule.Value -eq 'latest') + { + $modulesToSave += $requiredModule.Name + } + else + { + $modulesToSave += @{ + ModuleName = $requiredModule.Name + RequiredVersion = $requiredModule.Value + } + } + } + } + + if ($WithYAML) + { + $modulesToSave += 'PowerShell-Yaml' + } + + if ($UseModuleFast) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking ModuleFast' + + Write-Progress -Activity 'ModuleFast:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' + + $moduleFastPlan = $modulesToSave | Get-ModuleFastPlan + + if ($moduleFastPlan) + { + # Clear all modules in plan from the current session so they can be fetched again. + $moduleFastPlan.Name | Get-Module | Remove-Module -Force + + $installModuleFastParameters = @{ + ModulesToInstall = $moduleFastPlan + Destination = $PSDependTarget + NoPSModulePathUpdate = $true + NoProfileUpdate = $true + Update = $true + Confirm = $false + } + + Install-ModuleFast @installModuleFastParameters + } + else + { + Write-Verbose -Message 'All modules were already up to date' + } + + Write-Progress -Activity 'ModuleFast:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + + if ($UsePSResourceGet) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSResourceGet' + + $progressPercentage = 0 + + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' + + $percentagePerModule = [System.Math]::Floor(100 / $modulesToSave.Length) + + foreach ($currentModule in $modulesToSave) + { + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Saving module {0}' -f $savePSResourceParameters.Name) + + $savePSResourceParameters = @{ + Path = $PSDependTarget + TrustRepository = $true + Confirm = $false + } + + if ($currentModule -is [System.Collections.Hashtable]) + { + $savePSResourceParameters.Name = $currentModule.ModuleName + + if ($currentModule.RequiredVersion) + { + $savePSResourceParameters.Version = $currentModule.RequiredVersion + } + + if ($currentModule.Prerelease) + { + $savePSResourceParameters.Prerelease = $currentModule.Prerelease + } + } + else + { + $savePSResourceParameters.Name = $currentModule + } + + # Modules that Sampler depend on that cannot be refreshed without a new session. + $skipModule = @('powershell-yaml') + + if ($savePSResourceParameters.Name -in $skipModule -and (Get-Module -Name 'powershell-yaml')) + { + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Skipping module {0}' -f $savePSResourceParameters.Name) + + Write-Information -MessageData ('Skipping the module {0} since it cannot be refresh while loaded into the session. To refresh the module open a new session and resolve dependencies again.' -f $savePSResourceParameters.Name) -InformationAction 'Continue' + } + else + { + # Clear all module from the current session so any new version fetched will be re-imported. + Get-Module -Name $savePSResourceParameters.Name | Remove-Module -Force + + Save-PSResource @savePSResourceParameters -ErrorVariable 'savePSResourceError' + + if ($savePSResourceError) + { + Write-Warning -Message 'Save-PSResource could not save (replace) one or more dependencies. This can be due to the module is loaded into the session (and referencing assemblies). Close the current session and open a new session and try again.' + } + } + + $progressPercentage += $percentagePerModule + } + + Write-Progress -Activity 'PSResourceGet:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + } + else + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSDepend' + + Write-Progress -Activity 'PSDepend:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' + + $psDependParameters = @{ + Force = $true + Path = $DependencyFile + } + + # TODO: Handle when the Dependency file is in YAML, and -WithYAML is specified. + Invoke-PSDepend @psDependParameters + + Write-Progress -Activity 'PSDepend:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 100 -CurrentOperation 'Bootstrap complete' -Completed +} +finally +{ + if ($RegisterGallery) + { + Write-Verbose -Message "Removing private package repository '$Gallery'." + Unregister-PSRepository -Name $Gallery + } + + if ($unregisteredPreviousRepository) + { + Write-Verbose -Message "Reverting private package repository '$Gallery' to previous location URI:s." + + $registerPSRepositoryParameters = @{ + Name = $previousRegisteredRepository.Name + InstallationPolicy = $previousRegisteredRepository.InstallationPolicy + } + + if ($previousRegisteredRepository.SourceLocation) + { + $registerPSRepositoryParameters.SourceLocation = $previousRegisteredRepository.SourceLocation + } + + if ($previousRegisteredRepository.PublishLocation) + { + $registerPSRepositoryParameters.PublishLocation = $previousRegisteredRepository.PublishLocation + } + + if ($previousRegisteredRepository.ScriptSourceLocation) + { + $registerPSRepositoryParameters.ScriptSourceLocation = $previousRegisteredRepository.ScriptSourceLocation + } + + if ($previousRegisteredRepository.ScriptPublishLocation) + { + $registerPSRepositoryParameters.ScriptPublishLocation = $previousRegisteredRepository.ScriptPublishLocation + } + + Register-PSRepository @registerPSRepositoryParameters + } + + # Only try to revert installation policy if the repository exist + if ((Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue')) + { + if ($previousGalleryInstallationPolicy -ne $true) + { + # Reverting the Installation Policy for the given gallery if it was not already trusted + Set-PSRepository -Name $Gallery -InstallationPolicy 'Untrusted' + } + } + + Write-Verbose -Message 'Project Bootstrapped, returning to Invoke-Build.' +} diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/Resolve-Dependency.psd1 b/powershell-helpers/Microsoft.PowerShell.DSC/Resolve-Dependency.psd1 new file mode 100644 index 00000000..f7c33131 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/Resolve-Dependency.psd1 @@ -0,0 +1,49 @@ +@{ + <# + Default parameter values to be loaded by the Resolve-Dependency.ps1 script (unless set in bound parameters + when calling the script). + #> + + #PSDependTarget = './output/modules' + #Proxy = '' + #ProxyCredential = '$MyCredentialVariable' #TODO: find a way to support credentials in build (resolve variable) + + Gallery = 'PSGallery' + + # To use a private nuget repository change the following to your own feed. The locations must be a Nuget v2 feed due + # to limitation in PowerShellGet v2.x. Example below is for a Azure DevOps Server project-scoped feed. While resolving + # dependencies it will be registered as a trusted repository with the name specified in the property 'Gallery' above, + # unless property 'Name' is provided in the hashtable below, if so it will override the property 'Gallery' above. The + # registered repository will be removed when dependencies has been resolved, unless it was already registered to begin + # with. If repository is registered already but with different URL:s the repository will be re-registered and reverted + # after dependencies has been resolved. Currently only Windows integrated security works with private Nuget v2 feeds + # (or if it is a public feed with no security), it is not possible yet to securely provide other credentials for the feed. + # Private repositories will currently only work using PowerShellGet. + #RegisterGallery = @{ + # #Name = 'MyPrivateFeedName' + # GallerySourceLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' + # GalleryPublishLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' + # GalleryScriptSourceLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' + # GalleryScriptPublishLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' + # #InstallationPolicy = 'Trusted' + #} + + #AllowOldPowerShellGetModule = $true + #MinimumPSDependVersion = '0.3.0' + AllowPrerelease = $false + WithYAML = $true # Will also bootstrap PowerShell-Yaml to read other config files + + # Enable ModuleFast to resolve dependencies. Requires PowerShell 7.2 or higher. + # If this is not configured or set to $false then PowerShellGet and PackageManagement + # will be used to resolve dependencies. + #UseModuleFast = $true + + # Enable PSResourceGet to resolve dependencies. Requires PowerShell 7.2 or higher. + # If this is not configured or set to $false then PowerShellGet and PackageManagement + # will be used to resolve dependencies. + #UsePSResourceGet = $true + #PSResourceGetVersion = '1.0.0' + #UsePowerShellGetCompatibilityModule = $true + #UsePowerShellGetCompatibilityModuleVersion = '3.0.22-beta22' +} + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/build.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/build.ps1 new file mode 100644 index 00000000..f4a0faec --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/build.ps1 @@ -0,0 +1,538 @@ +<# + .DESCRIPTION + Bootstrap and build script for PowerShell module CI/CD pipeline. + + .PARAMETER Tasks + The task or tasks to run. The default value is '.' (runs the default task). + + .PARAMETER CodeCoverageThreshold + The code coverage target threshold to uphold. Set to 0 to disable. + The default value is '' (empty string). + + .PARAMETER BuildConfig + Not yet written. + + .PARAMETER OutputDirectory + Specifies the folder to build the artefact into. The default value is 'output'. + + .PARAMETER BuiltModuleSubdirectory + Subdirectory name to build the module (under $OutputDirectory). The default + value is '' (empty string). + + .PARAMETER RequiredModulesDirectory + Can be a path (relative to $PSScriptRoot or absolute) to tell Resolve-Dependency + and PSDepend where to save the required modules. It is also possible to use + 'CurrentUser' och 'AllUsers' to install missing dependencies. You can override + the value for PSDepend in the Build.psd1 build manifest. The default value is + 'output/RequiredModules'. + + .PARAMETER PesterScript + One or more paths that will override the Pester configuration in build + configuration file when running the build task Invoke_Pester_Tests. + + If running Pester 5 test, use the alias PesterPath to be future-proof. + + .PARAMETER PesterTag + Filter which tags to run when invoking Pester tests. This is used in the + Invoke-Pester.pester.build.ps1 tasks. + + .PARAMETER PesterExcludeTag + Filter which tags to exclude when invoking Pester tests. This is used in + the Invoke-Pester.pester.build.ps1 tasks. + + .PARAMETER DscTestTag + Filter which tags to run when invoking DSC Resource tests. This is used + in the DscResource.Test.build.ps1 tasks. + + .PARAMETER DscTestExcludeTag + Filter which tags to exclude when invoking DSC Resource tests. This is + used in the DscResource.Test.build.ps1 tasks. + + .PARAMETER ResolveDependency + Not yet written. + + .PARAMETER BuildInfo + The build info object from ModuleBuilder. Defaults to an empty hashtable. + + .PARAMETER AutoRestore + Not yet written. + + .PARAMETER UseModuleFast + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .PARAMETER UsePSResourceGet + Specifies to use PSResourceGet instead of PowerShellGet to resolve dependencies + faster. This can also be configured in Resolve-Dependency.psd1. + + .PARAMETER UsePowerShellGetCompatibilityModule + Specifies to use the compatibility module PowerShellGet. This parameter + only works then the method of downloading dependencies is PSResourceGet. + This can also be configured in Resolve-Dependency.psd1. +#> +[CmdletBinding()] +param +( + [Parameter(Position = 0)] + [System.String[]] + $Tasks = '.', + + [Parameter()] + [System.String] + $CodeCoverageThreshold = '', + + [Parameter()] + [System.String] + [ValidateScript( + { Test-Path -Path $_ } + )] + $BuildConfig, + + [Parameter()] + [System.String] + $OutputDirectory = 'output', + + [Parameter()] + [System.String] + $BuiltModuleSubdirectory = '', + + [Parameter()] + [System.String] + $RequiredModulesDirectory = $(Join-Path 'output' 'RequiredModules'), + + [Parameter()] + # This alias is to prepare for the rename of this parameter to PesterPath when Pester 4 support is removed + [Alias('PesterPath')] + [System.Object[]] + $PesterScript, + + [Parameter()] + [System.String[]] + $PesterTag, + + [Parameter()] + [System.String[]] + $PesterExcludeTag, + + [Parameter()] + [System.String[]] + $DscTestTag, + + [Parameter()] + [System.String[]] + $DscTestExcludeTag, + + [Parameter()] + [Alias('bootstrap')] + [System.Management.Automation.SwitchParameter] + $ResolveDependency, + + [Parameter(DontShow)] + [AllowNull()] + [System.Collections.Hashtable] + $BuildInfo, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AutoRestore, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseModuleFast, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePSResourceGet, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePowerShellGetCompatibilityModule +) + +<# + The BEGIN block (at the end of this file) handles the Bootstrap of the Environment + before Invoke-Build can run the tasks if the parameter ResolveDependency (or + parameter alias Bootstrap) is specified. +#> + +process +{ + if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1') + { + # Only run the process block through InvokeBuild (look at the Begin block at the bottom of this script). + return + } + + # Execute the Build process from the .build.ps1 path. + Push-Location -Path $PSScriptRoot -StackName 'BeforeBuild' + + try + { + Write-Host -Object "[build] Parsing defined tasks" -ForeGroundColor Magenta + + # Load the default BuildInfo if the parameter BuildInfo is not set. + if (-not $PSBoundParameters.ContainsKey('BuildInfo')) + { + try + { + if (Test-Path -Path $BuildConfig) + { + $configFile = Get-Item -Path $BuildConfig + + Write-Host -Object "[build] Loading Configuration from $configFile" + + $BuildInfo = switch -Regex ($configFile.Extension) + { + # Native Support for PSD1 + '\.psd1' + { + if (-not (Get-Command -Name Import-PowerShellDataFile -ErrorAction SilentlyContinue)) + { + Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion 3.1.0.0 + } + + Import-PowerShellDataFile -Path $BuildConfig + } + + # Support for yaml when module PowerShell-Yaml is available + '\.[yaml|yml]' + { + Import-Module -Name 'powershell-yaml' -ErrorAction Stop + + ConvertFrom-Yaml -Yaml (Get-Content -Raw $configFile) + } + + # Support for JSON and JSONC (by Removing comments) when module PowerShell-Yaml is available + '\.[json|jsonc]' + { + $jsonFile = Get-Content -Raw -Path $configFile + + $jsonContent = $jsonFile -replace '(?m)\s*//.*?$' -replace '(?ms)/\*.*?\*/' + + # Yaml is superset of JSON. + ConvertFrom-Yaml -Yaml $jsonContent + } + + # Unknown extension, return empty hashtable. + default + { + Write-Error -Message "Extension '$_' not supported. using @{}" + + @{ } + } + } + } + else + { + Write-Host -Object "Configuration file '$($BuildConfig.FullName)' not found" -ForegroundColor Red + + # No config file was found, return empty hashtable. + $BuildInfo = @{ } + } + } + catch + { + $logMessage = "Error loading Config '$($BuildConfig.FullName)'.`r`nAre you missing dependencies?`r`nMake sure you run './build.ps1 -ResolveDependency -tasks noop' before running build to restore the required modules." + + Write-Host -Object $logMessage -ForegroundColor Yellow + + $BuildInfo = @{ } + + Write-Error -Message $_.Exception.Message + } + } + + # If the Invoke-Build Task Header is specified in the Build Info, set it. + if ($BuildInfo.TaskHeader) + { + Set-BuildHeader -Script ([scriptblock]::Create($BuildInfo.TaskHeader)) + } + + <# + Add BuildModuleOutput to PSModule Path environment variable. + Moved here (not in begin block) because build file can contains BuiltSubModuleDirectory value. + #> + if ($BuiltModuleSubdirectory) + { + if (-not (Split-Path -IsAbsolute -Path $BuiltModuleSubdirectory)) + { + $BuildModuleOutput = Join-Path -Path $OutputDirectory -ChildPath $BuiltModuleSubdirectory + } + else + { + $BuildModuleOutput = $BuiltModuleSubdirectory + } + } # test if BuiltModuleSubDirectory set in build config file + elseif ($BuildInfo.ContainsKey('BuiltModuleSubDirectory')) + { + $BuildModuleOutput = Join-Path -Path $OutputDirectory -ChildPath $BuildInfo['BuiltModuleSubdirectory'] + } + else + { + $BuildModuleOutput = $OutputDirectory + } + + # Pre-pending $BuildModuleOutput folder to PSModulePath to resolve built module from this folder. + if ($powerShellModulePaths -notcontains $BuildModuleOutput) + { + Write-Host -Object "[build] Pre-pending '$BuildModuleOutput' folder to PSModulePath" -ForegroundColor Green + + $env:PSModulePath = $BuildModuleOutput + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + + <# + Import Tasks from modules via their exported aliases when defined in Build Manifest. + https://github.com/nightroman/Invoke-Build/tree/master/Tasks/Import#example-2-import-from-a-module-with-tasks + #> + if ($BuildInfo.ContainsKey('ModuleBuildTasks')) + { + foreach ($module in $BuildInfo['ModuleBuildTasks'].Keys) + { + try + { + Write-Host -Object "Importing tasks from module $module" -ForegroundColor DarkGray + + $loadedModule = Import-Module -Name $module -PassThru -ErrorAction Stop + + foreach ($TaskToExport in $BuildInfo['ModuleBuildTasks'].($module)) + { + $loadedModule.ExportedAliases.GetEnumerator().Where{ + Write-Host -Object "`t Loading $($_.Key)..." -ForegroundColor DarkGray + + # Using -like to support wildcard. + $_.Key -like $TaskToExport + }.ForEach{ + # Dot-sourcing the Tasks via their exported aliases. + . (Get-Alias $_.Key) + } + } + } + catch + { + Write-Host -Object "Could not load tasks for module $module." -ForegroundColor Red + + Write-Error -Message $_ + } + } + } + + # Loading Build Tasks defined in the .build/ folder (will override the ones imported above if same task name). + Get-ChildItem -Path '.build/' -Recurse -Include '*.ps1' -ErrorAction Ignore | + ForEach-Object { + "Importing file $($_.BaseName)" | Write-Verbose + + . $_.FullName + } + + # Synopsis: Empty task, useful to test the bootstrap process. + task noop { } + + # Define default task sequence ("."), can be overridden in the $BuildInfo. + task . { + Write-Build -Object 'No sequence currently defined for the default task' -ForegroundColor Yellow + } + + Write-Host -Object 'Adding Workflow from configuration:' -ForegroundColor DarkGray + + # Load Invoke-Build task sequences/workflows from $BuildInfo. + foreach ($workflow in $BuildInfo.BuildWorkflow.keys) + { + Write-Verbose -Message "Creating Build Workflow '$Workflow' with tasks $($BuildInfo.BuildWorkflow.($Workflow) -join ', ')." + + $workflowItem = $BuildInfo.BuildWorkflow.($workflow) + + if ($workflowItem.Trim() -match '^\{(?[\w\W]*)\}$') + { + $workflowItem = [ScriptBlock]::Create($Matches['sb']) + } + + Write-Host -Object " +-> $workflow" -ForegroundColor DarkGray + + task $workflow $workflowItem + } + + Write-Host -Object "[build] Executing requested workflow: $($Tasks -join ', ')" -ForeGroundColor Magenta + + } + finally + { + Pop-Location -StackName 'BeforeBuild' + } +} + +begin +{ + # Find build config if not specified. + if (-not $BuildConfig) + { + $config = Get-ChildItem -Path "$PSScriptRoot\*" -Include 'build.y*ml', 'build.psd1', 'build.json*' -ErrorAction Ignore + + if (-not $config -or ($config -is [System.Array] -and $config.Length -le 0)) + { + throw 'No build configuration found. Specify path via parameter BuildConfig.' + } + elseif ($config -is [System.Array]) + { + if ($config.Length -gt 1) + { + throw 'More than one build configuration found. Specify which path to use via parameter BuildConfig.' + } + + $BuildConfig = $config[0] + } + else + { + $BuildConfig = $config + } + } + + # Bootstrapping the environment before using Invoke-Build as task runner + + if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') + { + Write-Host -Object "[pre-build] Starting Build Init" -ForegroundColor Green + + Push-Location $PSScriptRoot -StackName 'BuildModule' + } + + if ($RequiredModulesDirectory -in @('CurrentUser', 'AllUsers')) + { + # Installing modules instead of saving them. + Write-Host -Object "[pre-build] Required Modules will be installed to the PowerShell module path that is used for $RequiredModulesDirectory." -ForegroundColor Green + + <# + The variable $PSDependTarget will be used below when building the splatting + variable before calling Resolve-Dependency.ps1, unless overridden in the + file Resolve-Dependency.psd1. + #> + $PSDependTarget = $RequiredModulesDirectory + } + else + { + if (-not (Split-Path -IsAbsolute -Path $OutputDirectory)) + { + $OutputDirectory = Join-Path -Path $PSScriptRoot -ChildPath $OutputDirectory + } + + # Resolving the absolute path to save the required modules to. + if (-not (Split-Path -IsAbsolute -Path $RequiredModulesDirectory)) + { + $RequiredModulesDirectory = Join-Path -Path $PSScriptRoot -ChildPath $RequiredModulesDirectory + } + + # Create the output/modules folder if not exists, or resolve the Absolute path otherwise. + if (Resolve-Path -Path $RequiredModulesDirectory -ErrorAction SilentlyContinue) + { + Write-Debug -Message "[pre-build] Required Modules path already exist at $RequiredModulesDirectory" + + $requiredModulesPath = Convert-Path -Path $RequiredModulesDirectory + } + else + { + Write-Host -Object "[pre-build] Creating required modules directory $RequiredModulesDirectory." -ForegroundColor Green + + $requiredModulesPath = (New-Item -ItemType Directory -Force -Path $RequiredModulesDirectory).FullName + } + + $powerShellModulePaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator + + # Pre-pending $requiredModulesPath folder to PSModulePath to resolve from this folder FIRST. + if ($RequiredModulesDirectory -notin @('CurrentUser', 'AllUsers') -and + ($powerShellModulePaths -notcontains $RequiredModulesDirectory)) + { + Write-Host -Object "[pre-build] Pre-pending '$RequiredModulesDirectory' folder to PSModulePath" -ForegroundColor Green + + $env:PSModulePath = $RequiredModulesDirectory + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + + $powerShellYamlModule = Get-Module -Name 'powershell-yaml' -ListAvailable + $invokeBuildModule = Get-Module -Name 'InvokeBuild' -ListAvailable + $psDependModule = Get-Module -Name 'PSDepend' -ListAvailable + + # Checking if the user should -ResolveDependency. + if (-not ($powerShellYamlModule -and $invokeBuildModule -and $psDependModule) -and -not $ResolveDependency) + { + if ($AutoRestore -or -not $PSBoundParameters.ContainsKey('Tasks') -or $Tasks -contains 'build') + { + Write-Host -Object "[pre-build] Dependency missing, running './build.ps1 -ResolveDependency -Tasks noop' for you `r`n" -ForegroundColor Yellow + + $ResolveDependency = $true + } + else + { + Write-Warning -Message "Some required Modules are missing, make sure you first run with the '-ResolveDependency' parameter. Running 'build.ps1 -ResolveDependency -Tasks noop' will pull required modules without running the build task." + } + } + + <# + The variable $PSDependTarget will be used below when building the splatting + variable before calling Resolve-Dependency.ps1, unless overridden in the + file Resolve-Dependency.psd1. + #> + $PSDependTarget = $requiredModulesPath + } + + if ($ResolveDependency) + { + Write-Host -Object "[pre-build] Resolving dependencies using preferred method." -ForegroundColor Green + + $resolveDependencyParams = @{ } + + # If BuildConfig is a Yaml file, bootstrap powershell-yaml via ResolveDependency. + if ($BuildConfig -match '\.[yaml|yml]$') + { + $resolveDependencyParams.Add('WithYaml', $true) + } + + $resolveDependencyAvailableParams = (Get-Command -Name '.\Resolve-Dependency.ps1').Parameters.Keys + + foreach ($cmdParameter in $resolveDependencyAvailableParams) + { + # The parameter has been explicitly used for calling the .build.ps1 + if ($MyInvocation.BoundParameters.ContainsKey($cmdParameter)) + { + $paramValue = $MyInvocation.BoundParameters.Item($cmdParameter) + + Write-Debug " adding $cmdParameter :: $paramValue [from user-provided parameters to Build.ps1]" + + $resolveDependencyParams.Add($cmdParameter, $paramValue) + } + # Use defaults parameter value from Build.ps1, if any + else + { + $paramValue = Get-Variable -Name $cmdParameter -ValueOnly -ErrorAction Ignore + + if ($paramValue) + { + Write-Debug " adding $cmdParameter :: $paramValue [from default Build.ps1 variable]" + + $resolveDependencyParams.Add($cmdParameter, $paramValue) + } + } + } + + Write-Host -Object "[pre-build] Starting bootstrap process." -ForegroundColor Green + + .\Resolve-Dependency.ps1 @resolveDependencyParams + } + + if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') + { + Write-Verbose -Message "Bootstrap completed. Handing back to InvokeBuild." + + if ($PSBoundParameters.ContainsKey('ResolveDependency')) + { + Write-Verbose -Message "Dependency already resolved. Removing task." + + $null = $PSBoundParameters.Remove('ResolveDependency') + } + + Write-Host -Object "[build] Starting build with InvokeBuild." -ForegroundColor Green + + Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInvocation.MyCommand.Path + + Pop-Location -StackName 'BuildModule' + + return + } +} diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/build.yaml b/powershell-helpers/Microsoft.PowerShell.DSC/build.yaml new file mode 100644 index 00000000..ea647a64 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/build.yaml @@ -0,0 +1,150 @@ +--- +#################################################### +# ModuleBuilder Configuration # +#################################################### + +BuiltModuleSubdirectory: module +CopyPaths: + - en-US + # - prefix.ps1 +# - DSCResources + # - Modules +Encoding: UTF8 +# Can be used to manually specify module's semantic version if the preferred method of +# using GitVersion is not available, and it is not possible to set the session environment +# variable `$env:ModuleVersion`, nor setting the variable `$ModuleVersion`, in the +# PowerShell session (parent scope) before running the task `build`. +#SemVer: '99.0.0-preview1' + +# Suffix to add to Root module PSM1 after merge (here, the Set-Alias exporting IB tasks) +# suffix: suffix.ps1 +prefix: prefix.ps1 +VersionedOutputDirectory: true + +#################################################### +# ModuleBuilder Submodules Configuration # +#################################################### + +NestedModule: +# HelperSubmodule: # This is the first submodule to build into the output +# Path: ./*/Modules/HelperSubmodule/HelperSubmodule.psd1 +# # is trimmed (remove metadata & Prerelease tag) and OutputDirectory expanded (the only one) +# OutputDirectory: ///Modules/HelperSubmodule +# VersionedOutputDirectory: false +# AddToManifest: false +# SemVer: +# # suffix: +# # prefix: + +#################################################### +# Sampler Pipeline Configuration # +#################################################### +# Defining 'Workflows' (suite of InvokeBuild tasks) to be run using their alias +BuildWorkflow: + '.': # "." is the default Invoke-Build workflow. It is called when no -Tasks is specified to the build.ps1 + - build + - test + + build: + - Clean + - Build_Module_ModuleBuilder + - Build_NestedModules_ModuleBuilder + - Create_changelog_release_output + + + pack: + - build + - package_module_nupkg + + + + # Defining test task to be run when invoking `./build.ps1 -Tasks test` + test: + # Uncomment to modify the PSModulePath in the test pipeline (also requires the build configuration section SetPSModulePath). + #- Set_PSModulePath + - Pester_Tests_Stop_On_Fail + # Use this task if pipeline uses code coverage and the module is using the + # pattern of Public, Private, Enum, Classes. + #- Convert_Pester_Coverage + - Pester_if_Code_Coverage_Under_Threshold + + # Use this task when you have multiple parallel tests, which produce multiple + # code coverage files and needs to get merged into one file. + #merge: + #- Merge_CodeCoverage_Files + + publish: + - publish_module_to_gallery + + - Publish_Release_To_GitHub + + +#################################################### +# PESTER Configuration # +#################################################### + +Pester: + OutputFormat: NUnitXML + # Excludes one or more paths from being used to calculate code coverage. + ExcludeFromCodeCoverage: + + # If no scripts are defined the default is to use all the tests under the project's + # tests folder or source folder (if present). Test script paths can be defined to + # only run tests in certain folders, or run specific test files, or can be use to + # specify the order tests are run. + Script: + # - tests/QA/module.tests.ps1 + # - tests/QA + # - tests/Unit + # - tests/Integration + ExcludeTag: + # - helpQuality + # - FunctionalQuality + # - TestQuality + Tag: + CodeCoverageThreshold: 85 # Set to 0 to bypass + #CodeCoverageOutputFile: JaCoCo_$OsShortName.xml + #CodeCoverageOutputFileEncoding: ascii + # Use this if code coverage should be merged from several pipeline test jobs. + # Any existing keys above should be replaced. See also CodeCoverage below. + # CodeCoverageOutputFile is the file that is created for each pipeline test job. + #CodeCoverageOutputFile: JaCoCo_Merge.xml + +# Use this to merged code coverage from several pipeline test jobs. +# CodeCoverageFilePattern - the pattern used to search all pipeline test job artifacts +# after the file specified in CodeCoverageOutputFile. +# CodeCoverageMergedOutputFile - the file that is created by the merge build task and +# is the file that should be uploaded to code coverage services. +#CodeCoverage: + #CodeCoverageFilePattern: JaCoCo_Merge.xml # the pattern used to search all pipeline test job artifacts + #CodeCoverageMergedOutputFile: JaCoCo_coverage.xml # the file that is created for the merged code coverage + + +# Import ModuleBuilder tasks from a specific PowerShell module using the build +# task's alias. Wildcard * can be used to specify all tasks that has a similar +# prefix and or suffix. The module contain the task must be added as a required +# module in the file RequiredModules.psd1. +ModuleBuildTasks: + Sampler: + - '*.build.Sampler.ib.tasks' + Sampler.GitHubTasks: + - '*.ib.tasks' + + +# Invoke-Build Header to be used to 'decorate' the terminal output of the tasks. +TaskHeader: | + param($Path) + "" + "=" * 79 + Write-Build Cyan "`t`t`t$($Task.Name.replace("_"," ").ToUpper())" + Write-Build DarkGray "$(Get-BuildSynopsis $Task)" + "-" * 79 + Write-Build DarkGray " $Path" + Write-Build DarkGray " $($Task.InvocationInfo.ScriptName):$($Task.InvocationInfo.ScriptLineNumber)" + "" + + + + + + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Classes/DscConfigurationResource.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Classes/DscConfigurationResource.ps1 new file mode 100644 index 00000000..1e9a36bc --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Classes/DscConfigurationResource.ps1 @@ -0,0 +1,6 @@ +class DscConfigurationResource +{ + [string] $name + [string] $type + [hashtable] $properties +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Microsoft.PowerShell.DSC.psd1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Microsoft.PowerShell.DSC.psd1 new file mode 100644 index 00000000..3d8aa2cc --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Microsoft.PowerShell.DSC.psd1 @@ -0,0 +1,143 @@ +# +# Module manifest for module 'Microsoft.PowerShell.DSC' +# +# Generated by: Microsoft Corporation +# +# Generated on: 29/07/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'Microsoft.PowerShell.DSC.psm1' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '43e1ffd7-6e7e-48d6-b8e8-0da35c39c9c4' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'PowerShell Desired State Configuration Module for working with `dsc.exe`' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '5.0' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @() + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = '' + + # Prerelease string of this module + Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + + + + + + + + + + + + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Microsoft.PowerShell.DSC.psm1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Microsoft.PowerShell.DSC.psm1 new file mode 100644 index 00000000..92d7cfb9 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Microsoft.PowerShell.DSC.psm1 @@ -0,0 +1,5 @@ +<# + This file is intentionally left empty. It is must be left here for the module + manifest to refer to. It is recreated during the build process. +#> + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscConfigurationDocument.ps1 new file mode 100644 index 00000000..bbced6fd --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscConfigurationDocument.ps1 @@ -0,0 +1,41 @@ +function Build-DscConfigurationDocument +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [ValidateSet('JSON', 'YAML', 'Default')] + [System.String] + $Format = 'JSON' + ) + + $configurationDocument = [ordered]@{ + "`$schema" = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json" + resources = Export-DscConfigurationDocument -Path $Path + } + + switch ($Format) + { + "JSON" { + $inputObject = ($configurationDocument | ConvertTo-Json -Depth 10) + } + "YAML" { + if (Test-YamlModule) + { + $inputObject = ($configurationDocument | ConvertTo-Yaml) + } + else + { + $inputObject = @{} + } + } + default { + $inputObject = $configurationDocument + } + } + + return $inputObject +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscPathBuilder.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscPathBuilder.ps1 new file mode 100644 index 00000000..baf9689e --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscPathBuilder.ps1 @@ -0,0 +1,84 @@ +function Build-DscPathBuilder +{ + [OutputType([System.Text.StringBuilder])] + Param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable] + $Data, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Text.StringBuilder] + $SubCommand, + + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [Alias('Path')] + [System.IO.FileInfo] + $ResourcePath, + + [Parameter(Mandatory = $false)] + [hashtable] + $ResourceInput = @{} + ) + + if (Test-Path $ResourcePath -ErrorAction SilentlyContinue) + { + if ($ResourcePath.Extension -ne '.json' -and $ResourcePath.Extension -ne '.yaml' -and $ResourcePath.Extension -ne '.ps1') + { + Throw "No JSON, YAML or PowerShell script file was provided. Please provide valid DSC Configuration Document." + } + + $command = " --path $($ResourcePath.FullName)" + + if ($ResourcePath.Extension -eq '.ps1') + { + # try converting to + } + + [void]$subCommand.Append(" --path $($ResourcePath.FullName)") + } + else + { + try + { + $jsonOutput = $ResourceInput | ConvertTo-Json -Compress + Write-Verbose -Message ("Starting input with:") + Write-Verbose -Message ($jsonOutput | Out-String) + if ($jsonOutput -eq '{}') + { + if ($data.exampleSnippet) + { + Write-Verbose -Message "Using example snippet" + $jsonOutput = $data.exampleSnippet | ConvertTo-Json -Compress + } + } + + $filePath = if ($IsWindows) + { + Join-Path -Path $env:LOCALAPPDATA -ChildPath "dsc\dsc_tmp_configuration_doc.json" + } + else + { + Join-Path -Path $env:HOME -ChildPath "dsc$([System.IO.Path]::DirectorySeparatorChar)dsc_tmp_configuration_doc.json" + } + + if (-not (Test-Path $(Split-Path $filePath -Parent))) + { + $null = New-Item -Path $(Split-Path $filePath -Parent) -ItemType Directory -Force + } + + Set-Content -Path $filePath -Value $jsonOutput -Force + # TODO: The --input does not always work correctly even ProcMon states the characters are escaped correctly. Workaround for now. + [void]$subCommand.Append(" --path $filePath") + } + catch + { + # TODO: Capture + } + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Export-DscConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Export-DscConfigurationDocument.ps1 new file mode 100644 index 00000000..9e25f3d0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Export-DscConfigurationDocument.ps1 @@ -0,0 +1,70 @@ +function Export-DscConfigurationDocument +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + if (-not (Test-PsPathExtension $Path)) + { + return @{} + } + + # Parse the abstract syntax tree to get all hash table values representing the configuration resources + [System.Management.Automation.Language.Token[]] $tokens = $null + [System.Management.Automation.Language.ParseError[]] $errors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) + $configurations = $ast.FindAll({$args[0].GetType().Name -like 'HashtableAst'}, $true) + + # Create configuration document resource class (can be re-used) + $configurationDocument = [DscConfigurationResource]::new() + + # Build simple regex + $regex = [regex]::new('Configuration\s+(\w+)') + $configValue = $regex.Matches($ast.Extent.Text).Value + + if (-not $configValue) + { + return + } + + $documentConfigurationName = $configValue.TrimStart('Configuration').Trim(" ") + + # Start to build the outer basic format + $configurationDocument.name = $documentConfigurationName + # Hardcoded PowerShell 7 adapter type info + $configurationDocument.type = 'Microsoft.DSC/PowerShell' + + # Bag to hold resources + $resourceProps = [System.Collections.Generic.List[object]]::new() + + foreach ($configuration in $configurations) + { + # Get parent configuration details + $resourceName = ($configuration.Parent.CommandElements.Value | Select-Object -Last 1 ) + $resourceConfigurationName = ($configuration.Parent.CommandElements.Value | Select-Object -First 1) + + # Get module details + $module = Get-DscResource -Name $resourceConfigurationName -ErrorAction SilentlyContinue + + # Build the module + $resource = [DscConfigurationResource]::new() + $resource.properties = $configuration.SafeGetValue() + $resource.name = $resourceName + $resource.type = ("{0}/{1}" -f $module.ModuleName, $resourceConfigurationName) + + Write-Verbose ("Adding document with data") + Write-Verbose ($resource | ConvertTo-Json | Out-String) + $resourceProps.Add($resource) + } + + # Add all the resources + $configurationDocument.properties = @{ + resources = $resourceProps + } + + return $configurationDocument +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscCommandData.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscCommandData.ps1 new file mode 100644 index 00000000..adaa18a3 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscCommandData.ps1 @@ -0,0 +1,106 @@ +function Get-DscCommandData +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $false)] + [System.String] + $CommandName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.SwitchParameter] + $IncludeProperties, + + [Parameter(Mandatory = $false)] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [ValidateSet('Get', 'Set', 'Test')] + [System.String] + $Operation = 'Get' + ) + + $exeLocation = Resolve-DscExe -ErrorAction SilentlyContinue + if ($exeLocation) + { + $files = Get-ChildItem -Path (Split-Path -Path $exeLocation -Parent) -Filter '*.dsc.resource.json' + $resources = $files | ForEach-Object { + (Get-Content $_ | ConvertFrom-Json -ErrorAction SilentlyContinue).type + + } + } + + $cmdData = @{ + 'Example' = @{ + '1.0' = @{ + SubCommand = 'resource list' + } + } + 'ExampleSnippet' = @{ + 'Microsoft.Windows/Registry' = @{ + Get = @{ keyPath = "HKCU\Microsoft"} + Set = @{ keyPath = "HKCU\1"; valueName = "Desired State"; valueData = @{"String" = "Configuration"} } + Test = @{ keyPath = "HKCU\Microsoft"} + } + } + 'Get-DscResourceCommand' = @{ + 'preview8' = @{ + SubCommand = 'resource get' + Resources = $resources + } + } + 'Set-DscResourceCommand' = @{ + 'preview8' = @{ + SubCommand = 'resource set' + Resources = $resources + } + } + 'Test-DscResourceCommand' = @{ + 'preview8' = @{ + SubCommand = 'resource test' + Resources = $resources + } + } + } + + # TODO: Add possible version info + $keyData = $cmdData.$CommandName.Keys + + if ($null -eq $keyData) + { + Throw "Cannot find data entry for '$CommandName'. Please make sure the $($MyInvocation.MyCommand.Name) is up to date with data." + } + + $result = ($cmdData.$CommandName | Where-Object keys -eq $keyData).Values + + if ($IncludeProperties) + { + if (-not $ResourceName) + { + Throw "When specifying '-IncludeProperties', you have to include the '-ResourceName' parameter also." + } + + if (-not (Test-DscResourceName -ResourceName $ResourceName -Resources $resources)) + { + $result.Add('properties', @{}) + } + else + { + # get schema details + $properties = Get-DscResourceSchemaProperty -ResourceName $ResourceName -Operation $Operation + + # add them as properties + Write-Verbose -Message ("Including propery data: $($properties | ConvertTo-Json | Out-String)") + $result.Add('properties', $properties) + + # get example snippet if available + $snippet = $cmdData.ExampleSnippet.$ResourceName.$Operation + Write-Verbose -Message ("Including example snippet data: $($snippet | ConvertTo-Json | Out-String)") + $result.Add('exampleSnippet', $snippet) + } + } + + Write-Verbose -Message "Selected data for '$CommandName'" + return $result +} diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceCommand.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceCommand.ps1 new file mode 100644 index 00000000..d9a41d9a --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceCommand.ps1 @@ -0,0 +1,46 @@ +function Get-DscResourceCommand +{ + [CmdletBinding(DefaultParameterSetName = 'ByInput')] + Param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false, ParameterSetName = 'ByPath')] + [Alias('Path')] + [System.IO.FileInfo] + $ResourcePath, + + [Parameter(Mandatory = $false, ParameterSetName = 'ByInput')] + [hashtable] + $ResourceInput = @{} + ) + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + + # get data + Write-Verbose -Message "Gathering command data for '$commandName'" + $data = Get-DscCommandData -CommandName $commandName -IncludeProperties -ResourceName $ResourceName -Operation $commandName.Split("-")[0] + + Write-Verbose -Message "Building sub command with:" + Write-Verbose -Message ("{0}{1}" -f $data.SubCommand, " --resource $resourceName") + $subCommand = New-SubCommand -SubCommand ("{0}{1}" -f $data.SubCommand, " --resource $resourceName") + } + + process + { + Build-DscPathBuilder -Data $data -SubCommand $SubCommand -ResourceName $ResourceName -ResourcePath $ResourcePath -ResourceInput $ResourceInput + + $inputObject = Invoke-DscExe -SubCommand $subCommand.ToString() + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceSchemaProperty.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceSchemaProperty.ps1 new file mode 100644 index 00000000..45ce39d0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceSchemaProperty.ps1 @@ -0,0 +1,90 @@ +function Get-DscResourceSchemaProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [ValidateSet('Get', 'Set', 'Test')] + [System.String] + $Operation = 'Get' + ) + + $resource = $ResourceName.Split("/")[-1] + $resourceManifestFile = Join-Path (Split-Path -Path $(Resolve-DscExe) -Parent) -ChildPath "$resource.dsc.resource.json" + + $result = @{} + + if (Test-Path $resourceManifestFile) + { + $content = Get-Content $resourceManifestFile | ConvertFrom-Json + + # only return resource kind types + $knownTypes = @('group', 'adapter') + if ($content.type -notin $knownTypes) + { + if (-not ($content.schema)) + { + # content does not have schema + # TODO: Check if this can be the case + return $result + } + + $fileExe = $content.schema.command.executable + + if ($fileExe) + { + $inputObject = (& $fileExe $content.schema.command.args | ConvertFrom-Json -ErrorAction SilentlyContinue) + } + + if ($content.schema.embedded) + { + $inputObject = $content.schema.embedded + } + + $result = [System.Collections.Generic.List[hashtable]]::new() + switch ($Operation) + { + 'Get' + { + $inputObject.required | ForEach-Object { + $add = @{ + $_ = "<$_>" + } + + [void]$result.Add($add) + } + } + 'Set' + { + $add = @{} + ($inputObject.properties | Get-Member -MemberType NoteProperty) | ForEach-Object { + if (-not $add["$_"]) + { + $add += @{ + $_.Name = "<$($_.Name)>" + } + } + } + [void]$result.Add($add) + } + 'Test' + { + $inputObject.required | ForEach-Object { + $add = @{ + $_ = "<$_>" + } + + [void]$result.Add($add) + } + } + Default { $result.Add(@{}) } + } + } + + return $result + } +} diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Invoke-DscExe.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Invoke-DscExe.ps1 new file mode 100644 index 00000000..023e82da --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Invoke-DscExe.ps1 @@ -0,0 +1,25 @@ +function Invoke-DscExe +{ + [CmdletBinding(SupportsShouldProcess)] + Param + ( + [Parameter(Mandatory = $false)] + [System.String] + $SubCommand = 'resource list' + ) + + begin + { + Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) + } + + process + { + $inputObject = Get-ProcessObjectResult -SubCommand $SubCommand + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Resolve-DscExe.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Resolve-DscExe.ps1 new file mode 100644 index 00000000..250272f3 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Resolve-DscExe.ps1 @@ -0,0 +1,39 @@ +function Resolve-DscExe +{ + [OutputType([System.String])] + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [System.IO.FileInfo] + $Path, + + [Parameter(Mandatory = $false)] + [ValidateSet('Machine', 'User', 'Process')] + [System.String] + $Scope = 'Machine' + ) + + if ($PSBoundParameters.ContainsKey('Path') -and (-not (Test-Path $Path -PathType Leaf))) + { + Throw "No file found at path '$Path'. Please specify the file path to 'dsc.exe'" + } + + if ($IsWindows) + { + $exe = Join-Path -Path (Get-EnvironmentVariable -Name 'DSC_RESOURCE_PATH' -Scope $Scope -Expanded) -ChildPath 'dsc.exe' + if (Test-Path $exe) + { + return $exe + } + + $exe = (Get-Command dsc -ErrorAction SilentlyContinue).Source + if (-not $exe) + { + Throw "Could not locate 'dsc.exe'. Please make sure it can be found through the PATH or DSC_RESOURCE_PATH environment variable." + } + + return $exe + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Set-DscResourceCommand.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Set-DscResourceCommand.ps1 new file mode 100644 index 00000000..bcfaa232 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Set-DscResourceCommand.ps1 @@ -0,0 +1,47 @@ +function Set-DscResourceCommand +{ + [CmdletBinding(SupportsShouldProcess)] + Param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [Alias('Path')] + [System.IO.FileInfo] + $ResourcePath, + + [Parameter(Mandatory = $false)] + [Alias('Input')] + [hashtable] + $ResourceInput = @{} + ) + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + + # get data + Write-Verbose -Message "Gathering command data for '$commandName'" + $data = Get-DscCommandData -CommandName $commandName -IncludeProperties -ResourceName $ResourceName -Operation $commandName.Split("-")[0] + + Write-Verbose -Message "Building sub command with:" + Write-Verbose -Message ("{0}{1}" -f $data.SubCommand, " --resource $resourceName") + $subCommand = New-SubCommand -Subcommand ("{0}{1}" -f $data.SubCommand, " --resource $resourceName") + } + + process + { + Build-DscPathBuilder -Data $data -SubCommand $SubCommand -ResourceName $ResourceName -ResourcePath $ResourcePath -ResourceInput $ResourceInput + + $inputObject = Invoke-DscExe -SubCommand $subCommand.ToString() + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceCommand.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceCommand.ps1 new file mode 100644 index 00000000..a22c2854 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceCommand.ps1 @@ -0,0 +1,47 @@ +function Test-DscResourceCommand +{ + [CmdletBinding(SupportsShouldProcess)] + Param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [Alias('Path')] + [System.IO.FileInfo] + $ResourcePath, + + [Parameter(Mandatory = $false)] + [Alias('Input')] + [hashtable] + $ResourceInput + ) + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + + # get data + Write-Verbose -Message "Gathering command data for '$commandName'" + $data = Get-DscCommandData -CommandName $commandName -IncludeProperties -ResourceName $ResourceName -Operation $commandName.Split("-")[0] + + Write-Verbose -Message "Building sub command with:" + Write-Verbose -Message ("{0}{1}" -f $data.SubCommand, " --resource $resourceName") + $subCommand = New-SubCommand -Subcommand ("{0}{1}" -f $data.SubCommand, " --resource $resourceName") + } + + process + { + Build-DscPathBuilder -Data $data -SubCommand $SubCommand -ResourceName $ResourceName -ResourcePath $ResourcePath -ResourceInput $ResourceInput + + $inputObject = Invoke-DscExe -SubCommand $subCommand.ToString() + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceName.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceName.ps1 new file mode 100644 index 00000000..86ad8cfd --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceName.ps1 @@ -0,0 +1,23 @@ +function Test-DscResourceName +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [System.String[]] + $Resources + ) + + if ($ResourceName -in $Resources) + { + return $true + } + + return $false +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/DscPs/Get-DscPsCacheProperties.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/DscPs/Get-DscPsCacheProperties.ps1 new file mode 100644 index 00000000..55e581f5 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/DscPs/Get-DscPsCacheProperties.ps1 @@ -0,0 +1,25 @@ +function Get-DscPsCacheProperties +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [object] + $Properties, + + [System.Management.Automation.SwitchParameter] + $Required + ) + + if ($Required) + { + $properties = $properties | Where-Object {$_.IsMandatory -eq $true } + } + + $inputObject = $properties | ForEach-Object { + @{$_.Name = "<$($_.Name)>"} + } + + return $inputObject +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/DscPs/Get-DscPsCacheRefreshPath.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/DscPs/Get-DscPsCacheRefreshPath.ps1 new file mode 100644 index 00000000..d8d2524a --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/DscPs/Get-DscPsCacheRefreshPath.ps1 @@ -0,0 +1,12 @@ +function Get-DscPsCacheRefreshPath +{ + $cacheFilePath = if ($IsWindows) { + # PS 6+ on Windows + Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" + } else { + # PS 6+ on Linux/Mac + Join-Path $env:HOME ".dsc" "PSAdapterCache.json" + } + + return $cacheFilePath +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-EnvironmentVariable.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-EnvironmentVariable.ps1 new file mode 100644 index 00000000..6a68e196 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-EnvironmentVariable.ps1 @@ -0,0 +1,95 @@ +function Get-EnvironmentVariable +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter()] + [System.EnvironmentVariableTarget] + $Scope = [System.EnvironmentVariableTarget]::Process, + + [Parameter()] + [Switch] + $Expanded, + + [Parameter()] + [Switch] + $ShowProperties + ) + + $Getter = [System.Environment]::GetEnvironmentVariable($Name, $Scope) + if ($null -eq $Getter) + { + $RawValue = $null + $GetterType = $null + } + else + { + if ($Scope -ne "Process") + { + if (!$Expanded) + { + $AllEnvironmentVariables = Get-Item -Path (Get-EnvironmentPath -Scope $Scope) + $GetterType = $AllEnvironmentVariables.GetValueKind($Name) + } + else + { + $AllEnvironmentVariables = [System.Environment]::GetEnvironmentVariables($Scope) + $GetterType = $Getter.GetTypeCode() + } + if ($GetterType -eq "ExpandString") + { + $RawValue = $AllEnvironmentVariables.GetValue( + $Name, $null, 'DoNotExpandEnvironmentNames' + ) + } + elseif ($GetterType -eq "String") + { + $RawValue = $Getter + if ($Expanded) + { + $Getter = [System.Environment]::ExpandEnvironmentVariables($Getter) + } + } + else + { + # inappropriate kind (dword, bytes, ...) + $RawValue = $null + $GetterType = $null + } + } + else + { + # $Scope -eq "Process" + $RawValue = $null + $GetterType = "String" + } + } + $params = @{ + Name = $Name + Value = $Getter + Scope = $Scope + ValueType = $GetterType + BeforeExpansion = $RawValue + } + $null = New-EnvironmentVariableObject @params | Set-Variable -Name NewEnvVar + + if ($ShowProperties) + { + $NewEnvVar | Add-Member ScriptMethod ToString { $this.Value } -Force -PassThru + } + else + { + if (!$Expanded) + { + $NewEnvVar | Add-Member ScriptMethod ToString { $this.Value } -Force -PassThru | Select-Object -ExpandProperty BeforeExpansion + } + else + { + $NewEnvVar | Add-Member ScriptMethod ToString { $this.Value } -Force -PassThru | Select-Object -ExpandProperty Value + } + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessObjectResult.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessObjectResult.ps1 new file mode 100644 index 00000000..5566baff --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessObjectResult.ps1 @@ -0,0 +1,51 @@ +function Get-ProcessObjectResult +{ + [CmdletBinding(SupportsShouldProcess)] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SubCommand + ) + + # TODO: Add global settings for resolving + $dscExePath = Resolve-DscExe + + # use System.Diagnostics.Process instead of & or Invoke-Expression + $proc = [System.Diagnostics.Process]::new() + + # create the starter information + $info = New-Object System.Diagnostics.ProcessStartInfo + $info.FileName = $dscExePath + # TODO: Somehow when we use input with JSON it doesn't work as expected + $info.Arguments = $SubCommand + $info.UseShellExecute = $false + $info.RedirectStandardOutput = $true + $info.RedirectStandardError = $true + + $proc.StartInfo = $info + + if ($PSCmdlet.ShouldProcess(("{0}", "{1}" -f $info.FileName, $info.Arguments))) + { + # start the process + $proc.Start() | Out-Null + + # read stream outputs + $stdOut = Get-ProcessOutput -Process $proc -ReadLine StandardOutput + $stErr = Get-ProcessOutput -Process $proc -ReadLine StandardError + + # TODO: Get process output when JSON cannot be returned + + # wait for exit + $proc.WaitForExit() + + $inputObject = New-Object -TypeName PSObject -Property ([Ordered]@{ + Executable = $info.FileName + Arguments = $SubCommand + ExitCode = $proc.ExitCode + Output = $stdOut + Error = $stErr + }) + return $inputObject + } +} diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessOutput.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessOutput.ps1 new file mode 100644 index 00000000..e5b0d32d --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessOutput.ps1 @@ -0,0 +1,45 @@ +function Get-ProcessOutput +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process] + $Process, + + [Parameter(Mandatory = $false)] + [ValidateSet('StandardOutput', 'StandardError')] + [System.String] + $ReadLine = 'StandardOutput' + ) + + # TODO: Can determine if classes can be created to have more strong-typed + $output = [System.Collections.Generic.List[object]]::new() + + do + { + $line = $Process.$ReadLine.ReadLine() + + if ($line) + { + try + { + $jsonOutput = $line | ConvertFrom-Json + + # add to output + $output.Add($jsonOutput) + } + catch + { + $msg = "Could not convert '$line' to JSON." + Write-Debug -Message $msg + } + } + else + { + break + } + } while ($true) + + return $output +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/ConvertTo-GitHubErrorRecord.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/ConvertTo-GitHubErrorRecord.ps1 new file mode 100644 index 00000000..2d706737 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/ConvertTo-GitHubErrorRecord.ps1 @@ -0,0 +1,44 @@ +function ConvertTo-GitHubErrorRecord +{ + [CmdletBinding()] + [OutputType([ErrorRecord])] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [ValidateNotNull()] + [PSObject] $Err + ) + process + { + $message = "" + $errorId = $null + $docUrl = $null + if ($null -ne $Err.PSObject.Properties['code']) + { + $errorId = $Err.code + $message += "$($Err.code): " + } + if ($null -ne $Err.PSObject.Properties['field']) + { + $message += "Field `"$($Err.field)`": " + } + if ($null -ne $Err.PSObject.Properties['message']) + { + $message += $Err.message + } + if ($null -ne $Err.PSObject.Properties['documentation_url']) + { + $docUrl = $Err.documentation_url + } + # Validation errors have nested errors + $exception = if ($null -ne $Err.PSObject.Properties['errors']) + { + [AggregateException]::new($message, @($Err.errors | ConvertTo-GitHubErrorRecord | ForEach-Object Exception -Confirm:$false)) + } + else + { + [Exception]::new($message) + } + $exception.HelpLink = $docUrl + [ErrorRecord]::new($exception, $errorId, [ErrorCategory]::NotSpecified, $null) + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Get-AvailablePackageManager.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Get-AvailablePackageManager.ps1 new file mode 100644 index 00000000..06ef1e79 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Get-AvailablePackageManager.ps1 @@ -0,0 +1,22 @@ +function Get-AvailablePackageManager +{ + if (Get-Command 'apt' -ErrorAction SilentlyContinue) + { + return 'apt' + } + + if (Get-Command 'dnf' -ErrorAction SilentlyContinue) + { + return 'dnf' + } + + if (Get-Command 'yum' -ErrorAction SilentlyContinue) + { + return 'yum' + } + + if (Get-Command 'zypper' -ErrorAction SilentlyContinue) + { + return 'zypper' + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Get-PlatformInformation.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Get-PlatformInformation.ps1 new file mode 100644 index 00000000..c194fba1 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Get-PlatformInformation.ps1 @@ -0,0 +1,101 @@ +function Get-PlatformInformation +{ + if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) + { + $os = 'Windows' + } + elseif ($IsLinux) + { + $os = 'Linux' + } + elseif ($IsMacOS) + { + $os = 'MacOS' + } + + else + { + throw 'Could not identify operating system' + } + + switch ($os) + { + 'Linux' + { + $pacMan = Get-AvailablePackageManager + + switch ($pacMan) + { + # 'apt' + # { + # $platform = 'linux-deb-x64' + # $ext = 'deb' + # break + # } + + # { 'dnf', 'yum', 'zypper' -contains $_ } + # { + # $platform = 'linux-rpm-x64' + # $ext = 'rpm' + # break + # } + + default + { + $platform = 'x86_64-unknown-linux-gnu' + break + } + } + + $exePath = '/usr/bin/dsc' + break + } + + 'MacOS' + { + $platform = 'x86_64-apple-darwin' + + $exePath = '/usr/local/bin/dsc' + break + } + + 'Windows' + { + $platform = 'x86_64-pc-windows-msvc' + + $exePath = if (Test-Administrator) { "$env:ProgramFiles\DSC" } else { "$env:LOCALAPPDATA\DSC" } + } + } + + # TODO: If latest is present, just change it to always point to latest release + $releases = Invoke-GitHubApi -Uri "repos/PowerShell/DSC/releases" + + [uri]$latestAsset = ($releases | Sort-Object created_at -Descending | Select-Object -First 1).assets_url + + # invoke the rest api call + $resp = Invoke-GitHubApi -Uri $latestAsset.LocalPath + + $downloadUrl = $resp.browser_download_url | Where-Object { $_ -like "*$platform*" } + + + + $info = @{ + FileName = ($downloadUrl.Split("/")[-1]) + ExePath = $exePath + Platform = $platform + FileUri = $downloadUrl + Extension = [System.IO.Path]::GetExtension($downloadUrl) + } + + if ($IsWindows) + { + $info['RunAsAdmin'] = Test-Administrator + } + + if ($pacMan) + { + $info['PackageManager'] = $pacMan + } + + return $info +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Invoke-GitHubApi.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Invoke-GitHubApi.ps1 new file mode 100644 index 00000000..348c1361 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Invoke-GitHubApi.ps1 @@ -0,0 +1,103 @@ +function Invoke-GitHubApi + { + [CmdletBinding()] + param ( + [Parameter(Mandatory, Position = 0)] + [string] $Uri, + [Uri] $BaseUri = [Uri]::new('https://api.github.com'), + + # HTTP headers + [HashTable] $Headers = @{Accept = 'application/vnd.github.v3+json' }, + + # HTTP request method + [Microsoft.PowerShell.Commands.WebRequestMethod] $Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Get, + + # Request body or query parameters for GET requests + $Body, + + # File path to use as body (instead of $Body). + [string] $InFile, + + # Accept header to be added (for accessing preview APIs or different resource representations) + [string[]] $Accept, + + [switch] $Anonymous, + [Security.SecureString] $Token = $null + ) + + $Headers['User-Agent'] = 'PowerShell PSGitHub' + + if ($Accept) + { + $Headers.Accept = ($Accept -join ',') + } + + # Resolve the Uri parameter with https://api.github.com as a base URI + # This allows to call this function with just a path, + # but also supply a full URI (e.g. for a GitHub enterprise instance) + $Uri = [Uri]::new($BaseUri, $Uri) + + $apiRequest = @{ + Headers = $Headers; + Uri = $Uri; + Method = $Method; + # enable automatic pagination + # use | Select-Object -First to limit the result + FollowRelLink = $true; + }; + + # If the caller hasn't specified the -Anonymous switch parameter, then add the HTTP Authorization header + # to authenticate the HTTP request. + if (!$Anonymous -and $Token) + { + $apiRequest.Authentication = 'Bearer' + $apiRequest.Token = $Token + } + else + { + Write-Verbose -Message 'Making request without API token' + } + + ### Append the HTTP message body (payload), if the caller specified one. + if ($Body) + { + $apiRequest.Body = $Body + Write-Debug -Message ("Request body: " + ($Body | Out-String)) + } + if ($InFile) + { + $apiRequest.InFile = $InFile + } + + # We need to communicate using TLS 1.2 against GitHub. + [Net.ServicePointManager]::SecurityProtocol = 'tls12' + + # Invoke the REST API + try + { + Write-Verbose ($apiRequest | ConvertTo-Json | Out-String) + Invoke-RestMethod @apiRequest -ResponseHeadersVariable responseHeaders + if ($responseHeaders.ContainsKey('X-RateLimit-Limit')) + { + Write-Verbose "Rate limit total: $($responseHeaders['X-RateLimit-Limit'])" + Write-Verbose "Rate limit remaining: $($responseHeaders['X-RateLimit-Remaining'])" + $resetUnixSeconds = [int]($responseHeaders['X-RateLimit-Reset'][0]) + $resetDateTime = ([System.DateTimeOffset]::FromUnixTimeSeconds($resetUnixSeconds)).DateTime + Write-Verbose "Rate limit resets: $resetDateTime" + } + } + catch + { + if ( + $_.Exception.PSObject.TypeNames -notcontains 'Microsoft.PowerShell.Commands.HttpResponseException' -and # PowerShell Core + $_.Exception -isnot [System.Net.WebException] # Windows PowerShell + ) + { + # Throw any error that is not a HTTP response error (e.g. server not reachable) + throw $_ + } + # This is the only way to get access to the response body for errors in old PowerShell versions. + # PowerShell >=7.0 could use -SkipHttpErrorCheck with -StatusCodeVariable + $_.ErrorDetails.Message | ConvertFrom-Json | ConvertTo-GitHubErrorRecord | Write-Error + } + } \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Save-WithBitsTransfer.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Save-WithBitsTransfer.ps1 new file mode 100644 index 00000000..d22fe110 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Save-WithBitsTransfer.ps1 @@ -0,0 +1,41 @@ +function Save-WithBitsTransfer +{ + param( + [Parameter(Mandatory = $true)] + [string] + $FileUri, + + [Parameter(Mandatory = $true)] + [string] + $Destination, + + [Parameter(Mandatory = $true)] + [string] + $AppName + ) + + Write-Information -MessageData "`nDownloading latest $AppName..." + Remove-Item -Force $Destination -ErrorAction SilentlyContinue + + $bitsDl = Start-BitsTransfer $FileUri -Destination $Destination -Asynchronous + + while (($bitsDL.JobState -eq 'Transferring') -or ($bitsDL.JobState -eq 'Connecting')) + { + Write-Progress -Activity "Downloading: $AppName" -Status "$([math]::round($bitsDl.BytesTransferred / 1mb))mb / $([math]::round($bitsDl.BytesTotal / 1mb))mb" -PercentComplete ($($bitsDl.BytesTransferred) / $($bitsDl.BytesTotal) * 100 ) + } + + switch ($bitsDl.JobState) + { + + 'Transferred' + { + Complete-BitsTransfer -BitsJob $bitsDl + break + } + + 'Error' + { + throw 'Error downloading installation media.' + } + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Test-Administrator.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Test-Administrator.ps1 new file mode 100644 index 00000000..7b7c8612 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Test-Administrator.ps1 @@ -0,0 +1,5 @@ +function Test-Administrator +{ + $user = [Security.Principal.WindowsIdentity]::GetCurrent(); + (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Test-IsOsArchX64.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Test-IsOsArchX64.ps1 new file mode 100644 index 00000000..9b546827 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Test-IsOsArchX64.ps1 @@ -0,0 +1,9 @@ +function Test-IsOsArchX64 +{ + if ($PSVersionTable.PSVersion.Major -lt 6) + { + return (Get-CimInstance -ClassName Win32_OperatingSystem).OSArchitecture -match '64' + } + + return [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::X64 +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-EnvironmentVariableObject.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-EnvironmentVariableObject.ps1 new file mode 100644 index 00000000..5cdda599 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-EnvironmentVariableObject.ps1 @@ -0,0 +1,35 @@ +function New-EnvironmentVariableObject +{ + param + ( + [OutputType([HashTable])] + [Parameter(Mandatory)] + [ValidatePattern("[^=]+")] + $Name, + [Parameter()] + [AllowNull()] + [String] + $Value, + [Parameter(Mandatory)] + [System.EnvironmentVariableTarget] + $Scope, + [Parameter()] + [AllowNull()] + [ValidateSet("String", "ExpandString", $null)] + [String] + $ValueType, + [Parameter()] + [AllowNull()] + [String] + $BeforeExpansion + ) + + $OutPut = [PSCustomObject]@{ + Name = $Name + Value = $Value + Scope = $Scope + ValueType = $ValueType + BeforeExpansion = $BeforeExpansion + } + $OutPut +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-SubCommand.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-SubCommand.ps1 new file mode 100644 index 00000000..2083caa6 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-SubCommand.ps1 @@ -0,0 +1,15 @@ +function New-SubCommand +{ + [CmdletBinding()] + [OutputType([System.Text.StringBuilder])] + Param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.String] + $SubCommand + ) + + $stringBuilder = New-Object -TypeName System.Text.StringBuilder -ArgumentList $SubCommand + + return $stringBuilder +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Add-ToPath.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Add-ToPath.ps1 new file mode 100644 index 00000000..fa538f02 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Add-ToPath.ps1 @@ -0,0 +1,81 @@ +function Add-ToPath +{ + [CmdletBinding()] + param + ( + [Parameter(ValueFromPipeline = $true, mandatory = $true)] + [System.String] + $Path, + + [Alias("p")] + [System.Boolean] + [switch] + $Persistent, + + [switch] + [System.Boolean] + $First, + + [System.Management.Automation.SwitchParameter] + [System.Boolean] + $User + ) + + process + { + if ($null -eq $path) { throw [System.ArgumentNullException]"path" } + if ($User) + { + $p = Get-PathEnv -User + } + elseif ($persistent) + { + $p = Get-PathEnv -Machine + } + else + { + $p = Get-PathEnv -Current + } + $p = $p | ForEach-Object { $_.trimend("\") } + $p = @($p) + $paths = @($path) + $paths | ForEach-Object { + $path = $_.trimend("\") + Write-Verbose "adding $path to PATH" + if ($first) + { + if ($p.length -eq 0 -or $p[0] -ine $path) + { + $p = @($path) + $p + } + } + else + { + if ($path -inotin $p) + { + $p += $path + } + } + } + + if ($User) + { + Write-Verbose "saving user PATH and adding to current proc" + [System.Environment]::SetEnvironmentVariable("PATH", [string]::Join(";", $p), [System.EnvironmentVariableTarget]::User); + #add also to process PATH + Add-ToPath $path -persistent:$false -first:$first + } + elseif ($persistent) + { + write-Verbose "Saving to global machine PATH variable" + [System.Environment]::SetEnvironmentVariable("PATH", [string]::Join(";", $p), [System.EnvironmentVariableTarget]::Machine); + #add also to process PATH + Add-ToPath $path -persistent:$false -first:$first + } + else + { + $env:path = [string]::Join(";", $p); + [System.Environment]::SetEnvironmentVariable("PATH", $env:path, [System.EnvironmentVariableTarget]::Process); + } + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Get-EnvVar.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Get-EnvVar.ps1 new file mode 100644 index 00000000..97b0ce75 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Get-EnvVar.ps1 @@ -0,0 +1,50 @@ +function Get-EnvVar +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [System.Management.Automation.SwitchParameter] + [System.Boolean] + $User, + + [System.Management.Automation.SwitchParameter] + [System.Boolean] + $Machine, + + [System.Management.Automation.SwitchParameter] + [System.Boolean] + $Current + ) + + $val = @() + if ($user) + { + $val += [System.Environment]::GetEnvironmentVariable($name, [System.EnvironmentVariableTarget]::User); + } + if ($machine) + { + $val += [System.Environment]::GetEnvironmentVariable($name, [System.EnvironmentVariableTarget]::Machine); + } + if (!$user.IsPresent -and !$machine.IsPresent) + { + $current = $true + } + if ($current) + { + $val = invoke-expression "`$env:$name" + } + if ($val -ne $null) + { + $p = $val.Split(';') + } + else + { + $p = @() + } + + return $p +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Get-PathEnv.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Get-PathEnv.ps1 new file mode 100644 index 00000000..78570c8f --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Get-PathEnv.ps1 @@ -0,0 +1,71 @@ +function Get-PathEnv +{ + [CmdLetBinding(DefaultParameterSetName = "scoped")] + param + ( + [Parameter(ParameterSetName = "scoped")] + [System.Management.Automation.SwitchParameter] + [System.Boolean] + $User, + + [Parameter(ParameterSetName = "scoped")] + [System.Management.Automation.SwitchParameter] + [System.Boolean] + $Machine, + + [Alias("process")] + [Parameter(ParameterSetName = "scoped")] + [System.Management.Automation.SwitchParameter] + [System.Boolean] + $Current, + + [Parameter(ParameterSetName = "all")] + [System.Management.Automation.SwitchParameter] + [System.Boolean] + $All + ) + + $scopespecified = $user.IsPresent -or $machine.IsPresent -or $current.IsPresent + $path = @() + $userpath = get-envvar "PATH" -user + if ($user) + { + $path += $userpath + } + $machinepath = get-envvar "PATH" -machine + if ($machine -or !$scopespecified) + { + $path += $machinepath + } + if (!$user.IsPresent -and !$machine.IsPresent) + { + $current = $true + } + $currentPath = get-envvar "PATH" -current + if ($current) + { + $path = $currentPath + } + + if ($all) + { + $h = @{ + user = $userpath + machine = $machinepath + process = $currentPath + } + return @( + "`r`n USER", + " -----------", + $h.user, + "`r`n MACHINE", + " -----------", + $h.machine, + "`r`n PROCESS", + " -----------", + $h.process + ) + } + + return $path +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-PsPathExtension.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-PsPathExtension.ps1 new file mode 100644 index 00000000..a48efcf0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-PsPathExtension.ps1 @@ -0,0 +1,24 @@ +function Test-PsPathExtension +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + $res = $true + + if (-not (Test-Path $Path)) + { + $res = $false + } + + if (([System.IO.Path]::GetExtension($Path) -ne ".ps1")) + { + $res = $false + } + + return $res +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-YamlModule.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-YamlModule.ps1 new file mode 100644 index 00000000..ca13c1b3 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-YamlModule.ps1 @@ -0,0 +1,9 @@ +function Test-YamlModule +{ + if (-not (Get-Command -Name 'ConvertTo-Yaml' -ErrorAction SilentlyContinue)) + { + return $false + } + + return $true +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/ConvertTo-DscJson.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/ConvertTo-DscJson.ps1 new file mode 100644 index 00000000..afb552e1 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/ConvertTo-DscJson.ps1 @@ -0,0 +1,87 @@ +function ConvertTo-DscJson +{ + <# + .SYNOPSIS + Convert DSC Configuration (v1/v2) Document to JSON + + .DESCRIPTION + The function ConvertTo-DscJson converts a DSC Configuration Document (v1/v2) to JSON + + .PARAMETER Path + The file path to a valid DSC Configuration Document + + .EXAMPLE + PS C:\> $path = 'myConfig.ps1' + PS C:\> ConvertTo-DscJson -Path $path + + .INPUTS + Input a valid DSC Configuration Document. + + configuration MyConfiguration { + Import-DscResource -ModuleName PSDesiredStateConfiguration + Node localhost + { + Environment CreatePathEnvironmentVariable + { + Name = 'TestPathEnvironmentVariable' + Value = 'TestValue' + Ensure = 'Present' + Path = $true + Target = @('Process') + } + } + } + + .OUTPUTS + Returns a JSON string. + + { + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", + "resources": { + "name": "MyConfiguration \r\n node", + "type": "Microsoft.DSC/PowerShell", + "properties": { + "resources": [ + { + "name": "CreatePathEnvironmentVariable", + "type": "PSDscResources/Environment", + "properties": { + "Value": "TestValue", + "Path": true, + "Name": "TestPathEnvironmentVariable", + "Ensure": "Present", + "Target": [ + "Process" + ] + } + } + ] + } + } + } + #> + [CmdletBinding()] + [OutputType([System.String])] + Param + ( + [Parameter(ValueFromPipeline = $true)] + [System.String] + $Path + ) + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + } + + process + { + $inputObject = Build-DscConfigurationDocument -Path $Path + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/ConvertTo-DscYaml.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/ConvertTo-DscYaml.ps1 new file mode 100644 index 00000000..4f1e43a0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/ConvertTo-DscYaml.ps1 @@ -0,0 +1,76 @@ +function ConvertTo-DscYaml +{ + <# + .SYNOPSIS + Convert DSC Configuration (v1/v2) Document to YAML + + .DESCRIPTION + The function ConvertTo-DscYaml converts a DSC Configuration Document (v1/v2) to YAML + + .PARAMETER Path + The file path to a valid DSC Configuration Document + + .EXAMPLE + PS C:\> $path = 'myConfig.ps1' + PS C:\> ConvertTo-DscYaml -Path $path + + .INPUTS + Input a valid DSC Configuration Document. + + configuration MyConfiguration { + Import-DscResource -ModuleName PSDesiredStateConfiguration + Node localhost + { + Environment CreatePathEnvironmentVariable + { + Name = 'TestPathEnvironmentVariable' + Value = 'TestValue' + Ensure = 'Present' + Path = $true + Target = @('Process') + } + } + } + + .OUTPUTS + Returns a YAML string. + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + name: MyConfiguration + type: Microsoft.DSC/PowerShell + properties: + resources: + - name: CreatePathEnvironmentVariable + type: PSDscResources/Environment + properties: + Value: TestValue + Path: true + Name: TestPathEnvironmentVariable + Ensure: Present + Target: + - Process + #> + [CmdletBinding()] + Param + ( + [Parameter(ValueFromPipeline = $true)] + [System.String] + $Path + ) + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + } + + process + { + $inputObject = Build-DscConfigurationDocument -Path $Path -Format YAML + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscCommand.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscCommand.ps1 new file mode 100644 index 00000000..241ce376 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscCommand.ps1 @@ -0,0 +1,71 @@ +function Install-DscCommand +{ + [CmdletBinding(SupportsShouldProcess)] + param + () + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + } + process + { + try + { + $prevProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + # get details about the platform + $platformInfo = Get-PlatformInformation + + # download the installer + $tmpdir = [System.IO.Path]::GetTempPath() + $installerPath = [System.IO.Path]::Combine($tmpDir, $platformInfo.FileName) + + if ($PSVersionTable.PSVersion.Major -le 5) + { + Save-WithBitsTransfer -FileUri $platformInfo.FileUri -Destination $installerPath -AppName $platformInfo.AppName + } + elseif ($PSCmdlet.ShouldProcess($platformInfo.FileUri, "Invoke-WebRequest -OutFile $installerPath")) + { + Invoke-WebRequest -Uri $platformInfo.FileUri -OutFile $installerPath + } + + # switch to install on different platforms based on extension + + switch ($platformInfo.Extension) + { + # On windows and ... + '.zip' + { + Expand-Archive -LiteralPath $installerPath -DestinationPath $platformInfo.ExePath -Force + } + + # TODO: Add other platforms + } + + if ($platformInfo.RunAsAdmin) + { + $platformInfo.ExePath | Add-ToPath -Persistent:$true + } + + else + { + # TODO: Check when user is assigned to throw a warning when multiple environment variables are present e.g. DSC_RESOURCE_PATH + $platformInfo.ExePath | Add-ToPath -User + } + + Write-Information -MessageData "Successfully installed 'dsc.exe' in: $($platformInfo.ExePath)" + dsc --version + } + finally { + $ProgressPreference = $prevProgressPreference + } + } + + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceCommand.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceCommand.ps1 new file mode 100644 index 00000000..fa3091ad --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceCommand.ps1 @@ -0,0 +1,69 @@ +function Invoke-DscResourceCommand +{ + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = '__AllParameterSets')] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [ValidateSet('Get', 'Set', 'Test')] + [System.String] + $Operation = 'Get', + + [Parameter(Mandatory = $false, ParameterSetName = 'ByPath')] + [AllowNull()] + [Alias('Path')] + [System.IO.FileInfo] + $ResourcePath, + + [Parameter(Mandatory = $false, ParameterSetName = 'ByInput')] + [AllowNull()] + [hashtable] + $ResourceInput + ) + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + } + + process + { + $arguments = @{ResourceName = $ResourceName } + # get argument data + switch ($PSCmdlet.ParameterSetName) + { + 'ResourcePath' { $arguments.Add('ResourcePath', $ResourcePath) } + 'ResourceInput' { $arguments.Add('ResourceInput', $ResourceInput) } + default { $arguments.Add('ResourceInput', @{}) } + } + + # go through operations + switch ($Operation) + { + 'Get' + { + $inputObject = Get-DscResourceCommand @arguments + } + 'Set' + { + $inputObject = Set-DscResourceCommand @arguments + } + 'Test' + { + $inputobject = Test-DscResourceCommand @arguments + } + default { $inputObject = @{} } + } + + return $inputObject + } + + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/New-DscPsResourceDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/New-DscPsResourceDocument.ps1 new file mode 100644 index 00000000..fb7848b9 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/New-DscPsResourceDocument.ps1 @@ -0,0 +1,59 @@ +function New-DscPsResourceDocument +{ + [CmdletBinding(DefaultParameterSetName = 'Required')] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ResourceName, + + [System.Management.Automation.SwitchParameter] + $OnlyRequired, + + [System.Management.Automation.SwitchParameter] + $IncludeProperties + ) + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + } + + process + { + $cacheFilePath = Get-DscPsCacheRefreshPath + + if (-not $cacheFilePath) + { + # TODO: It can be replaced after GitHub issue is solved to call the command directly from dsc.exe + Throw "Please execute 'Invoke-DscCacheRefresh' from the 'psDscAdapter.psm1' module file" + } + + $json = Get-Content $cacheFilePath | ConvertFrom-Json + + # try to find the object in the cache and always filter only one result + $resourceObject = $json.ResourceCache.DscResourceInfo | Where-Object {$_.Name -eq $ResourceName} | Select-Object -First 1 + + if (-not $resourceObject) + { + Throw "No resource found with name: '$ResourceName'. Please make sure you have installed the DSC PowerShell module that exports this resource." + } + + $propArgs = @{ + Properties = $resourceObject.Properties + } + + if ($OnlyRequired) + { + $propArgs.Add('Required', $true) + } + + return (Get-DscPsCacheProperties @propArgs) + } + + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/en-US/about_Microsoft.PowerShell.DSC.help.txt b/powershell-helpers/Microsoft.PowerShell.DSC/source/en-US/about_Microsoft.PowerShell.DSC.help.txt new file mode 100644 index 00000000..a723cac9 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/en-US/about_Microsoft.PowerShell.DSC.help.txt @@ -0,0 +1,24 @@ +TOPIC + about_Microsoft.PowerShell.DSC + +SHORT DESCRIPTION + PowerShell Desired State Configuration Module for working with `dsc.exe` + +LONG DESCRIPTION + PowerShell Desired State Configuration Module for working with `dsc.exe` + +EXAMPLES + PS C:\> {{ add examples here }} + +NOTE: + Thank you to all those who contributed to this module, by writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Look out on the Github repository for issues and new releases. + +SEE ALSO + - {{ Please add Project URI such as github }}} + +KEYWORDS + {{ Add comma separated keywords here }} + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/prefix.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/prefix.ps1 new file mode 100644 index 00000000..86a85a39 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/prefix.ps1 @@ -0,0 +1,31 @@ +[scriptblock]$dscExeSubCommand = { + param( + $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters + ) + + $exeLocation = Resolve-DscExe -ErrorAction SilentlyContinue + if ($exeLocation) + { + # TODO: Filter better + $files = Get-ChildItem -Path (Split-Path -Path $exeLocation -Parent) -Filter '*.dsc.resource.json' + $files | ForEach-Object { + $typeName = (Get-Content $_ | ConvertFrom-Json -ErrorAction SilentlyContinue).type + if ($typeName) + { + # register new completer + New-Object -Type System.Management.Automation.CompletionResult -ArgumentList @( + $typeName + $typeName + 'ParameterValue' + $typeName + ) + } + } + } +} + +Register-ArgumentCompleter -CommandName Invoke-DscResourceCommand -ParameterName ResourceName -ScriptBlock $dscExeSubCommand \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/tests/QA/module.tests.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/tests/QA/module.tests.ps1 new file mode 100644 index 00000000..439914c0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/tests/QA/module.tests.ps1 @@ -0,0 +1,243 @@ +BeforeDiscovery { + $projectPath = "$($PSScriptRoot)\..\.." | Convert-Path + + <# + If the QA tests are run outside of the build script (e.g with Invoke-Pester) + the parent scope has not set the variable $ProjectName. + #> + if (-not $ProjectName) + { + # Assuming project folder name is project name. + $ProjectName = Get-SamplerProjectName -BuildRoot $projectPath + } + + $script:moduleName = $ProjectName + + Remove-Module -Name $script:moduleName -Force -ErrorAction SilentlyContinue + + $mut = Get-Module -Name $script:moduleName -ListAvailable | + Select-Object -First 1 | + Import-Module -Force -ErrorAction Stop -PassThru +} + +BeforeAll { + # Convert-Path required for PS7 or Join-Path fails + $projectPath = "$($PSScriptRoot)\..\.." | Convert-Path + + <# + If the QA tests are run outside of the build script (e.g with Invoke-Pester) + the parent scope has not set the variable $ProjectName. + #> + if (-not $ProjectName) + { + # Assuming project folder name is project name. + $ProjectName = Get-SamplerProjectName -BuildRoot $projectPath + } + + $script:moduleName = $ProjectName + + $sourcePath = ( + Get-ChildItem -Path $projectPath\*\*.psd1 | + Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) ` + -and $( + try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + } + ) + } + ).Directory.FullName +} + +Describe 'Changelog Management' -Tag 'Changelog' { + It 'Changelog has been updated' -Skip:( + -not ([bool](Get-Command git -ErrorAction SilentlyContinue) -and + [bool](&(Get-Process -Id $PID).Path -NoProfile -Command 'git rev-parse --is-inside-work-tree 2>$null')) + ) { + <# + Get the list of changed files compared with branch main to verify + that required files are changed. + #> + + # Only run if there is a remote called origin + if (((git remote) -match 'origin')) + { + $headCommit = &git rev-parse HEAD + $defaultBranchCommit = &git rev-parse origin/main + $filesChanged = &git @('diff', "$defaultBranchCommit...$headCommit", '--name-only') + } + + $filesStagedAndUnstaged = &git @('diff', 'HEAD', '--name-only') 2>&1 + + $filesChanged += $filesStagedAndUnstaged + + # Only check if there are any changed files. + if ($filesChanged) + { + $filesChanged | Should -Contain 'CHANGELOG.md' -Because 'the CHANGELOG.md must be updated with at least one entry in the Unreleased section for each PR' + } + } + + It 'Changelog format compliant with keepachangelog format' -Skip:(![bool](Get-Command git -EA SilentlyContinue)) { + { Get-ChangelogData -Path (Join-Path $ProjectPath 'CHANGELOG.md') -ErrorAction Stop } | Should -Not -Throw + } + + It 'Changelog should have an Unreleased header' -Skip:$skipTest { + (Get-ChangelogData -Path (Join-Path -Path $ProjectPath -ChildPath 'CHANGELOG.md') -ErrorAction Stop).Unreleased | Should -Not -BeNullOrEmpty + } +} + +Describe 'General module control' -Tags 'FunctionalQuality' { + It 'Should import without errors' { + { Import-Module -Name $script:moduleName -Force -ErrorAction Stop } | Should -Not -Throw + + Get-Module -Name $script:moduleName | Should -Not -BeNullOrEmpty + } + + It 'Should remove without error' { + { Remove-Module -Name $script:moduleName -ErrorAction Stop } | Should -Not -Throw + + Get-Module $script:moduleName | Should -BeNullOrEmpty + } +} + +BeforeDiscovery { + # Must use the imported module to build test cases. + $allModuleFunctions = & $mut { Get-Command -Module $args[0] -CommandType Function } $script:moduleName + + # Build test cases. + $testCases = @() + + foreach ($function in $allModuleFunctions) + { + $testCases += @{ + Name = $function.Name + } + } +} + +Describe 'Quality for module' -Tags 'TestQuality' { + BeforeDiscovery { + if (Get-Command -Name Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue) + { + $scriptAnalyzerRules = Get-ScriptAnalyzerRule + } + else + { + if ($ErrorActionPreference -ne 'Stop') + { + Write-Warning -Message 'ScriptAnalyzer not found!' + } + else + { + throw 'ScriptAnalyzer not found!' + } + } + } + + It 'Should have a unit test for ' -ForEach $testCases { + Get-ChildItem -Path 'tests\' -Recurse -Include "$Name.Tests.ps1" | Should -Not -BeNullOrEmpty + } + + It 'Should pass Script Analyzer for ' -ForEach $testCases -Skip:(-not $scriptAnalyzerRules) { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $pssaResult = (Invoke-ScriptAnalyzer -Path $functionFile.FullName) + $report = $pssaResult | Format-Table -AutoSize | Out-String -Width 110 + $pssaResult | Should -BeNullOrEmpty -Because ` + "some rule triggered.`r`n`r`n $report" + } +} + +Describe 'Help for module' -Tags 'helpQuality' { + It 'Should have .SYNOPSIS for ' -ForEach $testCases { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } + + $parsedFunction = $abstractSyntaxTree.FindAll( $astSearchDelegate, $true ) | + Where-Object -FilterScript { + $_.Name -eq $Name + } + + $functionHelp = $parsedFunction.GetHelpContent() + + $functionHelp.Synopsis | Should -Not -BeNullOrEmpty + } + + It 'Should have a .DESCRIPTION with length greater than 40 characters for ' -ForEach $testCases { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } + + $parsedFunction = $abstractSyntaxTree.FindAll($astSearchDelegate, $true) | + Where-Object -FilterScript { + $_.Name -eq $Name + } + + $functionHelp = $parsedFunction.GetHelpContent() + + $functionHelp.Description.Length | Should -BeGreaterThan 40 + } + + It 'Should have at least one (1) example for ' -ForEach $testCases { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } + + $parsedFunction = $abstractSyntaxTree.FindAll( $astSearchDelegate, $true ) | + Where-Object -FilterScript { + $_.Name -eq $Name + } + + $functionHelp = $parsedFunction.GetHelpContent() + + $functionHelp.Examples.Count | Should -BeGreaterThan 0 + $functionHelp.Examples[0] | Should -Match ([regex]::Escape($function.Name)) + $functionHelp.Examples[0].Length | Should -BeGreaterThan ($function.Name.Length + 10) + + } + + It 'Should have described all parameters for ' -ForEach $testCases { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } + + $parsedFunction = $abstractSyntaxTree.FindAll( $astSearchDelegate, $true ) | + Where-Object -FilterScript { + $_.Name -eq $Name + } + + $functionHelp = $parsedFunction.GetHelpContent() + + $parameters = $parsedFunction.Body.ParamBlock.Parameters.Name.VariablePath.ForEach({ $_.ToString() }) + + foreach ($parameter in $parameters) + { + $functionHelp.Parameters.($parameter.ToUpper()) | Should -Not -BeNullOrEmpty -Because ('the parameter {0} must have a description' -f $parameter) + $functionHelp.Parameters.($parameter.ToUpper()).Length | Should -BeGreaterThan 25 -Because ('the parameter {0} must have descriptive description' -f $parameter) + } + } +} + diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Build-DscConfigurationDocument.tests.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Build-DscConfigurationDocument.tests.ps1 new file mode 100644 index 00000000..a2e8e90e --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Build-DscConfigurationDocument.tests.ps1 @@ -0,0 +1,71 @@ +BeforeAll { + $script:moduleName = 'Microsoft.PowerShell.DSC' + + # If the module is not found, run the build task 'noop'. + if (-not (Get-Module -Name $script:moduleName -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # Re-import the module using force to get any code changes between runs. + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + + Remove-Module -Name $script:moduleName +} + +Describe 'Build-DscConfigurationDocument' { + Context 'Build Desired State Configuration document' { + BeforeAll { + New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') -ItemType File + $content = @' +configuration MyConfiguration { + Import-DscResource -ModuleName PSDesiredStateConfiguration + Node localhost + { + Environment CreatePathEnvironmentVariable + { + Name = 'TestPathEnvironmentVariable' + Value = 'TestValue' + Ensure = 'Present' + Path = $true + Target = @('Process') + } + } +} +'@ + Set-Content -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') -Value $content + } + + AfterAll { + Remove-Item -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') -Recurse -Force + } + # TODO: Timing because of Get-DscResource in function + It 'Should return a valid DSC Configuration Document in JSON' { + InModuleScope -ScriptBlock { + $file = Build-DscConfigurationDocument -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') -Format JSON + $file | Should -Not -BeNullOrEmpty + } + } + + It 'Should return a valid DSC Configuration Document in YAML' { + InModuleScope -ScriptBlock { + $file = Build-DscConfigurationDocument -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') -Format YAML + $file | Should -Not -BeNullOrEmpty + } + } + + It 'Should return a valid DSC Configuration Document in default format' { + InModuleScope -ScriptBlock { + $file = Build-DscConfigurationDocument -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') -Format Default + $file | Should -Not -BeNullOrEmpty + } + } + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Export-DscConfigurationDocument.tests.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Export-DscConfigurationDocument.tests.ps1 new file mode 100644 index 00000000..e69de29b diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Test-PsPathExtension.tests.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Test-PsPathExtension.tests.ps1 new file mode 100644 index 00000000..ed71acc7 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Test-PsPathExtension.tests.ps1 @@ -0,0 +1,67 @@ +BeforeAll { + $script:moduleName = 'Microsoft.PowerShell.DSC' + + # If the module is not found, run the build task 'noop'. + if (-not (Get-Module -Name $script:moduleName -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # Re-import the module using force to get any code changes between runs. + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + + Remove-Module -Name $script:moduleName +} + +Describe 'Test-PsPathExtension' { + Context 'When file paths are correct' { + BeforeAll { + New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') -ItemType File + } + + AfterAll { + Remove-Item -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') -Recurse -Force + } + + It 'Should return PowerShell script file is true' { + InModuleScope -ScriptBlock { + $result = Test-PsPathExtension -Path (Join-Path -Path $TestDrive -ChildPath 'test.ps1') + + $result | Should -BeTrue + } + } + } + + Context 'When file paths are incorrect' { + BeforeAll { + New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'test.psm1') -ItemType File + } + + AfterAll { + Remove-Item -Path (Join-Path -Path $TestDrive -ChildPath 'test.psm1') -Recurse -Force + } + + It 'Should return PowerShell script file is false because it is a PowerShell module file' { + InModuleScope -ScriptBlock { + $result = Test-PsPathExtension -Path (Join-Path -Path $TestDrive -ChildPath 'test.psm1') + + $result | Should -BeFalse + } + } + + It 'Should return false because file path does not exist' { + InModuleScope -ScriptBlock { + $result = Test-PsPathExtension -Path (Join-Path -Path $TestDrive -ChildPath 'thisdoesnotexist.txt') + + $result | Should -BeFalse + } + } + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Test-YamlModule.tests.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Test-YamlModule.tests.ps1 new file mode 100644 index 00000000..a4ddfbe7 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Test-YamlModule.tests.ps1 @@ -0,0 +1,33 @@ +BeforeAll { + # TODO: Find way how to install / uninstall powershell-yaml or unload from current session + $script:moduleName = 'Microsoft.PowerShell.DSC' + + # If the module is not found, run the build task 'noop'. + if (-not (Get-Module -Name $script:moduleName -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # Re-import the module using force to get any code changes between runs. + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + + Remove-Module -Name $script:moduleName +} + +Describe 'Test-YamlModule' { + Context 'When the module is found' { + It 'Should return true because module exist' { + InModuleScope -ScriptBlock { + $result = Test-YamlModule + $result | Should -BeTrue + } + } + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/ConvertTo-DscJson.tests.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/ConvertTo-DscJson.tests.ps1 new file mode 100644 index 00000000..e69de29b diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/ConvertTo-DscYaml.tests.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/ConvertTo-DscYaml.tests.ps1 new file mode 100644 index 00000000..e69de29b diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/Invoke-DscResourceCommand.tests.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/Invoke-DscResourceCommand.tests.ps1 new file mode 100644 index 00000000..0351b1c0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/Invoke-DscResourceCommand.tests.ps1 @@ -0,0 +1 @@ +Describe 'Invoke-DscResourceCommand' {} \ No newline at end of file diff --git a/test.ps1 b/test.ps1 new file mode 100644 index 00000000..d376e184 --- /dev/null +++ b/test.ps1 @@ -0,0 +1,12 @@ +Configuration MyConfiguration { + Import-DscResource -ModuleName PSDesiredStateConfiguration + node localhost { + Environment CreatePathEnvironmentVariable { + Name = 'TestPathEnvironmentVariable' + Value = 'TestValue' + Ensure = 'Present' + Path = $true + Target = @('Process') + } + } +} \ No newline at end of file