@kumori/aurora-backend-handler 1.0.87 → 1.0.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/helpers/account-helper.ts +148 -91
- package/package.json +1 -1
- package/websocket-manager.ts +7 -1
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { Account, Notification } from "@kumori/aurora-interfaces";
|
|
2
|
+
const CREDENTIAL_ERROR_CODES = new Set([
|
|
3
|
+
"_error_retrieving_credentials_",
|
|
4
|
+
"_invalid_account_credentials_",
|
|
5
|
+
"_unsupported_region_",
|
|
6
|
+
]);
|
|
2
7
|
|
|
8
|
+
/**
|
|
9
|
+
* All statuses that mean the account has reached a final error state.
|
|
10
|
+
* Pollers and status-strategy consumers should treat these as terminal.
|
|
11
|
+
*/
|
|
12
|
+
export const ACCOUNT_ERROR_STATUSES = new Set([
|
|
13
|
+
"error",
|
|
14
|
+
"invalid_credentials",
|
|
15
|
+
"failed",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Map a raw backend error code to the normalized account status
|
|
20
|
+
* we write to accountsMap / the Account object.
|
|
21
|
+
*/
|
|
22
|
+
export const resolveErrorStatus = (
|
|
23
|
+
code: string,
|
|
24
|
+
): "invalid_credentials" | "error" =>
|
|
25
|
+
CREDENTIAL_ERROR_CODES.has(code) ? "invalid_credentials" : "error";
|
|
3
26
|
|
|
4
27
|
interface HandleAccountEventParams {
|
|
5
28
|
entityId: string;
|
|
@@ -39,10 +62,10 @@ interface HandleAccountOperationErrorResult {
|
|
|
39
62
|
eventType: "creationError" | "updateError" | "deletionError";
|
|
40
63
|
}
|
|
41
64
|
/**
|
|
42
|
-
* Extract cloud provider credentials from event data
|
|
65
|
+
* Extract cloud provider credentials from event data.
|
|
43
66
|
*/
|
|
44
67
|
const extractCloudProviderCredentials = (
|
|
45
|
-
eventData: any
|
|
68
|
+
eventData: any,
|
|
46
69
|
): { providerType: string; credentials: any } => {
|
|
47
70
|
let providerType = "";
|
|
48
71
|
let credentials = {};
|
|
@@ -52,18 +75,24 @@ const extractCloudProviderCredentials = (
|
|
|
52
75
|
credentials = {
|
|
53
76
|
region: eventData.spec.credentials.openstack?.region_name || "",
|
|
54
77
|
interface: eventData.spec.credentials.openstack?.interface || "",
|
|
55
|
-
apiVersion:
|
|
78
|
+
apiVersion:
|
|
79
|
+
eventData.spec.credentials.openstack?.identity_api_version || "",
|
|
56
80
|
authType: eventData.spec.credentials.openstack?.auth_type || "",
|
|
57
81
|
authUrl: eventData.spec.credentials.openstack?.auth?.auth_url || "",
|
|
58
|
-
credentialId:
|
|
59
|
-
|
|
82
|
+
credentialId:
|
|
83
|
+
eventData.spec.credentials.openstack?.auth?.application_credential_id ||
|
|
84
|
+
"",
|
|
85
|
+
credentialSecret:
|
|
86
|
+
eventData.spec.credentials.openstack?.auth
|
|
87
|
+
?.application_credential_secret || "",
|
|
60
88
|
};
|
|
61
89
|
} else if (eventData.spec.credentials.aws) {
|
|
62
90
|
providerType = "aws";
|
|
63
91
|
credentials = {
|
|
64
92
|
region: eventData.spec.credentials.aws?.region || "",
|
|
65
93
|
credentialId: eventData.spec.credentials.aws?.aws_access_key_id || "",
|
|
66
|
-
credentialSecret:
|
|
94
|
+
credentialSecret:
|
|
95
|
+
eventData.spec.credentials.aws?.aws_secret_access_key || "",
|
|
67
96
|
};
|
|
68
97
|
} else if (eventData.spec.credentials.azure) {
|
|
69
98
|
providerType = "azure";
|
|
@@ -87,28 +116,72 @@ const extractCloudProviderCredentials = (
|
|
|
87
116
|
};
|
|
88
117
|
|
|
89
118
|
/**
|
|
90
|
-
* Determine account status from event
|
|
119
|
+
* Determine the canonical account status from a WebSocket event.
|
|
120
|
+
*
|
|
121
|
+
* Priority order:
|
|
122
|
+
* 1. Soft-deleted accounts → 'deleting'
|
|
123
|
+
* 2. validCredentials status reported by the backend (covers both
|
|
124
|
+
* success and the async credential-validation path)
|
|
125
|
+
* 3. Keep the existing status so we never regress a terminal state
|
|
126
|
+
* (e.g. don't overwrite 'invalid_credentials' with 'pending')
|
|
127
|
+
* 4. Default to 'pending' for brand-new accounts
|
|
91
128
|
*/
|
|
92
129
|
const determineAccountStatus = (
|
|
93
130
|
eventData: any,
|
|
94
|
-
existingAccount: Account | undefined
|
|
131
|
+
existingAccount: Account | undefined,
|
|
95
132
|
): string => {
|
|
96
133
|
if (eventData.meta?.deleted) {
|
|
97
134
|
return "deleting";
|
|
98
135
|
}
|
|
99
|
-
|
|
100
|
-
|
|
136
|
+
const rawStatus: string | undefined =
|
|
137
|
+
eventData.status?.validCredentials?.status;
|
|
138
|
+
if (rawStatus) {
|
|
139
|
+
if (CREDENTIAL_ERROR_CODES.has(rawStatus)) {
|
|
140
|
+
return "invalid_credentials";
|
|
141
|
+
}
|
|
142
|
+
return rawStatus;
|
|
101
143
|
}
|
|
102
|
-
if (existingAccount?.status) {
|
|
144
|
+
if (existingAccount?.status && existingAccount.status !== "pending") {
|
|
103
145
|
return existingAccount.status;
|
|
104
146
|
}
|
|
105
147
|
return "pending";
|
|
106
148
|
};
|
|
107
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Translates account error notifications → account.status on accountsMap
|
|
152
|
+
* so that waitForAccountStatus only needs to poll account.status, not notifications.
|
|
153
|
+
*
|
|
154
|
+
* Called from the websocket-manager's `user` event case immediately after
|
|
155
|
+
* handleUserEvent so the status is always written in the same event-loop tick.
|
|
156
|
+
*/
|
|
157
|
+
export const syncAccountStatusFromNotifications = (
|
|
158
|
+
notifications: any[] = [],
|
|
159
|
+
accountsMap: Map<string, Account>,
|
|
160
|
+
): void => {
|
|
161
|
+
for (const notification of notifications) {
|
|
162
|
+
if (notification.type !== "error") continue;
|
|
163
|
+
if (
|
|
164
|
+
notification.subtype !== "account-creation-error" &&
|
|
165
|
+
notification.subtype !== "account-update-error"
|
|
166
|
+
)
|
|
167
|
+
continue;
|
|
168
|
+
|
|
169
|
+
const accountName: string = notification.data?.account;
|
|
170
|
+
if (!accountName) continue;
|
|
171
|
+
|
|
172
|
+
const account = accountsMap.get(accountName);
|
|
173
|
+
if (!account || ACCOUNT_ERROR_STATUSES.has(account.status)) continue;
|
|
174
|
+
|
|
175
|
+
const code: string = notification.info_content?.code ?? "";
|
|
176
|
+
accountsMap.set(accountName, {
|
|
177
|
+
...account,
|
|
178
|
+
status: resolveErrorStatus(code),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
};
|
|
108
182
|
|
|
109
183
|
/**
|
|
110
|
-
* Handles the "account" event from WebSocket messages
|
|
111
|
-
* Processes account data updates and cloud provider credentials
|
|
184
|
+
* Handles the "account" kind event from WebSocket messages.
|
|
112
185
|
*/
|
|
113
186
|
export const handleAccountEvent = ({
|
|
114
187
|
entityId,
|
|
@@ -117,11 +190,13 @@ export const handleAccountEvent = ({
|
|
|
117
190
|
accountsMap,
|
|
118
191
|
}: HandleAccountEventParams): HandleAccountEventResult => {
|
|
119
192
|
const accountTenantId = parentParts.tenant;
|
|
120
|
-
const { providerType, credentials } =
|
|
193
|
+
const { providerType, credentials } =
|
|
194
|
+
extractCloudProviderCredentials(eventData);
|
|
121
195
|
const accountLabels: Record<string, string> = eventData.meta.labels;
|
|
122
196
|
const hasCredentials = "__axebow::managedCredentials" in accountLabels;
|
|
123
197
|
const existingAccount = accountsMap.get(entityId);
|
|
124
198
|
const accountStatus = determineAccountStatus(eventData, existingAccount);
|
|
199
|
+
|
|
125
200
|
const newAccount: Account = {
|
|
126
201
|
id: entityId,
|
|
127
202
|
name: entityId,
|
|
@@ -196,9 +271,8 @@ export const handleAccountEvent = ({
|
|
|
196
271
|
};
|
|
197
272
|
};
|
|
198
273
|
|
|
199
|
-
|
|
200
274
|
/**
|
|
201
|
-
* Handles successful account operations (CREATE, UPDATE, DELETE)
|
|
275
|
+
* Handles successful account operations (CREATE, UPDATE, DELETE).
|
|
202
276
|
*/
|
|
203
277
|
export const handleAccountOperationSuccess = ({
|
|
204
278
|
action,
|
|
@@ -206,57 +280,37 @@ export const handleAccountOperationSuccess = ({
|
|
|
206
280
|
originalData,
|
|
207
281
|
}: HandleAccountOperationSuccessParams): HandleAccountOperationSuccessResult => {
|
|
208
282
|
if (action === "DELETE") {
|
|
209
|
-
const accountNotification: Notification = {
|
|
210
|
-
type: "success",
|
|
211
|
-
subtype: "account-deleted",
|
|
212
|
-
date: Date.now().toString(),
|
|
213
|
-
status: "unread",
|
|
214
|
-
callToAction: false,
|
|
215
|
-
data: {
|
|
216
|
-
account: originalData.name,
|
|
217
|
-
tenant: originalData.tenant,
|
|
218
|
-
},
|
|
219
|
-
};
|
|
220
|
-
|
|
221
283
|
return {
|
|
222
284
|
updatedAccount: null,
|
|
223
285
|
shouldDelete: true,
|
|
224
|
-
notification:
|
|
286
|
+
notification: {
|
|
287
|
+
type: "success",
|
|
288
|
+
subtype: "account-deleted",
|
|
289
|
+
date: Date.now().toString(),
|
|
290
|
+
status: "unread",
|
|
291
|
+
callToAction: false,
|
|
292
|
+
data: { account: originalData.name, tenant: originalData.tenant },
|
|
293
|
+
},
|
|
225
294
|
eventType: "deleted",
|
|
226
295
|
};
|
|
227
296
|
}
|
|
228
297
|
|
|
229
298
|
if (originalData) {
|
|
230
|
-
const updatedAccount = { ...originalData, status: "active" };
|
|
231
|
-
|
|
232
|
-
let subtype: string;
|
|
233
|
-
let eventType: "created" | "updated" | null = null;
|
|
234
|
-
|
|
235
|
-
if (action === "CREATE") {
|
|
236
|
-
subtype = "account-created";
|
|
237
|
-
eventType = "created";
|
|
238
|
-
} else {
|
|
239
|
-
subtype = "account-updated";
|
|
240
|
-
eventType = "updated";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const accountNotification: Notification = {
|
|
244
|
-
type: "success",
|
|
245
|
-
subtype,
|
|
246
|
-
date: Date.now().toString(),
|
|
247
|
-
status: "unread",
|
|
248
|
-
callToAction: false,
|
|
249
|
-
data: {
|
|
250
|
-
account: updatedAccount.name,
|
|
251
|
-
tenant: updatedAccount.tenant,
|
|
252
|
-
},
|
|
253
|
-
};
|
|
299
|
+
const updatedAccount: Account = { ...originalData, status: "active" };
|
|
300
|
+
const isCreate = action === "CREATE";
|
|
254
301
|
|
|
255
302
|
return {
|
|
256
303
|
updatedAccount,
|
|
257
304
|
shouldDelete: false,
|
|
258
|
-
notification:
|
|
259
|
-
|
|
305
|
+
notification: {
|
|
306
|
+
type: "success",
|
|
307
|
+
subtype: isCreate ? "account-created" : "account-updated",
|
|
308
|
+
date: Date.now().toString(),
|
|
309
|
+
status: "unread",
|
|
310
|
+
callToAction: false,
|
|
311
|
+
data: { account: updatedAccount.name, tenant: updatedAccount.tenant },
|
|
312
|
+
},
|
|
313
|
+
eventType: isCreate ? "created" : "updated",
|
|
260
314
|
};
|
|
261
315
|
}
|
|
262
316
|
|
|
@@ -275,9 +329,12 @@ export const handleAccountOperationSuccess = ({
|
|
|
275
329
|
};
|
|
276
330
|
};
|
|
277
331
|
|
|
278
|
-
|
|
279
332
|
/**
|
|
280
|
-
* Handles failed account operations (CREATE, UPDATE, DELETE)
|
|
333
|
+
* Handles failed account operations (CREATE, UPDATE, DELETE).
|
|
334
|
+
*
|
|
335
|
+
* For CREATE failures the account is removed from the map (shouldDelete = true).
|
|
336
|
+
* For UPDATE failures the existing account is kept but stamped with the
|
|
337
|
+
* resolved error status so waitForAccountStatus can terminate cleanly.
|
|
281
338
|
*/
|
|
282
339
|
export const handleAccountOperationError = ({
|
|
283
340
|
action,
|
|
@@ -285,52 +342,52 @@ export const handleAccountOperationError = ({
|
|
|
285
342
|
originalData,
|
|
286
343
|
error,
|
|
287
344
|
}: HandleAccountOperationErrorParams): HandleAccountOperationErrorResult => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
let shouldDelete = false;
|
|
345
|
+
const isCreate = action === "CREATE";
|
|
346
|
+
const isUpdate = action === "UPDATE";
|
|
291
347
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
348
|
+
const subtype = isCreate
|
|
349
|
+
? "account-creation-error"
|
|
350
|
+
: isUpdate
|
|
351
|
+
? "account-update-error"
|
|
352
|
+
: "account-deletion-error";
|
|
353
|
+
|
|
354
|
+
const eventType: "creationError" | "updateError" | "deletionError" = isCreate
|
|
355
|
+
? "creationError"
|
|
356
|
+
: isUpdate
|
|
357
|
+
? "updateError"
|
|
358
|
+
: "deletionError";
|
|
359
|
+
const errorCode: string = error?.error?.code ?? error?.code ?? "";
|
|
360
|
+
const errorStatus = resolveErrorStatus(errorCode);
|
|
303
361
|
|
|
304
|
-
const
|
|
362
|
+
const notification: Notification = {
|
|
305
363
|
type: "error",
|
|
306
364
|
subtype,
|
|
307
365
|
date: Date.now().toString(),
|
|
308
366
|
status: "unread",
|
|
309
367
|
info_content: {
|
|
310
|
-
code:
|
|
311
|
-
message:
|
|
312
|
-
|
|
368
|
+
code: errorCode || "UNKNOWN_ERROR",
|
|
369
|
+
message:
|
|
370
|
+
error?.error?.content ?? error?.error?.message ?? "Unknown error",
|
|
371
|
+
timestamp: error?.error?.timestamp ?? Date.now().toString(),
|
|
313
372
|
},
|
|
314
373
|
callToAction: false,
|
|
315
374
|
data: {
|
|
316
|
-
account:
|
|
317
|
-
|
|
375
|
+
account:
|
|
376
|
+
entityName || originalData?.name || originalData?.account || "unknown",
|
|
377
|
+
tenant: originalData?.tenant ?? "unknown",
|
|
318
378
|
},
|
|
319
379
|
};
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
status: "error",
|
|
380
|
+
if (isCreate) {
|
|
381
|
+
return {
|
|
382
|
+
updatedAccount: null,
|
|
383
|
+
shouldDelete: true,
|
|
384
|
+
notification,
|
|
385
|
+
eventType,
|
|
327
386
|
};
|
|
328
387
|
}
|
|
388
|
+
const updatedAccount: Account | null = originalData
|
|
389
|
+
? { ...originalData, status: errorStatus }
|
|
390
|
+
: null;
|
|
329
391
|
|
|
330
|
-
return {
|
|
331
|
-
|
|
332
|
-
shouldDelete,
|
|
333
|
-
notification: accountErrorNotification,
|
|
334
|
-
eventType,
|
|
335
|
-
};
|
|
336
|
-
};
|
|
392
|
+
return { updatedAccount, shouldDelete: false, notification, eventType };
|
|
393
|
+
};
|
package/package.json
CHANGED
package/websocket-manager.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
handleAccountEvent,
|
|
54
54
|
handleAccountOperationError,
|
|
55
55
|
handleAccountOperationSuccess,
|
|
56
|
+
syncAccountStatusFromNotifications,
|
|
56
57
|
} from "./helpers/account-helper";
|
|
57
58
|
import {
|
|
58
59
|
handleCAEvent,
|
|
@@ -191,7 +192,6 @@ const REPORTING_ITERATIONS = 5;
|
|
|
191
192
|
const REPORTING_INTERVAL = 1000;
|
|
192
193
|
let hasLoadedReportingOnce = false;
|
|
193
194
|
let recentlyUpdatedServices = new Map<string, Service>();
|
|
194
|
-
|
|
195
195
|
/**
|
|
196
196
|
* Helper function to safely stringify error objects
|
|
197
197
|
*/
|
|
@@ -644,6 +644,7 @@ const handleEvent = async (message: WSMessage) => {
|
|
|
644
644
|
userEventResult.deletedTenantKeys.forEach((key) => {
|
|
645
645
|
tenantsMap.delete(key);
|
|
646
646
|
});
|
|
647
|
+
syncAccountStatusFromNotifications(userData.notifications, accountsMap);
|
|
647
648
|
break;
|
|
648
649
|
case "environment":
|
|
649
650
|
const envEventResult = handleEnvironmentEvent({
|
|
@@ -1453,6 +1454,7 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
|
|
|
1453
1454
|
} else if (accErrorResult.updatedAccount) {
|
|
1454
1455
|
accountsMap.set(entityName, accErrorResult.updatedAccount);
|
|
1455
1456
|
}
|
|
1457
|
+
|
|
1456
1458
|
if (accErrorResult.eventType === "creationError") {
|
|
1457
1459
|
eventHelper.account.publish.creationError(originalData);
|
|
1458
1460
|
} else if (accErrorResult.eventType === "updateError") {
|
|
@@ -1480,6 +1482,10 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
|
|
|
1480
1482
|
environmentsMap.set(entityName, envErrorResult.updatedEnvironment);
|
|
1481
1483
|
break;
|
|
1482
1484
|
}
|
|
1485
|
+
setTimeout(async () => {
|
|
1486
|
+
rebuildHierarchy();
|
|
1487
|
+
await updateUserWithPlatformInfo();
|
|
1488
|
+
}, 100);
|
|
1483
1489
|
};
|
|
1484
1490
|
const handleDeleteEvent = async (message: WSMessage) => {
|
|
1485
1491
|
const keyParts = parseKeyPath(message.payload.key);
|