functions/New-PuppetDscModule.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
Function New-PuppetDscModule {
  <#
    .SYNOPSIS
      Puppetize a PowerShell module with DSC resources
    .DESCRIPTION
      This function builds a Puppet Module which wraps and calls PowerShell DSC resources
      via the Puppet resource_api. This module:
 
      - Includes a base resource_api provider which relies on ruby-pwsh and knows how to invoke DSC resources
      - Includes a type for each DSC resource, pulling in the appropriate metadata including help, default value
        and mandatory status, as well as whether or not it includes an embedded mof.
      - Allows for the tracking of changes on a property-by-property basis while using DSC and Puppet together
    .PARAMETER PowerShellModuleName
      The name of the PowerShell module on the gallery which has DSC resources you want to Puppetize
    .PARAMETER PowerShellModuleVersion
      The version of the PowerShell module on the gallery which has DSC resources you want to Puppetize.
      If left blank, will default to latest available.
    .PARAMETER PuppetModuleName
      The name of the Puppet module for the wrapper; if not specified, will default to the downcased name of
      the module to adhere to Puppet naming conventions.
    .PARAMETER PuppetModuleAuthor
      The name of the Puppet module author; if not specified, will default to your PDK configuration's author.
    .PARAMETER OutputDirectory
      The folder in which to build the Puppet module. Defaults to a folder called import in the current location.
    .PARAMETER PassThru
      If specified, the function returns the path to the root folder of the Puppetized module on the filesystem.
    .PARAMETER Confirm
      Prompts for confirmation before creating the Puppet module
    .PARAMETER WhatIf
      Shows what would happen if the function runs.
    .PARAMETER Repository
      Specifies a non-default PSRepository.
      If left blank, will default to PSGallery.
    .EXAMPLE
      New-PuppetDscModule -PowerShellModuleName PowerShellGet -PowerShellModuleVersion 2.2.3 -Repository PSGallery
 
      This function will create a new Puppet module, powershellget, which vendors and puppetizes the PowerShellGet
      PowerShell module at version 2.2.3 and its dependencies, exposing the DSC resources as Puppet resources.
  #>

  [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
  param(
    [Parameter(Mandatory=$True)]
    [string]$PowerShellModuleName,
    [string]$PowerShellModuleVersion,
    [string]$PuppetModuleName,
    [string]$PuppetModuleAuthor,
    [string]$OutputDirectory,
    [switch]$PassThru,
    [string]$Repository
  )

  Begin {
    # Unless specified, use a valid Puppet module name
    If ([string]::IsNullOrEmpty($PuppetModuleName)) {
      $PuppetModuleName = Get-PuppetizedModuleName -Name $PowerShellModuleName
    } Else {
      $PuppetizedName = Get-PuppetizedModuleName -Name $PuppetModuleName
      if ($PuppetizedName -ne $PuppetModuleName) {
        Throw "Invalid puppet module name '$PuppetModuleName' specified; must include only lowercase letters, digits, and underscores and not start with a digit"
      }
    }
    # If specified, canonicalize the Puppet module author name
    If (![string]::IsNullOrEmpty($PuppetModuleAuthor)) { $PuppetModuleAuthor = ConvertTo-CanonicalPuppetAuthorName -AuthorName $PuppetModuleAuthor }
    # Default to the `import` folder in the current path
    If ([string]::IsNullOrEmpty($OutputDirectory))  {
      $OutputDirectory  = Join-Path -Path (Get-Location) -ChildPath 'import'
    } Else {}

    # make sure that we're operating on a absolute path to avoid confusion from symlinks and relative paths
    $OutputDirectory = New-Item -Path $OutputDirectory -ItemType "directory" -Force

    $PuppetModuleRootFolderDirectory = Join-Path -Path $OutputDirectory                 -ChildPath $PuppetModuleName
    $VendoredDscResourcesDirectory   = Join-Path -Path $OutputDirectory                 -ChildPath "$PuppetModuleName/lib/puppet_x/dsc_resources"
    $PuppetModuleTypeDirectory       = Join-Path -Path $PuppetModuleRootFolderDirectory -ChildPath 'lib/puppet/type'
    $PuppetModuleProviderDirectory   = Join-Path -Path $PuppetModuleRootFolderDirectory -ChildPath 'lib/puppet/provider'
    $InitialPSModulePath          = $Env:PSModulePath
    $InitialErrorActionPreference = $ErrorActionPreference
    If (!(Test-RunningElevated)) {
      Write-PSFMessage -Message 'Running un-elevated: will not be able to parse embedded CIM instances; run again with Administrator permissions to map embedded CIM instances' -Level Warning
    } Else {
      If (Test-SymLinkedItem -Path $OutputDirectory -Recurse) {
        Stop-PsfFunction -EnableException $true -Message "The specified output folder '$OutputDirectory' has a symlink in the path; CIM class parsing will not work in a symlinked folder, specify another path"
      }
    }
  }

  Process {
    $ShouldProcessMessage = "Puppetize the '$PowerShellModuleName' module"
    If (![string]::IsNullOrEmpty($PowerShellModuleVersion)) { $ShouldProcessMessage += " at version '$PowerShellModuleVersion'"}
    If ([string]::IsNullOrEmpty($Repository)) { $Repository = "PSGallery"}
    If ($PSCmdlet.ShouldProcess($OutputDirectory, $ShouldProcessMessage)) {
      Try {
        $ErrorActionPreference = 'Stop'
        # Scaffold the module via the PDK
        Write-PSFMessage -Message 'Initializing the Puppet Module'
        Initialize-PuppetModule -OutputFolderPath $OutputDirectory -PuppetModuleName $PuppetModuleName -verbose

        # Vendor the PowerShell module and all of its dependencies
        Write-PSFMessage -Message 'Vendoring the DSC Resources'
        Add-DscResourceModule -Name $PowerShellModuleName -Path $VendoredDscResourcesDirectory -RequiredVersion $PowerShellModuleVersion -Repository $Repository

        # Update the Puppet module metadata
        Write-PSFMessage -Message 'Updating the Puppet Module metadata'
        $PowerShellModuleManifestPath = (Resolve-Path -Path "$VendoredDscResourcesDirectory/$PowerShellModuleName/$PowerShellModuleName.psd1")
        $MetadataParameters = @{
          PuppetModuleFolderPath       = $PuppetModuleRootFolderDirectory
          PowerShellModuleManifestPath = $PowerShellModuleManifestPath
          PuppetModuleAuthor           = $PuppetModuleAuthor
        }
        Update-PuppetModuleMetadata @MetadataParameters

        # Update the Puppet module test fixtures
        Write-PSFMessage -Message 'Updating the Puppet Module test fixtures'
        Update-PuppetModuleFixture -PuppetModuleFolderPath $PuppetModuleRootFolderDirectory

        # Write the Puppet module README
        Write-PSFMessage -Message 'Writing the Puppet Module readme'
        Update-PuppetModuleReadme -PuppetModuleFolderPath $PuppetModuleRootFolderDirectory -PowerShellModuleManifestPath $PowerShellModuleManifestPath

        # The PowerShell Module path needs to be munged because the Get-DscResource function always and only
        # checks the PSModulePath for DSC modules; you CANNOT point to a module by path.
        Write-PSFMessage -Message 'Converting the DSC resources to Puppet types and providers'
        Set-PSModulePath -Path $VendoredDscResourcesDirectory
        $Resources = Get-DscResource -Module $PowerShellModuleName | ConvertTo-PuppetResourceApi

        # Write the type and provider files for each DSC Resource
        foreach($Resource in $Resources){
          $PuppetTypeFilePath          = Join-Path -Path $PuppetModuleTypeDirectory     -ChildPath $Resource.RubyFileName
          $PuppetProviderDirectoryPath = Join-Path -Path $PuppetModuleProviderDirectory -ChildPath $Resource.Name
          $PuppetProviderFilePath      = Join-Path -Path $PuppetProviderDirectoryPath   -ChildPath $Resource.RubyFileName
          if(-not(Test-Path $PuppetModuleTypeDirectory)){
            New-Item -Path $PuppetModuleTypeDirectory -ItemType Directory -Force | Out-Null
          }
          Out-Utf8File -Path $PuppetTypeFilePath -InputObject $Resource.Type
          if(-not(Test-Path $PuppetProviderDirectoryPath)){
            New-Item -Path $PuppetProviderDirectoryPath -ItemType Directory -Force | Out-Null
          }
          Out-Utf8File -Path $PuppetProviderFilePath -InputObject $Resource.Provider
        }

        # Generate REFERENCE.md file for the Puppet module from the auto-generated types for each DSC resource
        Write-PSFMessage -Message 'Writing the reference documentation for the Puppet module'
        Set-PSModulePath -Path $InitialPsModulePath
        Add-PuppetReferenceDocumentation -PuppetModuleFolderPath $PuppetModuleRootFolderDirectory -verbose

        If ($PassThru) {
          # Return the folder containing the puppetized module
          Get-Item $PuppetModuleRootFolderDirectory
        }
      } Catch {
        $PSCmdlet.ThrowTerminatingError($PSitem)
      } Finally {
        # Reset the working envrionment
        Set-PSModulePath -Path $InitialPsModulePath
        $ErrorActionPreference = $InitialErrorActionPreference
      }
    }
  }

  End {}
}