From 3d98c62a242c445980b8e095904c7997c7006385 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 24 Jul 2024 13:22:19 +0200 Subject: [PATCH 1/6] DSC Configuration Migration Tool module --- powershell-helpers/README.md | 13 + powershell-helpers/dscCfgMigMod.psd1 | 47 +++ powershell-helpers/dscCfgMigMod.psm1 | 384 ++++++++++++++++++ .../tests/dscCfgMigMod.tests.ps1 | 24 ++ 4 files changed, 468 insertions(+) create mode 100644 powershell-helpers/README.md create mode 100644 powershell-helpers/dscCfgMigMod.psd1 create mode 100644 powershell-helpers/dscCfgMigMod.psm1 create mode 100644 powershell-helpers/tests/dscCfgMigMod.tests.ps1 diff --git a/powershell-helpers/README.md b/powershell-helpers/README.md new file mode 100644 index 00000000..d48f4e87 --- /dev/null +++ b/powershell-helpers/README.md @@ -0,0 +1,13 @@ +# Introduction + +The `powershell-adapters` folder contains helper modules that can be loaded into your PowerShell session to assist you in familiarizing yourself with new DSC concepts. To see the availability of helper modules, see the following list: + +- **DSC Configuration Migration Module**: - Aids in the assistance of grabbing configuration documents written in PowerShell code and transform them to valid configuration documents for the DSC version 3 core engine (e.g. YAML or JSON). + +## Getting started + +To get started using the helper modules, you can follow the below steps. This example uses the _DSC Configuration Migration Tool_ to be loaded into the session: + +1. Open a PowerShell terminal session +2. Execute the following command: `Import-Module "powershell-helpers\dscConfigurationMigrationTool.psm1"` +3. Discover examples using: `Get-Help ConvertTo-DscYaml` diff --git a/powershell-helpers/dscCfgMigMod.psd1 b/powershell-helpers/dscCfgMigMod.psd1 new file mode 100644 index 00000000..83d1ac09 --- /dev/null +++ b/powershell-helpers/dscCfgMigMod.psd1 @@ -0,0 +1,47 @@ +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'dscCfgMigMod.psm1' + + # Version number of this module. + moduleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = '42bf8cb0-210c-4dac-8614-319d9287c6dc' + + # 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 Migration Module helper' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('powershell-yaml') + + # 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 = @( + 'ConvertTo-DscJson' + 'ConvertTo-DscYaml' + ) + + # 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 = @() + + PrivateData = @{ + PSData = @{ + ProjectUri = 'https://github.com/PowerShell/dsc' + } + } +} diff --git a/powershell-helpers/dscCfgMigMod.psm1 b/powershell-helpers/dscCfgMigMod.psm1 new file mode 100644 index 00000000..c61ff4bf --- /dev/null +++ b/powershell-helpers/dscCfgMigMod.psm1 @@ -0,0 +1,384 @@ +#region Main functions +function ConvertTo-DscJson +{ + <# + .SYNOPSIS + Convert a PowerShell DSC configuration document to DSC version 3 JSON format. + + .DESCRIPTION + The function ConvertTo-DscJson converts a PowerShell DSC configuration document to DSC version 3 JSON format from a path. + + .PARAMETER Path + The path to valid PowerShell DSC configuration document + + .EXAMPLE + PS C:\> $configuration = @' + Configuration TestResource { + Import-DscResource -ModuleName TestResource + Node localhost { + TestResource 'Configure test resource' { + Ensure = 'Absent' + Name = 'MyTestResource' + } + } + } + '@ + PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' + PS C:\> $configuration | Out-File -FilePath $Path + PS C:\> ConvertTo-DscJson -Path $Path + + Returns: + { + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", + "resources": { + "name": "TestResource", + "type": "Microsoft.DSC/PowerShell", + "properties": { + "resources": [ + { + "name": "Configure test resource", + "type": "TestResource/TestResource", + "properties": { + "Name": "MyTestResource", + "Ensure": "Absent" + } + } + ] + } + } + } + + .NOTES + Tags: DSC, Migration, JSON + #> + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + begin + { + Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) + } + + process + { + $inputObject = BuildConfigurationDocument -Path $Path + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} + +function ConvertTo-DscYaml +{ + <# + .SYNOPSIS + Convert a PowerShell DSC configuration document to DSC version 3 YAML format. + + .DESCRIPTION + The function ConvertTo-DscYaml converts a PowerShell DSC configuration document to DSC version 3 YAML format from a path. + + .PARAMETER Path + The path to valid PowerShell DSC configuration document + + .EXAMPLE + PS C:\> $configuration = @' + Configuration TestResource { + Import-DscResource -ModuleName TestResource + Node localhost { + TestResource 'Configure test resource' { + Ensure = 'Absent' + Name = 'MyTestResource' + } + } + } + '@ + PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' + PS C:\> $configuration | Out-File -FilePath $Path + PS C:\> ConvertTo-DscYaml -Path $Path + + Returns: + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + name: TestResource + type: Microsoft.DSC/PowerShell + properties: + resources: + - name: Configure test resource + type: TestResource/TestResource + properties: + Name: MyTestResource + Ensure: Absent + + .NOTES + Tags: DSC, Migration, YAML + #> + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + begin + { + Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) + } + + process + { + $inputObject = BuildConfigurationDocument -Path $Path -Format YAML + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} +#endRegion Main functions + +#region Helper functions +function FindAndExtractConfigurationDocument +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + if (-not (TestPathExtension $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 + $configurationDocument.type = 'Microsoft.DSC/PowerShell' # TODO: Add functions later to valid the adapter type + + # 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) + # TODO: Might have to change because it takes time. If there is only one Import-DscResource statement, we can simply RegEx it out, else use Get-DscResource + # $document.ModuleName = $module.ModuleName + + 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 +} + +function BuildConfigurationDocument +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [ValidateSet('JSON', 'YAML')] + [System.String] + $Format = 'JSON' + ) + + $configurationDocument = [ordered]@{ + "`$schema" = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json" # TODO: Figure out how to extract latest document.json from schemas folder + resources = FindAndExtractConfigurationDocument -Path $Path + } + + switch ($Format) + { + "JSON" { + $inputObject = ($configurationDocument | ConvertTo-Json -Depth 10) + } + "YAML" { + if (TestYamlModule) + { + $inputObject = ($configurationDocument | ConvertTo-Yaml) + } + else + { + $inputObject = @{} + } + } + default { + $inputObject = $configurationDocument + } + } + + return $inputObject +} + +function TestPathExtension +{ + [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 +} + +function TestYamlModule +{ + if (-not (Get-Command -Name 'ConvertTo-Yaml' -ErrorAction SilentlyContinue)) + { + return $false + } + + return $true +} + +function GetPowerShellPath +{ + param + ( + $Path + ) + + $knownPath = @( + "$env:USERPROFILE\Documents\PowerShell\Modules", + "$env:ProgramFiles\PowerShell\Modules", + "$env:ProgramFiles\PowerShell\7\Modules" + ) + + foreach ($known in $knownPath) + { + if ($Path.StartsWith($known)) + { + return $true + } + } + + return $false +} + +function GetWindowsPowerShellPath +{ + param + ( + $Path + ) + + $knownPath = @( + "$env:USERPROFILE\Documents\WindowsPowerShell\Modules", + "$env:ProgramFiles\WindowsPowerShell\Modules", + "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules" + ) + + foreach ($known in $knownPath) + { + if ($Path.StartsWith($known)) + { + return $true + } + } + + return $false +} + +function ResolvePowerShellPath +{ + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + if (-not (Test-Path $Path)) + { + return + } + + if (([System.IO.Path]::GetExtension($Path) -ne ".psm1")) + { + return + } + + if (GetPowerShellPath -Path $Path) + { + return "Microsoft.DSC/PowerShell" + } + + if (GetWindowsPowerShellPath -Path $Path) + { + return "Microsoft.Windows/WindowsPowerShell" + } + + return $null # TODO: Or default Microsoft.DSC/PowerShell +} + +#endRegion Helper functions + +#region Classes +class DscConfigurationResource +{ + [string] $name + [string] $type + [hashtable] $properties +} +#endRegion classes \ No newline at end of file diff --git a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 new file mode 100644 index 00000000..b966993a --- /dev/null +++ b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 @@ -0,0 +1,24 @@ +Describe "DSC Configuration Migration Module tests" { + BeforeAll { + $modPath = (Resolve-Path -Path "$PSScriptRoot\..\dscCfgMigMod.psd1").Path + $modLoad = Import-Module $modPath -Force -PassThru + } + + Context "ConvertTo-DscYaml" { + It "Should create an empty resource block" { + $res = (ConvertTo-DscYaml -Path 'idonotexist' | ConvertFrom-Yaml) + $res.resources | Should -BeNullOrEmpty + } + } + + Context "ConvertTo-DscJson" { + It "Should create an empty resource block" { + $res = (ConvertTo-DscJson -Path 'idonotexist' | ConvertFrom-Json) + $res.resources | Should -BeNullOrEmpty + } + } + + AfterAll { + Remove-Module -Name $modLoad.Name -Force + } +} From 9443e1cdd619f85cdb29606093b449e65cd67c6e Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Thu, 25 Jul 2024 17:11:07 +0200 Subject: [PATCH 2/6] Add class-based operation methods in DSCResourceInfo --- .../psDscAdapter/psDscAdapter.psm1 | 270 +++++++++++++----- 1 file changed, 196 insertions(+), 74 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index 07b3763e..f31f672d 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -22,25 +22,19 @@ function Import-PSDSCModule { $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru } -function Get-DSCResourceModules -{ +function Get-DSCResourceModules { $listPSModuleFolders = $env:PSModulePath.Split([IO.Path]::PathSeparator) $dscModulePsd1List = [System.Collections.Generic.HashSet[System.String]]::new() - foreach ($folder in $listPSModuleFolders) - { - if (!(Test-Path $folder)) - { + foreach ($folder in $listPSModuleFolders) { + if (!(Test-Path $folder)) { continue } - foreach($moduleFolder in Get-ChildItem $folder -Directory) - { + foreach ($moduleFolder in Get-ChildItem $folder -Directory) { $addModule = $false - foreach($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) - { + foreach ($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) { $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' - if($null -ne $containsDSCResource) - { + if ($null -ne $containsDSCResource) { $dscModulePsd1List.Add($psd1) | Out-Null } } @@ -57,39 +51,32 @@ function Add-AstMembers { $Properties ) - foreach($TypeConstraint in $TypeAst.BaseTypes) { - $t = $AllTypeDefinitions | Where-Object {$_.Name -eq $TypeConstraint.TypeName.Name} + foreach ($TypeConstraint in $TypeAst.BaseTypes) { + $t = $AllTypeDefinitions | Where-Object { $_.Name -eq $TypeConstraint.TypeName.Name } if ($t) { Add-AstMembers $AllTypeDefinitions $t $Properties } } - foreach ($member in $TypeAst.Members) - { + foreach ($member in $TypeAst.Members) { $property = $member -as [System.Management.Automation.Language.PropertyMemberAst] - if (($property -eq $null) -or ($property.IsStatic)) - { + if (($property -eq $null) -or ($property.IsStatic)) { continue; } $skipProperty = $true $isKeyProperty = $false - foreach($attr in $property.Attributes) - { - if ($attr.TypeName.Name -eq 'DscProperty') - { + foreach ($attr in $property.Attributes) { + if ($attr.TypeName.Name -eq 'DscProperty') { $skipProperty = $false - foreach($attrArg in $attr.NamedArguments) - { - if ($attrArg.ArgumentName -eq 'Key') - { + foreach ($attrArg in $attr.NamedArguments) { + if ($attrArg.ArgumentName -eq 'Key') { $isKeyProperty = $true break } } } } - if ($skipProperty) - { + if ($skipProperty) { continue; } @@ -101,8 +88,7 @@ function Add-AstMembers { } } -function FindAndParseResourceDefinitions -{ +function FindAndParseResourceDefinitions { [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -111,13 +97,11 @@ function FindAndParseResourceDefinitions [string]$moduleVersion ) - if (-not (Test-Path $filePath)) - { + if (-not (Test-Path $filePath)) { return } - if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) - { + if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) { return } @@ -126,8 +110,7 @@ function FindAndParseResourceDefinitions [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$tokens, [ref]$errors) - foreach($e in $errors) - { + foreach ($e in $errors) { $e | Out-String | Write-DscTrace -Operation Error } @@ -140,12 +123,9 @@ function FindAndParseResourceDefinitions $resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new() - foreach($typeDefinitionAst in $typeDefinitions) - { - foreach($a in $typeDefinitionAst.Attributes) - { - if ($a.TypeName.Name -eq 'DscResource') - { + foreach ($typeDefinitionAst in $typeDefinitions) { + foreach ($a in $typeDefinitionAst.Attributes) { + if ($a.TypeName.Name -eq 'DscResource') { $DscResourceInfo = [DscResourceInfo]::new() $DscResourceInfo.Name = $typeDefinitionAst.Name $DscResourceInfo.ResourceType = $typeDefinitionAst.Name @@ -157,8 +137,10 @@ function FindAndParseResourceDefinitions $DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) $DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath) $DscResourceInfo.Version = $moduleVersion + $DscResourceInfo.Operations = GetResourceOperationMethods -resourceName $typeDefinitionAst.Name -filePath $filePath $DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new() + Add-AstMembers $typeDefinitions $typeDefinitionAst $DscResourceInfo.Properties $resourceList.Add($DscResourceInfo) @@ -169,8 +151,7 @@ function FindAndParseResourceDefinitions return $resourceList } -function LoadPowerShellClassResourcesFromModule -{ +function LoadPowerShellClassResourcesFromModule { [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -179,29 +160,24 @@ function LoadPowerShellClassResourcesFromModule "Loading resources from module '$($moduleInfo.Path)'" | Write-DscTrace -Operation Trace - if ($moduleInfo.RootModule) - { - if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and + if ($moduleInfo.RootModule) { + if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and ([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".ps1") -and - (-not $z.NestedModules)) - { + (-not $z.NestedModules)) { "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" | Write-DscTrace -Operation Trace return [System.Collections.Generic.List[DscResourceInfo]]::new() } $scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule } - else - { + else { $scriptPath = $moduleInfo.Path; } $Resources = FindAndParseResourceDefinitions $scriptPath $moduleInfo.Version - if ($moduleInfo.NestedModules) - { - foreach ($nestedModule in $moduleInfo.NestedModules) - { + if ($moduleInfo.NestedModules) { + foreach ($nestedModule in $moduleInfo.NestedModules) { $resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule if ($resourcesOfNestedModules) { $Resources.AddRange($resourcesOfNestedModules) @@ -212,6 +188,153 @@ function LoadPowerShellClassResourcesFromModule return $Resources } +function GetResourceOperationMethods { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $resourceName, + + [Parameter(Mandatory = $true)] + [string] $filePath + ) + + # dot source scope + try { + . (LoadClassAndEnumsFromModuleFile -filePath $filePath) + } catch { + ("Module: '{0}' not loaded for resource operation discovery."-f $filePath) | Write-DscTrace + } + + $inputObject = ReturnTypeNameObject -TypeName $resourceName + + if (-not $inputObject) { + return @( + 'Get', + 'Test', + 'Set' + ) + } + + # TODO: There might be more properties available + $knownMemberTypes = @('Equals', 'GetHashCode', 'GetType', 'ToString') + return ($inputObject | Get-Member | Where-Object { $_.MemberType -eq 'Method' -and $_.Name -notin $knownMemberTypes }).Name +} + +function ReturnTypeNameObject { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $TypeName + ) + + try { + $inputObject = New-Object -TypeName $TypeName -ErrorAction Stop + } + catch { + "Could not create: $TypeName" | Write-DscTrace + } + + return $inputObject +} + +function LoadClassAndEnumsFromModuleFile { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $filePath + ) + + if (-not (Test-Path $filePath -ErrorAction SilentlyContinue)) { + return + } + + $ctx = Get-Content $filePath + + $string = @( + 'using namespace System.Collections.Generic', # TODO: Figure away out to get using statements included + (GetEnumCodeBlock -Content $ctx), + (GetClassCodeBlock -Content $ctx) + ) + + # TODO: Might have to do something with the path + $outPath = Join-Path -Path $env:TEMP -ChildPath ("{0}.ps1" -f [System.Guid]::NewGuid().Guid) + $string | Out-File -FilePath $outPath + + return $outPath +} + +function GetClassCodeBlock { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content + ) + + $ctx = $Content + + $lines = ($ctx | Select-String -Pattern '\[DSCResource\(\)]').LineNumber + if ($lines.Count -eq 0 ) { + return + } + + $lastLineNumber = $lines[-1] + $index = 1 + # Bring all class strings together after the last one + $classStrings = foreach ($line in $lines) { + if ($line -eq $lastLineNumber) { + $lastModuleLine = $ctx.Length + + $line = $line - 1 + $block = $ctx[$line..$lastModuleLine] + $block + break + } + + $line = $line - 1 + $curlyBracketLine = FindCurlyBracket -Content $ctx -LineNumber $lines[$index] + $block = $ctx[$line..$curlyBracketLine] + + $index++ + $block + } + + return $classStrings +} + +function GetEnumCodeBlock { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content + ) + + # Build regex to catch enum blocks + $regex = [regex]::new('enum\s+(\w+)\s*\{([^}]+)\}') + + $hits = $regex.Matches($Content) + + # return as single lines + return ($hits.Value -Split " ") +} + +function FindCurlyBracket { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content, + + [Parameter(Mandatory = $true)] + [int] $LineNumber + ) + do { + if ($Content[$LineNumber] -eq "}") { + return $LineNumber + } + + $LineNumber-- + } while ($LineNumber -ne 0) +} + <# public function Invoke-DscCacheRefresh .SYNOPSIS This function caches the results of the Get-DscResource call to optimize performance. @@ -237,7 +360,8 @@ function Invoke-DscCacheRefresh { $cacheFilePath = if ($IsWindows) { # PS 6+ on Windows Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } else { + } + else { # PS 6+ on Linux/Mac Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } @@ -249,8 +373,9 @@ function Invoke-DscCacheRefresh { if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompatible version of cache in file '"+$cache.CacheSchemaVersion+"' (expected '"+$script:CurrentCacheSchemaVersion+"')" | Write-DscTrace - } else { + "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace + } + else { $dscResourceCacheEntries = $cache.ResourceCache if ($dscResourceCacheEntries.Count -eq 0) { @@ -259,8 +384,7 @@ function Invoke-DscCacheRefresh { "Filtered DscResourceCache cache is empty" | Write-DscTrace } - else - { + else { "Checking cache for stale entries" | Write-DscTrace foreach ($cacheEntry in $dscResourceCacheEntries) { @@ -268,20 +392,19 @@ function Invoke-DscCacheRefresh { $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { - if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) - { + if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) { "Detected stale cache entry '$($_.Name)'" | Write-DscTrace $refreshCache = $true break } } - if ($refreshCache) {break} + if ($refreshCache) { break } } "Checking cache for stale PSModulePath" | Write-DscTrace - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) @@ -309,11 +432,10 @@ function Invoke-DscCacheRefresh { $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() $dscResourceModulePsd1s = Get-DSCResourceModules - if($null -ne $dscResourceModulePsd1s) { + if ($null -ne $dscResourceModulePsd1s) { $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) $processedModuleNames = @{} - foreach ($mod in $modules) - { + foreach ($mod in $modules) { if (-not ($processedModuleNames.ContainsKey($mod.Name))) { $processedModuleNames.Add($mod.Name, $true) @@ -337,20 +459,20 @@ function Invoke-DscCacheRefresh { # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} - Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1","*.psd1","*psm1","*.mof" -ea Ignore | % { + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*psm1", "*.mof" -ea Ignore | % { $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) } $dscResourceCacheEntries += [dscResourceCacheEntry]@{ Type = "$moduleName/$($dscResource.Name)" DscResourceInfo = $dscResource - LastWriteTimes = $lastWriteTimes + LastWriteTimes = $lastWriteTimes } } [dscResourceCache]$cache = [dscResourceCache]::new() $cache.ResourceCache = $dscResourceCacheEntries - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } $cache.PSModulePaths = $m.FullName $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion @@ -462,12 +584,12 @@ function Invoke-DscOperation { } 'Test' { $Result = $dscResourceInstance.Test() - $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} + $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } } 'Export' { $t = $dscResourceInstance.GetType() $method = $t.GetMethod('Export') - $resultArray = $method.Invoke($null,$null) + $resultArray = $method.Invoke($null, $null) $addToActualState = $resultArray } } @@ -534,8 +656,7 @@ enum dscResourceType { Composite } -class DscResourcePropertyInfo -{ +class DscResourcePropertyInfo { [string] $Name [string] $PropertyType [bool] $IsMandatory @@ -556,4 +677,5 @@ class DscResourceInfo { [string] $ImplementedAs [string] $CompanyName [System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties + [System.String[]] $Operations } From 39c57b3067855607fe8cc67ef4c02d5f78163fc2 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Fri, 26 Jul 2024 05:31:49 +0200 Subject: [PATCH 3/6] Revert "Add class-based operation methods in DSCResourceInfo" This reverts commit 9443e1cdd619f85cdb29606093b449e65cd67c6e. --- .../psDscAdapter/psDscAdapter.psm1 | 270 +++++------------- 1 file changed, 74 insertions(+), 196 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index f31f672d..07b3763e 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -22,19 +22,25 @@ function Import-PSDSCModule { $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru } -function Get-DSCResourceModules { +function Get-DSCResourceModules +{ $listPSModuleFolders = $env:PSModulePath.Split([IO.Path]::PathSeparator) $dscModulePsd1List = [System.Collections.Generic.HashSet[System.String]]::new() - foreach ($folder in $listPSModuleFolders) { - if (!(Test-Path $folder)) { + foreach ($folder in $listPSModuleFolders) + { + if (!(Test-Path $folder)) + { continue } - foreach ($moduleFolder in Get-ChildItem $folder -Directory) { + foreach($moduleFolder in Get-ChildItem $folder -Directory) + { $addModule = $false - foreach ($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) { + foreach($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) + { $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' - if ($null -ne $containsDSCResource) { + if($null -ne $containsDSCResource) + { $dscModulePsd1List.Add($psd1) | Out-Null } } @@ -51,32 +57,39 @@ function Add-AstMembers { $Properties ) - foreach ($TypeConstraint in $TypeAst.BaseTypes) { - $t = $AllTypeDefinitions | Where-Object { $_.Name -eq $TypeConstraint.TypeName.Name } + foreach($TypeConstraint in $TypeAst.BaseTypes) { + $t = $AllTypeDefinitions | Where-Object {$_.Name -eq $TypeConstraint.TypeName.Name} if ($t) { Add-AstMembers $AllTypeDefinitions $t $Properties } } - foreach ($member in $TypeAst.Members) { + foreach ($member in $TypeAst.Members) + { $property = $member -as [System.Management.Automation.Language.PropertyMemberAst] - if (($property -eq $null) -or ($property.IsStatic)) { + if (($property -eq $null) -or ($property.IsStatic)) + { continue; } $skipProperty = $true $isKeyProperty = $false - foreach ($attr in $property.Attributes) { - if ($attr.TypeName.Name -eq 'DscProperty') { + foreach($attr in $property.Attributes) + { + if ($attr.TypeName.Name -eq 'DscProperty') + { $skipProperty = $false - foreach ($attrArg in $attr.NamedArguments) { - if ($attrArg.ArgumentName -eq 'Key') { + foreach($attrArg in $attr.NamedArguments) + { + if ($attrArg.ArgumentName -eq 'Key') + { $isKeyProperty = $true break } } } } - if ($skipProperty) { + if ($skipProperty) + { continue; } @@ -88,7 +101,8 @@ function Add-AstMembers { } } -function FindAndParseResourceDefinitions { +function FindAndParseResourceDefinitions +{ [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -97,11 +111,13 @@ function FindAndParseResourceDefinitions { [string]$moduleVersion ) - if (-not (Test-Path $filePath)) { + if (-not (Test-Path $filePath)) + { return } - if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) { + if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) + { return } @@ -110,7 +126,8 @@ function FindAndParseResourceDefinitions { [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$tokens, [ref]$errors) - foreach ($e in $errors) { + foreach($e in $errors) + { $e | Out-String | Write-DscTrace -Operation Error } @@ -123,9 +140,12 @@ function FindAndParseResourceDefinitions { $resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new() - foreach ($typeDefinitionAst in $typeDefinitions) { - foreach ($a in $typeDefinitionAst.Attributes) { - if ($a.TypeName.Name -eq 'DscResource') { + foreach($typeDefinitionAst in $typeDefinitions) + { + foreach($a in $typeDefinitionAst.Attributes) + { + if ($a.TypeName.Name -eq 'DscResource') + { $DscResourceInfo = [DscResourceInfo]::new() $DscResourceInfo.Name = $typeDefinitionAst.Name $DscResourceInfo.ResourceType = $typeDefinitionAst.Name @@ -137,10 +157,8 @@ function FindAndParseResourceDefinitions { $DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) $DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath) $DscResourceInfo.Version = $moduleVersion - $DscResourceInfo.Operations = GetResourceOperationMethods -resourceName $typeDefinitionAst.Name -filePath $filePath $DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new() - Add-AstMembers $typeDefinitions $typeDefinitionAst $DscResourceInfo.Properties $resourceList.Add($DscResourceInfo) @@ -151,7 +169,8 @@ function FindAndParseResourceDefinitions { return $resourceList } -function LoadPowerShellClassResourcesFromModule { +function LoadPowerShellClassResourcesFromModule +{ [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -160,24 +179,29 @@ function LoadPowerShellClassResourcesFromModule { "Loading resources from module '$($moduleInfo.Path)'" | Write-DscTrace -Operation Trace - if ($moduleInfo.RootModule) { - if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and + if ($moduleInfo.RootModule) + { + if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and ([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".ps1") -and - (-not $z.NestedModules)) { + (-not $z.NestedModules)) + { "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" | Write-DscTrace -Operation Trace return [System.Collections.Generic.List[DscResourceInfo]]::new() } $scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule } - else { + else + { $scriptPath = $moduleInfo.Path; } $Resources = FindAndParseResourceDefinitions $scriptPath $moduleInfo.Version - if ($moduleInfo.NestedModules) { - foreach ($nestedModule in $moduleInfo.NestedModules) { + if ($moduleInfo.NestedModules) + { + foreach ($nestedModule in $moduleInfo.NestedModules) + { $resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule if ($resourcesOfNestedModules) { $Resources.AddRange($resourcesOfNestedModules) @@ -188,153 +212,6 @@ function LoadPowerShellClassResourcesFromModule { return $Resources } -function GetResourceOperationMethods { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $resourceName, - - [Parameter(Mandatory = $true)] - [string] $filePath - ) - - # dot source scope - try { - . (LoadClassAndEnumsFromModuleFile -filePath $filePath) - } catch { - ("Module: '{0}' not loaded for resource operation discovery."-f $filePath) | Write-DscTrace - } - - $inputObject = ReturnTypeNameObject -TypeName $resourceName - - if (-not $inputObject) { - return @( - 'Get', - 'Test', - 'Set' - ) - } - - # TODO: There might be more properties available - $knownMemberTypes = @('Equals', 'GetHashCode', 'GetType', 'ToString') - return ($inputObject | Get-Member | Where-Object { $_.MemberType -eq 'Method' -and $_.Name -notin $knownMemberTypes }).Name -} - -function ReturnTypeNameObject { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $TypeName - ) - - try { - $inputObject = New-Object -TypeName $TypeName -ErrorAction Stop - } - catch { - "Could not create: $TypeName" | Write-DscTrace - } - - return $inputObject -} - -function LoadClassAndEnumsFromModuleFile { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $filePath - ) - - if (-not (Test-Path $filePath -ErrorAction SilentlyContinue)) { - return - } - - $ctx = Get-Content $filePath - - $string = @( - 'using namespace System.Collections.Generic', # TODO: Figure away out to get using statements included - (GetEnumCodeBlock -Content $ctx), - (GetClassCodeBlock -Content $ctx) - ) - - # TODO: Might have to do something with the path - $outPath = Join-Path -Path $env:TEMP -ChildPath ("{0}.ps1" -f [System.Guid]::NewGuid().Guid) - $string | Out-File -FilePath $outPath - - return $outPath -} - -function GetClassCodeBlock { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content - ) - - $ctx = $Content - - $lines = ($ctx | Select-String -Pattern '\[DSCResource\(\)]').LineNumber - if ($lines.Count -eq 0 ) { - return - } - - $lastLineNumber = $lines[-1] - $index = 1 - # Bring all class strings together after the last one - $classStrings = foreach ($line in $lines) { - if ($line -eq $lastLineNumber) { - $lastModuleLine = $ctx.Length - - $line = $line - 1 - $block = $ctx[$line..$lastModuleLine] - $block - break - } - - $line = $line - 1 - $curlyBracketLine = FindCurlyBracket -Content $ctx -LineNumber $lines[$index] - $block = $ctx[$line..$curlyBracketLine] - - $index++ - $block - } - - return $classStrings -} - -function GetEnumCodeBlock { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content - ) - - # Build regex to catch enum blocks - $regex = [regex]::new('enum\s+(\w+)\s*\{([^}]+)\}') - - $hits = $regex.Matches($Content) - - # return as single lines - return ($hits.Value -Split " ") -} - -function FindCurlyBracket { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content, - - [Parameter(Mandatory = $true)] - [int] $LineNumber - ) - do { - if ($Content[$LineNumber] -eq "}") { - return $LineNumber - } - - $LineNumber-- - } while ($LineNumber -ne 0) -} - <# public function Invoke-DscCacheRefresh .SYNOPSIS This function caches the results of the Get-DscResource call to optimize performance. @@ -360,8 +237,7 @@ function Invoke-DscCacheRefresh { $cacheFilePath = if ($IsWindows) { # PS 6+ on Windows Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } - else { + } else { # PS 6+ on Linux/Mac Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } @@ -373,9 +249,8 @@ function Invoke-DscCacheRefresh { if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace - } - else { + "Incompatible version of cache in file '"+$cache.CacheSchemaVersion+"' (expected '"+$script:CurrentCacheSchemaVersion+"')" | Write-DscTrace + } else { $dscResourceCacheEntries = $cache.ResourceCache if ($dscResourceCacheEntries.Count -eq 0) { @@ -384,7 +259,8 @@ function Invoke-DscCacheRefresh { "Filtered DscResourceCache cache is empty" | Write-DscTrace } - else { + else + { "Checking cache for stale entries" | Write-DscTrace foreach ($cacheEntry in $dscResourceCacheEntries) { @@ -392,19 +268,20 @@ function Invoke-DscCacheRefresh { $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { - if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) { + if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) + { "Detected stale cache entry '$($_.Name)'" | Write-DscTrace $refreshCache = $true break } } - if ($refreshCache) { break } + if ($refreshCache) {break} } "Checking cache for stale PSModulePath" | Write-DscTrace - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) @@ -432,10 +309,11 @@ function Invoke-DscCacheRefresh { $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() $dscResourceModulePsd1s = Get-DSCResourceModules - if ($null -ne $dscResourceModulePsd1s) { + if($null -ne $dscResourceModulePsd1s) { $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) $processedModuleNames = @{} - foreach ($mod in $modules) { + foreach ($mod in $modules) + { if (-not ($processedModuleNames.ContainsKey($mod.Name))) { $processedModuleNames.Add($mod.Name, $true) @@ -459,20 +337,20 @@ function Invoke-DscCacheRefresh { # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} - Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*psm1", "*.mof" -ea Ignore | % { + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1","*.psd1","*psm1","*.mof" -ea Ignore | % { $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) } $dscResourceCacheEntries += [dscResourceCacheEntry]@{ Type = "$moduleName/$($dscResource.Name)" DscResourceInfo = $dscResource - LastWriteTimes = $lastWriteTimes + LastWriteTimes = $lastWriteTimes } } [dscResourceCache]$cache = [dscResourceCache]::new() $cache.ResourceCache = $dscResourceCacheEntries - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} $cache.PSModulePaths = $m.FullName $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion @@ -584,12 +462,12 @@ function Invoke-DscOperation { } 'Test' { $Result = $dscResourceInstance.Test() - $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } + $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} } 'Export' { $t = $dscResourceInstance.GetType() $method = $t.GetMethod('Export') - $resultArray = $method.Invoke($null, $null) + $resultArray = $method.Invoke($null,$null) $addToActualState = $resultArray } } @@ -656,7 +534,8 @@ enum dscResourceType { Composite } -class DscResourcePropertyInfo { +class DscResourcePropertyInfo +{ [string] $Name [string] $PropertyType [bool] $IsMandatory @@ -677,5 +556,4 @@ class DscResourceInfo { [string] $ImplementedAs [string] $CompanyName [System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties - [System.String[]] $Operations } From 76396c2c36d0237e73a07679809b01f4c4b874fe Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 31 Jul 2024 08:50:52 +0200 Subject: [PATCH 4/6] Set up Microsoft.PowerShell.DSC --- .vscode/settings.json | 3 +- operation-methods | 1 + .../Microsoft.PowerShell.DSC/.gitattributes | 15 + .../Microsoft.PowerShell.DSC/.gitignore | 17 + .../.vscode/tasks.json | 125 +++ .../Microsoft.PowerShell.DSC/CHANGELOG.md | 31 + .../Microsoft.PowerShell.DSC/GitVersion.yml | 40 + .../Microsoft.PowerShell.DSC/README.md | 13 + .../RequiredModules.psd1 | 20 + .../Resolve-Dependency.ps1 | 919 ++++++++++++++++++ .../Resolve-Dependency.psd1 | 49 + .../Microsoft.PowerShell.DSC/build.ps1 | 538 ++++++++++ .../Microsoft.PowerShell.DSC/build.yaml | 150 +++ .../Classes/DscConfigurationResource.ps1 | 6 + .../source/Microsoft.PowerShell.DSC.psd1 | 143 +++ .../source/Microsoft.PowerShell.DSC.psm1 | 5 + .../Build-DscConfigurationDocument.ps1 | 41 + .../source/Private/Build-DscPathBuilder.ps1 | 64 ++ .../Export-DscConfigurationDocument.ps1 | 70 ++ .../source/Private/Get-DscCommandData.ps1 | 106 ++ .../Get-DscResourceConfigurationDocument.ps1 | 46 + .../Private/Get-DscResourceSchemaProperty.ps1 | 90 ++ .../Private/Get-EnvironmentVariable.ps1 | 95 ++ .../Private/Get-ProcessObjectResult.ps1 | 51 + .../source/Private/Get-ProcessOutput.ps1 | 90 ++ .../source/Private/Get-ResourceSchema.ps1 | 0 .../source/Private/Invoke-DscExe.ps1 | 25 + .../Private/New-EnvironmentVariableObject.ps1 | 34 + .../source/Private/New-SubCommand.ps1 | 15 + .../source/Private/Resolve-DscExe.ps1 | 39 + .../Set-DscResourceConfigurationDocument.ps1 | 47 + .../Test-DscResourceConfigurationDocument.ps1 | 47 + .../source/Private/Test-DscResourceName.ps1 | 23 + .../source/Private/Test-PsPathExtension.ps1 | 24 + .../source/Private/Test-YamlModule.ps1 | 9 + .../source/Public/ConvertTo-DscJson.ps1 | 87 ++ .../source/Public/ConvertTo-DscYaml.ps1 | 76 ++ .../source/Public/Install-DscExe.ps1 | 421 ++++++++ ...nvoke-DscResourceConfigurationDocument.ps1 | 75 ++ .../about_Microsoft.PowerShell.DSC.help.txt | 24 + .../source/prefix.ps1 | 31 + .../tests/QA/module.tests.ps1 | 243 +++++ .../Build-DscConfigurationDocument.tests.ps1 | 71 ++ .../Export-DscConfigurationDocument.tests.ps1 | 0 .../Private/Test-PsPathExtension.tests.ps1 | 67 ++ .../Unit/Private/Test-YamlModule.tests.ps1 | 33 + .../Unit/Public/ConvertTo-DscJson.tests.ps1 | 0 .../Unit/Public/ConvertTo-DscYaml.tests.ps1 | 0 test.ps1 | 12 + 49 files changed, 4130 insertions(+), 1 deletion(-) create mode 160000 operation-methods create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/.gitattributes create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/.gitignore create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/.vscode/tasks.json create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/CHANGELOG.md create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/GitVersion.yml create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/README.md create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/RequiredModules.psd1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/Resolve-Dependency.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/Resolve-Dependency.psd1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/build.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/build.yaml create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Classes/DscConfigurationResource.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Microsoft.PowerShell.DSC.psd1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Microsoft.PowerShell.DSC.psm1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscConfigurationDocument.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscPathBuilder.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Export-DscConfigurationDocument.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscCommandData.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceConfigurationDocument.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceSchemaProperty.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-EnvironmentVariable.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessObjectResult.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessOutput.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ResourceSchema.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Invoke-DscExe.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-EnvironmentVariableObject.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-SubCommand.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Resolve-DscExe.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Set-DscResourceConfigurationDocument.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceConfigurationDocument.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceName.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-PsPathExtension.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-YamlModule.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Public/ConvertTo-DscJson.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Public/ConvertTo-DscYaml.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscExe.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceConfigurationDocument.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/en-US/about_Microsoft.PowerShell.DSC.help.txt create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/prefix.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/tests/QA/module.tests.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Build-DscConfigurationDocument.tests.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Export-DscConfigurationDocument.tests.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Test-PsPathExtension.tests.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Private/Test-YamlModule.tests.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/ConvertTo-DscJson.tests.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/ConvertTo-DscYaml.tests.ps1 create mode 100644 test.ps1 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/Build-DscConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscConfigurationDocument.ps1 new file mode 100644 index 00000000..bbced6fd --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/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/Build-DscPathBuilder.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscPathBuilder.ps1 new file mode 100644 index 00000000..b80944b9 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscPathBuilder.ps1 @@ -0,0 +1,64 @@ +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') + { + Throw "No JSON or YAML file was provided. Please provide valid DSC Configuration Document." + } + + [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) + { + $jsonOutput = $data.exampleSnippet | ConvertTo-Json -Compress + } + } + + $outFile = Join-Path -Path 'C:\temp\' -ChildPath 'dsc_configuration_document.json' + + Set-Content -Path $outFile -Value $jsonOutput -Force + + [void]$subCommand.Append(" --path $outFile") + } + catch + { + # TODO: Capture + } + } +} \ No newline at end of file diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Export-DscConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Export-DscConfigurationDocument.ps1 new file mode 100644 index 00000000..9e25f3d0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/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/Get-DscCommandData.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscCommandData.ps1 new file mode 100644 index 00000000..3c2213bc --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/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-DscResourceConfigurationDocument' = @{ + 'preview8' = @{ + SubCommand = 'resource get' + Resources = $resources + } + } + 'Set-DscResourceConfigurationDocument' = @{ + 'preview8' = @{ + SubCommand = 'resource set' + Resources = $resources + } + } + 'Test-DscResourceConfigurationDocument' = @{ + '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/Get-DscResourceConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceConfigurationDocument.ps1 new file mode 100644 index 00000000..a006c1cc --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceConfigurationDocument.ps1 @@ -0,0 +1,46 @@ +function Get-DscResourceConfigurationDocument +{ + [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/Get-DscResourceSchemaProperty.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceSchemaProperty.ps1 new file mode 100644 index 00000000..45ce39d0 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/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/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..5c01d28c --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessOutput.ps1 @@ -0,0 +1,90 @@ +function Get-ProcessObjectResult { + 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 + $info.Arguments = $SubCommand + $info.UseShellExecute = $false + $info.RedirectStandardOutput = $true + $info.RedirectStandardError = $true + + $proc.StartInfo = $info + + # start process + $proc.Start() | Out-Null + + # read stream outputs + $stdOut = Get-ProcessOutput -Process $proc -ReadLine StandardOutput + $stErr = Get-ProcessOutput -Process $proc -ReadLine StandardError + + # 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 +} + +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/Get-ResourceSchema.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ResourceSchema.ps1 new file mode 100644 index 00000000..e69de29b diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Invoke-DscExe.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Invoke-DscExe.ps1 new file mode 100644 index 00000000..023e82da --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/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/New-EnvironmentVariableObject.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-EnvironmentVariableObject.ps1 new file mode 100644 index 00000000..ac98f9d5 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-EnvironmentVariableObject.ps1 @@ -0,0 +1,34 @@ +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/Resolve-DscExe.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Resolve-DscExe.ps1 new file mode 100644 index 00000000..250272f3 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/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/Set-DscResourceConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Set-DscResourceConfigurationDocument.ps1 new file mode 100644 index 00000000..9fad5f8d --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Set-DscResourceConfigurationDocument.ps1 @@ -0,0 +1,47 @@ +function Set-DscResourceConfigurationDocument +{ + [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/Test-DscResourceConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceConfigurationDocument.ps1 new file mode 100644 index 00000000..b3199b78 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceConfigurationDocument.ps1 @@ -0,0 +1,47 @@ +function Test-DscResourceConfigurationDocument +{ + [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/Test-DscResourceName.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceName.ps1 new file mode 100644 index 00000000..86ad8cfd --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/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/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-DscExe.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscExe.ps1 new file mode 100644 index 00000000..d9f51a34 --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscExe.ps1 @@ -0,0 +1,421 @@ +function Install-DscExe +{ + [CmdletBinding(SupportsShouldProcess)] + param + () + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + + function InvokeGitHubApi + { + [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 | ConvertToGitHubErrorRecord | Write-Error + } + } + + function ConvertToGitHubErrorRecord + { + [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) + } + } + + function TestAdministrator + { + $user = [Security.Principal.WindowsIdentity]::GetCurrent(); + (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) + } + + function TestIsOsArchX64 + { + 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 + } + + function GetAvailablePackageManager + { + 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' + } + } + + function GetPlatformInformation + { + 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 = GetAvailablePackageManager + + 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 (TestAdministrator) { "$env:ProgramFiles\DSC" } else { "$env:LOCALAPPDATA\DSC" } + } + } + + # TODO: If latest is present, just change it to always point to latest release + $releases = InvokeGitHubApi -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 = InvokeGitHubApi -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'] = TestAdministrator + } + + if ($pacMan) + { + $info['PackageManager'] = $pacMan + } + + return $info + } + + function SaveWithBitsTransfer { + param( + [Parameter(Mandatory=$true)] + [string] + $FileUri, + + [Parameter(Mandatory=$true)] + [string] + $Destination, + + [Parameter(Mandatory=$true)] + [string] + $AppName + ) + + Write-Host "`nDownloading latest $AppName..." -ForegroundColor Yellow + + 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.' + } + } + } + + function SetDscResourcePath + { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $false)] + [System.EnvironmentVariableTarget] + $VariableTarget = [System.EnvironmentVariableTarget]::User, + + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + if (-not ([System.Environment]::GetEnvironmentVariable("DSC_RESOURCE_PATH", $VariableTarget))) + { + Write-Verbose -Message "Adding '$Path' to 'DSC_RESOURCE_PATH' variable on '$VariableTarget'" + [System.Environment]::SetEnvironmentVariable("DSC_RESOURCE_PATH", $Path, $VariableTarget) + + $env:DSC_RESOURCE_PATH = [System.Environment]::GetEnvironmentVariable("DSC_RESOURCE_PATH", $VariableTarget) + + return + } + } + } + process + { + try + { + $prevProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + $platformInfo = GetPlatformInformation + # Download the installer + $tmpdir = [System.IO.Path]::GetTempPath() + + $installerPath = [System.IO.Path]::Combine($tmpDir, $platformInfo.FileName) + + if ($PSVersionTable.PSVersion.Major -le 5) + { + SaveWithBitsTransfer -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' + { + $zipDirPath = [System.IO.Path]::Combine($tmpdir, 'DSC') + Expand-Archive -LiteralPath $installerPath -DestinationPath $zipDirPath -Force + Move-Item "$zipDirPath/*" -Destination $platformInfo.ExePath -Force -ErrorAction SilentlyContinue + break + } + + # TODO: Add other platforms + } + + if ($platformInfo.RunAsAdministrator) + { + SetDscResourcePath -Path $platformInfo.ExePath -VariableTarget Machine + } + else + { + SetDscResourcePath -Path $platformInfo.ExePath + } + } + 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-DscResourceConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceConfigurationDocument.ps1 new file mode 100644 index 00000000..328fcedd --- /dev/null +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceConfigurationDocument.ps1 @@ -0,0 +1,75 @@ +function Invoke-DscResourceConfigurationDocument +{ + [CmdletBinding(SupportsShouldProcess)] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [ValidateSet('Get', 'Set', 'Test')] + [System.String] + $Operation = 'Get', + + [Parameter(Mandatory = $false)] + [AllowNull()] + [Alias('Path')] + [System.IO.FileInfo] + $ResourcePath, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [hashtable] + $ResourceInput + ) + + begin + { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose ("Starting: {0}" -f $commandName) + } + + process + { + # build arguments for each function + $arguments = @{ Name = $ResourceName} + if ($ResourcePath) + { + $arguments.Add('ResourcePath', $ResourcePath) + } + + if ($ResourceInput) + { + $arguments.Add('ResourceInput', $ResourceInput) + } + else + { + $arguments.Add('ResourceInput', @{}) + } + + switch ($Operation) + { + 'Get' + { + $inputObject = Get-DscResourceConfigurationDocument @arguments + } + 'Set' + { + $inputObject = Set-DscResourceConfigurationDocument @arguments + } + 'Test' + { + $inputobject = Test-DscResourceConfigurationDocument @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/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..0544dc1f --- /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-DscResourceConfigurationDocument -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/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 From 50839d2cbc5db9c3e01f5b7dce483797f61fed87 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 31 Jul 2024 14:45:50 +0200 Subject: [PATCH 5/6] New command introduced New-DscPsResourceDocument and restructure --- .../Build-DscConfigurationDocument.ps1 | 0 .../{ => Dsc}/Build-DscPathBuilder.ps1 | 32 +- .../Export-DscConfigurationDocument.ps1 | 0 .../Private/{ => Dsc}/Get-DscCommandData.ps1 | 6 +- .../Get-DscResourceCommand.ps1} | 2 +- .../Get-DscResourceSchemaProperty.ps1 | 0 .../Private/{ => Dsc}/Invoke-DscExe.ps1 | 0 .../Private/{ => Dsc}/Resolve-DscExe.ps1 | 0 .../Set-DscResourceCommand.ps1} | 4 +- .../Test-DscResourceCommand.ps1} | 2 +- .../{ => Dsc}/Test-DscResourceName.ps1 | 0 .../DscPs/Get-DscPsCacheProperties.ps1 | 25 ++ .../DscPs/Get-DscPsCacheRefreshPath.ps1 | 12 + .../source/Private/Get-ProcessOutput.ps1 | 45 -- .../source/Private/Get-ResourceSchema.ps1 | 0 .../Installer/ConvertTo-GitHubErrorRecord.ps1 | 44 ++ .../Installer/Get-AvailablePackageManager.ps1 | 22 + .../Installer/Get-PlatformInformation.ps1 | 101 +++++ .../Private/Installer/Invoke-GitHubApi.ps1 | 103 +++++ .../Installer/Save-WithBitsTransfer.ps1 | 41 ++ .../Private/Installer/Test-Administrator.ps1 | 5 + .../Private/Installer/Test-IsOsArchX64.ps1 | 9 + .../Private/New-EnvironmentVariableObject.ps1 | 3 +- .../source/Private/Path/Add-ToPath.ps1 | 81 ++++ .../source/Private/Path/Get-EnvVar.ps1 | 50 +++ .../source/Private/Path/Get-PathEnv.ps1 | 71 +++ .../source/Public/Install-DscCommand.ps1 | 71 +++ .../source/Public/Install-DscExe.ps1 | 421 ------------------ ...ment.ps1 => Invoke-DscResourceCommand.ps1} | 36 +- .../Public/New-DscPsResourceDocument.ps1 | 59 +++ .../source/prefix.ps1 | 2 +- .../Invoke-DscResourceCommand.tests.ps1 | 1 + 32 files changed, 746 insertions(+), 502 deletions(-) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{ => Dsc}/Build-DscConfigurationDocument.ps1 (100%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{ => Dsc}/Build-DscPathBuilder.ps1 (53%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{ => Dsc}/Export-DscConfigurationDocument.ps1 (100%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{ => Dsc}/Get-DscCommandData.ps1 (95%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{Get-DscResourceConfigurationDocument.ps1 => Dsc/Get-DscResourceCommand.ps1} (96%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{ => Dsc}/Get-DscResourceSchemaProperty.ps1 (100%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{ => Dsc}/Invoke-DscExe.ps1 (100%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{ => Dsc}/Resolve-DscExe.ps1 (100%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{Test-DscResourceConfigurationDocument.ps1 => Dsc/Set-DscResourceCommand.ps1} (95%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{Set-DscResourceConfigurationDocument.ps1 => Dsc/Test-DscResourceCommand.ps1} (96%) rename powershell-helpers/Microsoft.PowerShell.DSC/source/Private/{ => Dsc}/Test-DscResourceName.ps1 (100%) create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/DscPs/Get-DscPsCacheProperties.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/DscPs/Get-DscPsCacheRefreshPath.ps1 delete mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ResourceSchema.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/ConvertTo-GitHubErrorRecord.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Get-AvailablePackageManager.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Get-PlatformInformation.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Invoke-GitHubApi.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Save-WithBitsTransfer.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Test-Administrator.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Installer/Test-IsOsArchX64.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Add-ToPath.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Get-EnvVar.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Path/Get-PathEnv.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscCommand.ps1 delete mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscExe.ps1 rename powershell-helpers/Microsoft.PowerShell.DSC/source/Public/{Invoke-DscResourceConfigurationDocument.ps1 => Invoke-DscResourceCommand.ps1} (50%) create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/source/Public/New-DscPsResourceDocument.ps1 create mode 100644 powershell-helpers/Microsoft.PowerShell.DSC/tests/Unit/Public/Invoke-DscResourceCommand.tests.ps1 diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscConfigurationDocument.ps1 similarity index 100% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscConfigurationDocument.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscConfigurationDocument.ps1 diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscPathBuilder.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscPathBuilder.ps1 similarity index 53% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscPathBuilder.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscPathBuilder.ps1 index b80944b9..baf9689e 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Build-DscPathBuilder.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Build-DscPathBuilder.ps1 @@ -28,9 +28,16 @@ function Build-DscPathBuilder if (Test-Path $ResourcePath -ErrorAction SilentlyContinue) { - if ($ResourcePath.Extension -ne '.json' -and $ResourcePath.Extension -ne '.yaml') + if ($ResourcePath.Extension -ne '.json' -and $ResourcePath.Extension -ne '.yaml' -and $ResourcePath.Extension -ne '.ps1') { - Throw "No JSON or YAML file was provided. Please provide valid DSC Configuration Document." + 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)") @@ -46,15 +53,28 @@ function Build-DscPathBuilder { 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" + } - $outFile = Join-Path -Path 'C:\temp\' -ChildPath 'dsc_configuration_document.json' + if (-not (Test-Path $(Split-Path $filePath -Parent))) + { + $null = New-Item -Path $(Split-Path $filePath -Parent) -ItemType Directory -Force + } - Set-Content -Path $outFile -Value $jsonOutput -Force - - [void]$subCommand.Append(" --path $outFile") + 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 { diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Export-DscConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Export-DscConfigurationDocument.ps1 similarity index 100% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Export-DscConfigurationDocument.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Export-DscConfigurationDocument.ps1 diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscCommandData.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscCommandData.ps1 similarity index 95% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscCommandData.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscCommandData.ps1 index 3c2213bc..adaa18a3 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscCommandData.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscCommandData.ps1 @@ -44,19 +44,19 @@ function Get-DscCommandData Test = @{ keyPath = "HKCU\Microsoft"} } } - 'Get-DscResourceConfigurationDocument' = @{ + 'Get-DscResourceCommand' = @{ 'preview8' = @{ SubCommand = 'resource get' Resources = $resources } } - 'Set-DscResourceConfigurationDocument' = @{ + 'Set-DscResourceCommand' = @{ 'preview8' = @{ SubCommand = 'resource set' Resources = $resources } } - 'Test-DscResourceConfigurationDocument' = @{ + 'Test-DscResourceCommand' = @{ 'preview8' = @{ SubCommand = 'resource test' Resources = $resources diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceCommand.ps1 similarity index 96% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceConfigurationDocument.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceCommand.ps1 index a006c1cc..d9a41d9a 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceConfigurationDocument.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceCommand.ps1 @@ -1,4 +1,4 @@ -function Get-DscResourceConfigurationDocument +function Get-DscResourceCommand { [CmdletBinding(DefaultParameterSetName = 'ByInput')] Param diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceSchemaProperty.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceSchemaProperty.ps1 similarity index 100% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-DscResourceSchemaProperty.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Get-DscResourceSchemaProperty.ps1 diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Invoke-DscExe.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Invoke-DscExe.ps1 similarity index 100% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Invoke-DscExe.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Invoke-DscExe.ps1 diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Resolve-DscExe.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Resolve-DscExe.ps1 similarity index 100% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Resolve-DscExe.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Resolve-DscExe.ps1 diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Set-DscResourceCommand.ps1 similarity index 95% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceConfigurationDocument.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Set-DscResourceCommand.ps1 index b3199b78..bcfaa232 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceConfigurationDocument.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Set-DscResourceCommand.ps1 @@ -1,4 +1,4 @@ -function Test-DscResourceConfigurationDocument +function Set-DscResourceCommand { [CmdletBinding(SupportsShouldProcess)] Param @@ -16,7 +16,7 @@ function Test-DscResourceConfigurationDocument [Parameter(Mandatory = $false)] [Alias('Input')] [hashtable] - $ResourceInput + $ResourceInput = @{} ) begin diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Set-DscResourceConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceCommand.ps1 similarity index 96% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Set-DscResourceConfigurationDocument.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceCommand.ps1 index 9fad5f8d..a22c2854 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Set-DscResourceConfigurationDocument.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceCommand.ps1 @@ -1,4 +1,4 @@ -function Set-DscResourceConfigurationDocument +function Test-DscResourceCommand { [CmdletBinding(SupportsShouldProcess)] Param diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceName.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceName.ps1 similarity index 100% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Test-DscResourceName.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Dsc/Test-DscResourceName.ps1 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-ProcessOutput.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessOutput.ps1 index 5c01d28c..e5b0d32d 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessOutput.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ProcessOutput.ps1 @@ -1,48 +1,3 @@ -function Get-ProcessObjectResult { - 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 - $info.Arguments = $SubCommand - $info.UseShellExecute = $false - $info.RedirectStandardOutput = $true - $info.RedirectStandardError = $true - - $proc.StartInfo = $info - - # start process - $proc.Start() | Out-Null - - # read stream outputs - $stdOut = Get-ProcessOutput -Process $proc -ReadLine StandardOutput - $stErr = Get-ProcessOutput -Process $proc -ReadLine StandardError - - # 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 -} - function Get-ProcessOutput { [CmdletBinding()] diff --git a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ResourceSchema.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/Get-ResourceSchema.ps1 deleted file mode 100644 index e69de29b..00000000 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 index ac98f9d5..5cdda599 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-EnvironmentVariableObject.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Private/New-EnvironmentVariableObject.ps1 @@ -1,6 +1,7 @@ function New-EnvironmentVariableObject { - param ( + param + ( [OutputType([HashTable])] [Parameter(Mandatory)] [ValidatePattern("[^=]+")] 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/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/Install-DscExe.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscExe.ps1 deleted file mode 100644 index d9f51a34..00000000 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Install-DscExe.ps1 +++ /dev/null @@ -1,421 +0,0 @@ -function Install-DscExe -{ - [CmdletBinding(SupportsShouldProcess)] - param - () - - begin - { - $commandName = $MyInvocation.MyCommand.Name - Write-Verbose ("Starting: {0}" -f $commandName) - - function InvokeGitHubApi - { - [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 | ConvertToGitHubErrorRecord | Write-Error - } - } - - function ConvertToGitHubErrorRecord - { - [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) - } - } - - function TestAdministrator - { - $user = [Security.Principal.WindowsIdentity]::GetCurrent(); - (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) - } - - function TestIsOsArchX64 - { - 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 - } - - function GetAvailablePackageManager - { - 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' - } - } - - function GetPlatformInformation - { - 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 = GetAvailablePackageManager - - 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 (TestAdministrator) { "$env:ProgramFiles\DSC" } else { "$env:LOCALAPPDATA\DSC" } - } - } - - # TODO: If latest is present, just change it to always point to latest release - $releases = InvokeGitHubApi -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 = InvokeGitHubApi -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'] = TestAdministrator - } - - if ($pacMan) - { - $info['PackageManager'] = $pacMan - } - - return $info - } - - function SaveWithBitsTransfer { - param( - [Parameter(Mandatory=$true)] - [string] - $FileUri, - - [Parameter(Mandatory=$true)] - [string] - $Destination, - - [Parameter(Mandatory=$true)] - [string] - $AppName - ) - - Write-Host "`nDownloading latest $AppName..." -ForegroundColor Yellow - - 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.' - } - } - } - - function SetDscResourcePath - { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $false)] - [System.EnvironmentVariableTarget] - $VariableTarget = [System.EnvironmentVariableTarget]::User, - - [Parameter(Mandatory = $true)] - [System.String] - $Path - ) - - if (-not ([System.Environment]::GetEnvironmentVariable("DSC_RESOURCE_PATH", $VariableTarget))) - { - Write-Verbose -Message "Adding '$Path' to 'DSC_RESOURCE_PATH' variable on '$VariableTarget'" - [System.Environment]::SetEnvironmentVariable("DSC_RESOURCE_PATH", $Path, $VariableTarget) - - $env:DSC_RESOURCE_PATH = [System.Environment]::GetEnvironmentVariable("DSC_RESOURCE_PATH", $VariableTarget) - - return - } - } - } - process - { - try - { - $prevProgressPreference = $ProgressPreference - $ProgressPreference = 'SilentlyContinue' - - $platformInfo = GetPlatformInformation - # Download the installer - $tmpdir = [System.IO.Path]::GetTempPath() - - $installerPath = [System.IO.Path]::Combine($tmpDir, $platformInfo.FileName) - - if ($PSVersionTable.PSVersion.Major -le 5) - { - SaveWithBitsTransfer -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' - { - $zipDirPath = [System.IO.Path]::Combine($tmpdir, 'DSC') - Expand-Archive -LiteralPath $installerPath -DestinationPath $zipDirPath -Force - Move-Item "$zipDirPath/*" -Destination $platformInfo.ExePath -Force -ErrorAction SilentlyContinue - break - } - - # TODO: Add other platforms - } - - if ($platformInfo.RunAsAdministrator) - { - SetDscResourcePath -Path $platformInfo.ExePath -VariableTarget Machine - } - else - { - SetDscResourcePath -Path $platformInfo.ExePath - } - } - 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-DscResourceConfigurationDocument.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceCommand.ps1 similarity index 50% rename from powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceConfigurationDocument.ps1 rename to powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceCommand.ps1 index 328fcedd..fa3091ad 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceConfigurationDocument.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/Public/Invoke-DscResourceCommand.ps1 @@ -1,6 +1,6 @@ -function Invoke-DscResourceConfigurationDocument +function Invoke-DscResourceCommand { - [CmdletBinding(SupportsShouldProcess)] + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = '__AllParameterSets')] param ( [Parameter(Mandatory = $true)] @@ -12,13 +12,13 @@ function Invoke-DscResourceConfigurationDocument [System.String] $Operation = 'Get', - [Parameter(Mandatory = $false)] + [Parameter(Mandatory = $false, ParameterSetName = 'ByPath')] [AllowNull()] [Alias('Path')] [System.IO.FileInfo] $ResourcePath, - [Parameter(Mandatory = $false)] + [Parameter(Mandatory = $false, ParameterSetName = 'ByInput')] [AllowNull()] [hashtable] $ResourceInput @@ -32,37 +32,31 @@ function Invoke-DscResourceConfigurationDocument process { - # build arguments for each function - $arguments = @{ Name = $ResourceName} - if ($ResourcePath) - { - $arguments.Add('ResourcePath', $ResourcePath) - } - - if ($ResourceInput) - { - $arguments.Add('ResourceInput', $ResourceInput) - } - else + $arguments = @{ResourceName = $ResourceName } + # get argument data + switch ($PSCmdlet.ParameterSetName) { - $arguments.Add('ResourceInput', @{}) + 'ResourcePath' { $arguments.Add('ResourcePath', $ResourcePath) } + 'ResourceInput' { $arguments.Add('ResourceInput', $ResourceInput) } + default { $arguments.Add('ResourceInput', @{}) } } + # go through operations switch ($Operation) { 'Get' { - $inputObject = Get-DscResourceConfigurationDocument @arguments + $inputObject = Get-DscResourceCommand @arguments } 'Set' { - $inputObject = Set-DscResourceConfigurationDocument @arguments + $inputObject = Set-DscResourceCommand @arguments } 'Test' { - $inputobject = Test-DscResourceConfigurationDocument @arguments + $inputobject = Test-DscResourceCommand @arguments } - default {$inputObject = @{}} + default { $inputObject = @{} } } return $inputObject 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/prefix.ps1 b/powershell-helpers/Microsoft.PowerShell.DSC/source/prefix.ps1 index 0544dc1f..86a85a39 100644 --- a/powershell-helpers/Microsoft.PowerShell.DSC/source/prefix.ps1 +++ b/powershell-helpers/Microsoft.PowerShell.DSC/source/prefix.ps1 @@ -28,4 +28,4 @@ } } -Register-ArgumentCompleter -CommandName Invoke-DscResourceConfigurationDocument -ParameterName ResourceName -ScriptBlock $dscExeSubCommand \ No newline at end of file +Register-ArgumentCompleter -CommandName Invoke-DscResourceCommand -ParameterName ResourceName -ScriptBlock $dscExeSubCommand \ No newline at end of file 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 From ab19b63cb81d569ee2be7f021729615183b50a62 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 31 Jul 2024 15:17:12 +0200 Subject: [PATCH 6/6] Remove migration module --- powershell-helpers/README.md | 13 - powershell-helpers/dscCfgMigMod.psd1 | 47 --- powershell-helpers/dscCfgMigMod.psm1 | 384 ------------------ .../tests/dscCfgMigMod.tests.ps1 | 24 -- 4 files changed, 468 deletions(-) delete mode 100644 powershell-helpers/README.md delete mode 100644 powershell-helpers/dscCfgMigMod.psd1 delete mode 100644 powershell-helpers/dscCfgMigMod.psm1 delete mode 100644 powershell-helpers/tests/dscCfgMigMod.tests.ps1 diff --git a/powershell-helpers/README.md b/powershell-helpers/README.md deleted file mode 100644 index d48f4e87..00000000 --- a/powershell-helpers/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Introduction - -The `powershell-adapters` folder contains helper modules that can be loaded into your PowerShell session to assist you in familiarizing yourself with new DSC concepts. To see the availability of helper modules, see the following list: - -- **DSC Configuration Migration Module**: - Aids in the assistance of grabbing configuration documents written in PowerShell code and transform them to valid configuration documents for the DSC version 3 core engine (e.g. YAML or JSON). - -## Getting started - -To get started using the helper modules, you can follow the below steps. This example uses the _DSC Configuration Migration Tool_ to be loaded into the session: - -1. Open a PowerShell terminal session -2. Execute the following command: `Import-Module "powershell-helpers\dscConfigurationMigrationTool.psm1"` -3. Discover examples using: `Get-Help ConvertTo-DscYaml` diff --git a/powershell-helpers/dscCfgMigMod.psd1 b/powershell-helpers/dscCfgMigMod.psd1 deleted file mode 100644 index 83d1ac09..00000000 --- a/powershell-helpers/dscCfgMigMod.psd1 +++ /dev/null @@ -1,47 +0,0 @@ -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'dscCfgMigMod.psm1' - - # Version number of this module. - moduleVersion = '0.0.1' - - # ID used to uniquely identify this module - GUID = '42bf8cb0-210c-4dac-8614-319d9287c6dc' - - # 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 Migration Module helper' - - # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @('powershell-yaml') - - # 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 = @( - 'ConvertTo-DscJson' - 'ConvertTo-DscYaml' - ) - - # 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 = @() - - PrivateData = @{ - PSData = @{ - ProjectUri = 'https://github.com/PowerShell/dsc' - } - } -} diff --git a/powershell-helpers/dscCfgMigMod.psm1 b/powershell-helpers/dscCfgMigMod.psm1 deleted file mode 100644 index c61ff4bf..00000000 --- a/powershell-helpers/dscCfgMigMod.psm1 +++ /dev/null @@ -1,384 +0,0 @@ -#region Main functions -function ConvertTo-DscJson -{ - <# - .SYNOPSIS - Convert a PowerShell DSC configuration document to DSC version 3 JSON format. - - .DESCRIPTION - The function ConvertTo-DscJson converts a PowerShell DSC configuration document to DSC version 3 JSON format from a path. - - .PARAMETER Path - The path to valid PowerShell DSC configuration document - - .EXAMPLE - PS C:\> $configuration = @' - Configuration TestResource { - Import-DscResource -ModuleName TestResource - Node localhost { - TestResource 'Configure test resource' { - Ensure = 'Absent' - Name = 'MyTestResource' - } - } - } - '@ - PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' - PS C:\> $configuration | Out-File -FilePath $Path - PS C:\> ConvertTo-DscJson -Path $Path - - Returns: - { - "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", - "resources": { - "name": "TestResource", - "type": "Microsoft.DSC/PowerShell", - "properties": { - "resources": [ - { - "name": "Configure test resource", - "type": "TestResource/TestResource", - "properties": { - "Name": "MyTestResource", - "Ensure": "Absent" - } - } - ] - } - } - } - - .NOTES - Tags: DSC, Migration, JSON - #> - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - begin - { - Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) - } - - process - { - $inputObject = BuildConfigurationDocument -Path $Path - } - end - { - Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) - return $inputObject - } -} - -function ConvertTo-DscYaml -{ - <# - .SYNOPSIS - Convert a PowerShell DSC configuration document to DSC version 3 YAML format. - - .DESCRIPTION - The function ConvertTo-DscYaml converts a PowerShell DSC configuration document to DSC version 3 YAML format from a path. - - .PARAMETER Path - The path to valid PowerShell DSC configuration document - - .EXAMPLE - PS C:\> $configuration = @' - Configuration TestResource { - Import-DscResource -ModuleName TestResource - Node localhost { - TestResource 'Configure test resource' { - Ensure = 'Absent' - Name = 'MyTestResource' - } - } - } - '@ - PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' - PS C:\> $configuration | Out-File -FilePath $Path - PS C:\> ConvertTo-DscYaml -Path $Path - - Returns: - $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json - resources: - name: TestResource - type: Microsoft.DSC/PowerShell - properties: - resources: - - name: Configure test resource - type: TestResource/TestResource - properties: - Name: MyTestResource - Ensure: Absent - - .NOTES - Tags: DSC, Migration, YAML - #> - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - begin - { - Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) - } - - process - { - $inputObject = BuildConfigurationDocument -Path $Path -Format YAML - } - end - { - Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) - return $inputObject - } -} -#endRegion Main functions - -#region Helper functions -function FindAndExtractConfigurationDocument -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path - ) - - if (-not (TestPathExtension $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 - $configurationDocument.type = 'Microsoft.DSC/PowerShell' # TODO: Add functions later to valid the adapter type - - # 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) - # TODO: Might have to change because it takes time. If there is only one Import-DscResource statement, we can simply RegEx it out, else use Get-DscResource - # $document.ModuleName = $module.ModuleName - - 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 -} - -function BuildConfigurationDocument -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path, - - [ValidateSet('JSON', 'YAML')] - [System.String] - $Format = 'JSON' - ) - - $configurationDocument = [ordered]@{ - "`$schema" = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json" # TODO: Figure out how to extract latest document.json from schemas folder - resources = FindAndExtractConfigurationDocument -Path $Path - } - - switch ($Format) - { - "JSON" { - $inputObject = ($configurationDocument | ConvertTo-Json -Depth 10) - } - "YAML" { - if (TestYamlModule) - { - $inputObject = ($configurationDocument | ConvertTo-Yaml) - } - else - { - $inputObject = @{} - } - } - default { - $inputObject = $configurationDocument - } - } - - return $inputObject -} - -function TestPathExtension -{ - [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 -} - -function TestYamlModule -{ - if (-not (Get-Command -Name 'ConvertTo-Yaml' -ErrorAction SilentlyContinue)) - { - return $false - } - - return $true -} - -function GetPowerShellPath -{ - param - ( - $Path - ) - - $knownPath = @( - "$env:USERPROFILE\Documents\PowerShell\Modules", - "$env:ProgramFiles\PowerShell\Modules", - "$env:ProgramFiles\PowerShell\7\Modules" - ) - - foreach ($known in $knownPath) - { - if ($Path.StartsWith($known)) - { - return $true - } - } - - return $false -} - -function GetWindowsPowerShellPath -{ - param - ( - $Path - ) - - $knownPath = @( - "$env:USERPROFILE\Documents\WindowsPowerShell\Modules", - "$env:ProgramFiles\WindowsPowerShell\Modules", - "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules" - ) - - foreach ($known in $knownPath) - { - if ($Path.StartsWith($known)) - { - return $true - } - } - - return $false -} - -function ResolvePowerShellPath -{ - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - if (-not (Test-Path $Path)) - { - return - } - - if (([System.IO.Path]::GetExtension($Path) -ne ".psm1")) - { - return - } - - if (GetPowerShellPath -Path $Path) - { - return "Microsoft.DSC/PowerShell" - } - - if (GetWindowsPowerShellPath -Path $Path) - { - return "Microsoft.Windows/WindowsPowerShell" - } - - return $null # TODO: Or default Microsoft.DSC/PowerShell -} - -#endRegion Helper functions - -#region Classes -class DscConfigurationResource -{ - [string] $name - [string] $type - [hashtable] $properties -} -#endRegion classes \ No newline at end of file diff --git a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 deleted file mode 100644 index b966993a..00000000 --- a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -Describe "DSC Configuration Migration Module tests" { - BeforeAll { - $modPath = (Resolve-Path -Path "$PSScriptRoot\..\dscCfgMigMod.psd1").Path - $modLoad = Import-Module $modPath -Force -PassThru - } - - Context "ConvertTo-DscYaml" { - It "Should create an empty resource block" { - $res = (ConvertTo-DscYaml -Path 'idonotexist' | ConvertFrom-Yaml) - $res.resources | Should -BeNullOrEmpty - } - } - - Context "ConvertTo-DscJson" { - It "Should create an empty resource block" { - $res = (ConvertTo-DscJson -Path 'idonotexist' | ConvertFrom-Json) - $res.resources | Should -BeNullOrEmpty - } - } - - AfterAll { - Remove-Module -Name $modLoad.Name -Force - } -}