internal/functions/Get-DscResourceParameterInfoByCimClass.ps1

Function Get-DscResourceParameterInfoByCimClass {
  <#
    .SYNOPSIS
      Retrieve the DSC Parameter information, if possible, by CIM Instance.
 
    .DESCRIPTION
      Given a DSC Resource Info object, load its CIM Class by invoking it once (ignoring errors) and then
      introspecting its CIM class information in the DSC namespace. This requires running with administrator
      privileges, unfortunately, as access to the CIM classes is privilege-gated.
 
      It will discover help documentation if it is surfaced in the CIM class (not all resources do so), will
      retrieve and map embedded CIM instances (which `Get-DscResourceParameterInfo` cannot do), but cannot
      retrieve default values as these are not mapped.
 
    .PARAMETER DscResource
      A Dsc Resource Info object, as returned by Get-DscResource.
 
    .EXAMPLE
      Get-DscResource -Name PSRepository | Get-DscResourceParameterInfoByCimClass
 
      Retrieve the Parameter information from the CIM class for the PSRepository Dsc Resource.
  #>

  [CmdletBinding()]
  param (
    [Parameter(ValueFromPipeline)]
    [Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo]$DscResource
  )

  Begin {}

  Process {
    # We can assume it will find the right version because this is only ever called after we've munged the PSModulePath
    $ModulePath = Get-Module -ListAvailable -Name $DscResource.ModuleName | Select-Object -ExpandProperty Path
    # Invoke once to load the CIM class information, ignore all errors
    Initialize-DscResourceCimClass -Name $DscResource.Name -ModuleName $ModulePath -ModuleVersion $DscResource.Version -ErrorAction Stop

    # Look for embedded instances, store them for type-definition interpolation.
    $DefinedEmbeddedInstances = @{}
    $EmbeddedInstanceTypes = Get-EmbeddedCimInstance -ClassName $DscResource.ResourceType -Recurse
    If ($EmbeddedInstanceTypes.count -gt 0) {
      # Parse Embedded Instances in reverse, which should figure out nested instances before those that contain them
      [array]::Reverse($EmbeddedInstanceTypes)
      ForEach ($InstanceType in $EmbeddedInstanceTypes) {
        # Handle credential objects separately as they are well-known constructs
        If ($InstanceType -eq 'MSFT_Credential') {
          $DefinedEmbeddedInstances.$InstanceType = 'Optional[Struct[{ user => String[1], password => Sensitive[String[1]] }]]'
        } Else {
          # Capture the metadata in order to parse the Puppet type definition and retrieve the cim instance types.
          $EmbeddedInstanceMetadata = @{}
          $EmbeddedInstanceMetadata.$InstanceType = @{
            cim_instance_type = "Enum['$InstanceType']"
          }
          $CimClassProperties = Get-CimClassPropertiesList -ClassName $InstanceType
          ForEach ($Property in $CimClassProperties) {
            If ($Property.ReferenceClassName -in $DefinedEmbeddedInstances.Keys) {
              # Handle nested instances, wrapping them in the Array datatype if necessary
              If ($Property.CimType -match 'Array') {
                $EmbeddedInstanceMetadata.$InstanceType.$($Property.Name) = "Array[$($DefinedEmbeddedInstances.($Property.ReferenceClassName))]"
              } Else {
                $EmbeddedInstanceMetadata.$InstanceType.$($Property.Name) = $DefinedEmbeddedInstances.($Property.ReferenceClassName)
              }
            } Else {
              # If it's not a CIM instance the standard type mapper can handle it.
              $EmbeddedInstanceMetadata.$InstanceType.$($Property.Name) = Get-PuppetDataType -DscResourceProperty @{
                Values       = $Property.Qualifiers['Values'].Value
                IsMandatory  = $Property.Flags -Match 'Required'
                # Replace the Array identifier with [] to match current expectations
                PropertyType = $Property.CimType -Replace '(\S+)Array$', '$1[]'
              }
            }
          }
          # Nested CIM instances need to be reassembled into readable Structs; but we want to increase the indentation level by one
          # so that it's more visually distinct in the end file
          $StructComponents = $EmbeddedInstanceMetadata.$InstanceType.Keys.toLowerInvariant() |
            ForEach-Object -Process { "$_ => $($EmbeddedInstanceMetadata.$InstanceType.$_ -replace "`n", "`n ")" }
          # Assemble the current CIM instance as a struct, strip out any double quotes to prevent breaking parsing
          $DefinedEmbeddedInstances.$InstanceType = "Struct[{`n $($StructComponents -join ",`n " -replace '"')`n}]"
        }
      }
    }

    # Do some slight property handling to ignore properties we don't care about.
    # Minimally adapted from Ansible's implementation:
    # - https://github.com/ansible-collections/ansible.windows/blob/main/plugins/modules/win_dsc.ps1#L42-L62
    # Which itself borrows from core DSC:
    # - https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/DscSupport/CimDSCParser.cs#L1203
    $PropertiesToDiscard = @('ConfigurationName', 'DependsOn', 'ModuleName', 'ModuleVersion', 'ResourceID', 'SourceInfo')
    $DscResourceCimClassProperties = Get-CimClassPropertiesList -ClassName $DscResource.ResourceType |
      Where-Object { $_.Name -notin $PropertiesToDiscard }

    $DscResourceMetadata = @{}

    # Similarly to how the properties were resolved for embedded CIM instances, resolve them for each property
    ForEach ($Property in $DscResourceCimClassProperties) {
      $IsMandatory = $Property.Flags -Match '(Required|Key)'
      $DscResourceMetadata.$($Property.Name) = [ordered]@{
        Name              = $Property.Name.ToLowerInvariant()
        # The one thing we *can't* retrieve here is the default values; they still apply, but they're
        # not exposed in the definition here for some reason. In the alternate implementation, we can
        # only retrieve default values by parsing the AST, so this is acceptable, if not ideal.
        DefaultValue      = $null
        Help              = $Property.Qualifiers['Description'].Value
        is_parameter      = Test-DscResourcePropertyParameterStatus -Property $Property
        is_namevar        = ($Property.Flags -Match 'Key').ToString().ToLowerInvariant()
        is_read_only      = $Property.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::ReadOnly)
        mandatory_for_get = $IsMandatory.ToString().ToLowerInvariant()
        mandatory_for_set = $IsMandatory.ToString().ToLowerInvariant()
        mof_is_embedded   = 'false'
      }
      If ($Property.ReferenceClassName -in $DefinedEmbeddedInstances.Keys) {
        $DscResourceMetadata.$($Property.Name).mof_is_embedded = 'true'
        $MofType = $Property.ReferenceClassName
        # Munge the type name per the expectations/surface from Get-DscResource and existing provider.
        If ($MofType -eq 'MSFT_Credential') { $MofType = 'PSCredential' }
        $DscResourceMetadata.$($Property.Name).mof_type = if ($Property.CimType -match 'Array') {
          "$MofType[]"
        } Else {
          $MofType
        }
        # Split the definition for the struct and toss away the cim_instance_type key as this is a top-level property
        # and that information is captured in the mof_type key already.
        $SplitDefinition = $DefinedEmbeddedInstances.($Property.ReferenceClassName) -split "`n" |
          Where-Object -FilterScript { $_ -notmatch "cim_instance_type => Enum\['$($Property.ReferenceClassName)'\]" }
        # Recombine the struct definition appropriately mapped as an array or singleton
        If ($Property.CimType -match 'Array') {
          $PuppetType = "Array[$($SplitDefinition -Join "`n")]"
          $DscResourceMetadata.$($Property.Name).Type
        } Else {
          $PuppetType = "$($SplitDefinition -Join "`n")"
        }
        If ($IsMandatory -or ($PuppetType -match '^Optional\[')) {
          $DscResourceMetadata.$($Property.Name).Type = $PuppetType
        } Else {
          $DscResourceMetadata.$($Property.Name).Type = "Optional[$PuppetType]"
        }
      } Else {
        $DscResourceMetadata.$($Property.Name).mof_type = $Property.CimType -Replace '(\S+)Array$', '$1[]'
        $DscResourceMetadata.$($Property.Name).Type = Get-PuppetDataType -DscResourceProperty @{
          Values       = $Property.Qualifiers['Values'].Value
          IsMandatory  = $Property.Flags -Match '(Required|Key)'
          # Replace the Array identifier with [] to match current expectations
          PropertyType = $Property.CimType -Replace '(\S+)Array$', '[$1[]]'
        }
      }
    }

    ForEach ($Property in $DscResourceMetadata.Keys) {
      # Each object has the Name, DefaultValue, Help, mandatory_for_get, mandatory_for_set, mof_type, & Type properties
      # This is the surface that Get-TypeParameterContent expects for processing a resource.
      [PSCustomObject]$DscResourceMetadata.$Property
    }
  }

  End {}
}