diff --git a/OctopusDSC/DSCResources/cOctopusServer/cOctopusServer.psm1 b/OctopusDSC/DSCResources/cOctopusServer/cOctopusServer.psm1 index e9c9e9492..ef20edd86 100644 --- a/OctopusDSC/DSCResources/cOctopusServer/cOctopusServer.psm1 +++ b/OctopusDSC/DSCResources/cOctopusServer/cOctopusServer.psm1 @@ -46,7 +46,8 @@ function Get-TargetResource { [string]$ArtifactsDirectory = "$HomeDirectory\Artifacts", [string]$TaskLogsDirectory = "$HomeDirectory\TaskLogs", [bool]$LogTaskMetrics = $false, - [bool]$LogRequestMetrics = $false + [bool]$LogRequestMetrics = $false, + [int]$TaskCap = $null ) try { @@ -58,7 +59,8 @@ function Get-TargetResource { -WebListenPrefix $WebListenPrefix ` -SqlDbConnectionString $SqlDbConnectionString ` -OctopusAdminCredential $OctopusAdminCredential ` - -OctopusMasterKey $OctopusMasterKey + -OctopusMasterKey $OctopusMasterKey ` + -TaskCap $TaskCap $script:instancecontext = $Name @@ -108,6 +110,7 @@ function Get-TargetResource { $existingTaskLogsDirectory = $null $existingLogTaskMetrics = $null $existingLogRequestMetrics = $null + $existingTaskCap = $null if ($existingEnsure -eq "Present") { @@ -144,6 +147,7 @@ function Get-TargetResource { $existingArtifactsDirectory = $existingConfig.OctopusFoldersArtifactsDirectory $existingLogTaskMetrics = $existingConfig.OctopusTasksRecordTaskMetrics $existingLogRequestMetrics = $existingConfig.OctopusWebPortalRequestMetricLoggingEnabled + $existingTaskCap = $existingConfig.OctopusNodeTaskCap #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 @@ -200,6 +204,7 @@ function Get-TargetResource { TaskLogsDirectory = $existingTaskLogsDirectory; LogTaskMetrics = $existingLogTaskMetrics; LogRequestMetrics = $existingLogRequestMetrics; + TaskCap = $existingTaskCap } return $currentResource @@ -259,6 +264,7 @@ function Import-ServerConfig { OctopusFoldersPackagesDirectory = $config.Octopus.Folders.PackagesDirectory OctopusTasksRecordTaskMetrics = [System.Convert]::ToBoolean($config.Octopus.Tasks.RecordTaskMetrics) OctopusWebPortalRequestMetricLoggingEnabled = [System.Convert]::ToBoolean($config.Octopus.WebPortal.RequestMetricLoggingEnabled) + OctopusNodeTaskCap = $config.Octopus.Node.TaskCap } } else { @@ -320,6 +326,10 @@ function Test-OctopusVersionSupportsTaskMetricsLogging { return Test-OctopusVersionNewerThan (New-Object System.Version 2018, 2, 7) } +function Test-OctopusVersionSupportsTaskCap { + return Test-OctopusVersionNewerThan (New-Object System.Version 2018, 6, 13) +} + function Test-OctopusVersionNewerThan($targetVersion) { if (-not (Test-Path -LiteralPath $octopusServerExePath)) { throw "Octopus.Server.exe path '$octopusServerExePath' does not exist." @@ -369,7 +379,8 @@ function Set-TargetResource { [string]$ArtifactsDirectory = "$HomeDirectory\Artifacts", [string]$TaskLogsDirectory = "$HomeDirectory\TaskLogs", [bool]$LogTaskMetrics = $false, - [bool]$LogRequestMetrics = $false + [bool]$LogRequestMetrics = $false, + [int]$TaskCap = $null ) try { @@ -380,7 +391,8 @@ function Set-TargetResource { -WebListenPrefix $WebListenPrefix ` -SqlDbConnectionString $SqlDbConnectionString ` -OctopusAdminCredential $OctopusAdminCredential ` - -OctopusMasterKey $OctopusMasterKey + -OctopusMasterKey $OctopusMasterKey ` + -TaskCap $TaskCap # update the global $script:instancecontext = $Name @@ -410,7 +422,8 @@ function Set-TargetResource { -ArtifactsDirectory $ArtifactsDirectory ` -TaskLogsDirectory $TaskLogsDirectory ` -LogTaskMetrics $LogTaskMetrics ` - -LogRequestMetrics $LogRequestMetrics) + -LogRequestMetrics $LogRequestMetrics ` + -TaskCap $TaskCap) $params = Get-ODSCParameter $MyInvocation.MyCommand.Parameters Test-RequestedConfiguration $currentResource $params @@ -467,7 +480,8 @@ function Set-TargetResource { -artifactsDirectory $ArtifactsDirectory ` -taskLogsDirectory $TaskLogsDirectory ` -logTaskMetrics $LogTaskMetrics ` - -logRequestMetrics $LogRequestMetrics + -logRequestMetrics $LogRequestMetrics ` + -taskCap $TaskCap } else { #have they asked for a new msi? if ($installAndConfigureRequested -and $currentResource["DownloadUrl"] -ne $DownloadUrl) { @@ -502,7 +516,8 @@ function Set-TargetResource { -artifactsDirectory $ArtifactsDirectory ` -taskLogsDirectory $TaskLogsDirectory ` -logTaskMetrics $LogTaskMetrics ` - -logRequestMetrics $LogRequestMetrics + -logRequestMetrics $LogRequestMetrics ` + -taskCap $TaskCap if ((Test-ReconfigurationRequiresServiceRestart $currentResource $params) -and $isCurrentlyStarted) { Stop-OctopusDeployService -name $Name $isCurrentlyStopped = $true @@ -570,7 +585,8 @@ function Set-OctopusDeployConfiguration { [string]$artifactsDirectory = $null, [string]$taskLogsDirectory = $null, [bool]$logTaskMetrics = $false, - [bool]$logRequestMetrics = $false + [bool]$logRequestMetrics = $false, + [int]$TaskCap ) Write-Log "Configuring Octopus Deploy instance ..." @@ -666,6 +682,16 @@ function Set-OctopusDeployConfiguration { Invoke-OctopusServerCommand $args } + if ((Test-OctopusVersionSupportsTaskCap) -and ($taskCap -ne 0) -and ($currentState['TaskCap'] -ne $taskCap)) { + $args = $( + 'node', + '--console', + '--instance', $name, + '--taskCap', $taskCap + ) + Invoke-OctopusServerCommand $args + } + if (Test-PSCredentialChanged $currentState['OctopusServiceCredential'] $OctopusServiceCredential) { $args = @( 'service', @@ -867,7 +893,7 @@ function Update-OctopusDeploy($name, $downloadUrl, $state, $webListenPrefix, $cu function Start-OctopusDeployService($name, $webListenPrefix) { Write-Log "Checking Octopus Deploy service state:" - get-service $name -ErrorAction SilentlyContinue | write-verbose + get-service (Get-ServiceName $name) -ErrorAction SilentlyContinue | write-verbose Write-Log "Starting Octopus Deploy instance ..." $args = @( @@ -910,7 +936,7 @@ function Test-OctopusDeployServerResponding($url) { function Stop-OctopusDeployService($name) { Write-Log "Checking Octopus Deploy service state:" - get-service $name -ErrorAction SilentlyContinue | write-verbose + get-service (Get-ServiceName $name) -ErrorAction SilentlyContinue | write-verbose Write-Log "Stopping Octopus Deploy instance ..." $args = @( @@ -1069,7 +1095,8 @@ function Install-OctopusDeploy { [string]$packagesDirectory = $null, [string]$artifactsDirectory = $null, [bool]$logTaskMetrics = $false, - [bool]$logRequestMetrics = $false + [bool]$logRequestMetrics = $false, + [int]$taskCap ) Write-Verbose "Installing Octopus Deploy..." @@ -1297,6 +1324,16 @@ function Install-OctopusDeploy { Invoke-OctopusServerCommand $args } + if ((Test-OctopusVersionSupportsTaskCap) -and ($taskCap -ne 0)) { + $args = $( + 'node', + '--console', + '--instance', $name, + '--taskCap', $taskCap + ) + Invoke-OctopusServerCommand $args + } + Write-Log "Install Octopus Deploy service ..." $args = @( 'service', @@ -1374,7 +1411,8 @@ function Test-TargetResource { [string]$ArtifactsDirectory = "$HomeDirectory\Artifacts", [string]$TaskLogsDirectory = "$HomeDirectory\TaskLogs", [bool]$LogTaskMetrics = $false, - [bool]$LogRequestMetrics = $false + [bool]$LogRequestMetrics = $false, + [int]$taskCap = $null ) try { @@ -1385,7 +1423,8 @@ function Test-TargetResource { -WebListenPrefix $WebListenPrefix ` -SqlDbConnectionString $SqlDbConnectionString ` -OctopusAdminCredential $OctopusAdminCredential ` - -OctopusMasterKey $OctopusMasterKey + -OctopusMasterKey $OctopusMasterKey ` + -TaskCap $TaskCap # make sure the global is up to date $script:instancecontext = $Name @@ -1494,7 +1533,8 @@ function Test-ParameterSet { [string]$WebListenPrefix, [string]$SqlDbConnectionString, [PSCredential]$OctopusAdminCredential, - [PSCredential]$OctopusMasterKey + [PSCredential]$OctopusMasterKey, + [int]$TaskCap ) if ([string]::IsNullOrEmpty($Ensure)) { @@ -1536,6 +1576,14 @@ function Test-ParameterSet { if ((Test-PSCredentialIsNullOrEmpty $OctopusAdminCredential) -and (Test-PSCredentialIsNullOrEmpty $OctopusMasterKey)) { throw "Parameter 'OctopusAdminCredential' must be supplied when 'Ensure' is 'Present' and you have not supplied a master key to use an existing database." } + + #todo: DSC is converting null to 0 :facepalm:. I wonder if we can detect that somehow? + if ($TaskCap -ne $null -and $TaskCap -lt 0) { + throw "Parameter 'TaskCap' must be greater than 0 when 'Ensure' is 'Present'." + } + if ($TaskCap -ne $null -and $TaskCap -gt 50) { + throw "Parameter 'TaskCap' must be less than 50 when 'Ensure' is 'Present'." + } } if ($State -eq "Installed" -and ![string]::IsNullOrEmpty($SqlDbConnectionString)) { diff --git a/OctopusDSC/DSCResources/cOctopusServer/cOctopusServer.schema.mof b/OctopusDSC/DSCResources/cOctopusServer/cOctopusServer.schema.mof index 7b28c4da6..aeef880f6 100644 --- a/OctopusDSC/DSCResources/cOctopusServer/cOctopusServer.schema.mof +++ b/OctopusDSC/DSCResources/cOctopusServer/cOctopusServer.schema.mof @@ -27,4 +27,5 @@ class cOctopusServer : OMI_BaseResource [Write] string TaskLogsDirectory; [Write] boolean LogTaskMetrics; [Write] boolean LogRequestMetrics; + [Write] uint64 TaskCap; }; diff --git a/OctopusDSC/Examples/cOctopusServer.ps1 b/OctopusDSC/Examples/cOctopusServer.ps1 index 06897f225..1b87cca28 100644 --- a/OctopusDSC/Examples/cOctopusServer.ps1 +++ b/OctopusDSC/Examples/cOctopusServer.ps1 @@ -51,6 +51,8 @@ Configuration SampleConfig # the user account to use for run-on-server tasks (optional) OctopusBuiltInWorkerCredential = $runOnServerCred + + TaskCap = 10 } } } diff --git a/OctopusDSC/Tests/OctopusServerExeInvocationFiles/NewInstance/ExpectedResult.ps1 b/OctopusDSC/Tests/OctopusServerExeInvocationFiles/NewInstance/ExpectedResult.ps1 index 46caf9187..7d53bb93b 100644 --- a/OctopusDSC/Tests/OctopusServerExeInvocationFiles/NewInstance/ExpectedResult.ps1 +++ b/OctopusDSC/Tests/OctopusServerExeInvocationFiles/NewInstance/ExpectedResult.ps1 @@ -6,6 +6,7 @@ return @( "admin --console --instance OctopusServer --username Admin --password S3cur3P4ssphraseHere!", "license --console --instance OctopusServer --free", "path --console --instance OctopusServer --nugetRepository F:\Packages --artifacts G:\Artifacts --taskLogs E:\TaskLogs", + "node --console --instance OctopusServer --taskCap 10", "service --console --instance OctopusServer --install --reconfigure --stop", "service --start --console --instance OctopusServer" ) diff --git a/OctopusDSC/Tests/OctopusServerExeInvocationFiles/NewInstance/RequestedState.ps1 b/OctopusDSC/Tests/OctopusServerExeInvocationFiles/NewInstance/RequestedState.ps1 index c67e0fa7e..1ab976bd3 100644 --- a/OctopusDSC/Tests/OctopusServerExeInvocationFiles/NewInstance/RequestedState.ps1 +++ b/OctopusDSC/Tests/OctopusServerExeInvocationFiles/NewInstance/RequestedState.ps1 @@ -17,4 +17,5 @@ return @{ TaskLogsDirectory = "E:\TaskLogs"; PackagesDirectory = "F:\Packages"; ArtifactsDirectory = "G:\Artifacts"; + TaskCap = 10; } diff --git a/OctopusDSC/Tests/OctopusServerExeInvocationFiles/UpgradeExistingInstance/ExpectedResult.ps1 b/OctopusDSC/Tests/OctopusServerExeInvocationFiles/UpgradeExistingInstance/ExpectedResult.ps1 index 3ef1a231a..4aa94c9ea 100644 --- a/OctopusDSC/Tests/OctopusServerExeInvocationFiles/UpgradeExistingInstance/ExpectedResult.ps1 +++ b/OctopusDSC/Tests/OctopusServerExeInvocationFiles/UpgradeExistingInstance/ExpectedResult.ps1 @@ -2,5 +2,6 @@ return @( "service --stop --console --instance OctopusServer", "configure --console --instance OctopusServer --upgradeCheck True --upgradeCheckWithStatistics False --webForceSSL False --webListenPrefixes http://localhost:82 --commsListenPort 10935 --home C:\Octopus --autoLoginEnabled True --hstsEnabled False --hstsMaxAge 3600", "metrics --console --instance OctopusServer --tasks True --webapi True", + "node --console --instance OctopusServer --taskCap 10", "service --start --console --instance OctopusServer" ) diff --git a/OctopusDSC/Tests/OctopusServerExeInvocationFiles/UpgradeExistingInstance/RequestedState.ps1 b/OctopusDSC/Tests/OctopusServerExeInvocationFiles/UpgradeExistingInstance/RequestedState.ps1 index 61e63d1dd..554696298 100644 --- a/OctopusDSC/Tests/OctopusServerExeInvocationFiles/UpgradeExistingInstance/RequestedState.ps1 +++ b/OctopusDSC/Tests/OctopusServerExeInvocationFiles/UpgradeExistingInstance/RequestedState.ps1 @@ -18,4 +18,5 @@ return @{ LicenseKey = "PExpY2Vuc2UgU2lnbmF0dXJlPSJoUE5sNFJvYWx2T2wveXNUdC9Rak4xcC9PeVVQc0l6b0FJS282bk9VM1kzMUg4OHlqaUI2cDZGeFVDWEV4dEttdWhWV3hVSTR4S3dJcU9vMTMyVE1FUT09Ij4gICA8TGljZW5zZWRUbz5PY3RvVGVzdCBDb21wYW55PC9MaWNlbnNlZFRvPiAgIDxMaWNlbnNlS2V5PjI0NDE0LTQ4ODUyLTE1NDI3LTQxMDgyPC9MaWNlbnNlS2V5PiAgIDxWZXJzaW9uPjIuMDwhLS0gTGljZW5zZSBTY2hlbWEgVmVyc2lvbiAtLT48L1ZlcnNpb24+ICAgPFZhbGlkRnJvbT4yMDE3LTEyLTA4PC9WYWxpZEZyb20+ICAgPE1haW50ZW5hbmNlRXhwaXJlcz4yMDIzLTAxLTAxPC9NYWludGVuYW5jZUV4cGlyZXM+ICAgPFByb2plY3RMaW1pdD5VbmxpbWl0ZWQ8L1Byb2plY3RMaW1pdD4gICA8TWFjaGluZUxpbWl0PjE8L01hY2hpbmVMaW1pdD4gICA8VXNlckxpbWl0PlVubGltaXRlZDwvVXNlckxpbWl0PiA8L0xpY2Vuc2U+" LogTaskMetrics=$true; LogRequestMetrics=$true; + TaskCap = 10; } diff --git a/OctopusDSC/Tests/cOctopusServer.Tests.ps1 b/OctopusDSC/Tests/cOctopusServer.Tests.ps1 index 5b69ef653..99ee0fdcc 100644 --- a/OctopusDSC/Tests/cOctopusServer.Tests.ps1 +++ b/OctopusDSC/Tests/cOctopusServer.Tests.ps1 @@ -144,6 +144,14 @@ try It "Should throw if 'SqlDbConnectionString' not supplied" { { Test-ParameterSet -Ensure 'Present' -State 'Stopped' -Name "blah1" -DownloadUrl "blah2" -WebListenPrefix "blah3"} | Should throw "Parameter 'SqlDbConnectionString' must be supplied when 'Ensure' is 'Present'." } + It "Should throw if 'TaskCap' is less than 1" { + $creds = New-Object System.Management.Automation.PSCredential ("username", (new-object System.Security.SecureString)) + { Test-ParameterSet -Ensure 'Present' -State 'Stopped' -Name "blah1" -DownloadUrl "blah2" -WebListenPrefix "blah3" -SqlDbConnectionString "blah4" -OctopusAdminCredential $creds -TaskCap -1} | Should throw "Parameter 'TaskCap' must be greater than 0 when 'Ensure' is 'Present'." + } + It "Should throw if 'TaskCap' is greater than 50" { + $creds = New-Object System.Management.Automation.PSCredential ("username", (new-object System.Security.SecureString)) + { Test-ParameterSet -Ensure 'Present' -State 'Stopped' -Name "blah1" -DownloadUrl "blah2" -WebListenPrefix "blah3" -SqlDbConnectionString "blah4" -OctopusAdminCredential $creds -TaskCap 51} | Should throw "Parameter 'TaskCap' must be less than 50 when 'Ensure' is 'Present'." + } It "Should not throw if 'OctopusAdminCredential' not supplied but 'OctopusMasterKey' is supplied" { $creds = New-Object System.Management.Automation.PSCredential ("username", (new-object System.Security.SecureString)) { Test-ParameterSet -Ensure 'Present' -State 'Stopped' -Name "blah1" -DownloadUrl "blah2" -WebListenPrefix "blah3" -SqlDbConnectionString "blah4" -OctopusAdminCredential $null -OctopusMasterKey $creds} | Should not throw diff --git a/README-cOctopusServer.md b/README-cOctopusServer.md index 475c58302..4797aac5b 100644 --- a/README-cOctopusServer.md +++ b/README-cOctopusServer.md @@ -64,6 +64,8 @@ Configuration SampleConfig # whether to log metrics LogTaskMetrics = $false LogRequestMetrics = $false + + TaskCap = 10 } } } @@ -123,6 +125,7 @@ When `State` is `Started`, the resource will ensure that the Octopus Servr windo | `OctopusBuiltInWorkerCredential` | `PSCredential` | `[PSCredential]::Empty` | The user account to use to execute run-on-server scripts. If not supplied, executes scripts under the service account used for `Octopus.Server.exe` | | `LogTaskMetrics` | `boolean` | `$false` | Whether to log task metrics | | `LogRequestMetrics` | `boolean` | `$false` | Whether to log api requests metrics | +| `TaskCap` | `int` | | The number of tasks this Octopus Server node should attempt to process at once | ## Drift diff --git a/Tests/OctopusDSC.Tests.ps1 b/Tests/OctopusDSC.Tests.ps1 index dca47349a..3cc2804dd 100644 --- a/Tests/OctopusDSC.Tests.ps1 +++ b/Tests/OctopusDSC.Tests.ps1 @@ -3,7 +3,6 @@ Describe "PSScriptAnalyzer" { $excludedRules = @( 'PSUseShouldProcessForStateChangingFunctions' ) - $excludedRules | % { Write-Warning "Excluding Rule $_" } $path = Resolve-Path "$PSCommandPath/../../OctopusDSC/DSCResources" Write-Output "Running PsScriptAnalyzer against $path" @@ -54,8 +53,8 @@ Describe "Mandatory Parameters" { if ($null -ne $param.name -and $param.Name.ToString() -eq "`$$propertyName") { $function = $param.Parent.Parent.Parent if ($function -is [System.Management.Automation.Language.FunctionDefinitionAst]) { - $functionName = ([System.Management.Automation.Language.FunctionDefinitionAst]$function).Name - if ($functionName -eq $functionName) { + $funcName = ([System.Management.Automation.Language.FunctionDefinitionAst]$function).Name + if ($funcName -eq $functionName) { return $param.Attributes.Extent.Text } } @@ -65,7 +64,7 @@ Describe "Mandatory Parameters" { function Test-Parameter($functionName, $ast, $propertyName, $moduleFile, $propertyType) { $param = Get-ParameterFromFunction -functionName $functionName -ast $ast -propertyName $propertyName - It "Parameter '$propertyName' in function '$functionName' should be marked with [Parameter(Mandatory)} in $($moduleFile.Name) as its a $propertyType property" { + It "Parameter '$propertyName' in function '$functionName' should be marked with [Parameter(Mandatory)] in $($moduleFile.Name) as its a $propertyType property" { $param -contains "[Parameter(Mandatory)]" | should be $true } } @@ -98,6 +97,66 @@ Describe "Mandatory Parameters" { } } +Describe "Test/Get/Set-TargetResource all implement the same properties" { + $path = Resolve-Path "$PSCommandPath/../../OctopusDSC/DSCResources" + $schemaMofFiles = Get-ChildItem $path -Recurse -Filter cOctopusServer.schema.mof + foreach ($schemaMofFile in $schemaMofFiles) { + $schemaMofFileContent = Get-Content $schemaMofFile.FullName + $moduleFile = Get-Item ($schemaMofFile.FullName -replace ".schema.mof", ".psm1") + + function Get-ParameterFromFunction($functionName, $ast, $propertyName) { + $filter = { $true } + $result = $ast.FindAll($filter, $true); + + $foundMatchingParameter = $false + foreach($param in $result) { + if ($null -ne $param.name -and $param.Name.ToString() -eq "`$$propertyName") { + $function = $param.Parent.Parent.Parent + if ($function -is [System.Management.Automation.Language.FunctionDefinitionAst]) { + $funcName = ([System.Management.Automation.Language.FunctionDefinitionAst]$function).Name + if ($funcName -eq $functionName) { + return $param + } + } + } + } + } + + function Test-Parameter($functionName, $ast, $propertyName, $moduleFile, $propertyType) { + $param = Get-ParameterFromFunction -functionName $functionName -ast $ast -propertyName $propertyName + It "Function '$functionName' should have parameter '$propertyName' in $($moduleFile.Name) as its a defined in the .schema.mof file" { + $param | should not be $null + } + } + + Describe "Module $($moduleFile.Name)" { + $tokens = $null; + $parseErrors = $null; + $ast = [System.Management.Automation.Language.Parser]::ParseFile($moduleFile.FullName, [ref] $tokens, [ref] $parseErrors); + + It "The module $($moduleFile.Name) should have no parse errors" { + $parseErrors | should be $null + } + + foreach($line in $schemaMofFileContent) { + if ($line -match "\s*(\[.*\])\s*(.*) (.*);") { + $propertyType = $matches[1] + $propertyName = $matches[3].Replace("[]", ""); + + $filter = { $true } + $result = $ast.FindAll($filter, $true); + + $foundMatchingParameter = $false + + Test-Parameter -functionName "Get-TargetResource" -ast $ast -propertyName $propertyName -moduleFile $moduleFile -propertyType $propertyType + Test-Parameter -functionName "Set-TargetResource" -ast $ast -propertyName $propertyName -moduleFile $moduleFile -propertyType $propertyType + Test-Parameter -functionName "Test-TargetResource" -ast $ast -propertyName $propertyName -moduleFile $moduleFile -propertyType $propertyType + } + } + } + } +} + Describe "Configuration Scenarios" { $path = Resolve-Path "$PSCommandPath/../../Tests/Scenarios" $configurationFiles = Get-ChildItem $path -Recurse -Filter *.ps1