Repository navigation
Expand file tree
/
Copy pathapply.js
More file actions
301 lines (261 loc) · 8.18 KB
/
Copy pathapply.js
File metadata and controls
301 lines (261 loc) · 8.18 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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
import {
changedPaths,
configDir,
dispatchWorkflow,
folderExists,
gitopsRepo,
headOfMain,
includesCommit,
lastApplied,
latestDeployment,
readJson,
verifyRelease,
writeEnvironmentsConfig,
writeSummary
} from './github.js'
import { environmentName, OutcomeError, pinPath } from './gitops.js'
import { verifyRulesets } from './rulesets.js'
const STATES = new Set(['platform', 'tenants'])
const TARGETS = new Set(['tenants', 'deployments'])
function stateOf(value) {
if (!STATES.has(value)) {
throw new OutcomeError('failed', 'state must be platform or tenants')
}
return value
}
/**
* Where an environment's apply jobs run, from its registry entry: standard
* runner labels in Phase 1, a larger-runner group with Azure private
* networking in the target.
*
* @param {{runner: {labels?: string[], group?: string}}} entry - Registry entry.
* @returns {string} JSON for `runs-on`.
*/
function runnerFor(entry) {
return JSON.stringify(
entry.runner.group ? { group: entry.runner.group } : entry.runner.labels
)
}
async function changedSinceApplied(github, context, environment) {
const folder = `environments/${environment}`
const base = await lastApplied(github, { ...context.repo, environment })
if (!base) {
return folderExists(github, {
...context.repo,
path: folder,
ref: context.sha
})
}
const paths = await changedPaths(github, {
...context.repo,
base,
head: context.sha
})
return paths === null || paths.some((path) => path.startsWith(`${folder}/`))
}
// R5: the platform's latest apply succeeded, at the pin's commit or a
// later one.
async function platformApplied(app, owner, environment) {
const deployments = gitopsRepo(owner, 'deployments')
const { data: commits } = await app.rest.repos.listCommits({
...deployments,
path: pinPath(environment),
sha: 'main',
per_page: 1
})
const latest = await latestDeployment(app, { ...deployments, environment })
if (!commits[0] || !latest?.succeeded) {
return false
}
return includesCommit(app, {
...deployments,
base: commits[0].sha,
head: latest.sha
})
}
async function legFor({ github, app, context, core, state, entry, forced }) {
const { owner, repo } = context.repo
const name = entry.name
const pin =
state === 'platform'
? await readJson(github, {
owner,
repo,
path: pinPath(name),
ref: context.sha
})
: await readJson(app, {
...gitopsRepo(owner, 'deployments'),
path: pinPath(name),
ref: 'main'
})
if (!pin) {
core.notice(
`${name} has no platform pin yet, so there is nothing to apply.`
)
return null
}
await verifyRelease(app, owner, pin)
if (!forced && !(await changedSinceApplied(github, context, name))) {
return null
}
if (state === 'tenants' && !(await platformApplied(app, owner, name))) {
core.notice(
`The platform apply of ${name} at ${pin.release} has not succeeded yet; ` +
'it dispatches this workflow when it does (R5).'
)
return null
}
return {
environment: name,
runner: runnerFor(entry),
entra_enabled: entry.entra.enabled,
team_registrations:
entry.entra.enabled && entry.entra.teamRegistrations !== false,
release: pin.release,
sha: pin.sha
}
}
/**
* resolve-apply: which environments this run applies, as the apply matrix,
* with their parameters copied to `AIP_CONFIG` for the apply jobs. A run whose
* commit is no longer the head of main applies nothing (R1); the run for the
* head does. A dispatch applies the environment it names; a push applies
* every environment whose folder changed since its last successful apply,
* so a superseded run's changes are never lost; `all` (reconcile-all)
* applies every environment from main, as a dispatch does for one. Tenants
* apply only once the platform apply covers the environment's pin (R5).
*
* Inputs: AIP_STATE, AIP_ENVIRONMENT (dispatch only), AIP_ALL (`true` for
* every environment; not with AIP_ENVIRONMENT), AIP_CONFIG.
*
* @param {object} input - Command context from main.js.
* @returns {Promise<{matrix: object[]}>} The apply matrix.
*/
async function resolveApply({ github, app, context, core, env }) {
const state = stateOf(env.AIP_STATE)
const requested = env.AIP_ENVIRONMENT
? environmentName(env.AIP_ENVIRONMENT)
: null
const all = env.AIP_ALL === 'true'
if (all && requested) {
throw new OutcomeError(
'failed',
'all applies every environment; it takes no environment'
)
}
const head = await headOfMain(github, context.repo)
if (head !== context.sha) {
core.notice(
`${context.sha} is superseded by ${head} on main; the run for ${head} applies it (R1).`
)
return { matrix: [] }
}
const { owner } = context.repo
const registry = await readJson(app, {
...gitopsRepo(owner, 'environments'),
path: 'registry.json',
ref: 'main'
})
const matrix = []
for (const name of requested
? [requested]
: registry.environments.map((e) => e.name)) {
const entry = registry.environments.find((e) => e.name === name)
if (!entry) {
throw new OutcomeError('failed', `${name} is not in the registry`)
}
const leg = await legFor({
github,
app,
context,
core,
state,
entry,
forced: Boolean(requested) || all
})
if (leg) {
matrix.push(leg)
}
}
if (matrix.length > 0) {
await writeEnvironmentsConfig(app, {
owner,
environments: matrix.map((leg) => leg.environment),
dir: configDir(env)
})
}
await writeSummary(
core,
`${state} apply`,
matrix.length > 0
? matrix.map((leg) => `- \`${leg.environment}\` at \`${leg.release}\``)
: ['Nothing to apply.']
)
return { matrix }
}
/**
* preflight: the apply job's first step. Refuses a run whose commit is no
* longer the head of main, which is how a re-run of an old run is refused
* (R1), and one whose state repository no longer has the machine rulesets
* as the design has them (`rulesets-changed`, mitigation 2). Exports
* AIP_BASE, the commit of the environment's last successful apply, for
* plan-guard and the run report.
*
* Inputs: AIP_ENVIRONMENT.
*
* @param {object} input - Command context from main.js; `app` is a token for
* this repository with Administration: read.
* @returns {Promise<{base: string | null}>} The last applied commit.
*/
async function preflight({ github, app, context, core, env }) {
const environment = environmentName(env.AIP_ENVIRONMENT)
const head = await headOfMain(github, context.repo)
if (head !== context.sha) {
throw new OutcomeError(
'superseded',
`This run's commit ${context.sha} is no longer the head of main (${head}). ` +
'Re-runs are refused (R1); dispatch a fresh run.'
)
}
await verifyRulesets(app, context.repo)
const base = await lastApplied(github, { ...context.repo, environment })
core.exportVariable('AIP_BASE', base ?? '')
return { base }
}
/**
* dispatch-apply: starts `apply.yml` on main of ai-platform-{AIP_TARGET} for
* each environment, recording every run started in the job summary.
*
* Inputs: AIP_TARGET (tenants or deployments), AIP_ENVIRONMENTS (JSON
* array), AIP_REASON.
*
* @param {object} input - Command context from main.js.
* @returns {Promise<{runs: Array<{environment: string, url: string | null}>}>}
*/
async function dispatchApply({ github, context, core, env }) {
if (!TARGETS.has(env.AIP_TARGET)) {
throw new OutcomeError('failed', 'target must be tenants or deployments')
}
const target = gitopsRepo(context.repo.owner, env.AIP_TARGET)
const runs = []
for (const environment of JSON.parse(env.AIP_ENVIRONMENTS).map(
environmentName
)) {
const url = await dispatchWorkflow(github, {
...target,
workflow: 'apply.yml',
inputs: { environment, reason: env.AIP_REASON ?? '' }
})
runs.push({ environment, url })
}
await writeSummary(
core,
`Dispatched the ${env.AIP_TARGET} apply`,
runs.map(
({ environment, url }) => `- \`${environment}\`: ${url ?? 'started'}`
)
)
return { runs }
}
export { dispatchApply, preflight, resolveApply, runnerFor }