-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathapply-updates-schedule-audit.yml
More file actions
230 lines (208 loc) · 11.4 KB
/
Copy pathapply-updates-schedule-audit.yml
File metadata and controls
230 lines (208 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# AZLOCAL-PIPELINE-ID: apply-updates-schedule-audit
# Step.03 - Apply-Updates Schedule Coverage Audit Pipeline
# This pipeline runs the read-only Test-AzLocalApplyUpdatesScheduleCoverage advisor
# weekly to detect drift between the cron schedule(s) in your apply-updates pipeline
# YAML and the UpdateStartWindow tags that operators have placed on Azure Local clusters.
#
# WHY THIS PIPELINE EXISTS
# ------------------------
# apply-updates.yml ships with NO default schedule (trigger:none) on purpose, so
# customers must consciously choose when updates fire. After you have configured
# UpdateStartWindow tags on your clusters and added cron entries to apply-updates.yml, this
# pipeline is the safety net that catches:
# - a new ring tagged with UpdateStartWindow that no cron in apply-updates.yml will reach
# - an UpdateStartWindow tag value that fails to parse (typo / wrong syntax)
# - apply-updates.yml cron entries that the advisor cannot reason about
# (DayOfMonth restrictions, step values) so you can audit them manually
#
# REPORTS GENERATED:
# - Pipeline run summary (uploaded via ##vso[task.uploadsummary]): Audit + Recommend views
# - JUnit XML: published to the Test Results tab. One <testcase time="0"> per (UpdateRing,
# UpdateStartWindow) pair; uncovered pairs become <failure>.
# - CSV: schedule-coverage-audit.csv + schedule-coverage-matrix.csv
# - Markdown: schedule-coverage-recommend.md with the ready-to-paste cron block
#
# AUTHENTICATION:
# Uses Workload Identity Federation (OIDC) - recommended for secretless authentication.
# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers
# Edits inside this block are preserved across Update-AzLocalPipelineExample
# module upgrades. Add or modify `schedules:` blocks below.
trigger: none # Manual or scheduled only
schedules:
# Weekly on Monday at 05:00 UTC - well before any common Saturday/Sunday or weekday
# evening update window so a coverage drift report lands in the inbox before the
# next scheduled apply-updates run.
- cron: '0 5 * * 1'
displayName: 'Weekly Apply-Updates Schedule Coverage Audit'
branches:
include:
- main
always: true
# END-AZLOCAL-CUSTOMIZE:schedule-triggers
parameters:
- name: pipelinePath
displayName: 'Path (file or folder) to apply-updates.yml to audit, repo-relative. REQUIRED so the Recommend view can diff its proposed crons against what is already present in apply-updates.yml and only emit a snippet for the truly missing entries. Default `.azure-pipelines` matches the standard Azure DevOps consumer layout; for repos that keep apply-updates.yml at the repo root pass `.`'
type: string
default: '.azure-pipelines'
- name: schedulePath
displayName: 'Path to apply-updates-schedule.yml. When set, the audit also reports RingMissingFromSchedule (fleet ring with no schedule row) and RingOrphanedInSchedule (schedule ring no cluster carries). Leave empty to skip the schedule-file two-way diff.'
type: string
default: './config/apply-updates-schedule.yml'
- name: leadTimeMinutes
displayName: 'Minutes before UpdateStartWindow opens that the pipeline should fire (0-60)'
type: number
default: 5
- name: firesPerWindow
displayName: 'Cron entries Recommend should emit per UpdateStartWindow segment. 2 (default, recommended) = belt-and-braces: opening edge + one mid-window retry capped at +60min, so schedule jitter and transient first-fire failures cannot silently skip a cluster for the day. 1 = opening edge only. Audit semantics are unchanged either way.'
type: number
default: 2
values:
- 1
- 2
- name: includeUntagged
displayName: 'Surface clusters with no UpdateStartWindow tag as their own row'
type: boolean
default: true
- name: clusterCsvPath
displayName: 'Path to the source-controlled cluster inventory CSV (the same file Step.2 consumes - default `config/ClusterUpdateRings.csv`). When set, the Recommend view emits a NoWindowTag remediation section: for each cluster missing an UpdateStartWindow tag, the advisor proposes a peer-derived value and tells the operator which row of the CSV to edit (or to re-run Step.1 if the cluster is absent from the CSV). Leave empty to skip the CSV-driven remediation.'
type: string
default: 'config/ClusterUpdateRings.csv'
- name: moduleVersion
displayName: 'Pin AzLocal.UpdateManagement version (empty = latest from PSGallery). See Automation-Pipeline-Examples/README.md section 5 "Optional configuration".'
type: string
default: ''
- name: debug
displayName: 'Enable verbose/debug PowerShell streams for self-service triage. Surfaces $VerbosePreference=Continue, $DebugPreference=Continue, plus a one-shot environment snapshot before the audit runs.'
type: boolean
default: false
variables:
GENERATED_AGAINST_MODULE_VERSION: '0.8.78'
REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}'
reportsPath: '$(Build.ArtifactStagingDirectory)/reports'
# v0.8.7 sideload advisor defaults. Override at the pipeline / variable-group
# level (set SIDELOAD_UPDATES=true to enable the recommended-sideload-schedule
# section). Defaults keep the audit byte-identical to v0.8.6 when unset.
SIDELOAD_UPDATES: 'false'
SIDELOAD_LEAD_DAYS: '7'
stages:
- stage: ScheduleCoverage
displayName: 'Audit Apply-Updates Schedule Coverage'
jobs:
- job: Audit
displayName: 'Run Schedule Coverage Advisor'
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
displayName: 'Checkout repository'
- task: PowerShell@2
displayName: 'Install AzLocal.UpdateManagement from PSGallery'
# v0.8.5 thin-YAML: drift detection + banner + output variables are
# all produced by Add-AzLocalPipelineVersionBanner (Public cmdlet).
name: moduleVersion
env:
REQUIRED_MODULE_VERSION: $(REQUIRED_MODULE_VERSION)
GENERATED_AGAINST_MODULE_VERSION: $(GENERATED_AGAINST_MODULE_VERSION)
inputs:
targetType: 'inline'
pwsh: true
script: |
$ErrorActionPreference = 'Stop'
$installArgs = @{ Name = 'AzLocal.UpdateManagement'; Scope = 'CurrentUser'; Force = $true; AllowClobber = $true }
if ($env:REQUIRED_MODULE_VERSION) {
$installArgs.RequiredVersion = $env:REQUIRED_MODULE_VERSION
Write-Host "REQUIRED_MODULE_VERSION is set - pinning install to v$($env:REQUIRED_MODULE_VERSION)."
} else {
Write-Host "REQUIRED_MODULE_VERSION is empty - installing the latest version from PSGallery (default fix-forward behaviour)."
}
Install-Module @installArgs
Import-Module AzLocal.UpdateManagement -Force
Add-AzLocalPipelineVersionBanner `
-GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION `
-PinnedVersion $env:REQUIRED_MODULE_VERSION
- task: AzureCLI@2
displayName: 'Run Schedule Coverage Audit'
# v0.8.5 thin-YAML: the inline inlineScript body (~210-line audit
# block + ~185-line summary block: 3 calls to
# Test-AzLocalApplyUpdatesScheduleCoverage, inline JUnit
# StringBuilder, 12-row markdown summary, schedule-row detail tables,
# allow-list coverage v1/v2 sections, cycle calendar render, and 12
# output variables) is now the Public cmdlet
# Export-AzLocalApplyUpdatesScheduleAudit. The cmdlet writes the 4
# $(reportsPath)/schedule-coverage-* artifacts, pushes the markdown
# summary via ##vso[task.uploadsummary], and sets the 12 lowercase
# output variables on the ``audit`` task via Set-AzLocalPipelineOutput.
# **v0.8.5 fix**: the cycle calendar now renders UNCONDITIONALLY
# whenever schedulePath is supplied, eliminating the v0.8.4
# ``$hasIssues``-gate regression that silently dropped the calendar
# on clean-fleet runs.
name: audit
env:
INPUT_PIPELINE_PATH: ${{ parameters.pipelinePath }}
INPUT_SCHEDULE_PATH: ${{ parameters.schedulePath }}
INPUT_LEAD_TIME_MINUTES: ${{ parameters.leadTimeMinutes }}
INPUT_FIRES_PER_WINDOW: ${{ parameters.firesPerWindow }}
INPUT_INCLUDE_UNTAGGED: ${{ lower(parameters.includeUntagged) }}
INPUT_CLUSTER_CSV_PATH: ${{ parameters.clusterCsvPath }}
INPUT_DEBUG: ${{ lower(parameters.debug) }}
INSTALLED_MODULE_VERSION: $(moduleVersion.installed_module_version)
REPORTS_PATH: $(reportsPath)
# v0.8.7 sideload advisor (opt-in). When SIDELOAD_UPDATES=true the audit
# appends a "Recommended sideload schedule" section. Inert when unset/false.
SIDELOAD_UPDATES: $(SIDELOAD_UPDATES)
SIDELOAD_LEAD_DAYS: $(SIDELOAD_LEAD_DAYS)
inputs:
# Replace with your service connection name
azureSubscription: 'AzureLocal-ServiceConnection' # Update with your service connection name
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
$ErrorActionPreference = 'Stop'
az extension add --name resource-graph --yes
Import-Module AzLocal.UpdateManagement -Force
if ($env:INPUT_DEBUG -eq 'true') {
$VerbosePreference = 'Continue'
$DebugPreference = 'Continue'
Write-Host "##[group]Debug environment"
Write-Host "PSVersion : $($PSVersionTable.PSVersion)"
Write-Host "PSEdition : $($PSVersionTable.PSEdition)"
$mod = Get-Module AzLocal.UpdateManagement | Sort-Object Version -Descending | Select-Object -First 1
Write-Host "AzLocal.UpdateManagement : v$($mod.Version) (path: $($mod.Path))"
Write-Host "##[endgroup]"
}
$params = @{
PipelineYamlPath = $env:INPUT_PIPELINE_PATH
LeadTimeMinutes = [int]$env:INPUT_LEAD_TIME_MINUTES
RecommendFiresPerWindow = [int]$env:INPUT_FIRES_PER_WINDOW
Platform = 'AzureDevOps'
OutputDirectory = $env:REPORTS_PATH
InstalledModuleVersion = $env:INSTALLED_MODULE_VERSION
}
if ($env:INPUT_SCHEDULE_PATH) { $params['SchedulePath'] = $env:INPUT_SCHEDULE_PATH }
if ($env:INPUT_CLUSTER_CSV_PATH) { $params['ClusterCsvPath'] = $env:INPUT_CLUSTER_CSV_PATH }
if ($env:INPUT_INCLUDE_UNTAGGED -eq 'true') { $params['IncludeUntagged'] = $true }
Export-AzLocalApplyUpdatesScheduleAudit @params
# compute a UTC timestamp variable so every downloadable artifact name is unique per run.
- pwsh: |
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss')
Write-Host "##vso[task.setvariable variable=artifactStamp;isOutput=true]$stamp"
Write-Host "Artifact timestamp: $stamp"
displayName: 'Compute Artifact Timestamp'
condition: always()
name: stamp
- task: PublishBuildArtifacts@1
displayName: 'Publish Schedule Coverage Reports'
condition: always()
inputs:
PathtoPublish: '$(reportsPath)'
ArtifactName: 'azlocal-step.3-apply-updates-schedule-audit-report_$(stamp.artifactStamp)'
publishLocation: 'Container'
- task: PublishTestResults@2
displayName: 'Publish Schedule Coverage as Test Results'
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '$(reportsPath)/schedule-coverage-audit.xml'
testRunTitle: 'Apply-Updates Schedule Coverage - $(Build.BuildNumber)'
mergeTestResults: false
failTaskOnFailedTests: false