1 | |
2 |
|
3 | |
4 | * Approve change control |
5 | * Approve a change control (resource API changecontrol.v1). The approval is tied to the change control's current version, read just before, so a change control edited since is not approved by mistake. |
6 | */ |
7 | export async function main( |
8 | auth: RT.AristaCloudvision, |
9 | change_control_id: string, |
10 | notes: string = "" |
11 | ) { |
12 | const current = await cv( |
13 | auth, |
14 | `/api/resources/changecontrol/v1/ChangeControl?key.id=${encodeURIComponent(change_control_id)}`, |
15 | { notFound: `Change control ${change_control_id} not found` } |
16 | ) |
17 | const version = current.value?.change?.time |
18 | if (!version) { |
19 | throw new Error( |
20 | `Change control ${change_control_id} has no change time to approve against` |
21 | ) |
22 | } |
23 | return await cv(auth, "/api/resources/changecontrol/v1/ApproveConfig", { |
24 | body: { |
25 | key: { id: change_control_id }, |
26 | approve: { value: true, notes: notes || "" }, |
27 | version, |
28 | }, |
29 | }) |
30 | } |
31 |
|
32 | |
33 | async function cv( |
34 | auth: RT.AristaCloudvision, |
35 | path: string, |
36 | options: { body?: unknown; notFound?: string; optional?: boolean } = {} |
37 | ): Promise<any> { |
38 | const url = new URL(path, auth.base_url) |
39 | const response = await fetch(url, { |
40 | method: options.body === undefined ? "GET" : "POST", |
41 | headers: { |
42 | Authorization: `Bearer ${auth.token}`, |
43 | ...(options.body === undefined |
44 | ? {} |
45 | : { "Content-Type": "application/json" }), |
46 | Accept: "application/json", |
47 | }, |
48 | body: options.body === undefined ? undefined : JSON.stringify(options.body), |
49 | }) |
50 | const text = await response.text() |
51 | if ( |
52 | response.status === 401 || |
53 | response.status === 403 || |
54 | (!response.ok && text.includes("User is unauthorized")) |
55 | ) { |
56 | throw new Error( |
57 | `CloudVision refused the token (HTTP ${response.status}): check that the service-account token is valid, not expired, and that its role allows this call. ${text}` |
58 | ) |
59 | } |
60 | const data = |
61 | text.trim() === "" |
62 | ? {} |
63 | : /^\s*[[{]/.test(text) |
64 | ? JSON.parse(text) |
65 | : undefined |
66 | const error = findError(data) |
67 | if (response.ok && data !== undefined && !error) return data |
68 | const detail = error |
69 | ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}` |
70 | : `HTTP ${response.status} ${text.slice(0, 500)}` |
71 | |
72 | |
73 | if ( |
74 | response.status === 404 || |
75 | (error && (options.optional || options.notFound)) |
76 | ) { |
77 | if (options.optional) return undefined |
78 | if (options.notFound) throw new Error(`${options.notFound} (${detail})`) |
79 | } |
80 | if (response.ok && !error) { |
81 | throw new Error( |
82 | `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}` |
83 | ) |
84 | } |
85 | throw new Error(`CloudVision ${url.pathname} failed: ${detail}`) |
86 | } |
87 |
|
88 | function findError(value: any): any { |
89 | if (!value || typeof value !== "object") return undefined |
90 | |
91 | if (!Array.isArray(value) && value.errorCode) return value |
92 | for (const item of Object.values(value)) { |
93 | const error = findError(item) |
94 | if (error) return error |
95 | } |
96 | return undefined |
97 | } |
98 |
|