Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dd41634
Init
Gijsreyn Aug 21, 2025
9e04ed5
Use concurrentbag
Gijsreyn Aug 21, 2025
dae94e4
Use .NET
Gijsreyn Aug 21, 2025
6ec70d7
Use .NET with enumeration option
Gijsreyn Aug 21, 2025
d13045d
Init
Gijsreyn Aug 21, 2025
33b7ab2
Use concurrentbag
Gijsreyn Aug 21, 2025
f12038e
Use .NET
Gijsreyn Aug 21, 2025
991f7ce
Use .NET with enumeration option
Gijsreyn Aug 21, 2025
1e80819
Remove undiscovered resource
Gijsreyn Aug 27, 2025
6337045
Merge branch 'implement-powershell-discover' of https://github.com/Gi…
Gijsreyn Aug 27, 2025
69ef042
Init
Gijsreyn Aug 21, 2025
f7c606a
Use concurrentbag
Gijsreyn Aug 21, 2025
f93e691
Use .NET
Gijsreyn Aug 21, 2025
16bf2f5
Use .NET with enumeration option
Gijsreyn Aug 21, 2025
34375b6
Init
Gijsreyn Aug 21, 2025
16b41c4
Use concurrentbag
Gijsreyn Aug 21, 2025
3037ffe
Use .NET
Gijsreyn Aug 21, 2025
1ef6482
Use .NET with enumeration option
Gijsreyn Aug 21, 2025
f15195a
Remove undiscovered resource
Gijsreyn Aug 27, 2025
1caa83a
Merge branch 'implement-powershell-discover' of https://github.com/Gi…
Gijsreyn Sep 3, 2025
a7263ba
Fix point 2
Gijsreyn Sep 3, 2025
640c664
Add newline
Gijsreyn Sep 4, 2025
1b58423
Merge branch 'main' into implement-powershell-discover
Gijsreyn Sep 9, 2025
6992c43
Merge branch 'main' into implement-powershell-discover
Gijsreyn Sep 18, 2025
5ec950e
Merge branch 'main' into implement-powershell-discover
Gijsreyn Sep 22, 2025
c139f8a
Merge branch 'main' into implement-powershell-discover
Gijsreyn Oct 7, 2025
861a017
Add caching
Gijsreyn Oct 8, 2025
48202c7
Debug tests
Gijsreyn Oct 8, 2025
e99c662
Format file
Gijsreyn Oct 8, 2025
0dfe4cd
Revert to working situation
Gijsreyn Oct 8, 2025
ff104c2
Create function
Gijsreyn Oct 8, 2025
091e017
Catch empty value
Gijsreyn Oct 8, 2025
9b6c993
Re-add tests
Gijsreyn Oct 8, 2025
5c736a7
Update tests
Gijsreyn Oct 8, 2025
12009a9
Remove Write-DscTrace
Gijsreyn Oct 8, 2025
4438bf9
Move test plus docs fix
Gijsreyn Oct 8, 2025
55bb84b
Merge branch 'main' of https://github.com/Gijsreyn/operation-methods …
Gijsreyn Oct 8, 2025
d9082b8
Merge conflict
Gijsreyn Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ $filesForWindowsPackage = @(
'NOTICE.txt',
'osinfo.exe',
'osinfo.dsc.resource.json',
'powershell.discover.ps1',
'powershell.dsc.extension.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
Expand Down Expand Up @@ -101,6 +103,8 @@ $filesForLinuxPackage = @(
'NOTICE.txt',
'osinfo',
'osinfo.dsc.resource.json',
'powershell.discover.ps1',
'powershell.dsc.extension.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
Expand All @@ -126,6 +130,8 @@ $filesForMacPackage = @(
'NOTICE.txt',
'osinfo',
'osinfo.dsc.resource.json',
'powershell.discover.ps1',
'powershell.dsc.extension.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
Expand Down Expand Up @@ -347,9 +353,9 @@ if (!$SkipBuild) {
New-Item -ItemType Directory $target -ErrorAction Ignore > $null

# make sure dependencies are built first so clippy runs correctly
$windows_projects = @("lib/dsc-lib-pal", "lib/dsc-lib-registry", "resources/registry", "resources/reboot_pending", "adapters/wmi", "configurations/windows", 'extensions/appx')
$macOS_projects = @("resources/brew")
$linux_projects = @("resources/apt")
$windows_projects = @("lib/dsc-lib-pal", "lib/dsc-lib-registry", "resources/registry", "resources/reboot_pending", "adapters/wmi", "configurations/windows", "extensions/appx", "extensions/powershell")
$macOS_projects = @("resources/brew", "extensions/powershell")
$linux_projects = @("resources/apt", "extensions/powershell")

# projects are in dependency order
$projects = @(
Expand Down
8 changes: 7 additions & 1 deletion docs/reference/schemas/extension/stdout/discover.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ Type: object

## Description

Represents the actual state of a resource instance in DSCpath to a discovered DSC resource or
Represents the actual state of a resource instance in DSC path to a discovered DSC resource or
extension manifest on the system. DSC expects every JSON Line emitted to stdout for the
**Discover** operation to adhere to this schema.

The output must be a JSON object. The object must define the full path to the discovered manifest.
If an extension returns JSON that is invalid against this schema, DSC raises an error.

If the extension doesn't discover any manifests, it must return nothing to stdout. An empty output
indicates no resources were found.

## Required Properties

The output for the `discover` operation must include these properties:
Expand All @@ -43,6 +46,9 @@ The value for this property must be the absolute path to a manifest file on the
manifest can be for a DSC resource or extension. If the returned path doesn't exist, DSC raises an
error.

Each discovered manifest must be emitted as a separate JSON Line to stdout. If no manifests are
discovered, the extension must not emit any output to stdout.

```yaml
Type: string
Required: true
Expand Down
45 changes: 22 additions & 23 deletions dsc/tests/dsc_extension_discover.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,29 @@ Describe 'Discover extension tests' {
It 'Discover extensions' {
$out = dsc extension list | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
if ($IsWindows) {
$out.Count | Should -Be 3 -Because ($out | Out-String)
$out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep'
$out[0].version | Should -Be '0.1.0'
$out[0].capabilities | Should -BeExactly @('import')
$out[0].manifest | Should -Not -BeNullOrEmpty
$out[1].type | Should -Be 'Microsoft.Windows.Appx/Discover'
$out[1].version | Should -Be '0.1.0'
$out[1].capabilities | Should -BeExactly @('discover')
$out[1].manifest | Should -Not -BeNullOrEmpty
$out[2].type | Should -BeExactly 'Test/Discover'
$out[2].version | Should -BeExactly '0.1.0'
$out[2].capabilities | Should -BeExactly @('discover')
$out[2].manifest | Should -Not -BeNullOrEmpty
$expectedExtensions = if ($IsWindows) {
@(
@{ type = 'Microsoft.DSC.Extension/Bicep'; version = '0.1.0'; capabilities = @('import') }
@{ type = 'Microsoft.Windows.Appx/Discover'; version = '0.1.0'; capabilities = @('discover') }
@{ type = 'Microsoft.PowerShell/Discover'; version = '0.1.0'; capabilities = @('discover') }
@{ type = 'Test/Discover'; version = '0.1.0'; capabilities = @('discover') }
)
} else {
$out.Count | Should -Be 2 -Because ($out | Out-String)
$out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep'
$out[0].version | Should -Be '0.1.0'
$out[0].capabilities | Should -BeExactly @('import')
$out[0].manifest | Should -Not -BeNullOrEmpty
$out[1].type | Should -BeExactly 'Test/Discover'
$out[1].version | Should -BeExactly '0.1.0'
$out[1].capabilities | Should -BeExactly @('discover')
$out[1].manifest | Should -Not -BeNullOrEmpty
@(
@{ type = 'Microsoft.DSC.Extension/Bicep'; version = '0.1.0'; capabilities = @('import') }
@{ type = 'Microsoft.PowerShell/Discover'; version = '0.1.0'; capabilities = @('discover') }
@{ type = 'Test/Discover'; version = '0.1.0'; capabilities = @('discover') }
)
}

$out.Count | Should -Be $expectedExtensions.Count -Because ($out | Out-String)

foreach ($expected in $expectedExtensions) {
$extension = $out | Where-Object { $_.type -eq $expected.type }
$extension | Should -Not -BeNullOrEmpty -Because "Extension $($expected.type) should exist"
$extension.version | Should -BeExactly $expected.version
$extension.capabilities | Should -BeExactly $expected.capabilities
$extension.manifest | Should -Not -BeNullOrEmpty
}
}

Expand Down
2 changes: 2 additions & 0 deletions extensions/powershell/copy_files.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
powershell.discover.ps1
powershell.dsc.extension.json
113 changes: 113 additions & 0 deletions extensions/powershell/powershell.discover.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

[CmdletBinding()]
param ()

function Get-CacheFilePath {
if ($IsWindows) {
Join-Path $env:LocalAppData "dsc\PowerShellDiscoverCache.json"
} else {
Join-Path $env:HOME ".dsc" "PowerShellDiscoverCache.json"
}
}

function Test-CacheValid {
param([string]$CacheFilePath, [string[]]$PSPaths)

if (-not (Test-Path $CacheFilePath)) {
return $false
}

try {
$cache = Get-Content -Raw $CacheFilePath | ConvertFrom-Json

foreach ($entry in $cache.PathInfo.PSObject.Properties) {
$path = $entry.Name
if (-not (Test-Path $path)) {
return $false
}

$currentLastWrite = (Get-Item $path).LastWriteTimeUtc
$cachedLastWrite = [DateTime]$entry.Value

if ($currentLastWrite -ne $cachedLastWrite) {
return $false
}
}

$cachedPaths = [string[]]$cache.PSModulePaths
if ($cachedPaths.Count -ne $PSPaths.Count) {
return $false
}

$diff = Compare-Object $cachedPaths $PSPaths
if ($null -ne $diff) {
return $false
}

return $true
} catch {
return $false
}
}

function Invoke-DscResourceDiscovery {
[CmdletBinding()]
param()

begin {
$psPaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator | Where-Object { $_ -notmatch 'WindowsPowerShell' }

$cacheFilePath = Get-CacheFilePath
$useCache = Test-CacheValid -CacheFilePath $cacheFilePath -PSPaths $psPaths
}
process {
if ($useCache) {
$cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json
$manifests = $cache.Manifests
} else {
$manifests = $psPaths | ForEach-Object -Parallel {
$searchPatterns = @('*.dsc.resource.json', '*.dsc.resource.yaml', '*.dsc.resource.yml')
$enumOptions = [System.IO.EnumerationOptions]@{ IgnoreInaccessible = $false; RecurseSubdirectories = $true }
foreach ($pattern in $searchPatterns) {
try {
[System.IO.Directory]::EnumerateFiles($_, $pattern, $enumOptions) | ForEach-Object {
@{ manifestPath = $_ }
}
} catch { }
}
} -ThrottleLimit 10

$pathInfo = @{}
foreach ($path in $psPaths) {
if (Test-Path $path) {
$pathInfo[$path] = (Get-Item $path).LastWriteTimeUtc
}
}

$cacheObject = @{
PSModulePaths = $psPaths
PathInfo = $pathInfo
Manifests = $manifests
}

$cacheDir = Split-Path $cacheFilePath -Parent
if (-not (Test-Path $cacheDir)) {
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
}

$cacheObject | ConvertTo-Json -Depth 10 | Set-Content -Path $cacheFilePath -Force
}
}
end {
if ($null -eq $manifests -or [string]::IsNullOrEmpty($manifests)) {
# Return nothing
} else {
$manifests | ForEach-Object { $_ | ConvertTo-Json -Compress }
}
}
}

Invoke-DscResourceDiscovery

125 changes: 125 additions & 0 deletions extensions/powershell/powershell.discover.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

BeforeAll {
$fakeManifest = @{
'$schema' = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json"
type = "Test/FakeResource"
version = "0.1.0"
get = @{
executable = "fakeResource"
args = @(
"get",
@{
jsonInputArg = "--input"
mandatory = $true
}
)
}
}

$manifestPath = Join-Path $TestDrive "fake.dsc.resource.json"
$fakeManifest | ConvertTo-Json -Depth 10 | Set-Content -Path $manifestPath
$env:PSModulePath += [System.IO.Path]::PathSeparator + $TestDrive

$script:discoverScript = Join-Path $PSScriptRoot "powershell.discover.ps1"

$cacheFilePath = if ($IsWindows) {
Join-Path $env:LocalAppData "dsc\PowerShellDiscoverCache.json"
} else {
Join-Path $env:HOME ".dsc" "PowerShellDiscoverCache.json"
}
$script:cacheFilePath = $cacheFilePath

Remove-Item -Force -ErrorAction SilentlyContinue -Path $script:cacheFilePath
}

Describe 'Tests for PowerShell resource discovery' {
It 'Should create cache file on first run' {
$script:cacheFilePath | Should -Not -Exist
$null = & $script:discoverScript 2>&1
$script:cacheFilePath | Should -Exist

$cache = Get-Content -Raw $script:cacheFilePath | ConvertFrom-Json
$cache.PSModulePaths | Should -Not -BeNullOrEmpty
$cache.PathInfo | Should -Not -BeNullOrEmpty
$cache.Manifests | Should -Not -BeNullOrEmpty
}

It 'Should find DSC PowerShell resources' {
$out = dsc resource list | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.manifest.type | Should -Contain 'Test/FakeResource'
}

It 'Should use cache on subsequent runs' {
$null = & $script:discoverScript 2>&1
$script:cacheFilePath | Should -Exist

$cacheLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc

Start-Sleep -Milliseconds 100

$null = & $script:discoverScript 2>&1

$newLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc
$newLastWriteTime | Should -Be $cacheLastWriteTime
}

It 'Should invalidate cache when PSModulePath changes' {
$null = & $script:discoverScript 2>&1
$script:cacheFilePath | Should -Exist

$cache = Get-Content -Raw $script:cacheFilePath | ConvertFrom-Json
$originalPaths = $cache.PSModulePaths
$cache.PSModulePaths = @($originalPaths[0]) # Remove some paths
$cache | ConvertTo-Json -Depth 10 | Set-Content -Path $script:cacheFilePath -Force

$cacheLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc
Start-Sleep -Milliseconds 100

$null = & $script:discoverScript 2>&1

$newLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc
$newLastWriteTime | Should -Not -Be $cacheLastWriteTime
}

It 'Should invalidate cache when module directory is modified' {
$null = & $script:discoverScript 2>&1
$script:cacheFilePath | Should -Exist

$cache = Get-Content -Raw $script:cacheFilePath | ConvertFrom-Json

$firstPath = $cache.PathInfo.PSObject.Properties | Select-Object -First 1
if ($firstPath) {
$oldTimestamp = [DateTime]$firstPath.Value
$newTimestamp = $oldTimestamp.AddDays(-1)
$cache.PathInfo.($firstPath.Name) = $newTimestamp
$cache | ConvertTo-Json -Depth 10 | Set-Content -Path $script:cacheFilePath -Force

$cacheLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc
Start-Sleep -Milliseconds 100

$null = & $script:discoverScript 2>&1

$newLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc
$newLastWriteTime | Should -Not -Be $cacheLastWriteTime
}
}

It 'Should rebuild cache if cache file is corrupted' {
"{ invalid json }" | Set-Content -Path $script:cacheFilePath -Force
$script:cacheFilePath | Should -Exist

$null = & $script:discoverScript 2>&1

$cache = Get-Content -Raw $script:cacheFilePath | ConvertFrom-Json
$cache.PSModulePaths | Should -Not -BeNullOrEmpty
$cache.PathInfo | Should -Not -BeNullOrEmpty
}

It 'Should include test manifest in discovery results' {
$out = & $script:discoverScript | ConvertFrom-Json
$out.manifestPath | Should -Contain $manifestPath
}
}
18 changes: 18 additions & 0 deletions extensions/powershell/powershell.dsc.extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
"type": "Microsoft.PowerShell/Discover",
"version": "0.1.0",
"description": "Discovers DSC resources packaged in PowerShell 7 modules.",
"discover": {
"executable": "pwsh",
"args": [
"-NoLogo",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-NoProfile",
"-Command",
"./powershell.discover.ps1"
]
}
}
Loading