DSCResources/ChocolateyPackages/ChocolateyPackages.schema.psm1

configuration ChocolateyPackages {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]

    param (
        [Parameter()]
        [hashtable]$Software,

        [Parameter()]
        [hashtable[]]$Sources,

        [Parameter()]
        [hashtable[]]$Features,

        [Parameter()]
        [boolean]
        $ForceRebootBefore = $false,

        [Parameter()]
        [hashtable[]]$Packages
    )

    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Import-DscResource -ModuleName ComputerManagementDsc
    Import-DscResource -ModuleName Chocolatey

    $chocoSwExecName = 'Choco_Software'

    if ($Software -ne $null)
    {
        if ([String]::IsNullOrWhiteSpace($Software.OfflineInstallZip) -eq $false)
        {
            # reduce footprint of serialized script parameter objects
            $swInstallationDirectory = $Software.InstallationDirectory
            $swOfflineInstallZip = $Software.OfflineInstallZip

            Script OfflineInstallChocolatey
            {
                TestScript = {
                    # origin: https://github.com/chocolatey-community/Chocolatey/blob/master/Chocolatey/public/Test-ChocolateyInstall.ps1

                    try
                    {
                        Write-Verbose "Loading machine Path Environment variable into session."
                        $envPath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
                        [Environment]::SetEnvironmentVariable($envPath, 'Process')

                        $installDir = $using:swInstallationDirectory

                        if ($installDir -and (Test-Path $installDir) )
                        {
                            $installDir = (Resolve-Path $installDir -ErrorAction Stop).Path
                        }

                        if ($chocoCmd = get-command choco.exe -CommandType Application -ErrorAction SilentlyContinue)
                        {
                            if (
                                !$installDir -or
                                $chocoCmd.Path -match [regex]::Escape($installDir)
                            )
                            {
                                Write-Verbose ('Chocolatey Software found in {0}' -f $chocoCmd.Path)
                                return $true
                            }
                            else
                            {
                                Write-Verbose (
                                    'Chocolatey Software not installed in {0}`n but in {1}' -f $installDir, $chocoCmd.Path
                                )
                                return $false
                            }
                        }
                        else
                        {
                            Write-Verbose "Chocolatey Software not found."
                            return $false
                        }
                    }
                    catch
                    {
                        Write-Verbose "Test for Chocolatey Software aborted with an exception.`n$_"
                        return $false
                    }
                }

                SetScript  = {
                    # origin: https://github.com/chocolatey-community/Chocolatey/blob/master/Chocolatey/public/Install-ChocolateySoftware.ps1

                    if ($null -eq $env:TEMP)
                    {
                        $env:TEMP = Join-Path $Env:SYSTEMDRIVE 'temp'
                    }

                    $tempDir = [io.path]::Combine($Env:TEMP, 'chocolatey', 'chocInstall')
                    if (![System.IO.Directory]::Exists($tempDir))
                    {
                        $null = New-Item -path $tempDir -ItemType Directory
                    }

                    if ( -not (Test-Path $using:swOfflineInstallZip) )
                    {
                        Write-Error "ERROR: Offline installation package '$($using:swOfflineInstallZip)' not found."
                        return
                    }

                    # copy the package with zip extension
                    $file = Resolve-Path $using:swOfflineInstallZip
                    $zipFile = [io.path]::Combine($Env:TEMP, 'chocolatey', 'chocolatey.zip')

                    Write-Verbose "Copy install package '$file' to '$zipFile'..."

                    Copy-Item -Path $file -Destination $zipFile -Force

                    # unzip the package
                    Write-Verbose "Extracting $zipFile to $tempDir..."

                    if ($PSVersionTable.PSVersion.Major -ge 5)
                    {
                        Expand-Archive -Path "$zipFile" -DestinationPath "$tempDir" -Force
                    }
                    else
                    {
                        try
                        {
                            $shellApplication = new-object -com shell.application
                            $zipPackage = $shellApplication.NameSpace($zipFile)
                            $destinationFolder = $shellApplication.NameSpace($tempDir)
                            $destinationFolder.CopyHere($zipPackage.Items(), 0x10)
                        }
                        catch
                        {
                            Write-Error "ERROR: Unable to unzip package using built-in compression. Error: `n $_"
                            return
                        }
                    }

                    # Call chocolatey install
                    Write-Verbose "Installing chocolatey on this machine."
                    $TempTools = [io.path]::combine($tempDir, 'tools')
                    # To be able to mock
                    $chocInstallPS1 = Join-Path $TempTools 'chocolateyInstall.ps1'

                    $chocoInstallDir = $using:swInstallationDirectory

                    if ($chocoInstallDir -ne $null -and $chocoInstallDir -ne '')
                    {
                        Write-Verbose "Set Chocolatey installation directory to '$chocoInstallDir'"

                        [Environment]::SetEnvironmentVariable('ChocolateyInstall', $chocoInstallDir, 'Machine')
                        [Environment]::SetEnvironmentVariable('ChocolateyInstall', $chocoInstallDir, 'Process')
                    }

                    Write-Verbose "EnvVar 'ChocolateyInstall': $([Environment]::GetEnvironmentVariable('ChocolateyInstall'))"

                    & $chocInstallPS1 | Write-Verbose

                    Write-Verbose 'Ensuring chocolatey commands are on the path.'
                    $chocoPath = [Environment]::GetEnvironmentVariable('ChocolateyInstall')
                    if ($chocoPath -eq $null -or $chocoPath -eq '')
                    {
                        $chocoPath = "$env:ALLUSERSPROFILE\Chocolatey"
                    }

                    if (!(Test-Path ($chocoPath)))
                    {
                        $chocoPath = "$env:SYSTEMDRIVE\ProgramData\Chocolatey"
                    }

                    $chocoExePath = Join-Path $chocoPath 'bin'

                    if ($($env:Path).ToLower().Contains($($chocoExePath).ToLower()) -eq $false)
                    {
                        $env:Path = [Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::Machine)
                    }

                    Write-Verbose 'Ensuring chocolatey.nupkg is in the lib folder'
                    $chocoPkgDir = Join-Path $chocoPath 'lib\chocolatey'
                    $nupkg = Join-Path $chocoPkgDir 'chocolatey.nupkg'
                    $null = [System.IO.Directory]::CreateDirectory($chocoPkgDir)
                    Copy-Item "$file" "$nupkg" -Force -ErrorAction SilentlyContinue

                    if ($ChocoVersion = & "$chocoPath\choco.exe" -v)
                    {
                        Write-Verbose "Installed Chocolatey Version: $ChocoVersion"
                    }

                    # reboot machine to activate the new environment variables for DSC
                    $global:DSCMachineStatus = 1
                }

                GetScript  = { return `
                    @{
                        result = 'N/A'
                    }
                }
            }

            $Software.Remove('OfflineInstallZip')
            $Software.DependsOn = '[Script]OfflineInstallChocolatey'
        }

        (Get-DscSplattedResource -ResourceName ChocolateySoftware -ExecutionName $chocoSwExecName -Properties $Software -NoInvoke).Invoke($Software)
    }

    if ( $Sources -ne $null )
    {
        foreach ($s in $Sources)
        {
            # Remove Case Sensitivity of ordered Dictionary or Hashtables
            $s = @{} + $s

            $executionName = $s.Name -replace '\(|\)|\.| ', ''
            $executionName = "Choco_Source_$executionName"

            if (-not $s.ContainsKey('Ensure'))
            {
                $s.Ensure = 'Present'
            }

            if ( $Software -ne $null )
            {
                $s.DependsOn = "[ChocolateySoftware]$chocoSwExecName"
            }

            (Get-DscSplattedResource -ResourceName ChocolateySource -ExecutionName $executionName -Properties $s -NoInvoke).Invoke($s)
        }
    }

    if ( $Features -ne $null )
    {
        foreach ($f in $Features)
        {
            # Remove Case Sensitivity of ordered Dictionary or Hashtables
            $f = @{} + $f

            $executionName = $f.Name -replace '\(|\)|\.| ', ''
            $executionName = "ChocolateyFeature_$executionName"
            if (-not $f.ContainsKey('Ensure'))
            {
                $f.Ensure = 'Present'
            }
            (Get-DscSplattedResource -ResourceName ChocolateyFeature -ExecutionName $executionName -Properties $f -NoInvoke).Invoke($f)
        }
    }

    if ($ForceRebootBefore -eq $true)
    {
        $rebootKeyName = 'HKLM:\SOFTWARE\DSC Community\CommonTasks\RebootRequests'
        $rebootVarName = 'RebootBefore_ChocolateyPackages'

        Script $rebootVarName
        {
            TestScript = {
                $val = Get-ItemProperty -Path $using:rebootKeyName -Name $using:rebootVarName -ErrorAction SilentlyContinue

                if ($val -ne $null -and $val.$rebootVarName -gt 0)
                {
                    return $true
                }
                return $false
            }
            SetScript  = {
                if ( -not (Test-Path -Path $using:rebootKeyName) )
                {
                    New-Item -Path $using:rebootKeyName -Force
                }
                Set-ItemProperty -Path $rebootKeyName -Name $using:rebootVarName -value 1
                $global:DSCMachineStatus = 1
            }
            GetScript  = { return `
                @{
                    result = 'result'
                }
            }
        }
    }

    if ( $Packages -ne $null )
    {
        $clonedPackageList = [System.Collections.ArrayList]@()

        [uint16]$i = 0

        # set Rank attribute to allow a later ordering
        foreach ($p in $Packages)
        {
            # Remove Case Sensitivity of ordered Dictionary or Hashtables
            $p = @{} + $p
            # counter to keep original list order on equal rank values
            $i++

            if ( [string]::IsNullOrWhiteSpace($p.Rank) )
            {
                # set default Rank to 1000
                $p.Rank = [UInt64](1000 * 100000 + $i)
            }
            else
            {
                $p.Rank = [UInt64]($p.Rank * 100000 + $i)
            }

            $clonedPackageList.Add( $p )
        }

        foreach ($p in ($clonedPackageList | Sort-Object { [UInt64]($_.Rank) }) )
        {
            $p.Remove( 'Rank' )

            $executionName = $p.Name -replace '\(|\)|\.| ', ''
            $executionName = "Chocolatey_$executionName"
            $p.ChocolateyOptions = [hashtable]$p.ChocolateyOptions

            if (-not $p.ContainsKey('Ensure'))
            {
                $p.Ensure = 'Present'
            }

            if ( $Software -ne $null )
            {
                $p.DependsOn = "[ChocolateySoftware]$chocoSwExecName"
            }

            [boolean]$forceReboot = $false
            if ($p.ContainsKey('ForceReboot'))
            {
                $forceReboot = $p.ForceReboot
                $p.Remove( 'ForceReboot' )
            }

            (Get-DscSplattedResource -ResourceName ChocolateyPackage -ExecutionName $executionName -Properties $p -NoInvoke).Invoke($p)

            if ($forceReboot -eq $true)
            {
                $rebootKeyName = 'HKLM:\SOFTWARE\DSC Community\CommonTasks\RebootRequests'
                $rebootVarName = "RebootAfter_$executionName"

                Script $rebootVarName
                {
                    TestScript = {
                        $val = Get-ItemProperty -Path $using:rebootKeyName -Name $using:rebootVarName -ErrorAction SilentlyContinue

                        if ($val -ne $null -and $val.$rebootVarName -gt 0)
                        {
                            return $true
                        }
                        return $false
                    }
                    SetScript  = {
                        if ( -not (Test-Path -Path $using:rebootKeyName) )
                        {
                            New-Item -Path $using:rebootKeyName -Force
                        }
                        Set-ItemProperty -Path $rebootKeyName -Name $using:rebootVarName -value 1
                        $global:DSCMachineStatus = 1
                    }
                    GetScript  = { return `
                        @{
                            result = 'result'
                        }
                    }
                    DependsOn  = "[ChocolateyPackage]$executionName"
                }
            }
        }
    }
}