Hello, I was wondering if we can have an dedicated validation step that will perform custom checks for any project before deployment, something similar to admissionwebhook from Kubernetes
Here are few restriction examples I wish to check:
- channel version rules are configured
- project has well known variables in correct format
- resource requests and limits set for kubernetes deployments
- …
Majority of this checks are doable via API but the problem here is how to wireup everything together?
Things I did already tried:
- subscriptions - may be triggered after deployment starts
- dedicated process step - may be disabled by operator
- triggers - does not have anything related
- …
Wondering if I am missing something else or may be someone did tried something similar
Hi @marchenko_alexandr,
Thanks for reaching out and for all of the information. There are two ways I believe you could achievet this, but I think the easiest way to achieve this is to have an informational step as Step 1 and then Step 2 is a Manual Intervention step.
For example in Step 1, you could have powershell code do API calls against your project and get the information for channels, variables, etc, and write-highlight those pieces of data. Step 2 would then stop the deployment so you can see that data and let you decide if everything is in good working order before allowing the deployment to proceed. Documentation: Manual intervention and approval step - Octopus Deploy
You can also potentially do this with Output Variables and Run Conditions on steps. You would do all of your checks in step 1, and based on the results you would save an Output Variable, then base your Run Condition of the next step on the result of that.
For example in Step 1, you could have powershell code do API calls against your project to check if it has channel version rules, and all the other requirements, and only if all of these pass, you would set an output variable like this, Set-OctopusVariable -name "TestResult" -value "True"
, and then later use that as a run condition for the next step like this: #{if Octopus.Action[Step01].Output.TestResult== "True"}True#{/if}
Supplementary output variable docs: Output variables - Octopus Deploy
Supplementary run condition docs: Conditions - Octopus Deploy
It’s two ways to approach it depending on how you would prefer to have it work.
Please let me know if you have any questions about it or if that helps.
Best,
Jeremy
Thank you for advise, I did consider additional step in a first place, it can be even simpler - aka run script on octopus server and just fail if anything went wrong
The problem with all this approaches is that someone who performs deployment can just skip it
Ok, you may complain that we may mark it as required
But in our case team allowed to edit projects on their own, which means they can just uncheck it from required steps
And this is done by intent, otherwise, if no one can create/edit projects it will be never ending train of requests from teammates to edit something in their projects, but if that’s the case, then there will be no need for such automated checks
The end goal is to allow teams to be empowered while having some rules in place
Like with Kubernetes - anyone can deploy to it, but because of admission webhook policies are applied
I was wondering if there is some kind of webhooks before project process being saved, or release being created or deployment task is going to be queued any of such things may be good starting point to wireup such checks
Another alternative may be to configure roles in a such a way that while allowing create projects teammates can not edit this step with checks (but not sure if thats possible with current roles)
Hi @marchenko_alexandr,
Thanks for following up with that additional information. This is a really good question, though unfortunately I don’t imagine there being any good solution to achieve this directly. You’re correct that even marking the step as required, a teammate with ProcessEdit permission on this project simply has the permission to modify this setting on the step, and the permissions aren’t granular enough to allow you to select which steps a user with the permission can edit and those they can’t. Additionally, there’s no way I’m aware of to send a webhook notification before your mentioned process change event.
I think you’ve just hit the limitation of these features. I think the closest solution is what has already been discussed, which of course leaves the possibility open for users who have ProcessEdit permission granted to them to modify this required setting on the step. You could put in a note on the applicable step to try to bring attention to your users to not skip the step, which would end up looking like this on the Process page.
I’m sorry I can’t give you the news you were hoping for. Let me know what you think, or if you have any further questions at all!
Best regards,
Kenny
True
As a workaround what I am thinking about is to have:
- an dedicated external script that will make sure that each and every project has such “custom checks” required step
- an subscription listening for deployments and checking if step was skipped
if such event occur we are going:
- send slack notification
- reconfigure project so this step is required again
Considerations: indeed it does not solve the problem, but sooner or later teammates should be tiered of disabling this step instead of fixing issues, plus notifications to slack won’t allow them to skip it often
At least it is better than nothing
Little bit later will give it a try and come back with details of how it might be implemented
PS: hope in future something like that but more reliable may be done without such tricks
@Kenneth_Bates thank you for ideas and brainstorming, here is what I have so far:
check
for checks we are going to use step template with script like this one:
if ($OctopusParameters) { # we are inside octopus
$headers = @{"X-Octopus-ApiKey" = $OCTOPUS_APIKEY}
$projectId = $OctopusParameters["Octopus.Project.Id"]
} else { # local run
$headers = @{"X-Octopus-ApiKey" = $env:OCTOPUS_APIKEY}
$projectId = "Projects-1"
}
$project = Invoke-RestMethod "https://robota.octopus.app/api/projects/$projectId" -Headers $headers
$failed = 0
# VARIABLES
$variables = Invoke-RestMethod "https://robota.octopus.app$($project.Links.Variables)" -Headers $headers | Select-Object -ExpandProperty Variables
$owner = $variables | Where-Object Name -EQ 'Owner'
if (-not $owner) {
Write-Host "- 'Owner' variable is missing"
$failed += 1
} else {
# TODO: check if owner is a valid email
# TODO: cehck if owner is not deactivated
}
# TODO: other checks
if ($failed) {
Write-Host ""
Write-Host "$failed checks failed"
exit 1
} else {
Write-Host "All checks passed"
}
For script to work we need to add parameter for Octopus Deploy API Key (in my case it has default value from common variables)
install
Now we need somehow add this script as required step to all projects, here is starting point:
$headers = @{"X-Octopus-ApiKey" = $env:OCTOPUS_APIKEY}
$templates = Invoke-RestMethod "https://example.octopus.app/api/actiontemplates/all" -Headers $headers
$template = $templates | Where-Object Name -EQ 'octochecks'
$step = @{
Name = $template.Name
Actions = @(@{
Name = $template.Name
ActionType = $template.ActionType
IsRequired = $true
WorkerPoolVariable = 'octoworker' # NOTE: if using default workers remove me
Properties = @{
"Octopus.Action.Script.ScriptSource" = $template.Properties.'Octopus.Action.Script.ScriptSource'
"Octopus.Action.Script.Syntax" = $template.Properties.'Octopus.Action.Script.Syntax'
"Octopus.Action.Template.Version" = $template.Version
"Octopus.Action.Script.ScriptBody" = $template.Properties.'Octopus.Action.Script.ScriptBody'
"Octopus.Action.RunOnServer" = $true
"Octopus.Action.Template.Id" = $template.Id
}
})
}
$projects = Invoke-RestMethod "https://example.octopus.app/api/projects/all" -Headers $headers
foreach($project in $projects) {
# $project = $projects | Where-Object Name -EQ 'Prometheus'
$process = Invoke-RestMethod "https://example.octopus.app$($project.Links.DeploymentProcess)" -Headers $headers
if ($process.Steps.Count -eq 0) { continue } # project without any step
$steps = @()
$found = $process.Steps | Where-Object { $_.Actions[0].Properties.'Octopus.Action.Template.Id' -eq $template.Id }
if ($found) {
$steps += $found
} else {
$steps += $step
}
foreach($item in $process.Steps) {
if ($item.Actions[0].Properties.'Octopus.Action.Template.Id' -ne $template.Id) {
$steps += $item
}
}
$changed = $false
if ($steps[0].Actions[0].IsDisabled) {
$steps[0].Actions[0].IsDisabled = $false
$changed = $true
}
if (-not $steps[0].Actions[0].IsRequired) {
$steps[0].Actions[0].IsRequired = $true
$changed = $true
}
if ((ConvertTo-Json -Depth 100 -InputObject $steps) -ne (ConvertTo-Json -Depth 100 -InputObject $process.Steps)) {
$changed = $true
}
if ($changed) {
try {
$process.Steps = $steps
Invoke-RestMethod -Method Put "https://example.octopus.app$($project.Links.DeploymentProcess)" -Headers $headers -Body (ConvertTo-Json -Depth 100 -Compress -InputObject $process) | Out-Null
Write-Host $project.Name -ForegroundColor Green
} catch {
Write-Host $project.Name -ForegroundColor Red
}
} else {
Write-Host $project.Name -ForegroundColor Cyan
}
}
Notes:
- this script is ment to be running externally, aka github scheduled workflows or kubernetes cronjob
- be careful and do not copy paste it as is, it will be different for each setup, for example in my case we are using dedicated worker pool variable
- be even more careful before running such scripts because they may corrupt current projects steps, double check everything semi manually first
subsription
very last one - is a subscription listening for deployment events
with fallback code like this
#!/usr/bin/pwsh
Write-Host 'content-type: text/plain'
Write-Host 'cache-control: no-store'
Write-Host ''
$body = $env:REQUEST_BODY | ConvertFrom-Json
<#
$body = @{
Payload = @{
Event = @{
RelatedDocumentIds = @(
"Deployments-69481"
)
}
}
}
#>
$headers = @{"X-Octopus-ApiKey" = $env:OCTOPUS_APIKEY}
$deployment = $body.Payload.Event.RelatedDocumentIds | Where-Object { $_.StartsWith("Deployments-") }
$test = $body.Payload.Event.RelatedDocumentIds.Count -eq 1
$deployment = Invoke-RestMethod "https://example.octopus.app/api/deployments/$deployment" -Headers $headers
$project = Invoke-RestMethod "https://example.octopus.app/api/projects/$($deployment.ProjectId)" -Headers $headers
$process = Invoke-RestMethod "https://example.octopus.app$($project.Links.DeploymentProcess)" -Headers $headers
$skipped = $process.Steps | Select-Object -ExpandProperty Actions | Where-Object Id -in $deployment.SkipActions | Select-Object -ExpandProperty Name
$templates = Invoke-RestMethod "https://example.octopus.app/api/actiontemplates/all" -Headers $headers
$template = $templates | Where-Object Name -EQ 'octochecks'
$step = @{
Name = $template.Name
Actions = @(@{
Name = $template.Name
ActionType = $template.ActionType
IsRequired = $true
WorkerPoolVariable = 'octoworker' # NOTE: if using default workers remove me
Properties = @{
"Octopus.Action.Script.ScriptSource" = $template.Properties.'Octopus.Action.Script.ScriptSource'
"Octopus.Action.Script.Syntax" = $template.Properties.'Octopus.Action.Script.Syntax'
"Octopus.Action.Template.Version" = $template.Version
"Octopus.Action.Script.ScriptBody" = $template.Properties.'Octopus.Action.Script.ScriptBody'
"Octopus.Action.RunOnServer" = $true
"Octopus.Action.Template.Id" = $template.Id
}
})
}
$steps = @()
$found = $process.Steps | Where-Object { $_.Actions[0].Properties.'Octopus.Action.Template.Id' -eq $template.Id }
if ($found) {
$steps += $found
} else {
$steps += $step
}
foreach($item in $process.Steps) {
if ($item.Actions[0].Properties.'Octopus.Action.Template.Id' -ne $template.Id) {
$steps += $item
}
}
$changed = $false
if ($steps[0].Actions[0].IsDisabled) {
$steps[0].Actions[0].IsDisabled = $false
$changed = $true
}
if (-not $steps[0].Actions[0].IsRequired) {
$steps[0].Actions[0].IsRequired = $true
$changed = $true
}
if ((ConvertTo-Json -Depth 100 -InputObject $steps) -ne (ConvertTo-Json -Depth 100 -InputObject $process.Steps)) {
$changed = $true
}
if ($changed) {
try {
$process.Steps = $steps
Invoke-RestMethod -Method Put "https://example.octopus.app$($project.Links.DeploymentProcess)" -Headers $headers -Body (ConvertTo-Json -Depth 100 -Compress -InputObject $process) | Out-Null
Write-Host "$($project.Name) - updated"
} catch {
Write-Host "$($project.Name) - failed"
}
}
if ($skipped -contains 'octochecks') {
Invoke-RestMethod -Method Post "https://slack.com/api/chat.postMessage" -ContentType 'application/json' -Headers @{ Authorization = "Bearer $($env:SLACK_TOKEN)" } -Body (@{channel = '@mac'; text = "$($project.Name) deployed without checks by $($deployment.DeployedBy)"} | ConvertTo-Json)
}
Notes:
- technically it does repeats install script but for concrete project
- its job is to make sure that required step is in place
- also notify about deployments where this step was missing
- in my case this script was running behind fastcgi and nginx
- complete config examples can be found here
recap
Once again, it does not solve the original goal but still better than nothing, and I do believe that sooner or later issues will be fixed, and notifications in slack wont allow engineers to bypass checks often
1 Like