M365DSC.CRG.psm1
#Region './Private/Convert-Indent.ps1' -1 function Convert-Indent { <# .Synopsis Converts a numbered indentation into spaces or tabs .Description This function converts a numbered indentation into spaces or tabs. .Example Convert-Ident -Indent 2 .Parameter Indent The Indent parameter is the number of spaces or tabs that need to be returned. #> [CmdletBinding()] [OutputType([String])] param ( [Parameter()] [System.String] $Indent ) process { switch ($Indent) { '' { return ' ' } '1' { return "`t" } '2' { return ' ' } '4' { return ' ' } '0' { return '' } } $Indent } } #EndRegion './Private/Convert-Indent.ps1' 53 #Region './Private/ConvertTo-Psd.ps1' -1 function ConvertTo-Psd { <# .Synopsis Converts the inputted object to a hashtable in string format, which can be saved as PSD. .Description This function converts the inputted object to a string format, which can then be saved to a PSD1 file. .Example $configData = @{ Value1 = "String1" Value2 = 25 Value3 = @{ Value4 = "String2" Value5 = 50 } Value6 = @( @{ Value7 = "String3" Value8 = 75 } ) } $configData | ConvertTo-Psd .Parameter InputObject The InputObject parameter specified the object that has to be converted into PSD format. .Parameter Depth The Depth parameter specifies how deep the recursion should go. .Parameter Indent The Indent parameter is the number of spaces that need to be indented. #> [CmdletBinding()] [OutputType([String])] param ( [Parameter(Position = 0, ValueFromPipeline = 1)] [System.Object] $InputObject, [Parameter()] [System.Int32] $Depth, [Parameter()] [System.String] $Indent ) begin { $objects = [System.Collections.Generic.List[object]]@() } process { $objects.Add($InputObject) } end { trap { Invoke-TerminatingError $_ } $script:Depth = $Depth $script:Pruned = 0 $script:Indent = Convert-Indent -Indent $Indent $script:Writer = New-Object System.IO.StringWriter try { foreach ($object in $objects) { Write-Psd -Object $object } $script:Writer.ToString().TrimEnd() if ($script:Pruned) { Write-Warning "ConvertTo-Psd truncated $script:Pruned objects." } } finally { $script:Writer = $null } } } #EndRegion './Private/ConvertTo-Psd.ps1' 91 #Region './Private/Get-AttributeString.ps1' -1 function Get-AttributeString { <# .Synopsis Adds all the provided properties to the configuration data object. .Description This function loops through all the provided properties and adds them to the specified configuration data object. .Example Get-AttributeString -Property $property -ConfigData $currentDataObject .Parameter Property The Property parameter is a property from the MOF schema that needs to be added to the configuration data object. .Parameter ConfigData The ConfigData parameter is the configuration data object that needs to be updated with the provided property. #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [System.Object] $Property, [Parameter(Mandatory = $true)] [System.Object] $ConfigData ) process { $script:currentDepth++ if ($script:currentDepth -gt $script:maxDepth) { Write-Verbose 'MaxDepth reached!' $script:currentDepth-- return } # Property is an EmbeddedInstance, so we have to use that in generating the properties. $embeddedSchemas = $mofSchemas | Where-Object -FilterScript { ($_.ClassName -eq $Property.EmbeddedInstance) } Write-Verbose "CurrentDepth: $($script:currentDepth)" if ($Property.IsArray) { $ConfigData.$($Property.Name) = @(@{}) if ($embeddedSchemas.Attributes.Name -notcontains "Id" -and $embeddedSchemas.Attributes.Name -notcontains "Identity") { $ConfigData.$($property.Name)[0].UniqueId = ('{0} | {1} | {2}' -f "String", "Required", "[Unique ID to identify this specific object]") } foreach ($embeddedProperty in $embeddedSchemas.Attributes) { if ($null -eq $embeddedProperty.EmbeddedInstance) { $state = 'Optional' if ($embeddedProperty.State -in @('Key', 'Required')) { $state = 'Required' } if ($null -eq $embeddedProperty.ValueMap) { if ($embeddedProperty.DataType -like "*Array") { $dataType = $embeddedProperty.DataType -replace "Array" $result = @(('{0} | {1} | {2}' -f $dataType, $state, $embeddedProperty.Description)) } else { $result = ('{0} | {1} | {2}' -f $embeddedProperty.DataType, $state, $embeddedProperty.Description) } } else { if ($embeddedProperty.DataType -like "*Array") { $dataType = $embeddedProperty.DataType -replace "Array" $result = @(('{0} | {1} | {2} | {3}' -f $dataType, $state, $embeddedProperty.Description, ($embeddedProperty.ValueMap -join ' / '))) } else { $result = ('{0} | {1} | {2} | {3}' -f $embeddedProperty.DataType, $state, $embeddedProperty.Description, ($embeddedProperty.ValueMap -join ' / ')) } } $ConfigData.$($property.Name)[0].$($embeddedProperty.Name) = $result } else { Get-AttributeString -Property $embeddedProperty -ConfigData ($ConfigData.$($Property.Name)[0]) } } } else { $ConfigData.$($property.Name) = @{} foreach ($embeddedProperty in $embeddedSchemas.Attributes) { if ($null -eq $embeddedProperty.EmbeddedInstance) { $state = 'Optional' if ($embeddedProperty.State -in @('Key', 'Required')) { $state = 'Required' } if ($null -eq $embeddedProperty.ValueMap) { if ($embeddedProperty.DataType -like "*Array") { $dataType = $embeddedProperty.DataType -replace "Array" $result = @(('{0} | {1} | {2}' -f $dataType, $state, $embeddedProperty.Description)) } else { $result = ('{0} | {1} | {2}' -f $embeddedProperty.DataType, $state, $embeddedProperty.Description) } } else { if ($embeddedProperty.DataType -like "*Array") { $dataType = $embeddedProperty.DataType -replace "Array" $result = @(('{0} | {1} | {2} | {3}' -f $dataType, $state, $embeddedProperty.Description, ($embeddedProperty.ValueMap -join ' / '))) } else { $result = ('{0} | {1} | {2} | {3}' -f $embeddedProperty.DataType, $state, $embeddedProperty.Description, ($embeddedProperty.ValueMap -join ' / ')) } } $ConfigData.$($property.Name).$($embeddedProperty.Name) = $result } else { Get-AttributeString -Property $embeddedProperty -ConfigData ($ConfigData.$($Property.Name)) } } } $script:currentDepth-- } } #EndRegion './Private/Get-AttributeString.ps1' 151 #Region './Private/Get-EmbeddedPropertyString.ps1' -1 function Get-EmbeddedPropertyString { <# .Synopsis Adds all the provided embedded properties to the composite resource code. .Description This function loops through all the provided properties and adds references to them to the generated composite resource. .Example $result = Get-EmbeddedPropertyString -Properties $embeddedSchema.Attributes -Indentation $Indentation -ParameterName "`$_" .Parameter Properties The Properties parameter are the properties from the MOF schema that needs to be added to the composite resource. .Parameter Indentation The Indentation parameter is number of indentations that should be used for the generated code. .Parameter ParameterName The ParameterName parameter is the name of the parameter that should be used for the generated code. #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [System.Object] $Properties, [Parameter(Mandatory = $true)] [System.Int32] $Indentation, [Parameter(Mandatory = $true)] [System.String] $ParameterName ) process { $script:currentDepth++ if ($script:currentDepth -gt $script:maxDepth) { Write-Verbose 'MaxDepth reached!' $script:currentDepth-- return } $propertyString = [System.Text.StringBuilder]::new() $embeddedProperties = $Properties | Where-Object { $null -ne $_.EmbeddedInstance -and $_.EmbeddedInstance -ne 'MSFT_Credential' } if ($embeddedProperties.Count -ne 0) { Write-Verbose "CurrentDepth: $($script:currentDepth)" foreach ($Property in $embeddedProperties) { Write-Verbose "$($Property.Name) - $($Property.EmbeddedInstance)" $embeddedSchema = $mofSchemas | Where-Object -FilterScript { ($_.ClassName -eq $Property.EmbeddedInstance) } if ($null -ne $embeddedSchema) { $propertyName = $Property.Name [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), "if ($ParameterName.ContainsKey('$propertyName'))")) [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '{')) $Indentation++ [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), "$ParameterName.$propertyName = $ParameterName.$propertyName | ForEach-Object {")) $Indentation++ $result = Get-EmbeddedPropertyString -Properties $embeddedSchema.Attributes -Indentation $Indentation -ParameterName "`$_" if ([String]::IsNullOrEmpty($result) -eq $false) { [void]$propertyString.Append($result) } [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), "if (`$_.ContainsKey('UniqueId'))")) [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '{')) [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), " `$_.Remove('UniqueId')")) [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '}')) [void]$propertyString.AppendLine('') [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), "(Get-DscSplattedResource -ResourceName '$($Property.EmbeddedInstance)' -Properties `$_ -NoInvoke).Invoke(`$_)")) $Indentation-- [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '}')) $Indentation-- [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '}')) [void]$propertyString.AppendLine('') } else { Write-Warning -Message "Specified embedded instance '$($Property.EmbeddedInstance)' for property '$($Property.Name)' cannot be found!" $script:errors += [PSCustomObject]@{ Type = 'Warning' Message = "Specified embedded instance '$($Property.EmbeddedInstance)' for property '$($Property.Name)' cannot be found for resource '$shortResourceName'!" } } } } $script:currentDepth-- return $propertyString.ToString() } } #EndRegion './Private/Get-EmbeddedPropertyString.ps1' 111 #Region './Private/Get-IndentationString.ps1' -1 function Get-IndentationString { <# .SYNOPSIS Converts a number into a number of spaces, used for indentation. .DESCRIPTION This function converts a number into a number of spaces (4x the provided number), so it can be used for indentation. .EXAMPLE Get-IndentationString -Indentation 2 .PARAMETER Indentation The number of indentations (four spaces) that should be returned. #> [CmdletBinding()] [OutputType([String])] param ( [Parameter(Mandatory = $true)] [System.Int32] $Indentation ) process { return (' ' * $Indentation) } } #EndRegion './Private/Get-IndentationString.ps1' 30 #Region './Private/Get-MofSchemaObject.ps1' -1 function Get-MofSchemaObject { <# .Synopsis Reads and parses the Mof schema file .Description This function reads a Mof schema file and parses the results into an object. .Parameter FileName Specifies the full path of the Mof schema file. .Example # Loops through all schemas in the Microsoft365DSC module $m365modulePath = Split-Path -Path (Get-Module Microsoft365DSC -ListAvailable) -Parent $mofSearchPath = Join-Path -Path $m365modulePath -ChildPath '\**\**.schema.mof' $mofSchemaFiles = @(Get-ChildItem -Path $mofSearchPath -Recurse) foreach ($mofSchemaFile in $mofSchemaFiles) { $mofSchemas = Get-MofSchemaObject -FileName $mofSchemaFile.FullName # Your code to use the schemas } #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [System.String] $FileName ) process { if ($IsMacOS) { throw 'NotImplemented: Currently there is an issue using the type [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache] on macOS. See issue https://github.com/PowerShell/PowerShell/issues/5970 and issue https://github.com/PowerShell/MMI/issues/33.' } $temporaryPath = Get-TemporaryPath #region Workaround for OMI_BaseResource inheritance not resolving. $filePath = (Resolve-Path -Path $FileName).Path $tempFilePath = Join-Path -Path $temporaryPath -ChildPath "DscMofHelper_$((New-Guid).Guid).tmp" $rawContent = (Get-Content -Path $filePath -Raw) -replace '\s*:\s*OMI_BaseResource' Set-Content -LiteralPath $tempFilePath -Value $rawContent -ErrorAction 'Stop' # .NET methods don't like PowerShell drives $tempFilePath = Convert-Path -Path $tempFilePath #endregion try { $exceptionCollection = [System.Collections.ObjectModel.Collection[System.Exception]]::new() $moduleInfo = [System.Tuple]::Create('Module', [System.Version] '1.0.0') $class = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportClasses( $tempFilePath, $moduleInfo, $exceptionCollection ) } catch { throw "Failed to import classes from file $FileName. Error $_" } finally { Remove-Item -LiteralPath $tempFilePath -Force } foreach ($currentCimClass in $class) { $attributes = foreach ($property in $currentCimClass.CimClassProperties) { $state = switch ($property.flags) { { $_ -band [Microsoft.Management.Infrastructure.CimFlags]::Key } { 'Key' } { $_ -band [Microsoft.Management.Infrastructure.CimFlags]::Required } { 'Required' } { $_ -band [Microsoft.Management.Infrastructure.CimFlags]::ReadOnly } { 'Read' } default { 'Write' } } @{ Name = $property.Name State = $state DataType = $property.CimType ValueMap = $property.Qualifiers.Where( { $_.Name -eq 'ValueMap' }).Value IsArray = $property.CimType -gt 16 Description = $property.Qualifiers.Where( { $_.Name -eq 'Description' }).Value EmbeddedInstance = $property.Qualifiers.Where( { $_.Name -eq 'EmbeddedInstance' }).Value } } @{ ClassName = $currentCimClass.CimClassName Attributes = $attributes ClassVersion = $currentCimClass.CimClassQualifiers.Where( { $_.Name -eq 'ClassVersion' }).Value FriendlyName = $currentCimClass.CimClassQualifiers.Where( { $_.Name -eq 'FriendlyName' }).Value } } } } #EndRegion './Private/Get-MofSchemaObject.ps1' 116 #Region './Private/Get-TemporaryPath.ps1' -1 function Get-TemporaryPath { <# .Synopsis Gets the temporary path for the operating system .Description This function retrieves the system temporary path for the operating system. .Example Get-TemporaryPath #> [CmdletBinding()] [OutputType([System.String])] param () process { $temporaryPath = $null switch ($true) { (-not (Test-Path -Path variable:IsWindows) -or ((Get-Variable -Name 'IsWindows' -ValueOnly -ErrorAction SilentlyContinue) -eq $true)) { # Windows PowerShell or PowerShell 6+ $temporaryPath = (Get-Item -Path env:TEMP).Value } ((Get-Variable -Name 'IsMacOs' -ValueOnly -ErrorAction SilentlyContinue) -eq $true) { $temporaryPath = (Get-Item -Path env:TMPDIR).Value } ((Get-Variable -Name 'IsLinux' -ValueOnly -ErrorAction SilentlyContinue) -eq $true) { $temporaryPath = '/tmp' } default { throw 'Cannot set the temporary path. Unknown operating system.' } } return $temporaryPath } } #EndRegion './Private/Get-TemporaryPath.ps1' 48 #Region './Private/Initialize-Module.ps1' -1 function Initialize-Module { <# .Synopsis Initializes the module by creating all required files and folders .Description This function initializes the M365DSC.CompositeResources module by creating the required folder structure and the module manifest file. .Parameter Version Specifies the version of the module. .Parameter OutputPath Path to which files of the new module are written. .Example Initialize-Module -Version 1.23.1122.100 #> [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true)] [System.String] $Version, [Parameter(Mandatory = $true)] [System.String] $OutputPath ) process { # Create the folder structure $modulePath = Join-Path -Path $OutputPath -ChildPath "M365DSC.CompositeResources\$Version" if ((Test-Path -Path $modulePath) -eq $false) { $null = New-Item -Path $modulePath -ItemType Directory } $dscResourcesPath = Join-Path -Path $modulePath -ChildPath 'DscResources' if ((Test-Path -Path $dscResourcesPath) -eq $false) { $null = New-Item -Path $dscResourcesPath -ItemType Directory } $m365dscVersion = $Version.Substring(0, $Version.LastIndexOf('.') + 2) # Create the module manifest content $moduleManifestString = [System.Text.StringBuilder]::new() [void]$moduleManifestString.AppendLine('#') [void]$moduleManifestString.AppendLine("# Module manifest for module 'M365DSC.CompositeResources'") [void]$moduleManifestString.AppendLine('#') [void]$moduleManifestString.AppendLine("# Generated on: $(Get-Date -f "d-M-yyyy")") [void]$moduleManifestString.AppendLine('#') [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine('@{') [void]$moduleManifestString.AppendLine(' # Script module or binary module file associated with this manifest.') [void]$moduleManifestString.AppendLine(" RootModule = 'M365DSC.CompositeResources.psm1'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # Version number of this module.') [void]$moduleManifestString.AppendLine(" ModuleVersion = '$Version'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # ID used to uniquely identify this module') [void]$moduleManifestString.AppendLine(" GUID = '8c07a295-6a8d-465d-933d-9f598d77fdfb'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # Author of this module') [void]$moduleManifestString.AppendLine(" Author = 'Yorick Kuijs'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # Company or vendor of this module') [void]$moduleManifestString.AppendLine(" CompanyName = 'Microsoft'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # Modules that must be imported into the global environment prior to importing this module') [void]$moduleManifestString.AppendLine(' RequiredModules = @(') [void]$moduleManifestString.AppendLine(" @{ModuleName='DscBuildHelpers'; ModuleVersion ='0.2.1'; GUID='23ccd4bf-0a52-4077-986f-c153893e5a6a'}") [void]$moduleManifestString.AppendLine(" @{ModuleName='Microsoft365DSC'; RequiredVersion='$m365dscVersion'; GUID='39f599a6-d212-4480-83b3-a8ea2124d8cf'}") [void]$moduleManifestString.AppendLine(' )') [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # 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.') [void]$moduleManifestString.AppendLine(" FunctionsToExport = @('New-M365DSCExampleDataFile')") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # 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.') [void]$moduleManifestString.AppendLine(" CmdletsToExport = @()") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # Variables to export from this module') [void]$moduleManifestString.AppendLine(" VariablesToExport = @()") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # 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.') [void]$moduleManifestString.AppendLine(" AliasesToExport = @()") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # DSC resources to export from this module') [void]$moduleManifestString.AppendLine(" DscResourcesToExport = @('*')") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # Description of the functionality provided by this module') [void]$moduleManifestString.AppendLine(" Description = 'DSC composite resource for configuring Microsoft 365'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # Minimum version of the Windows PowerShell engine required by this module') [void]$moduleManifestString.AppendLine(" PowerShellVersion = '5.0'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # 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.') [void]$moduleManifestString.AppendLine(' PrivateData = @{') [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' PSData = @{') [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # Tags applied to this module. These help with module discovery in online galleries.') [void]$moduleManifestString.AppendLine(" Tags = @('DSC', 'DesiredStateConfiguration', 'M365DSC', 'Microsoft365DSC', 'Microsoft365', 'CompositeResource')") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # A URL to the license for this module.') [void]$moduleManifestString.AppendLine(" LicenseUri = 'https://github.com/ykuijs/M365DSC.CompositeResources/blob/main/LICENSE'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # A URL to the main website for this project.') [void]$moduleManifestString.AppendLine(" ProjectUri = 'https://github.com/ykuijs/M365DSC.CompositeResources'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # A URL to an icon representing this module.') [void]$moduleManifestString.AppendLine(" IconUri = 'https://github.com/microsoft/Microsoft365DSC/blob/Dev/Modules/Microsoft365DSC/Dependencies/Images/Logo.png?raw=true'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' # ReleaseNotes of this module') [void]$moduleManifestString.AppendLine(" ReleaseNotes = 'Module belongs to Microsoft365DSC v$($m365dscVersion)'") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(" ExternalModuleDependencies = @('Microsoft365DSC')") [void]$moduleManifestString.AppendLine('') [void]$moduleManifestString.AppendLine(' }') [void]$moduleManifestString.AppendLine(' }') [void]$moduleManifestString.AppendLine('}') $moduleManifestFileName = 'M365DSC.CompositeResources.psd1' $moduleManifestFilePath = Join-Path -Path $modulePath -ChildPath $moduleManifestFileName # Save the module manifest content to file Set-Content -Path $moduleManifestFilePath -Value $moduleManifestString.ToString() } } #EndRegion './Private/Initialize-Module.ps1' 134 #Region './Private/Invoke-TerminatingError.ps1' -1 function Invoke-TerminatingError { <# .Synopsis Throws a terminating error. .Description This function throws a terminating error, which makes sure the code actually stops. .Example Invoke-TerminatingError .Parameter M The M parameter is the message that needs to be displayed when the error is thrown. #> [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true)] [System.Object] $M ) process { $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([Exception]"$M"), $null, 0, $null)) } } #EndRegion './Private/Invoke-TerminatingError.ps1' 30 #Region './Private/Save-Resource.ps1' -1 function Save-Resource { <# .Synopsis Saves the composite resource content to file .Description This function saves the composite resource content to the schema file and creates the resource data file. .Parameter Config Specifies the content of the composite resource. .Parameter Workload Specifies the workload of the composite resource. .Parameter Version Specifies the version of the composite resource. .Parameter OutputPath Path to which files of the new composite resource are written. .Example Save-Resource -Config $content.ToString() -Workload 'Teams' -Version 1.23.1122.100 -OutputPath $OutputPath #> [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true)] [System.String] $Config, [Parameter(Mandatory = $true)] [System.String] $Workload, [Parameter(Mandatory = $true)] [System.String] $Version, [Parameter(Mandatory = $true)] [System.String] $OutputPath ) process { $modulePath = Join-Path -Path $OutputPath -ChildPath "M365DSC.CompositeResources\$Version" $dscResourcesPath = Join-Path -Path $modulePath -ChildPath 'DscResources' # Save the schema file of the composite resource $resourceSavePath = Join-Path -Path $dscResourcesPath -ChildPath $Workload if ((Test-Path -Path $resourceSavePath) -eq $false) { $null = New-Item -Path $resourceSavePath -ItemType Directory } $schemaFileName = '{0}.schema.psm1' -f $Workload $schemaFilePath = Join-Path -Path $resourceSavePath -ChildPath $schemaFileName Set-Content -Path $schemaFilePath -Value $Config # Create the manifest content for the composite resource $manifestString = [System.Text.StringBuilder]::new() [void]$manifestString.AppendLine('@{') [void]$manifestString.AppendLine(" RootModule = '$Workload.schema.psm1'") [void]$manifestString.AppendLine('') [void]$manifestString.AppendLine(" ModuleVersion = '$Version'") [void]$manifestString.AppendLine('') [void]$manifestString.AppendLine(" GUID = '$(New-Guid)'") [void]$manifestString.AppendLine('') [void]$manifestString.AppendLine(" Author = 'Yorick Kuijs'") [void]$manifestString.AppendLine('') [void]$manifestString.AppendLine(" CompanyName = 'Microsoft'") [void]$manifestString.AppendLine('') [void]$manifestString.AppendLine(" Copyright = 'Copyright to Microsoft Corporation. All rights reserved.'") [void]$manifestString.AppendLine('') [void]$manifestString.AppendLine(" DscResourcesToExport = @('$Workload')") [void]$manifestString.AppendLine('}') $manifestFileName = '{0}.psd1' -f $Workload $schemaFilePath = Join-Path -Path $resourceSavePath -ChildPath $manifestFileName # Save the manifest content of the composite resource to file Set-Content -Path $schemaFilePath -Value $manifestString.ToString() } } #EndRegion './Private/Save-Resource.ps1' 88 #Region './Private/Write-Psd.ps1' -1 function Write-Psd { <# .Synopsis Converts an object into a string so it can be written to PSD file. .Description This function converts an inputted object into a string so it can be written to a PSD1 file. .Example Write-Psd -Object $configData .Parameter Object Specifies the object that needs to be converted to a string. .Parameter Depth Specifies how deep the recursion should go. The default is 0, which means no recursion. .Parameter NoIndent Specifies that the output should not be indented. #> [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true)] [System.Object] $Object, [Parameter()] [System.Int32] $Depth = 0, [Parameter()] [switch] $NoIndent ) process { $indent1 = $script:Indent * $Depth if (!$NoIndent) { $script:Writer.Write($indent1) } if ($null -eq $Object) { $script:Writer.WriteLine('$null') return } $type = $Object.GetType() switch ([System.Type]::GetTypeCode($type)) { Object { if ($type -eq [System.Guid] -or $type -eq [System.Version]) { $script:Writer.WriteLine("'{0}'", $Object) return } if ($type -eq [System.Management.Automation.SwitchParameter]) { $script:Writer.WriteLine($(if ($Object) { '$true' } else { '$false' })) return } if ($type -eq [System.Uri]) { $script:Writer.WriteLine("'{0}'", $Object.ToString().Replace("'", "''")) return } if ($script:Depth -and $Depth -ge $script:Depth) { $script:Writer.WriteLine("''''") ++$script:Pruned return } if ($Object -is [System.Collections.IDictionary]) { if ($Object.Count) { $itemNo = 0 $script:Writer.WriteLine('@{') $indent2 = $script:Indent * ($Depth + 1) foreach ($e in $Object.GetEnumerator()) { $key = $e.Key $value = $e.Value $keyType = $key.GetType() if ($keyType -eq [string]) { if ($key -match '^\w+$' -and $key -match '^\D') { $script:Writer.Write('{0}{1} = ', $indent2, $key) } else { $script:Writer.Write("{0}'{1}' = ", $indent2, $key.Replace("'", "''")) } } elseif ($keyType -eq [int]) { $script:Writer.Write('{0}{1} = ', $indent2, $key) } elseif ($keyType -eq [long]) { $script:Writer.Write('{0}{1}L = ', $indent2, $key) } elseif ($script:Depth) { ++$script:Pruned $script:Writer.Write('{0}item__{1} = ', $indent2, ++$itemNo) $value = New-Object 'System.Collections.Generic.KeyValuePair[object, object]' $key, $value } else { throw "Not supported key type '$($keyType.FullName)'." } Write-Psd -Object $value -Depth ($Depth + 1) -NoIndent } $script:Writer.WriteLine("$indent1}") } else { $script:Writer.WriteLine('@{}') } return } if ($Object -is [System.Collections.IEnumerable]) { $script:Writer.Write('@(') $empty = $true foreach ($e in $Object) { if ($empty) { $empty = $false $script:Writer.WriteLine() } Write-Psd -Object $e -Depth ($Depth + 1) } if ($empty) { $script:Writer.WriteLine(')') } else { $script:Writer.WriteLine("$indent1)" ) } return } if ($Object -is [scriptblock]) { $script:Writer.WriteLine('{{{0}}}', $Object) return } if ($Object -is [PSCustomObject] -or $script:Depth) { $script:Writer.WriteLine('@{') $indent2 = $script:Indent * ($Depth + 1) foreach ($e in $Object.PSObject.Properties) { $key = $e.Name if ($key -match '^\w+$' -and $key -match '^\D') { $script:Writer.Write('{0}{1} = ', $indent2, $key) } else { $script:Writer.Write("{0}'{1}' = ", $indent2, $key.Replace("'", "''")) } Write-Psd -Object $e.Value -Depth ($Depth + 1) -NoIndent } $script:Writer.WriteLine("$indent1}") return } } String { $script:Writer.WriteLine("'{0}'", $Object.Replace("'", "''")) return } Boolean { $script:Writer.WriteLine($(if ($Object) { '$true' } else { '$false' })) return } DateTime { $script:Writer.WriteLine("[DateTime] '{0}'", $Object.ToString('o')) return } Char { $script:Writer.WriteLine("'{0}'", $Object.Replace("'", "''")) return } DBNull { $script:Writer.WriteLine('$null') return } default { if ($type.IsEnum) { $script:Writer.WriteLine("'{0}'", $Object) } else { $script:Writer.WriteLine($Object) } return } } throw "Not supported type '{0}'." -f $type.FullName } } #EndRegion './Private/Write-Psd.ps1' 235 #Region './Public/New-CompositeResourceModule.ps1' -1 function New-CompositeResourceModule { <# .Synopsis Generate a new module with composite resources based on Microsoft365DSC. .Description This function generates a new module with composite resources for Microsoft365DSC, split into workloads. .Example New-CompositeResourceModule -OutputPath 'C:\Temp' -Version '11.23.1122.100' .Parameter OutputPath Specifies the path in which the new module should be generated. .Parameter Version Specifies the version of the new module should be generated. This should be related to the version of Microsoft365DSC. E.g. Version is 1.23.1122.100 when the Microsoft365DSC version is 1.23.1122.1 #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="Using Write-Host to format output")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Not changing state")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $OutputPath, [Parameter(Mandatory = $true)] [System.String] $Version ) process { # These properties should be ignored by the code because they are not used in the composite resource $ignoredProperties = @('Credential', 'CertificatePassword', 'CertificatePath', 'ApplicationSecret', 'ManagedIdentity', 'DependsOn', 'PsDscRunAsCredential') $prerequisitesDatafile = Join-Path -Path $PSScriptRoot -ChildPath 'Dependencies.psd1' if (Test-Path -Path $prerequisitesDatafile) { $resourceDependencies = Import-PowerShellDataFile -Path $prerequisitesDatafile } else { Write-Host -Object "[ERROR] Prerequisites data file '$prerequisitesDatafile' not found!" -ForegroundColor Red return $false } $script:errors = @() $configData = [Ordered]@{ AllNodes = @( @{ NodeName = 'String | Required | Name of the host of which the LCM is used, normally this is localhost' CertificateFile = 'String | Required | Relative path to the public key of the DSC credential encryption certificate, e.g. .\DSCCertificate.cer' } ) NonNodeData = [Ordered]@{ Environment = @{ Name = 'String | Required | Name of your environment, e.g. TestEnvironment' ShortName = 'String | Required | Abbreviation of the environment name, e.g. TST' TenantId = 'String | Required | Tenant URL, e.g. test.onmicrosoft.com' OrganizationName = 'String | Required | Name of your organization, prefix of the tenant id, e.g. test' CICD = @{ DependsOn = 'String | Required | Name of the environment this environment depends on, e.g. TestEnvironment' UseCodeBranch = 'String | Required | Name of the branch that is used for the CICD (Script) repository, e.g. main' Approvers = @( @{ Principal = 'String | Required | Principal of the user or groups that needs to get added to the approvers list.' Type = 'String | Required | Type of principal | User / Group' } ) } UsedWorkloads = @{} Tokens = @{ ExampleToken = 'String | Optional | Example of a token that can be used anywhere in the config, by specifying {{ExampleToken}}' } } AppCredentials = @( @{ Workload = 'String | Required | Name of the Workload for which this credential will be used | AzureAD / Exchange / Intune / Office365 / OneDrive / Planner / PowerPlatform / SecurityCompliance / SharePoint / Teams' ApplicationId = 'Guid | Required | The GUID of the Entra ID Service Principal' CertThumbprint = 'String | Required | The Certificate Thumbprint of the certificate used for authentication' } ) } } #endregion Initialize Variables #region Main script # Get the Microsoft365DSC module $m365dscVersion = $Version.Substring(0, $Version.LastIndexOf('.') + 2) [Array]$m365Module = Get-Module Microsoft365DSC -ListAvailable | Where-Object -FilterScript { $_.Version -eq $m365dscVersion } # Check if the module has been retrieved or not if ($m365Module.Count -gt 0) { if ($m365Module.Count -gt 1) { Write-Host -Object 'Found multiple versions of Microsoft365DSC. Using the latest version.' $m365Module = $m365Module | Select-Object -First 1 } # Initialize the module Initialize-Module -Version $Version -OutputPath $OutputPath # Get the module root path and find all schema.mof files in the module $m365modulePath = Split-Path -Path $m365Module.Path -Parent $mofSearchPath = Join-Path -Path $m365modulePath -ChildPath '\**\**.schema.mof' $mofSchemaFiles = @(Get-ChildItem -Path $mofSearchPath -Recurse) Write-Host -Object ("Found {0} MOF files in path '{1}'." -f $mofSchemaFiles.Count, $m365modulePath) -ForegroundColor Cyan if ($mofSchemaFiles.Count -eq 0) { return $false } Write-Host -Object ' ' Write-Host -Object 'Processing resources:' -ForegroundColor Cyan $lastWorkload = '' # Loop through all the Schema files found in the modules folder :schemaloop foreach ($mofSchemaFile in $mofSchemaFiles) { # Read schema $mofSchemas = Get-MofSchemaObject -FileName $mofSchemaFile.FullName $dscResourceName = $mofSchemaFile.Name.Replace('.schema.mof', '') $shortResourceName = $dscResourceName -replace '^MSFT_' Write-Host -Object " - $shortResourceName" -ForegroundColor Green # Geth the main CIM class of the resource $resourceSchema = $mofSchemas | Where-Object -FilterScript { ($_.ClassName -eq $dscResourceName) -and ($null -ne $_.FriendlyName) } # Determine which workload the current resource is in switch ($shortResourceName) { { $_.StartsWith('AAD') } { $resourceWorkload = 'AzureAD' $customResourceName = $shortResourceName -replace "^AAD" } { $_.StartsWith('EXO') } { $resourceWorkload = 'Exchange' $customResourceName = $shortResourceName -replace '^EXO' } { $_.StartsWith('Fabric') } { $resourceWorkload = 'Fabric' $customResourceName = $shortResourceName -replace "^$resourceWorkload" } { $_.StartsWith('Intune') } { $resourceWorkload = 'Intune' $customResourceName = $shortResourceName -replace "^$resourceWorkload" } { $_.StartsWith('O365') } { $resourceWorkload = 'Office365' $customResourceName = $shortResourceName -replace '^O365' } { $_.StartsWith('OD') } { $resourceWorkload = 'OneDrive' $customResourceName = $shortResourceName -replace '^OD' } { $_.StartsWith('Planner') } { $resourceWorkload = 'Planner' $customResourceName = $shortResourceName -replace "^$resourceWorkload" } { $_.StartsWith('PP') } { $resourceWorkload = 'PowerPlatform' $customResourceName = $shortResourceName -replace '^PP' } { $_.StartsWith('SC') } { $resourceWorkload = 'SecurityCompliance' $customResourceName = $shortResourceName -replace '^SC' } { $_.StartsWith('SPO') } { $resourceWorkload = 'SharePoint' $customResourceName = $shortResourceName -replace '^SPO' } { $_.StartsWith('Teams') } { $resourceWorkload = 'Teams' $customResourceName = $shortResourceName -replace "^$resourceWorkload" } { $_.StartsWith('M365DSC') } { Write-Host ' Skipping M365DSC workload (RuleEvaluation)' -ForegroundColor 'Yellow' continue schemaloop } Default { Write-Error 'Unknown workload for this resource!' $script:errors += [PSCustomObject]@{ Type = 'Error' Message = "Unknown workload for resource '$shortResourceName'" } continue } } # Check if the last workload matches the current workload if ($lastWorkload -ne $resourceWorkload) { # Last workload is different from the current workload. # Check if this is the first workload or a subsequent workload if ([String]::IsNullOrEmpty($lastWorkload) -eq $false) { # Is a subsequent workload, so wrap up previous workload and save resource [void]$configString.Append('}') Save-Resource -Config $configString.ToString() -Workload $lastWorkload -Version $Version -OutputPath $OutputPath } $lastWorkload = $resourceWorkload if ($null -eq $configData.NonNodeData.$resourceWorkload) { $configData.NonNodeData.$resourceWorkload = [Ordered]@{} } $configData.NonNodeData.Environment.UsedWorkloads.$resourceWorkload = 'Boolean | Required | Specifies if the workload should be included in the configuration' # Initialize new composite resource content $configString = [System.Text.StringBuilder]::new() [void]$configString.AppendLine("# ($(Get-Date -f 'yyyy-MM-dd HH:mm:ss')) Generated using Microsoft365DSC v$($m365Module.Version)") [void]$configString.AppendLine("Configuration '$($resourceWorkload)'") [void]$configString.AppendLine('{') [void]$configString.AppendLine(' param') [void]$configString.AppendLine(' (') [void]$configString.AppendLine(' [Parameter(Mandatory = $true)]') [void]$configString.AppendLine(' [System.String]') [void]$configString.AppendLine(' $ApplicationId,') [void]$configString.AppendLine('') [void]$configString.AppendLine(' [Parameter(Mandatory = $true)]') [void]$configString.AppendLine(' [System.String]') [void]$configString.AppendLine(' $TenantId,') [void]$configString.AppendLine('') [void]$configString.AppendLine(' [Parameter(Mandatory = $true)]') [void]$configString.AppendLine(' [System.String]') [void]$configString.AppendLine(' $CertificateThumbprint') [void]$configString.AppendLine(' )') [void]$configString.AppendLine('') [void]$configString.AppendLine(' Import-DscResource -ModuleName Microsoft365DSC') } $identityGlobalResource = $false $indent = 1 # Generate Configuration Data location of the settings $configDataLocation = '$ConfigurationData.NonNodeData.{0}.{1}' -f $lastWorkload, $customResourceName # Checking for missing CertificateThumbprint parameter and skip resource if it is (TeamsUserCallingSettings resource) if (($resourceSchema.Attributes | Where-Object -FilterScript { $_.Name -eq 'CertificateThumbprint' }) -isnot [System.Collections.Hashtable]) { Write-Host ' Resource does not support CertificateThumbprint authentication. Skipping resource for now!' -ForegroundColor Red $script:errors += [PSCustomObject]@{ Type = 'Warning' Message = "Resource '$shortResourceName' does not support CertificateThumbprint authentication. Resource has been skipped!" } continue } # Check if the current DSC resource is a SingleInstance resource if (($resourceSchema.Attributes | Where-Object -FilterScript { $_.Name -eq 'IsSingleInstance' }) -is [System.Collections.Hashtable]) { # IsSingleInstance resources can only exist once and therefore should not loop through an array Write-Debug -Message ' * Has IsSingleInstance' $configData.NonNodeData.$resourceWorkload.$customResourceName = [Ordered]@{} $currentDataObject = $configData.NonNodeData.$resourceWorkload.$customResourceName # Single instance resources should [void]$configString.AppendLine('') [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "if (`$ConfigurationData.NonNodeData.$resourceWorkload.ContainsKey('$customResourceName'))")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '{')) $indent++ $resourceTitle = "$($customResourceName)Defaults" [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$resourceTitle = '$resourceTitle'")) [void]$configString.AppendLine('') # Adding parameters hashtable initialization to the code [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters = $configDataLocation")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.IsSingleInstance = 'Yes'")) } # Check if the current DSC resource has an Identity parameter which only accepts the value 'Global' elseif (($resourceSchema.Attributes | Where-Object -FilterScript { $_.Name -eq 'Identity' -and $_.ValueMap -eq 'Global' }) -is [System.Collections.Hashtable]) { # Global resources can only exist once and therefore should not loop through an array $identityGlobalResource = $true Write-Debug -Message ' * Identity = Global' $configData.NonNodeData.$resourceWorkload.$customResourceName = @{} $currentDataObject = $configData.NonNodeData.$resourceWorkload.$customResourceName [void]$configString.AppendLine('') [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "if (`$ConfigurationData.NonNodeData.$resourceWorkload.ContainsKey('$customResourceName'))")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '{')) $indent++ $resourceTitle = "$($customResourceName)Defaults" [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$resourceTitle = '$resourceTitle'")) [void]$configString.AppendLine('') # Adding parameters hashtable initialization to the code [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters = $configDataLocation")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.Identity = 'Global'")) } # All other resources should be processed similar else { # All other resources can exist multiple times and therefore should loop through an array [void]$configString.AppendLine('') # Generate the plural name of the data node if ($shortResourceName.EndsWith('y')) { $dataName = $customResourceName -replace 'y$', 'ies' } elseif ($shortResourceName -like '*Policy*') { $dataName = $customResourceName -replace 'Policy', 'Policies' } elseif ($shortResourceName -like '*Profile*') { $dataName = $customResourceName -replace 'Profile', 'Profiles' } elseif ($shortResourceName.EndsWith('Settings')) { $dataName = $customResourceName += 'Items' } else { $dataName = $customResourceName + 's' } $configData.NonNodeData.$resourceWorkload.$dataName = @(@{}) $currentDataObject = $configData.NonNodeData.$resourceWorkload.$dataName[0] # Add foreach to loop through the array [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "foreach (`$$($customResourceName) in `$ConfigurationData.NonNodeData.$($lastWorkload).$($dataName))")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '{')) $indent++ # Shorten the Configuration Data location $configDataLocation = '${0}' -f $customResourceName # Generate name of the resource, using the Key parameter(s) $mandatoryProperties = $resourceSchema.Attributes | Where-Object { $_.State -in @('Key') } if ($mandatoryProperties.Name -is [System.Array]) { # Multiple key parameters found, combine these names $paramFullPlaceholder = '' $paramPlaceholder = '' for ($i = 0; $i -lt $mandatoryProperties.Name.Count; $i++) { $paramFullPlaceholder += "$configDataLocation.{$i}," $paramPlaceholder += "{$i}-" } $paramFullPlaceholder = $paramFullPlaceholder.TrimEnd(',') $paramPlaceholder = $paramPlaceholder.TrimEnd('-') $resourceTitle = '$resourceTitle = ''{0}-[parameter]'' -f {1}' -f $shortResourceName, $paramFullPlaceholder $resourceTitle = $resourceTitle -f $mandatoryProperties.Name $resourceTitle = $resourceTitle -replace '\[parameter\]', $paramPlaceholder } else { # Single key parameter found $resourceTitle = '$resourceTitle = ''{0}-{1}'' -f {2}.{3}' -f $shortResourceName, '{0}', $configDataLocation, $mandatoryProperties.Name } # Adding resource title generation [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), $resourceTitle)) [void]$configString.AppendLine('') # Adding parameters hashtable initialization to the code [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters = $configDataLocation")) } [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.ApplicationId = `$ApplicationId")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.TenantId = `$TenantId")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.CertificateThumbprint = `$CertificateThumbprint")) [void]$configString.AppendLine('') [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "if (`$parameters.ContainsKey('UniqueId'))")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '{')) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), " `$parameters.Remove('UniqueId')")) [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '}')) $script:currentDepth = 0 $script:maxDepth = 8 # Process all parameters, but handle embedded parameters separately $result = Get-EmbeddedPropertyString -Properties $resourceSchema.Attributes -Indentation $indent -ParameterName '$parameters' if ([String]::IsNullOrEmpty($result) -eq $false) { [void]$configString.Append($result) } # Add DependsOn parameter, when necessary. if ($resourceDependencies.ContainsKey($shortResourceName)) { $dependsString = $resourceDependencies.$shortResourceName [void]$configString.AppendLine(("{0}{1} = {2}" -f (Get-IndentationString -Indentation $indent), '$parameters.DependsOn', $dependsString)) [void]$configString.AppendLine('') } [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "(Get-DscSplattedResource -ResourceName '$shortResourceName' -ExecutionName `$resourceTitle -Properties `$parameters -NoInvoke).Invoke(`$parameters)")) # Get all the properties in the resource and filter out those that should be ignored $filteredProperties = $resourceSchema.Attributes | Where-Object { $_.Name -notin $ignoredProperties } | Sort-Object -Property Name $script:currentDepth = 0 # Check if the resource needs a UniqueId parameter by checking if the names of the properties contain # any of the below property names. If they don't, a UniqueId is required. $propertiesNeedUniqueId = @("Id", "Identity", "IsSingleInstance") $diff = Compare-Object -ReferenceObject $filteredProperties.Name -DifferenceObject $propertiesNeedUniqueId -ExcludeDifferent -IncludeEqual if ($null -eq $diff) { $currentDataObject.UniqueId = ('{0} | {1} | {2}' -f "String", "Required", "Unique ID to identify this specific object") } # Loop through all the filtered properties and build example config data file foreach ($property in $filteredProperties) { # Check if the property is an EmbeddedInstance if ($null -ne $property.EmbeddedInstance -and $property.EmbeddedInstance -ne 'MSFT_Credential') { $null = Get-AttributeString -Property $property -ConfigData $currentDataObject } else { if ($property.Name -notin @('IsSingleInstance','ApplicationId', 'CertificateThumbprint', 'TenantId') -and (($identityGlobalResource -eq $false) -or ($identityGlobalResource -and $property.Name -ne 'Identity'))) { # For all other parameter, generate the correct parameter value name from the Configuration Data $propertyDataType = $property.DataType if ($propertyDataType -eq 'Instance' -and $property.EmbeddedInstance -eq 'MSFT_Credential') { $propertyDataType = 'PSCredential' } $state = 'Optional' if ($property.State -in @('Key', 'Required')) { $state = 'Required' } if ($null -eq $property.ValueMap) { if ($propertyDataType -like "*Array") { $propertyDataType = $propertyDataType -replace "Array" $result = @(('{0} | {1} | {2}' -f $propertyDataType, $state, $property.Description)) } else { $result = ('{0} | {1} | {2}' -f $propertyDataType, $state, $property.Description) } } else { if ($propertyDataType -like "*Array") { $propertyDataType = $propertyDataType -replace "Array" $result = @(('{0} | {1} | {2} | {3}' -f $propertyDataType, $state, $property.Description, ($property.ValueMap -join ' / '))) } else { $result = ('{0} | {1} | {2} | {3}' -f $propertyDataType, $state, $property.Description, ($property.ValueMap -join ' / ')) } } $currentDataObject.$($property.Name) = $result } } } $indent-- [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '}')) } # Last workload is processed. Make sure the this resource is also saved to file [void]$configString.Append('}') Save-Resource -Config $configString.ToString() -Workload $lastWorkload -Version $Version -OutputPath $OutputPath Write-Host -Object 'Writing ConfigurationData file' -ForegroundColor Cyan $psdStringData = "# ($(Get-Date -f 'yyyy-MM-dd HH:mm:ss')) Generated using Microsoft365DSC v$($m365Module.Version)`n" $psdStringData += $configData | ConvertTo-Psd $psdPath = Join-Path -Path $OutputPath -ChildPath "M365DSC.CompositeResources\$Version\M365ConfigurationDataExample.psd1" Set-Content -Path $psdPath -Value $psdStringData Write-Host -Object 'Encountered issues:' -ForegroundColor Cyan foreach ($err in $script:errors) { Write-Host -Object ("{0,-10} | {1}"-f $err.Type,$err.Message) } return $true } else { # Can't generate the module, since the Microsoft365DSC module cannot be found. Write-Host -Object 'Microsoft365DSC not found!' -ForegroundColor Red return $false } #endregion } } #EndRegion './Public/New-CompositeResourceModule.ps1' 534 #Region './suffix.ps1' -1 # Inspired from https://github.com/nightroman/Invoke-Build/blob/64f3434e1daa806814852049771f4b7d3ec4d3a3/Tasks/Import/README.md#example-2-import-from-a-module-with-tasks Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'tasks\*') -Include '*.build.*' | ForEach-Object -Process { $ModuleName = ([System.IO.FileInfo] $MyInvocation.MyCommand.Name).BaseName $taskFileAliasName = "$($_.BaseName).$ModuleName.ib.tasks" Set-Alias -Name $taskFileAliasName -Value $_.FullName Export-ModuleMember -Alias $taskFileAliasName } #EndRegion './suffix.ps1' 10 |