DSCResources/cOctopusServer/cOctopusServer.psm1
$ErrorActionPreference = "Stop" $octopusServerExePath = "$($env:ProgramFiles)\Octopus Deploy\Octopus\Octopus.Server.exe" $script:instancecontext = '' # a global to hold the name of the current instance's context # dot-source the helper file (cannot load as a module due to scope considerations) . (Join-Path -Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -ChildPath 'OctopusDSCHelpers.ps1') function Get-TargetResource { [OutputType([Hashtable])] param ( [ValidateSet("Present", "Absent")] [string]$Ensure = "Present", [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, [ValidateSet("Started", "Stopped")] [string]$State = "Started", [ValidateNotNullOrEmpty()] [string]$DownloadUrl = "https://octopus.com/downloads/latest/WindowsX64/OctopusServer", [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WebListenPrefix, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SqlDbConnectionString, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [PSCredential]$OctopusAdminCredential, [bool]$AllowUpgradeCheck = $true, [bool]$AllowCollectionOfUsageStatistics = $true, [ValidateSet("UsernamePassword", "Domain", "Ignore")] [string]$LegacyWebAuthenticationMode = 'Ignore', [bool]$ForceSSL = $false, [bool]$HSTSEnabled = $false, [Int64]$HSTSMaxAge = 3600, # 1 hour [int]$ListenPort = 10943, [Nullable[bool]]$AutoLoginEnabled = $null, [PSCredential]$OctopusServiceCredential, [string]$HomeDirectory, [PSCredential]$OctopusMasterKey = [PSCredential]::Empty, [string]$LicenseKey = $null, [bool]$GrantDatabasePermissions = $true, [PSCredential]$OctopusBuiltInWorkerCredential = [PSCredential]::Empty ) $script:instancecontext = $Name Write-Verbose "Checking if Octopus Server is installed" $installLocation = (Get-ItemProperty -path "HKLM:\Software\Octopus\OctopusServer" -ErrorAction SilentlyContinue).InstallLocation $present = ($null -ne $installLocation) Write-Verbose "Octopus Server present: $present" $existingEnsure = if ($present) { "Present" } else { "Absent" } $serviceName = (Get-ServiceName $Name) Write-Verbose "Checking for Windows Service: $serviceName" $serviceInstance = Get-Service -Name $serviceName -ErrorAction SilentlyContinue $existingState = "Stopped" if ($null -ne $serviceInstance) { Write-Verbose "Windows service: $($serviceInstance.Status)" if ($serviceInstance.Status -eq "Running") { $existingState = "Started" } if ($existingEnsure -eq "Absent") { Write-Verbose "Since the Windows Service is still installed, the service is present" $existingEnsure = "Present" } } else { Write-Verbose "Windows service: Not installed" $existingEnsure = "Absent" } $existingDownloadUrl = $null $existingWebListenPrefix = $null $existingSqlDbConnectionString = $null $existingForceSSL = $null $existingHSTSEnabled = $null $existingHSTSMaxAge = $null $existingOctopusUpgradesAllowChecking = $null $existingOctopusUpgradesIncludeStatistics = $null $existingListenPort = $null $existingOctopusAdminCredential = [PSCredential]::Empty $existingAutoLoginEnabled = $null $existingOctopusServiceCredential = [PSCredential]::Empty $existingOctopusBuiltInWorkerCredential = [PSCredential]::Empty $existingHomeDirectory = $null if ($existingEnsure -eq "Present") { if(Test-Path "$($env:SystemDrive)\Octopus\OctopusServer-$Name.config") { $configPath = "$($env:SystemDrive)\Octopus\OctopusServer-$Name.config" } else { $configPath = "$($env:SystemDrive)\Octopus\OctopusServer.config" } $existingConfig = Import-ServerConfig $configPath $Name $existingSqlDbConnectionString = $existingConfig.OctopusStorageExternalDatabaseConnectionString $existingWebListenPrefix = $existingConfig.OctopusWebPortalListenPrefixes $existingForceSSL = $existingConfig.OctopusWebPortalForceSsl $existingHSTSEnabled = $existingConfig.OctopusWebPortalHstsEnabled $existingHSTSMaxAge = $existingConfig.OctopusWebPortalHstsMaxAge $existingOctopusUpgradesAllowChecking = $existingConfig.OctopusUpgradesAllowChecking $existingOctopusUpgradesIncludeStatistics = $existingConfig.OctopusUpgradesIncludeStatistics $existingListenPort = $existingConfig.OctopusCommunicationsServicesPort $existingAutoLoginEnabled = $existingConfig.OctopusWebPortalAutoLoginEnabled $existingLegacyWebAuthenticationMode = $existingConfig.OctopusWebPortalAuthenticationMode $existingHomeDirectory = $existingConfig.OctopusHomeDirectory if ($existingConfig.OctopusLicenseKey -eq "<unknown>") { $existingLicenseKey = $LicenseKey #if we weren't able to determine the existing key, assume its correct } else { $existingLicenseKey = $existingConfig.OctopusLicenseKey } $existingDownloadUrl = Get-InstallStateValue 'DownloadUrl' #note: this can get out of sync with reality. Ideally we'd read from `show-configuration`, # but the catch is there can be multple admins. We'd probably need to add support for # an `--assert` or `--validate` parameter to the `admin` command and check its valid $user = Get-InstallStateValue 'OctopusAdminUsername' $pass = Get-InstallStateValue 'OctopusAdminPassword' if (($null -ne $user) -and ($null -ne $pass)) { $existingOctopusAdminCredential = New-Object System.Management.Automation.PSCredential ($user, ($pass | ConvertTo-SecureString)) } #note: this should read from the service. How do we validate the password though? We moght # need to add support for an `--assert` or `--validate` parameter to the `service` # command and check its valid $user = Get-InstallStateValue 'OctopusServiceUsername' $pass = Get-InstallStateValue 'OctopusServicePassword' if (($null -ne $user) -and ($null -ne $pass)) { $existingOctopusServiceCredential = New-Object System.Management.Automation.PSCredential ($user, ($pass | ConvertTo-SecureString)) } #note: this should read from `show-configuration`. That wont validate the password is set # correctly though. We'd probably need to add support for an `--assert` or # `--validate` parameter to the `runonserver` command and check its valid $user = Get-InstallStateValue 'OctopusRunAsUsername' $pass = Get-InstallStateValue 'OctopusRunAsPassword' if (($null -ne $user) -and ($null -ne $pass)) { $existingOctopusBuiltInWorkerCredential = New-Object System.Management.Automation.PSCredential ($user, ($pass | ConvertTo-SecureString)) } } $currentResource = @{ Name = $Name; Ensure = $existingEnsure; State = $existingState; DownloadUrl = $existingDownloadUrl; WebListenPrefix = $existingWebListenPrefix; SqlDbConnectionString = $existingSqlDbConnectionString; ForceSSL = $existingForceSSL; HSTSEnabled = $existingHSTSEnabled; HSTSMaxAge = $existingHSTSMaxAge; AllowUpgradeCheck = $existingOctopusUpgradesAllowChecking; AllowCollectionOfUsageStatistics = $existingOctopusUpgradesIncludeStatistics; ListenPort = $existingListenPort; OctopusAdminCredential = $existingOctopusAdminCredential; LegacyWebAuthenticationMode = $existingLegacyWebAuthenticationMode; AutoLoginEnabled = $existingAutoLoginEnabled; OctopusServiceCredential = $existingOctopusServiceCredential; HomeDirectory = $existingHomeDirectory; LicenseKey = $existingLicenseKey; GrantDatabasePermissions = $GrantDatabasePermissions; OctopusBuiltInWorkerCredential = $existingOctopusBuiltInWorkerCredential; } return $currentResource } function Import-ServerConfig { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $InstanceName ) Write-Verbose "Importing server configuration file from '$Path'" if (-not (Test-Path -LiteralPath $Path)) { throw "Config path '$Path' does not exist." } $file = Get-Item -LiteralPath $Path -ErrorAction Stop if ($file -isnot [System.IO.FileInfo]) { throw "Config path '$Path' does not refer to a file." } if (Test-OctopusVersionSupportsShowConfiguration) { $rawConfig = & $octopusServerExePath show-configuration --format=json-hierarchical --noconsolelogging --console --instance $InstanceName $config = $rawConfig | ConvertFrom-Json # show-configuration only added support for the license from 4.1.3 # unfortunately, $null implies that its the free license, so it would trigger a change every time DSC runs if ([bool]($config.Octopus.Server.psobject.properties.name -match 'License')) { $license = $config.Octopus.Server.License } else { $license = '<unknown>' } $result = [pscustomobject] @{ OctopusStorageExternalDatabaseConnectionString = $config.Octopus.Storage.ExternalDatabaseConnectionString OctopusWebPortalListenPrefixes = $config.Octopus.WebPortal.ListenPrefixes OctopusWebPortalForceSsl = [System.Convert]::ToBoolean($config.Octopus.WebPortal.ForceSSL) OctopusWebPortalHstsEnabled = [System.Convert]::ToBoolean($config.Octopus.WebPortal.HttpStrictTransportSecurityEnabled) OctopusWebPortalHstsMaxAge = $config.Octopus.WebPortal.HttpStrictTransportSecurityMaxAge OctopusUpgradesAllowChecking = [System.Convert]::ToBoolean($config.Octopus.Upgrades.AllowChecking) OctopusUpgradesIncludeStatistics = [System.Convert]::ToBoolean($config.Octopus.Upgrades.IncludeStatistics) OctopusCommunicationsServicesPort = $config.Octopus.Communications.ServicesPort OctopusWebPortalAuthenticationMode = "Ignore" OctopusWebPortalAutoLoginEnabled = [System.Convert]::ToBoolean($config.Octopus.WebPortal.AutoLoginEnabled) OctopusHomeDirectory = $config.Octopus.Home OctopusLicenseKey = $license } } else { $xml = New-Object xml try { $xml.Load($file.FullName) } catch { throw } $result = [pscustomobject] @{ OctopusStorageExternalDatabaseConnectionString = $xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.Storage.ExternalDatabaseConnectionString"]/text()').Value OctopusWebPortalListenPrefixes = $xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.WebPortal.ListenPrefixes"]/text()').Value OctopusWebPortalForceSsl = [System.Convert]::ToBoolean($xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.WebPortal.ForceSsl"]/text()').Value) OctopusUpgradesAllowChecking = [System.Convert]::ToBoolean($xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.Upgrades.AllowChecking"]/text()').Value) OctopusUpgradesIncludeStatistics = [System.Convert]::ToBoolean($xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.Upgrades.IncludeStatistics"]/text()').Value) OctopusCommunicationsServicesport = $xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.Communications.ServicesPort"]/text()').Value OctopusWebPortalAuthenticationMode = $xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.WebPortal.AuthenticationMode"]/text()').Value OctopusWebPortalAutoLoginEnabled = $xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.WebPortal.AutoLoginEnabled"]/text()').Value OctopusHomeDirectory = $xml.SelectSingleNode('/octopus-settings/set[@key="Octopus.Home"]/text()').Value OctopusLicenseKey = '<unknown>' # we have no easy way to get this } if ($result.OctopusWebPortalAuthenticationMode -eq '0') { $result.OctopusWebPortalAuthenticationMode = 'UsernamePassword' } elseif ($result.OctopusWebPortalAuthenticationMode -eq '1') { $result.OctopusWebPortalAuthenticationMode = 'Domain' } } return $result } function Test-OctopusVersionSupportsAutoLoginEnabled { return Test-OctopusVersionNewerThan (New-Object System.Version 3, 5, 0) } function Test-OctopusVersionSupportsHsts { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param() return Test-OctopusVersionNewerThan (New-Object System.Version 3, 13, 0) } function Test-OctopusVersionSupportsRunAsCredential { return Test-OctopusVersionNewerThan (New-Object System.Version 2018, 1, 0) } function Test-OctopusVersionSupportsShowConfiguration { return Test-OctopusVersionNewerThan (New-Object System.Version 3, 5, 0) } function Test-OctopusVersionSupportsHomeDirectoryDuringCreateInstance { return Test-OctopusVersionNewerThan (New-Object System.Version 3, 16, 4) } function Test-OctopusVersionRequiresDatabaseBeforeConfigure { return Test-OctopusVersionNewerThan (New-Object System.Version 4, 0, 0) } function Test-OctopusVersionNewerThan($targetVersion) { if (-not (Test-Path -LiteralPath $octopusServerExePath)) { throw "Octopus.Server.exe path '$octopusServerExePath' does not exist." } $exeFile = Get-Item -LiteralPath $octopusServerExePath -ErrorAction Stop if ($exeFile -isnot [System.IO.FileInfo]) { throw "Octopus.Server.exe path '$octopusServerExePath ' does not refer to a file." } $fileVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($octopusServerExePath).FileVersion $octopusServerVersion = New-Object System.Version $fileVersion return ($octopusServerVersion -ge $targetVersion) } function Set-TargetResource { #The Write-Verbose calls are in other methods [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")] param ( [ValidateSet("Present", "Absent")] [string]$Ensure = "Present", [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, [ValidateSet("Started", "Stopped")] [string]$State = "Started", [ValidateNotNullOrEmpty()] [string]$DownloadUrl = "https://octopus.com/downloads/latest/WindowsX64/OctopusServer", [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WebListenPrefix, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SqlDbConnectionString, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [PSCredential]$OctopusAdminCredential, [bool]$AllowUpgradeCheck = $true, [bool]$AllowCollectionOfUsageStatistics = $true, [ValidateSet("UsernamePassword", "Domain", "Ignore")] [string]$LegacyWebAuthenticationMode = 'Ignore', [bool]$ForceSSL = $false, [bool]$HSTSEnabled = $false, [Int64]$HSTSMaxAge = 3600, # 1 hour [int]$ListenPort = 10943, [Nullable[bool]]$AutoLoginEnabled = $null, [PSCredential]$OctopusServiceCredential, [string]$HomeDirectory, [PSCredential]$OctopusMasterKey = [PSCredential]::Empty, [string]$LicenseKey = $null, [bool]$GrantDatabasePermissions = $true, [PSCredential]$OctopusBuiltInWorkerCredential = [PSCredential]::Empty ) # update the global $script:instancecontext = $Name $currentResource = (Get-TargetResource -Ensure $Ensure ` -Name $Name ` -State $State ` -DownloadUrl $DownloadUrl ` -WebListenPrefix $WebListenPrefix ` -SqlDbConnectionString $SqlDbConnectionString ` -OctopusAdminCredential $OctopusAdminCredential ` -AllowUpgradeCheck $AllowUpgradeCheck ` -AllowCollectionOfUsageStatistics $AllowCollectionOfUsageStatistics ` -LegacyWebAuthenticationMode $LegacyWebAuthenticationMode ` -ForceSSL $ForceSSL ` -HSTSEnabled $HSTSEnabled ` -HSTSMaxAge $HSTSMaxAge ` -ListenPort $ListenPort ` -AutoLoginEnabled $AutoLoginEnabled ` -OctopusServiceCredential $OctopusServiceCredential ` -HomeDirectory $HomeDirectory ` -OctopusMasterKey $OctopusMasterKey ` -LicenseKey $LicenseKey ` -GrantDatabasePermissions $GrantDatabasePermissions ` -OctopusBuiltInWorkerCredential $OctopusBuiltInWorkerCredential) $params = Get-ODSCParameter $MyInvocation.MyCommand.Parameters Test-RequestedConfiguration $currentResource $params if ($State -eq "Stopped" -and $currentResource["State"] -eq "Started") { Stop-OctopusDeployService $Name } if ($Ensure -eq "Absent" -and $currentResource["Ensure"] -eq "Present") { Uninstall-OctopusDeploy $Name } elseif ($Ensure -eq "Present" -and $currentResource["Ensure"] -eq "Absent") { Install-OctopusDeploy -name $Name ` -downloadUrl $DownloadUrl ` -webListenPrefix $WebListenPrefix ` -sqlDbConnectionString $SqlDbConnectionString ` -OctopusAdminCredential $OctopusAdminCredential ` -allowUpgradeCheck $AllowUpgradeCheck ` -allowCollectionOfUsageStatistics $AllowCollectionOfUsageStatistics ` -legacyWebAuthenticationMode $LegacyWebAuthenticationMode ` -forceSSL $ForceSSL ` -hstsEnabled $HSTSEnabled ` -hstsMaxAge $HSTSMaxAge ` -listenPort $ListenPort ` -autoLoginEnabled $AutoLoginEnabled ` -homeDirectory $HomeDirectory ` -octopusServiceCredential $OctopusServiceCredential ` -OctopusMasterKey $OctopusMasterKey ` -licenseKey $LicenseKey ` -grantDatabasePermissions $GrantDatabasePermissions ` -octopusBuiltInWorkerCredential $OctopusBuiltInWorkerCredential } else { if ($Ensure -eq "Present" -and $currentResource["DownloadUrl"] -ne $DownloadUrl) { Update-OctopusDeploy -name $Name ` -downloadUrl $DownloadUrl ` -state $State ` -webListenPrefix $webListenPrefix } if (Test-ReconfigurationRequired $currentResource $params) { Set-OctopusDeployConfiguration ` -currentState $currentResource ` -name $Name ` -webListenPrefix $WebListenPrefix ` -allowUpgradeCheck $AllowUpgradeCheck ` -allowCollectionOfUsageStatistics $AllowCollectionOfUsageStatistics ` -legacyWebAuthenticationMode $LegacyWebAuthenticationMode ` -forceSSL $ForceSSL ` -hstsEnabled $HSTSEnabled ` -hstsMaxAge $HSTSMaxAge ` -listenPort $ListenPort ` -autoLoginEnabled $AutoLoginEnabled ` -homeDirectory $HomeDirectory ` -octopusServiceCredential $OctopusServiceCredential ` -OctopusMasterKey $OctopusMasterKey ` -licenseKey $LicenseKey ` -octopusBuiltInWorkerCredential $OctopusBuiltInWorkerCredential } } if ($State -eq "Started" -and $currentResource["State"] -eq "Stopped") { Start-OctopusDeployService -name $Name -webListenPrefix $webListenPrefix } } function Test-RequestedConfiguration($currentState, $desiredState) { if ($desiredState.Item('Ensure') -eq "Absent" -and $desiredState.Item('State') -eq "Started") { throw "Invalid configuration requested. " + ` "You have asked for the service to not exist, but also be running at the same time. " + ` "You probably want 'State = `"Stopped`"." } if ($currentState.Item['Ensure'] -eq "Present") { if (Test-OctopusVersionSupportsShowConfiguration) { if ($desiredState.Item('LegacyWebAuthenticationMode') -ne 'Ignore') { #todo: add note to use new auth resources throw "LegacyWebAuthenticationMode is only supported for Octopus versions older than 3.5.0." } } else { if ($desiredState.Item('LegacyWebAuthenticationMode') -eq 'Ignore') { throw "LegacyWebAuthenticationMode = 'ignore' is only supported from Octopus 3.5.0." } } } } function Set-OctopusDeployConfiguration { param ( [Parameter(Mandatory = $True)] [Hashtable]$currentState, [Parameter(Mandatory = $True)] [string]$name, [Parameter(Mandatory = $True)] [string]$webListenPrefix, [Parameter(Mandatory)] [bool]$allowUpgradeCheck = $true, [bool]$allowCollectionOfUsageStatistics = $true, [ValidateSet("UsernamePassword", "Domain", "Ignore")] [string]$legacyWebAuthenticationMode = 'Ignore', [bool]$forceSSL = $false, [bool]$hstsEnabled = $false, [Int64]$hstsMaxAge = 3600, # 1 hour [int]$listenPort = 10943, [Nullable[bool]]$autoLoginEnabled = $null, [string]$homeDirectory = $null, [PSCredential]$OctopusServiceCredential, [PSCredential]$OctopusMasterKey, [string]$licenseKey = $null, [PSCredential]$OctopusBuiltInWorkerCredential = [PSCredential]::Empty ) Write-Log "Configuring Octopus Deploy instance ..." $args = @( 'configure', '--console', '--instance', $name, '--upgradeCheck', $allowUpgradeCheck, '--upgradeCheckWithStatistics', $allowCollectionOfUsageStatistics, '--webForceSSL', $forceSSL, '--webListenPrefixes', $webListenPrefix, '--commsListenPort', $listenPort ) if (($homeDirectory -ne "") -and ($null -ne $homeDirectory)) { $args += @('--home', $homeDirectory) } if ($null -ne $autoLoginEnabled) { if (Test-OctopusVersionSupportsAutoLoginEnabled) { $args += @('--autoLoginEnabled', $autoLoginEnabled) } else { throw "AutoLoginEnabled is only supported from Octopus 3.5.0. Please pass `$null for versions older than this." } } if (Test-OctopusVersionSupportsHsts) { $args += @( '--hstsEnabled', $hstsEnabled, '--hstsMaxAge', $hstsMaxAge ) } elseif ($hstsEnabled) { throw "HSTS is only supported for Octopus versions newer than 3.13.0" } if (Test-OctopusVersionSupportsShowConfiguration) { if ($legacyWebAuthenticationMode -ne 'Ignore') { #todo: add note to use new auth resources throw "LegacyWebAuthenticationMode is only supported for Octopus versions older than 3.5.0." } } else { if ($legacyWebAuthenticationMode -eq 'Ignore') { throw "LegacyWebAuthenticationMode = 'ignore' is only supported from Octopus 3.5.0." } $args += @('--webAuthenticationMode', $legacyWebAuthenticationMode) } Invoke-OctopusServerCommand $args if (-not (Test-PSCredential $currentState['OctopusServiceCredential'] $OctopusServiceCredential)) { $args = @( 'service', '--console' '--instance', $name, '--uninstall', '--install', '--reconfigure' ) if (($null -ne $octopusServiceCredential) -and ($octopusServiceCredential -ne [PSCredential]::Empty)) { Write-Log "Reconfiguring Octopus Deploy service to use run as $($octopusServiceCredential.UserName) ..." $args += @( '--username', $octopusServiceCredential.UserName, '--password', $octopusServiceCredential.GetNetworkCredential().Password ) Update-InstallState "OctopusServiceUsername" $octopusServiceCredential.UserName Update-InstallState "OctopusServicePassword" ($octopusServiceCredential.Password | ConvertFrom-SecureString) } else { Write-Log "Reconfiguring Octopus Deploy service to run as Local System ..." Update-InstallState "OctopusServiceUsername" $null Update-InstallState "OctopusServicePassword" $null } Invoke-OctopusServerCommand $args } if (-not (Test-PSCredential $currentState['OctopusAdminCredential'] $OctopusAdminCredential)) { if (($null -ne $octopusAdminCredential) -and ($octopusAdminCredential -ne [PSCredential]::Empty)) { Write-Log "Updating Octopus Deploy admin user to $($octopusAdminCredential.UserName) ..." $args = @( 'admin', '--console' '--instance', $name, '--username', $octopusAdminCredential.UserName, '--password', $octopusAdminCredential.GetNetworkCredential().Password ) Update-InstallState "OctopusAdminUsername" $octopusAdminCredential.UserName Update-InstallState "OctopusAdminPassword" ($octopusAdminCredential.Password | ConvertFrom-SecureString) } else { throw "'OctopusBuiltInWorkerCredential' must be supplied" } Invoke-OctopusServerCommand $args } if ($currentState['LicenseKey'] -ne $licenseKey) { $args = @( 'license', '--console', '--instance', $name ) if (($null -eq $licenseKey) -or ($licenseKey -eq "")) { Write-Log "Configuring Octopus Deploy instance to use free license ..." $args += @('--free') } else { Write-Log "Configuring Octopus Deploy instance to use supplied license ..." $args += @('--licenseBase64', $licenseKey) } Invoke-OctopusServerCommand $args } if (-not (Test-PSCredential $currentState['OctopusBuiltInWorkerCredential'] $octopusBuiltInWorkerCredential)) { if (Test-OctopusVersionSupportsRunAsCredential) { if (($null -ne $octopusBuiltInWorkerCredential) -and ($octopusBuiltInWorkerCredential -ne [PSCredential]::Empty)) { Write-Log "Configuring Octopus Deploy to execute run-on-server scripts as $($octopusBuiltInWorkerCredential.UserName) ..." $args = @( 'builtin-worker', '--instance', $name, '--username', $octopusBuiltInWorkerCredential.UserName '--password', $octopusBuiltInWorkerCredential.GetNetworkCredential().Password ) Update-InstallState "OctopusRunAsUsername" $octopusBuiltInWorkerCredential.UserName Update-InstallState "OctopusRunAsPassword" ($octopusBuiltInWorkerCredential.Password | ConvertFrom-SecureString) } else { Write-Log "Configuring Octopus Deploy to execute run-on-server scripts under the same account as the octopus.server.exe process..." $args = @( 'builtin-worker', '--instance', $name, '--reset' ) Update-InstallState "OctopusRunAsUsername" $null Update-InstallState "OctopusRunAsPassword" $null } Invoke-OctopusServerCommand $args } else { throw "'OctopusBuiltInWorkerCredential' is only supported from Octopus 4.2 and newer." } } } function Test-ReconfigurationRequired($currentState, $desiredState) { $reconfigurableProperties = @('ListenPort', 'WebListenPrefix', 'ForceSSL', 'HSTSEnabled', 'HSTSMaxAge', 'AllowCollectionOfUsageStatistics', 'AllowUpgradeCheck', 'LegacyWebAuthenticationMode', 'HomeDirectory', 'LicenseKey', 'OctopusServiceCredential', 'OctopusAdminCredential', 'SqlDbConnectionString', 'AutoLoginEnabled', 'OctopusBuiltInWorkerCredential') foreach ($property in $reconfigurableProperties) { write-verbose "Checking property '$property'" if ($currentState.Item($property) -is [PSCredential]) { if (-not (Test-PSCredential $currentState.Item($property) $desiredState.Item($property))) { return $true } } elseif ($currentState.Item($property) -ne ($desiredState.Item($property))) { return $true } } return $false } function Uninstall-OctopusDeploy($name) { Write-Log "Uninstalling Octopus Deploy service ..." $args = @( 'service', '--stop', '--uninstall', '--console', '--instance', $name ) Invoke-OctopusServerCommand $args Write-Log "Deleting Octopus Deploy instance ..." $args = @( 'delete-instance', '--console', '--instance', $name ) Invoke-OctopusServerCommand $args $otherServices = @(Get-CimInstance win32_service | Where-Object {$_.PathName -like "`"$($env:ProgramFiles)\Octopus Deploy\Octopus\Octopus.Server.exe*"}) if ($otherServices.length -eq 0) { # Uninstall msi Write-Verbose "Uninstalling Octopus..." if (-not (Test-Path "$($env:SystemDrive)\Octopus\logs")) { New-Item -type Directory "$($env:SystemDrive)\Octopus\logs" | out-null } $msiPath = "$($env:SystemDrive)\Octopus\Octopus-x64.msi" $msiLog = "$($env:SystemDrive)\Octopus\logs\Octopus-x64.msi.uninstall.log" if (Test-Path $msiPath) { $msiExitCode = (Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $msiPath /quiet /l*v $msiLog" -Wait -Passthru).ExitCode Write-Verbose "MSI uninstaller returned exit code $msiExitCode" if ($msiExitCode -ne 0) { throw "Removal of Octopus Server failed, MSIEXEC exited with code: $msiExitCode. View the log at $msiLog" } } else { throw "Octopus Server cannot be removed, because the MSI could not be found." } } else { Write-Verbose "Skipping uninstall, as other instances still exist:" foreach ($otherService in $otherServices) { Write-Verbose " - $($otherService.Name)" } } } function Update-OctopusDeploy($name, $downloadUrl, $state, $webListenPrefix) { Write-Verbose "Upgrading Octopus Deploy..." Stop-OctopusDeployService -name $name Install-MSI $downloadUrl if ($state -eq "Started") { Start-OctopusDeployService -name $name -webListenPrefix $webListenPrefix } Write-Verbose "Octopus Deploy upgraded!" } function Start-OctopusDeployService($name, $webListenPrefix) { Write-Log "Starting Octopus Deploy instance ..." $args = @( 'service', '--start', '--console', '--instance', $name ) Invoke-OctopusServerCommand $args # split on semi colons for backwards compat $url = ($webListenPrefix -split ';')[0] # but also split on commas, as Octopus supports both $url = ($url -split ',')[0] $timeout = new-timespan -Minutes 5 $sw = [diagnostics.stopwatch]::StartNew() while (($sw.elapsed -lt $timeout) -and (-not (Test-OctopusDeployServerResponding $url))) { Write-Verbose "$(date) Waiting until server completes startup" Start-Sleep -Seconds 5 } if (-not (Test-OctopusDeployServerResponding $url)) { throw "Server did not come online at $url after $($timeout.TotalMinutes) minutes" } } function Test-OctopusDeployServerResponding($url) { try { Write-Verbose "Checking if $url/api is responding..." Invoke-WebRequest "$url/api" -UseBasicParsing | Out-Null Write-Verbose "Got a successful response from $url/api" return $true } catch { write-verbose "Server returned error $($_)" return $false } } function Stop-OctopusDeployService($name) { Write-Log "Stopping Octopus Deploy instance ..." $args = @( 'service', '--stop', '--console', '--instance', $name ) Invoke-OctopusServerCommand $args } function Get-ServiceName { param ( [string]$instanceName ) if ($instanceName -eq "OctopusServer") { return "OctopusDeploy" } else { return "OctopusDeploy: $instanceName" } } function Install-MSI { param ( [string]$downloadUrl ) Write-Verbose "Beginning installation" mkdir "$($env:SystemDrive)\Octopus" -ErrorAction SilentlyContinue $msiPath = "$($env:SystemDrive)\Octopus\Octopus-x64.msi" Request-File $downloadUrl $msiPath Write-Verbose "Installing MSI..." if (-not (Test-Path "$($env:SystemDrive)\Octopus\logs")) { New-Item -type Directory "$($env:SystemDrive)\Octopus\logs" } $msiLog = "$($env:SystemDrive)\Octopus\logs\Octopus-x64.msi.log" $msiExitCode = (Start-Process -FilePath "msiexec.exe" -ArgumentList "/i $msiPath /quiet /l*v $msiLog" -Wait -Passthru).ExitCode Write-Verbose "MSI installer returned exit code $msiExitCode" if ($msiExitCode -ne 0) { throw "Installation of the MSI failed; MSIEXEC exited with code: $msiExitCode. View the log at $msiLog" } Update-InstallState "DownloadUrl" $downloadUrl } function Update-InstallState { param ( [string]$key, [string]$value ) if(Test-Path "$($env:SystemDrive)\Octopus\Octopus.Server.DSC.installstate") # do we already have a legacy installstate file? { $installStateFile = "$($env:SystemDrive)\Octopus\Octopus.Server.DSC.installstate" } else { $installStateFile = "$($env:SystemDrive)\Octopus\Octopus.Server.DSC.$script:instancecontext.installstate" } $currentInstallState = @{} if (Test-Path $installStateFile) { $fileContent = (Get-Content -Raw -Path $installStateFile | ConvertFrom-Json) $fileContent.psobject.properties | ForEach-Object { $currentInstallState[$_.Name] = $_.Value } } $currentInstallState.Set_Item($key, $value) $currentInstallState | ConvertTo-Json | set-content $installStateFile } function Get-InstallStateValue { [CmdletBinding()] param($key) if(Test-Path "$($env:SystemDrive)\Octopus\Octopus.Server.DSC.installstate") # do we already have a legacy installstate file? { $installStateFile = "$($env:SystemDrive)\Octopus\Octopus.Server.DSC.installstate" Write-Verbose "Legacy installstate found" } else { $installStateFile = "$($env:SystemDrive)\Octopus\Octopus.Server.DSC.$script:instancecontext.installstate" Write-Verbose "New style installstate found" } if(-not (Test-Path $installStateFile)) { return $null } else { $installState = (Get-Content -Raw -Path $installStateFile | ConvertFrom-Json) $returnValue = $installstate | Select-Object -expand $Key -ErrorAction Ignore if("" -eq $returnValue -or $null -eq $returnValue) { return $null } else { return $returnvalue } } } function Get-RegistryValue { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()]$Path, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()]$Value ) try { return Get-ItemProperty -Path $Path | Select-Object -ExpandProperty $Value -ErrorAction Stop } catch { return "" } } Function Test-IsOctopusUpgrade { # if the binary exists before installing the MSI, we're considering this an upgrade return (Test-Path -LiteralPath $OctopusServerExePath) } function Install-OctopusDeploy { param ( [Parameter(Mandatory = $True)] [string]$name, [Parameter(Mandatory = $True)] [string]$downloadUrl, [Parameter(Mandatory = $True)] [string]$webListenPrefix, [Parameter(Mandatory = $True)] [string]$sqlDbConnectionString, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [PSCredential]$octopusAdminCredential, [bool]$allowUpgradeCheck = $true, [bool]$allowCollectionOfUsageStatistics = $true, [ValidateSet("UsernamePassword", "Domain", "Ignore")] [string]$legacyWebAuthenticationMode = 'Ignore', [bool]$forceSSL = $false, [bool]$hstsEnabled = $false, [Int64]$hstsMaxAge = 3600, # 1 hour [int]$listenPort = 10943, [Nullable[bool]]$autoLoginEnabled = $null, [string]$homeDirectory = $null, [PSCredential]$octopusServiceCredential, [PSCredential]$OctopusMasterKey, [string]$licenseKey = $null, [bool]$grantDatabasePermissions = $true, [PSCredential]$OctopusBuiltInWorkerCredential ) Write-Verbose "Installing Octopus Deploy..." Write-Log "Checking to make sure .net 4.5.1+ is installed" $dotnetVersion = Get-RegistryValue "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" "Release" if (($dotnetVersion -eq "") -or ([int]$dotnetVersion -lt 378675)) { throw "Octopus Server requires .NET 4.5.1. Please install it before attempting to install Octopus Server." } $extractedUserName = $octopusAdminCredential.GetNetworkCredential().UserName $extractedPassword = $octopusAdminCredential.GetNetworkCredential().Password # check if we're upgrading an existing instance $isUpgrade = Test-IsOctopusUpgrade # check if we're joining a cluster, or joining to an existing Database $isMasterKeyProvided = ($OctopusMasterKey -ne [PSCredential]::Empty) Write-Log "Setting up new instance of Octopus Deploy with name '$name'" Install-MSI $downloadUrl Write-Log "Creating Octopus Deploy instance ..." $args = @( 'create-instance', '--console', '--instance', $name, '--config', "$($env:SystemDrive)\Octopus\OctopusServer-$name.config" ) if (Test-OctopusVersionSupportsHomeDirectoryDuringCreateInstance) { if (($homeDirectory -ne "") -and ($null -ne $homeDirectory)) { $args += @('--home', $homeDirectory) } else { $args += @('--home', "$($env:SystemDrive)\Octopus") } } Invoke-OctopusServerCommand $args Write-Log "Configuring Octopus Deploy instance ..." $args = @( 'configure', '--console', '--instance', $name, '--upgradeCheck', $allowUpgradeCheck, '--upgradeCheckWithStatistics', $allowCollectionOfUsageStatistics, '--webForceSSL', $forceSSL, '--webListenPrefixes', $webListenPrefix, '--commsListenPort', $listenPort ) if (Test-OctopusVersionRequiresDatabaseBeforeConfigure) { if ($isMasterKeyProvided) { Write-Log "Running Octopus Deploy database command for existing database with provided masterkey" $dbargs = @( 'database', '--instance', $name, '--connectionstring', $sqlDbConnectionString, "--masterKey", $OctopusMasterKey.GetNetworkCredential().Password ) } else { if ($isUpgrade -eq $true) { Write-Log "Upgrading Octopus Deploy database for v4" $action = '--upgrade' } else { Write-Log "Creating Octopus Deploy database for v4" $action = '--create' } $dbargs = @( 'database', '--instance', $name, '--connectionstring', $sqlDbConnectionString, $action ) } if ($GrantDatabasePermissions) { if (($null -ne $OctopusServiceCredential) -and ($OctopusServiceCredential -ne [PSCredential]::Empty)) { $databaseusername = $OctopusServiceCredential.UserName } else { $databaseusername = "NT AUTHORITY\SYSTEM" } Write-Log "Granting database permissions to account $databaseusername" $dbargs += @('--grant', $databaseusername) } Invoke-OctopusServerCommand $dbargs } else { $args += @("--StorageConnectionString", $sqlDbConnectionString) if ($isMasterKeyProvided) { $args += @("--masterKey", $OctopusMasterKey.GetNetworkCredential().Password) } } if (-not (Test-OctopusVersionSupportsHomeDirectoryDuringCreateInstance)) { if (($homeDirectory -ne "") -and ($null -ne $homeDirectory)) { $args += @('--home', $homeDirectory) } else { $args += @('--home', "$($env:SystemDrive)\Octopus") } } if ($null -ne $autoLoginEnabled) { if (Test-OctopusVersionSupportsAutoLoginEnabled) { $args += @('--autoLoginEnabled', $autoLoginEnabled) } else { throw "AutoLoginEnabled is only supported from Octopus 3.5.0. Please pass `$null for versions older than this." } } if (Test-OctopusVersionSupportsHsts) { $args += @( '--hstsEnabled', $hstsEnabled, '--hstsMaxAge', $hstsMaxAge ) } elseif ($hstsEnabled) { throw "HSTS is only supported for Octopus versions newer than 3.13.0" } if (Test-OctopusVersionSupportsShowConfiguration) { if ($legacyWebAuthenticationMode -ne 'Ignore') { #todo: add note to use new auth resources throw "LegacyWebAuthenticationMode is only supported for Octopus versions older than 3.5.0." } } else { if ($legacyWebAuthenticationMode -eq 'Ignore') { throw "LegacyWebAuthenticationMode = 'ignore' is only supported from Octopus 3.5.0." } $args += @('--webAuthenticationMode', $legacyWebAuthenticationMode) } Invoke-OctopusServerCommand $args if (-not (Test-OctopusVersionNewerThan (New-Object System.Version 4, 0, 0))) { Write-Log "Creating Octopus Deploy database for v3..." $args = @( 'database', '--console', '--instance', $name, '--create' ) if ($isMasterKeyProvided) { $args += @("--masterKey", $OctopusMasterKey.GetNetworkCredential().Password) } if ($GrantDatabasePermissions) { if (($null -ne $OctopusServiceCredential) -and ($OctopusServiceCredential -ne [PSCredential]::Empty)) { $databaseusername = $OctopusServiceCredential.UserName } else { $databaseusername = "NT AUTHORITY\SYSTEM" } Write-Log "Granting database permissions to account $databaseusername" $args += @('--grant', $databaseusername) } Invoke-OctopusServerCommand $args } Write-Log "Stopping Octopus Deploy instance ..." $args = @( 'service', '--console', '--instance', $name, '--stop' ) Invoke-OctopusServerCommand $args Write-Log "Creating Admin User for Octopus Deploy instance ..." $args = @( 'admin', '--console', '--instance', $name, '--username', $extractedUsername, '--password', $extractedPassword ) Invoke-OctopusServerCommand $args Update-InstallState "OctopusAdminUsername" $extractedUsername Update-InstallState "OctopusAdminPassword" ($OctopusAdminCredential.Password | ConvertFrom-SecureString) $args = @( 'license', '--console', '--instance', $name ) if (($null -eq $licenseKey) -or ($licenseKey -eq "")) { Write-Log "Configuring Octopus Deploy instance to use free license ..." $args += @('--free') } else { Write-Log "Configuring Octopus Deploy instance to use supplied license ..." $args += @('--licenseBase64', $licenseKey) } Invoke-OctopusServerCommand $args Write-Log "Install Octopus Deploy service ..." $args = @( 'service', '--console', '--instance', $name, '--install', '--reconfigure', '--stop' ) if ($octopusServiceCredential) { Write-Log "Configuring service to run as $($octopusServiceCredential.UserName)" $args += @( "--username", $octopusServiceCredential.UserName, "--password", $octopusServiceCredential.GetNetworkCredential().Password ) Update-InstallState "OctopusServiceUsername" $octopusServiceCredential.UserName Update-InstallState "OctopusServicePassword" ($octopusServiceCredential.Password | ConvertFrom-SecureString) } else { Write-Log "Configuring service to run as Local System" Update-InstallState "OctopusServiceUsername" $null Update-InstallState "OctopusServicePassword" $null } Invoke-OctopusServerCommand $args if (($null -ne $octopusBuiltInWorkerCredential) -and ($octopusBuiltInWorkerCredential -ne [PSCredential]::Empty)) { if (Test-OctopusVersionSupportsRunAsCredential) { Write-Log "Configuring Octopus Deploy to execute run-on-server scripts as $($octopusBuiltInWorkerCredential.UserName) ..." $args = @( 'builtin-worker', '--instance', $name, '--username', $octopusBuiltInWorkerCredential.UserName, '--password', $octopusBuiltInWorkerCredential.GetNetworkCredential().Password ) Update-InstallState "OctopusRunAsUsername" $octopusBuiltInWorkerCredential.UserName Update-InstallState "OctopusRunAsPassword" ($octopusBuiltInWorkerCredential.Password | ConvertFrom-SecureString) Invoke-OctopusServerCommand $args } else { throw "'OctopusBuiltInWorkerCredential' is only supported from Octopus 4.2 and newer." } } Write-Verbose "Octopus Deploy installed!" } function Test-TargetResource { param ( [ValidateSet("Present", "Absent")] [string]$Ensure = "Present", [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, [ValidateSet("Started", "Stopped")] [string]$State = "Started", [ValidateNotNullOrEmpty()] [string]$DownloadUrl = "https://octopus.com/downloads/latest/WindowsX64/OctopusServer", [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WebListenPrefix, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SqlDbConnectionString, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [PSCredential]$OctopusAdminCredential, [bool]$AllowUpgradeCheck = $true, [bool]$AllowCollectionOfUsageStatistics = $true, [ValidateSet("UsernamePassword", "Domain", "Ignore")] [string]$LegacyWebAuthenticationMode = 'Ignore', [bool]$ForceSSL = $false, [bool]$HSTSEnabled = $false, [Int64]$HSTSMaxAge = 3600, # 1 hour [int]$ListenPort = 10943, [Nullable[bool]]$AutoLoginEnabled = $null, [PSCredential]$OctopusServiceCredential, [string]$HomeDirectory, [PSCredential]$OctopusMasterKey = [PSCredential]::Empty, [string]$LicenseKey = $null, [bool]$GrantDatabasePermissions = $true, [PSCredential]$OctopusBuiltInWorkerCredential = [PSCredential]::Empty ) # make sure the global is up to date $script:instancecontext = $Name $currentResource = (Get-TargetResource -Ensure $Ensure ` -Name $Name ` -State $State ` -DownloadUrl $DownloadUrl ` -WebListenPrefix $WebListenPrefix ` -SqlDbConnectionString $SqlDbConnectionString ` -OctopusAdminCredential $OctopusAdminCredential ` -AllowUpgradeCheck $AllowUpgradeCheck ` -AllowCollectionOfUsageStatistics $AllowCollectionOfUsageStatistics ` -LegacyWebAuthenticationMode $LegacyWebAuthenticationMode ` -ForceSSL $ForceSSL ` -HSTSEnabled $HSTSEnabled ` -HSTSMaxAge $HSTSMaxAge ` -ListenPort $ListenPort ` -AutoLoginEnabled $AutoLoginEnabled ` -OctopusServiceCredential $OctopusServiceCredential ` -HomeDirectory $HomeDirectory ` -LicenseKey $LicenseKey ` -GrantDatabasePermissions $GrantDatabasePermissions ` -OctopusBuiltInWorkerCredential $OctopusBuiltInWorkerCredential) $paramsWhereNullMeansIgnore = @('AutoLoginEnabled') $params = Get-ODSCParameter $MyInvocation.MyCommand.Parameters $currentConfigurationMatchesRequestedConfiguration = $true foreach ($key in $currentResource.Keys) { $currentValue = $currentResource.Item($key) $requestedValue = $params.Item($key) if ($currentValue -is [PSCredential]) { if (-not (Test-PSCredential $currentValue $requestedValue)) { Write-Verbose "(FOUND MISMATCH) Configuration parameter '$key' with value '********' mismatched the specified value '********'" $currentConfigurationMatchesRequestedConfiguration = $false } else { Write-Verbose "Configuration parameter '$key' matches the requested value '********'" } } elseif (($null -eq $requestedValue) -and $paramsWhereNullMeansIgnore.contains($key)) { Write-Verbose "Configuration parameter '$key' has value '$currentValue' - requested value not set" } elseif ($currentValue -ne $requestedValue) { Write-Verbose "(FOUND MISMATCH) Configuration parameter '$key' with value '$currentValue' mismatched the specified value '$requestedValue'" $currentConfigurationMatchesRequestedConfiguration = $false } else { Write-Verbose "Configuration parameter '$key' matches the requested value '$requestedValue'" } } return $currentConfigurationMatchesRequestedConfiguration } function Test-PSCredential($currentValue, $requestedValue) { if (($null -ne $currentValue) -and ($currentValue -ne [PSCredential]::Empty)) { $currentUsername = $currentValue.GetNetworkCredential().UserName $currentPassword = $currentValue.GetNetworkCredential().Password } else { $currentUsername = "" $currentPassword = "" } if (($null -ne $requestedValue) -and ($requestedValue -ne [PSCredential]::Empty)) { $requestedUsername = $requestedValue.GetNetworkCredential().UserName $requestedPassword = $requestedValue.GetNetworkCredential().Password } else { $requestedUsername = "" $requestedPassword = "" } if ($currentPassword -ne $requestedPassword -or $currentUsername -ne $requestedUsername) { return $false } return $true } |