DSC PullServer automation with GitLab

Prerequisites

  1. Install a Pullserver 1
  2. Install GitLab runner on the Pullserver with the "shell" executor and tags "shell", "powershell5", "windows" and "pullserver".
  3. Create a gitlab project where the Project Name is the name of your intended DSC config. e.g. "Finance", omitting spaces and punctuation.
  4. Register the Gitlab runner with your DSC configuration project in GitLab.
  5. Stop the Gitlab Runner service and open c:\Gitlab-Runner\config.toml in a text editor.
  6. Change the [[runners]] > shell setting from "pwsh" to "powershell" and restart the Gitlab Runner service.
  7. Add a .gitignore file with contents [PROJECT TITLE]/ to ignore the compiled mof.
  8. Add .gitlab-ci.yml file to your project with the following contents
stages:
  - dependencies
  - test
  - run


manage_dependencies:
  stage: dependencies
  tags:
    - shell  # <-- executor, must change config.toml shell from "pwsh" to "powershell" 
    - powershell5 # <-- only use runner with powershell 5
    - windows # <-- only use windows runner
    - pullserver # <-- only use runner with Pullserver installed
  script: |
    # Configure Environment
    $ErrorActionPreference = 'Stop'
    Set-Location -Path $env:CI_PROJECT_DIR


    # Initialize variables
    [string[]]$detectedmodules = @()
    [string[]]$stagedmodules = @()
    [string[]]$ignoremodules = "PSDesiredStateConfiguration"
    [string]$modulespath = 'C:\Program Files\WindowsPowerShell\DscService\Modules'
    write-warning -Message "This script does not upgrade existing modules. Please test your scripts with modules already installed on the pull server."


    write-output "Setting PSModulePath..."
    $env:PSModulePath = 'C:\Program Files\WindowsPowerShell\Modules;C:\Program Files (x86)\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules'


    Write-Output "Detecting required modules..."
    Get-Content -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE).ps1" | Where-Object {$_ -match '(import-dscresource\s+)'} | ForEach-Object {
        $detectedmodules += $_.ToString().Trim().Split(" ") | Select-Object -Last 1
    }
    $detectedmodules = $detectedmodules | Where-Object{$_ -notcontains $ignoremodules}


    Write-Output "Getting currently staged modules..."
    Get-ChildItem -Path $modulespath -Filter *.zip | ForEach-Object {
        $stagedmodules += $_.Name.ToString().Trim().Split("_")[0]
    }


    Write-Output "Checking for modules in the runners's powershell library..."
    foreach($detectedmodule In $detectedmodules){
        write-output "Checking for $detectedmodule..."
        $checkmod = Get-DscResource -Module $detectedmodule -WarningAction SilentlyContinue
        If($checkmod){
            write-output "$detectedmodule already installed."
        }else{
            Write-Output "Installing $detectedmodule..."
            Install-Module -Name $detectedmodule
        }
    }


    Write-Output "Identifying modules missing from the pullserver repository..."
    $missingmodules = $detectedmodules | Where {$stagedmodules -NotContains $_}


    foreach($missingmodule In $missingmodules){
        Write-Output "Downloading $missingmodule..."
        Save-Module -Name $missingmodule -Path .

        Write-Output "Unhiding files to enable compression..."
        Get-ChildItem -path ".\$missingmodule\$version\*" -force -Recurse | where{$_.Attributes -match "hidden"} | foreach{$_.Attributes=""}

        Write-Output "Compressing module..."
        $version = (Get-ChildItem -Path ".\$missingmodule").Name
        $archive = '.\' + $missingmodule + '_' + $version + '.zip'
        Compress-Archive -Path ".\$missingmodule\$version\*" -DestinationPath $archive

        Write-Output "Creating checksum..."
        New-DscChecksum -Path $archive -OutPath .

        write-output "Copying modules to module share..."
        Copy-Item -Path $archive -Destination $modulespath

        write-output "Copying checksum to module share..."
        Copy-Item -Path "$archive.checksum" -Destination $modulespath
    }

  only:
    - pushes

test_dsc:
  stage: test
  tags:
    - shell
    - powershell5
    - windows
    - pullserver
  script: |
    BeforeAll { 
      . "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE).ps1"
    }

    Describe 'Pester-Test' {
      It 'Passes Script Analyzer' {
        Invoke-ScriptAnalyzer -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE).ps1" -Severity Error | Should -BeNullOrEmpty
      }

      It 'Generated MOF' {
        "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE)\localhost.mof" | Should -Exist
      }
    }

    AfterAll {
      If(Test-Path -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE)\localhost.mof"){Remove-Item -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE)\localhost.mof"}
    }

  only:
    - pushes

run_dsc:
  stage: run
  tags:
    - shell
    - powershell5
    - windows
    - pullserver
  script: |
    # Configure Environment
    $ErrorActionPreference = 'Stop'
    Set-Location -Path $env:CI_PROJECT_DIR


    # Initialize variables
    [string]$configpath = 'C:\Program Files\WindowsPowerShell\DscService\Configuration'


    write-output "Setting PSModulePath..."
    $env:PSModulePath = 'C:\Program Files\WindowsPowerShell\Modules;C:\Program Files (x86)\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules'


    Write-Output "Writing MOF..."
    Try{
        & "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE).ps1" -Verbose
    }Catch{
        Throw $_.Exception.Message
    }


    Write-Output "Creating Checksum..."
    New-DscChecksum -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE)\localhost.mof"


    Write-Output "Renaming MOF file..."
    Rename-Item -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE)\localhost.mof" -NewName "$($env:CI_PROJECT_TITLE).mof"


    Write-Output "Renaming checksum file..."
    Rename-Item -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE)\localhost.mof.checksum" -NewName "$($env:CI_PROJECT_TITLE).mof.checksum"


    Write-Output "Copying MOF to $configpath..."
    Copy-Item -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE)\$($env:CI_PROJECT_TITLE).mof" -Destination $configpath


    Write-Output "Copying checksum to $configpath..."
    Copy-Item -Path "$($env:CI_PROJECT_DIR)\$($env:CI_PROJECT_TITLE)\$($env:CI_PROJECT_TITLE).mof.checksum" -Destination $configpath

  only:
    - master

1 If your Pullserver cannot reach the internet, consider creating a local Nuget server to host required modules and add a Register-PSrepository command to the Configure Environment section of the Dependencies stage to register your internal Nuget server. You will need to upload all required modules and resources to the internal Nuget Server.

Document Actions