@oneuptime/common 10.0.67 → 10.0.69
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/Models/AnalyticsModels/AuditLog.ts +370 -0
- package/Models/DatabaseModels/Alert.ts +2 -0
- package/Models/DatabaseModels/ApiKey.ts +2 -0
- package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +3 -0
- package/Models/DatabaseModels/Incident.ts +2 -0
- package/Models/DatabaseModels/KubernetesResource.ts +19 -0
- package/Models/DatabaseModels/Label.ts +2 -0
- package/Models/DatabaseModels/Monitor.ts +2 -0
- package/Models/DatabaseModels/OnCallDutyPolicy.ts +2 -0
- package/Models/DatabaseModels/Project.ts +80 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +2 -0
- package/Models/DatabaseModels/StatusPage.ts +2 -0
- package/Models/DatabaseModels/Team.ts +2 -0
- package/Models/DatabaseModels/UserTelegram.ts +1 -1
- package/Server/API/KubernetesResourceAPI.ts +2 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +14 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/AuditLogService.ts +574 -0
- package/Server/Services/DatabaseService.ts +103 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/KubernetesResourceService.ts +300 -8
- package/Server/Utils/VM/VMRunner.ts +39 -22
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +1 -0
- package/Types/AuditLog/AuditLogAction.ts +7 -0
- package/Types/BaseDatabase/EnableAuditLogOn.ts +5 -0
- package/Types/Database/EnableAuditLog.ts +18 -0
- package/Types/IsolatedVM/ReturnResult.ts +6 -0
- package/Types/Kubernetes/KubernetesInventoryExtractor.ts +15 -1
- package/Types/Permission.ts +13 -0
- package/build/dist/Models/AnalyticsModels/AuditLog.js +337 -0
- package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Alert.js +2 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ApiKey.js +2 -0
- package/build/dist/Models/DatabaseModels/ApiKey.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +2 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +20 -0
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Label.js +2 -0
- package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +2 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js +2 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +82 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +2 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Team.js +2 -0
- package/build/dist/Models/DatabaseModels/Team.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTelegram.js +1 -1
- package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
- package/build/dist/Server/API/KubernetesResourceAPI.js +2 -0
- package/build/dist/Server/API/KubernetesResourceAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AuditLogService.js +402 -0
- package/build/dist/Server/Services/AuditLogService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseService.js +79 -8
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +202 -8
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +33 -19
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +1 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/AuditLog/AuditLogAction.js +8 -0
- package/build/dist/Types/AuditLog/AuditLogAction.js.map +1 -0
- package/build/dist/Types/BaseDatabase/EnableAuditLogOn.js +2 -0
- package/build/dist/Types/BaseDatabase/EnableAuditLogOn.js.map +1 -0
- package/build/dist/Types/Database/EnableAuditLog.js +15 -0
- package/build/dist/Types/Database/EnableAuditLog.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js +7 -1
- package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js.map +1 -1
- package/build/dist/Types/Permission.js +11 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/package.json +1 -1
|
@@ -772,6 +772,31 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
772
772
|
);
|
|
773
773
|
}
|
|
774
774
|
|
|
775
|
+
if (
|
|
776
|
+
!createBy.props.ignoreHooks &&
|
|
777
|
+
this.getModel().enableAuditLogOn?.create
|
|
778
|
+
) {
|
|
779
|
+
/*
|
|
780
|
+
* Lazy require to avoid circular dependency between DatabaseService and
|
|
781
|
+
* AuditLogService (which depends on ProjectService/UserService, both of
|
|
782
|
+
* which extend DatabaseService). A top-level import leaves
|
|
783
|
+
* DatabaseService undefined at class-extension time for subclasses.
|
|
784
|
+
*/
|
|
785
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
786
|
+
const auditLogService: {
|
|
787
|
+
recordCreate: (data: {
|
|
788
|
+
model: TBaseModel;
|
|
789
|
+
createdItem: TBaseModel;
|
|
790
|
+
props: DatabaseCommonInteractionProps;
|
|
791
|
+
}) => Promise<void>;
|
|
792
|
+
} = require("./AuditLogService").default;
|
|
793
|
+
await auditLogService.recordCreate({
|
|
794
|
+
model: this.getModel(),
|
|
795
|
+
createdItem: createBy.data,
|
|
796
|
+
props: createBy.props,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
775
800
|
return createBy.data;
|
|
776
801
|
} catch (error) {
|
|
777
802
|
await this.onCreateError(error as Exception);
|
|
@@ -1117,6 +1142,18 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1117
1142
|
(select as any)[this.getModel().getTenantColumn() as string] = true;
|
|
1118
1143
|
}
|
|
1119
1144
|
|
|
1145
|
+
/*
|
|
1146
|
+
* If audit logging on delete is enabled, fetch all scalar columns so we
|
|
1147
|
+
* can record a full snapshot of the record before it is deleted.
|
|
1148
|
+
*/
|
|
1149
|
+
if (this.getModel().enableAuditLogOn?.delete) {
|
|
1150
|
+
const allColumns: Array<string> =
|
|
1151
|
+
this.getModel().getTableColumns().columns;
|
|
1152
|
+
for (const columnName of allColumns) {
|
|
1153
|
+
(select as any)[columnName] = true;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1120
1157
|
const items: Array<TBaseModel> = await this._findBy({
|
|
1121
1158
|
query: beforeDeleteBy.query,
|
|
1122
1159
|
skip: beforeDeleteBy.skip.toNumber(),
|
|
@@ -1199,6 +1236,28 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1199
1236
|
);
|
|
1200
1237
|
}
|
|
1201
1238
|
|
|
1239
|
+
if (this.getModel().enableAuditLogOn?.delete && items.length > 0) {
|
|
1240
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1241
|
+
const auditLogService: {
|
|
1242
|
+
recordDelete: (args: {
|
|
1243
|
+
model: TBaseModel;
|
|
1244
|
+
deletedItem: TBaseModel;
|
|
1245
|
+
itemId: ObjectID;
|
|
1246
|
+
props: DatabaseCommonInteractionProps;
|
|
1247
|
+
}) => Promise<void>;
|
|
1248
|
+
} = require("./AuditLogService").default;
|
|
1249
|
+
for (const item of items) {
|
|
1250
|
+
if (item.id) {
|
|
1251
|
+
await auditLogService.recordDelete({
|
|
1252
|
+
model: this.getModel(),
|
|
1253
|
+
deletedItem: item,
|
|
1254
|
+
itemId: item.id,
|
|
1255
|
+
props: deleteBy.props,
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1202
1261
|
return numberOfDocsAffected;
|
|
1203
1262
|
} catch (error) {
|
|
1204
1263
|
await this.onDeleteError(error as Exception);
|
|
@@ -1533,6 +1592,26 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1533
1592
|
true;
|
|
1534
1593
|
}
|
|
1535
1594
|
|
|
1595
|
+
/*
|
|
1596
|
+
* When audit logging on update is enabled, ensure the resource's display
|
|
1597
|
+
* name is loaded on the `before` snapshot so the audit entry records the
|
|
1598
|
+
* human-readable resource name even when the update doesn't touch it.
|
|
1599
|
+
*/
|
|
1600
|
+
if (this.getModel().enableAuditLogOn?.update) {
|
|
1601
|
+
const nameCandidates: ReadonlyArray<string> = [
|
|
1602
|
+
"name",
|
|
1603
|
+
"title",
|
|
1604
|
+
"displayName",
|
|
1605
|
+
];
|
|
1606
|
+
const modelColumns: Array<string> =
|
|
1607
|
+
this.getModel().getTableColumns().columns;
|
|
1608
|
+
for (const candidate of nameCandidates) {
|
|
1609
|
+
if (modelColumns.includes(candidate)) {
|
|
1610
|
+
(selectColumns as any)[candidate] = true;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1536
1615
|
const items: Array<TBaseModel> = await this._findBy({
|
|
1537
1616
|
query: beforeUpdateBy.query,
|
|
1538
1617
|
skip: updateBy.skip.toNumber(),
|
|
@@ -1582,6 +1661,30 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1582
1661
|
);
|
|
1583
1662
|
}
|
|
1584
1663
|
}
|
|
1664
|
+
|
|
1665
|
+
if (
|
|
1666
|
+
this.getModel().enableAuditLogOn?.update &&
|
|
1667
|
+
!this.hasSameValues({ item, updatedItem }) &&
|
|
1668
|
+
item.id
|
|
1669
|
+
) {
|
|
1670
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1671
|
+
const auditLogService: {
|
|
1672
|
+
recordUpdate: (args: {
|
|
1673
|
+
model: TBaseModel;
|
|
1674
|
+
before: TBaseModel;
|
|
1675
|
+
updatedFields: JSONObject;
|
|
1676
|
+
itemId: ObjectID;
|
|
1677
|
+
props: DatabaseCommonInteractionProps;
|
|
1678
|
+
}) => Promise<void>;
|
|
1679
|
+
} = require("./AuditLogService").default;
|
|
1680
|
+
await auditLogService.recordUpdate({
|
|
1681
|
+
model: this.getModel(),
|
|
1682
|
+
before: item,
|
|
1683
|
+
updatedFields: data as JSONObject,
|
|
1684
|
+
itemId: item.id,
|
|
1685
|
+
props: updateBy.props,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1585
1688
|
}
|
|
1586
1689
|
|
|
1587
1690
|
/*
|
package/Server/Services/Index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import LabelService from "./LabelService";
|
|
|
36
36
|
import KubernetesClusterService from "./KubernetesClusterService";
|
|
37
37
|
import DockerHostService from "./DockerHostService";
|
|
38
38
|
import LlmProviderService from "./LlmProviderService";
|
|
39
|
+
import AuditLogService from "./AuditLogService";
|
|
39
40
|
import LogService from "./LogService";
|
|
40
41
|
import MailService from "./MailService";
|
|
41
42
|
import MetricService from "./MetricService";
|
|
@@ -447,6 +448,7 @@ export const AnalyticsServices: Array<
|
|
|
447
448
|
MonitorLogService,
|
|
448
449
|
ProfileService,
|
|
449
450
|
ProfileSampleService,
|
|
451
|
+
AuditLogService,
|
|
450
452
|
];
|
|
451
453
|
|
|
452
454
|
export default services;
|
|
@@ -22,10 +22,28 @@ import logger from "../Utils/Logger";
|
|
|
22
22
|
|
|
23
23
|
export type { ParsedKubernetesResource };
|
|
24
24
|
|
|
25
|
+
export interface DegradedPod {
|
|
26
|
+
name: string;
|
|
27
|
+
namespace: string;
|
|
28
|
+
phase: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DegradedNode {
|
|
34
|
+
name: string;
|
|
35
|
+
isReady: boolean;
|
|
36
|
+
hasMemoryPressure: boolean;
|
|
37
|
+
hasDiskPressure: boolean;
|
|
38
|
+
hasPidPressure: boolean;
|
|
39
|
+
reason: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export interface InventorySummary {
|
|
26
44
|
countsByKind: Record<string, number>;
|
|
27
45
|
/*
|
|
28
|
-
* Sum of
|
|
46
|
+
* Sum of the denormalized containerCount column across all pods in
|
|
29
47
|
* the cluster. Containers aren't a top-level kind in the inventory,
|
|
30
48
|
* so we derive the total server-side so the sidebar badge and the
|
|
31
49
|
* Containers page agree.
|
|
@@ -47,6 +65,201 @@ export interface InventorySummary {
|
|
|
47
65
|
diskPressure: number;
|
|
48
66
|
pidPressure: number;
|
|
49
67
|
};
|
|
68
|
+
/*
|
|
69
|
+
* Top offenders that explain a Degraded/Unhealthy cluster state. Capped
|
|
70
|
+
* so a pathological cluster can't blow up the overview payload; the
|
|
71
|
+
* dedicated Pods/Nodes pages are the source of truth for the full list.
|
|
72
|
+
*/
|
|
73
|
+
degradedPods: Array<DegradedPod>;
|
|
74
|
+
degradedNodes: Array<DegradedNode>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const DEGRADED_SAMPLE_LIMIT: number = 20;
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* Pull the first meaningful reason/message off a pod's status block.
|
|
81
|
+
* KubernetesInventoryExtractor stores containerStatuses as an array of
|
|
82
|
+
* { name, ready, state: "running"|"waiting"|"terminated", reason, message, ... }.
|
|
83
|
+
* A waiting container with a reason (ImagePullBackOff, CrashLoopBackOff,
|
|
84
|
+
* CreateContainerConfigError, ...) is exactly what the user needs to see,
|
|
85
|
+
* so we surface that first. We fall back to terminated reasons (OOMKilled,
|
|
86
|
+
* Error, ContainerCannotRun) and then to status-level conditions.
|
|
87
|
+
*/
|
|
88
|
+
function buildDegradedPod(row: {
|
|
89
|
+
name: string;
|
|
90
|
+
namespaceKey: string;
|
|
91
|
+
phase: string | null;
|
|
92
|
+
status: unknown;
|
|
93
|
+
}): DegradedPod {
|
|
94
|
+
const status: Record<string, unknown> =
|
|
95
|
+
row.status && typeof row.status === "object"
|
|
96
|
+
? (row.status as Record<string, unknown>)
|
|
97
|
+
: {};
|
|
98
|
+
|
|
99
|
+
let reason: string = "";
|
|
100
|
+
let message: string = "";
|
|
101
|
+
|
|
102
|
+
const containerStatuses: Array<Record<string, unknown>> = Array.isArray(
|
|
103
|
+
status["containerStatuses"],
|
|
104
|
+
)
|
|
105
|
+
? (status["containerStatuses"] as Array<Record<string, unknown>>)
|
|
106
|
+
: [];
|
|
107
|
+
const initContainerStatuses: Array<Record<string, unknown>> = Array.isArray(
|
|
108
|
+
status["initContainerStatuses"],
|
|
109
|
+
)
|
|
110
|
+
? (status["initContainerStatuses"] as Array<Record<string, unknown>>)
|
|
111
|
+
: [];
|
|
112
|
+
|
|
113
|
+
const scanForReason: (
|
|
114
|
+
list: Array<Record<string, unknown>>,
|
|
115
|
+
targetState: string,
|
|
116
|
+
) => { reason: string; message: string } | null = (list, targetState) => {
|
|
117
|
+
for (const cs of list) {
|
|
118
|
+
if (cs["state"] !== targetState) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const r: unknown = cs["reason"];
|
|
122
|
+
if (typeof r === "string" && r) {
|
|
123
|
+
const m: unknown = cs["message"];
|
|
124
|
+
return {
|
|
125
|
+
reason: r,
|
|
126
|
+
message: typeof m === "string" ? m : "",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const waitingHit: { reason: string; message: string } | null =
|
|
134
|
+
scanForReason(containerStatuses, "waiting") ||
|
|
135
|
+
scanForReason(initContainerStatuses, "waiting");
|
|
136
|
+
const terminatedHit: { reason: string; message: string } | null = waitingHit
|
|
137
|
+
? null
|
|
138
|
+
: scanForReason(containerStatuses, "terminated") ||
|
|
139
|
+
scanForReason(initContainerStatuses, "terminated");
|
|
140
|
+
const hit: { reason: string; message: string } | null =
|
|
141
|
+
waitingHit || terminatedHit;
|
|
142
|
+
|
|
143
|
+
if (hit) {
|
|
144
|
+
reason = hit.reason;
|
|
145
|
+
message = hit.message;
|
|
146
|
+
} else {
|
|
147
|
+
// Fall back to the pod-level reason/message fields set by the scheduler
|
|
148
|
+
// (e.g. "Unschedulable" with "0/3 nodes are available: ...").
|
|
149
|
+
const topReason: unknown = status["reason"];
|
|
150
|
+
const topMessage: unknown = status["message"];
|
|
151
|
+
if (typeof topReason === "string") {
|
|
152
|
+
reason = topReason;
|
|
153
|
+
}
|
|
154
|
+
if (typeof topMessage === "string") {
|
|
155
|
+
message = topMessage;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If still nothing, pull from the first non-True condition.
|
|
159
|
+
if (!reason) {
|
|
160
|
+
const conditions: Array<Record<string, unknown>> = Array.isArray(
|
|
161
|
+
status["conditions"],
|
|
162
|
+
)
|
|
163
|
+
? (status["conditions"] as Array<Record<string, unknown>>)
|
|
164
|
+
: [];
|
|
165
|
+
for (const cond of conditions) {
|
|
166
|
+
if (cond["status"] !== "True") {
|
|
167
|
+
const r: unknown = cond["reason"];
|
|
168
|
+
const m: unknown = cond["message"];
|
|
169
|
+
if (typeof r === "string" && r) {
|
|
170
|
+
reason = r;
|
|
171
|
+
message = typeof m === "string" ? m : "";
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
name: row.name,
|
|
181
|
+
namespace: row.namespaceKey || "",
|
|
182
|
+
phase: row.phase || "Unknown",
|
|
183
|
+
reason,
|
|
184
|
+
message,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/*
|
|
189
|
+
* For a Node: if isReady is false, the "Ready" condition carries the real
|
|
190
|
+
* story (e.g. "KubeletNotReady: PLEG is not healthy"). If only pressure
|
|
191
|
+
* flags are tripped, pick the tripped condition's reason/message.
|
|
192
|
+
*/
|
|
193
|
+
function buildDegradedNode(row: {
|
|
194
|
+
name: string;
|
|
195
|
+
isReady: boolean | null;
|
|
196
|
+
hasMemoryPressure: boolean | null;
|
|
197
|
+
hasDiskPressure: boolean | null;
|
|
198
|
+
hasPidPressure: boolean | null;
|
|
199
|
+
status: unknown;
|
|
200
|
+
}): DegradedNode {
|
|
201
|
+
const status: Record<string, unknown> =
|
|
202
|
+
row.status && typeof row.status === "object"
|
|
203
|
+
? (row.status as Record<string, unknown>)
|
|
204
|
+
: {};
|
|
205
|
+
|
|
206
|
+
const conditions: Array<Record<string, unknown>> = Array.isArray(
|
|
207
|
+
status["conditions"],
|
|
208
|
+
)
|
|
209
|
+
? (status["conditions"] as Array<Record<string, unknown>>)
|
|
210
|
+
: [];
|
|
211
|
+
|
|
212
|
+
const findCondition: (
|
|
213
|
+
predicate: (c: Record<string, unknown>) => boolean,
|
|
214
|
+
) => Record<string, unknown> | null = (predicate) => {
|
|
215
|
+
for (const c of conditions) {
|
|
216
|
+
if (predicate(c)) {
|
|
217
|
+
return c;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
let picked: Record<string, unknown> | null = null;
|
|
224
|
+
if (row.isReady === false) {
|
|
225
|
+
picked = findCondition((c: Record<string, unknown>) => {
|
|
226
|
+
return c["type"] === "Ready" && c["status"] !== "True";
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (!picked && row.hasMemoryPressure === true) {
|
|
230
|
+
picked = findCondition((c: Record<string, unknown>) => {
|
|
231
|
+
return c["type"] === "MemoryPressure" && c["status"] === "True";
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (!picked && row.hasDiskPressure === true) {
|
|
235
|
+
picked = findCondition((c: Record<string, unknown>) => {
|
|
236
|
+
return c["type"] === "DiskPressure" && c["status"] === "True";
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (!picked && row.hasPidPressure === true) {
|
|
240
|
+
picked = findCondition((c: Record<string, unknown>) => {
|
|
241
|
+
return c["type"] === "PIDPressure" && c["status"] === "True";
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const reason: string =
|
|
246
|
+
picked && typeof picked["reason"] === "string"
|
|
247
|
+
? (picked["reason"] as string)
|
|
248
|
+
: "";
|
|
249
|
+
const message: string =
|
|
250
|
+
picked && typeof picked["message"] === "string"
|
|
251
|
+
? (picked["message"] as string)
|
|
252
|
+
: "";
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
name: row.name,
|
|
256
|
+
isReady: row.isReady === true,
|
|
257
|
+
hasMemoryPressure: row.hasMemoryPressure === true,
|
|
258
|
+
hasDiskPressure: row.hasDiskPressure === true,
|
|
259
|
+
hasPidPressure: row.hasPidPressure === true,
|
|
260
|
+
reason,
|
|
261
|
+
message,
|
|
262
|
+
};
|
|
50
263
|
}
|
|
51
264
|
|
|
52
265
|
const UPSERT_BATCH_SIZE: number = 500;
|
|
@@ -72,6 +285,7 @@ const UPSERT_COLUMNS: Array<keyof ParsedKubernetesResource | string> = [
|
|
|
72
285
|
"annotations",
|
|
73
286
|
"ownerReferences",
|
|
74
287
|
"spec",
|
|
288
|
+
"containerCount",
|
|
75
289
|
"status",
|
|
76
290
|
"lastSeenAt",
|
|
77
291
|
"resourceCreationTimestamp",
|
|
@@ -133,6 +347,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
133
347
|
r.annotations ? JSON.stringify(r.annotations) : null,
|
|
134
348
|
r.ownerReferences ? JSON.stringify(r.ownerReferences) : null,
|
|
135
349
|
r.spec ? JSON.stringify(r.spec) : null,
|
|
350
|
+
r.containerCount,
|
|
136
351
|
r.status ? JSON.stringify(r.status) : null,
|
|
137
352
|
r.lastSeenAt,
|
|
138
353
|
r.resourceCreationTimestamp,
|
|
@@ -145,7 +360,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
145
360
|
"projectId", "kubernetesClusterId", "kind", "namespaceKey", "name",
|
|
146
361
|
"uid", "phase", "isReady",
|
|
147
362
|
"hasMemoryPressure", "hasDiskPressure", "hasPidPressure",
|
|
148
|
-
"labels", "annotations", "ownerReferences", "spec", "status",
|
|
363
|
+
"labels", "annotations", "ownerReferences", "spec", "containerCount", "status",
|
|
149
364
|
"lastSeenAt", "resourceCreationTimestamp", "version"
|
|
150
365
|
)
|
|
151
366
|
VALUES ${valueFragments.join(", ")}
|
|
@@ -161,6 +376,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
161
376
|
"annotations" = EXCLUDED."annotations",
|
|
162
377
|
"ownerReferences" = EXCLUDED."ownerReferences",
|
|
163
378
|
"spec" = EXCLUDED."spec",
|
|
379
|
+
"containerCount" = EXCLUDED."containerCount",
|
|
164
380
|
"status" = EXCLUDED."status",
|
|
165
381
|
"lastSeenAt" = EXCLUDED."lastSeenAt",
|
|
166
382
|
"resourceCreationTimestamp" = EXCLUDED."resourceCreationTimestamp",
|
|
@@ -218,7 +434,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
218
434
|
const manager: ReturnType<Service["getRepository"]>["manager"] =
|
|
219
435
|
this.getRepository().manager;
|
|
220
436
|
|
|
221
|
-
const [
|
|
437
|
+
const [
|
|
438
|
+
kindRows,
|
|
439
|
+
podRows,
|
|
440
|
+
nodeRows,
|
|
441
|
+
containerRows,
|
|
442
|
+
degradedPodRows,
|
|
443
|
+
degradedNodeRows,
|
|
444
|
+
]: [
|
|
222
445
|
Array<{ kind: string; count: string }>,
|
|
223
446
|
Array<{ phase: string | null; count: string }>,
|
|
224
447
|
Array<{
|
|
@@ -229,6 +452,20 @@ export class Service extends DatabaseService<Model> {
|
|
|
229
452
|
pidPressure: string;
|
|
230
453
|
}>,
|
|
231
454
|
Array<{ total: string }>,
|
|
455
|
+
Array<{
|
|
456
|
+
name: string;
|
|
457
|
+
namespaceKey: string;
|
|
458
|
+
phase: string | null;
|
|
459
|
+
status: unknown;
|
|
460
|
+
}>,
|
|
461
|
+
Array<{
|
|
462
|
+
name: string;
|
|
463
|
+
isReady: boolean | null;
|
|
464
|
+
hasMemoryPressure: boolean | null;
|
|
465
|
+
hasDiskPressure: boolean | null;
|
|
466
|
+
hasPidPressure: boolean | null;
|
|
467
|
+
status: unknown;
|
|
468
|
+
}>,
|
|
232
469
|
] = await Promise.all([
|
|
233
470
|
manager.query(
|
|
234
471
|
`SELECT "kind", COUNT(*)::text AS count
|
|
@@ -256,15 +493,61 @@ export class Service extends DatabaseService<Model> {
|
|
|
256
493
|
[data.projectId.toString(), data.kubernetesClusterId.toString()],
|
|
257
494
|
),
|
|
258
495
|
manager.query(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
496
|
+
/*
|
|
497
|
+
* containerCount is cached on the row during ingest
|
|
498
|
+
* (KubernetesInventoryExtractor sets it from
|
|
499
|
+
* spec.containers.length), so this is a plain int sum instead
|
|
500
|
+
* of a JSONB scan. Rows written before that ingest change may
|
|
501
|
+
* have NULL; SUM treats those as 0, which matches the old
|
|
502
|
+
* behavior.
|
|
503
|
+
*/
|
|
504
|
+
`SELECT COALESCE(SUM("containerCount"), 0)::text AS total
|
|
264
505
|
FROM "KubernetesResource"
|
|
265
506
|
WHERE "projectId" = $1 AND "kubernetesClusterId" = $2 AND "kind" = 'Pod' AND "deletedAt" IS NULL`,
|
|
266
507
|
[data.projectId.toString(), data.kubernetesClusterId.toString()],
|
|
267
508
|
),
|
|
509
|
+
/*
|
|
510
|
+
* Top-N offenders powering the "Why is this cluster degraded?" card.
|
|
511
|
+
* Failed first (hardest outage), then Pending, then Unknown, so the
|
|
512
|
+
* user sees the worst stuff first without having to sort client-side.
|
|
513
|
+
*/
|
|
514
|
+
manager.query(
|
|
515
|
+
`SELECT "name", "namespaceKey", "phase", "status"
|
|
516
|
+
FROM "KubernetesResource"
|
|
517
|
+
WHERE "projectId" = $1
|
|
518
|
+
AND "kubernetesClusterId" = $2
|
|
519
|
+
AND "kind" = 'Pod'
|
|
520
|
+
AND "deletedAt" IS NULL
|
|
521
|
+
AND ("phase" IS NULL OR "phase" NOT IN ('Running', 'Succeeded'))
|
|
522
|
+
ORDER BY
|
|
523
|
+
CASE "phase"
|
|
524
|
+
WHEN 'Failed' THEN 0
|
|
525
|
+
WHEN 'Pending' THEN 1
|
|
526
|
+
ELSE 2
|
|
527
|
+
END,
|
|
528
|
+
"lastSeenAt" DESC
|
|
529
|
+
LIMIT ${DEGRADED_SAMPLE_LIMIT}`,
|
|
530
|
+
[data.projectId.toString(), data.kubernetesClusterId.toString()],
|
|
531
|
+
),
|
|
532
|
+
manager.query(
|
|
533
|
+
`SELECT "name", "isReady", "hasMemoryPressure", "hasDiskPressure", "hasPidPressure", "status"
|
|
534
|
+
FROM "KubernetesResource"
|
|
535
|
+
WHERE "projectId" = $1
|
|
536
|
+
AND "kubernetesClusterId" = $2
|
|
537
|
+
AND "kind" = 'Node'
|
|
538
|
+
AND "deletedAt" IS NULL
|
|
539
|
+
AND (
|
|
540
|
+
"isReady" IS FALSE
|
|
541
|
+
OR "hasMemoryPressure" IS TRUE
|
|
542
|
+
OR "hasDiskPressure" IS TRUE
|
|
543
|
+
OR "hasPidPressure" IS TRUE
|
|
544
|
+
)
|
|
545
|
+
ORDER BY
|
|
546
|
+
CASE WHEN "isReady" IS FALSE THEN 0 ELSE 1 END,
|
|
547
|
+
"lastSeenAt" DESC
|
|
548
|
+
LIMIT ${DEGRADED_SAMPLE_LIMIT}`,
|
|
549
|
+
[data.projectId.toString(), data.kubernetesClusterId.toString()],
|
|
550
|
+
),
|
|
268
551
|
]);
|
|
269
552
|
|
|
270
553
|
const countsByKind: Record<string, number> = {};
|
|
@@ -308,6 +591,13 @@ export class Service extends DatabaseService<Model> {
|
|
|
308
591
|
const containerCount: number =
|
|
309
592
|
parseInt(containerRows[0]?.total || "0", 10) || 0;
|
|
310
593
|
|
|
594
|
+
const degradedPods: Array<DegradedPod> = degradedPodRows.map((row) => {
|
|
595
|
+
return buildDegradedPod(row);
|
|
596
|
+
});
|
|
597
|
+
const degradedNodes: Array<DegradedNode> = degradedNodeRows.map((row) => {
|
|
598
|
+
return buildDegradedNode(row);
|
|
599
|
+
});
|
|
600
|
+
|
|
311
601
|
return {
|
|
312
602
|
countsByKind,
|
|
313
603
|
containerCount,
|
|
@@ -321,6 +611,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
321
611
|
diskPressure: parseInt(nodeRow?.diskPressure || "0", 10) || 0,
|
|
322
612
|
pidPressure: parseInt(nodeRow?.pidPressure || "0", 10) || 0,
|
|
323
613
|
},
|
|
614
|
+
degradedPods,
|
|
615
|
+
degradedNodes,
|
|
324
616
|
};
|
|
325
617
|
}
|
|
326
618
|
|
|
@@ -224,7 +224,7 @@ function createSandboxProxy(
|
|
|
224
224
|
* Recursively unwraps sandbox proxies in a return value so the host code
|
|
225
225
|
* receives original objects (e.g. Buffers that pass `instanceof` checks).
|
|
226
226
|
*/
|
|
227
|
-
function deepUnwrapProxies(
|
|
227
|
+
export function deepUnwrapProxies(
|
|
228
228
|
value: unknown,
|
|
229
229
|
visited?: WeakSet<GenericObject>,
|
|
230
230
|
): unknown {
|
|
@@ -469,33 +469,50 @@ export default class VMRunner {
|
|
|
469
469
|
})()`;
|
|
470
470
|
|
|
471
471
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
reject(new Error("Script execution timed out"));
|
|
484
|
-
}, timeout + 5000);
|
|
485
|
-
// Don't let this timer keep the process alive
|
|
486
|
-
handle.unref();
|
|
487
|
-
},
|
|
488
|
-
);
|
|
472
|
+
let returnVal: unknown;
|
|
473
|
+
let scriptError: Error | undefined;
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
/*
|
|
477
|
+
* vm timeout only covers synchronous CPU time, so wrap with
|
|
478
|
+
* Promise.race to also cover async operations (network, timers, etc.)
|
|
479
|
+
*/
|
|
480
|
+
const vmPromise: Promise<unknown> = vm.runInContext(script, sandbox, {
|
|
481
|
+
timeout: timeout,
|
|
482
|
+
});
|
|
489
483
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
484
|
+
const overallTimeout: Promise<never> = new Promise(
|
|
485
|
+
(
|
|
486
|
+
_resolve: (value: never) => void,
|
|
487
|
+
reject: (reason: Error) => void,
|
|
488
|
+
) => {
|
|
489
|
+
const handle: NodeJS.Timeout = global.setTimeout(() => {
|
|
490
|
+
reject(new Error("Script execution timed out"));
|
|
491
|
+
}, timeout + 5000);
|
|
492
|
+
// Don't let this timer keep the process alive
|
|
493
|
+
handle.unref();
|
|
494
|
+
},
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
returnVal = await Promise.race([vmPromise, overallTimeout]);
|
|
498
|
+
} catch (err: unknown) {
|
|
499
|
+
/*
|
|
500
|
+
* Capture user-thrown errors (including timeouts) so the caller can
|
|
501
|
+
* still access side-channel data collected before the throw — e.g.
|
|
502
|
+
* screenshots assigned to a host-realm object passed via `context`.
|
|
503
|
+
* Rethrowing here would discard those partial results.
|
|
504
|
+
*/
|
|
505
|
+
scriptError =
|
|
506
|
+
err instanceof Error
|
|
507
|
+
? err
|
|
508
|
+
: new Error(typeof err === "string" ? err : String(err));
|
|
509
|
+
}
|
|
494
510
|
|
|
495
511
|
return {
|
|
496
512
|
returnValue: deepUnwrapProxies(returnVal),
|
|
497
513
|
logMessages,
|
|
498
514
|
capturedMetrics,
|
|
515
|
+
scriptError,
|
|
499
516
|
};
|
|
500
517
|
} finally {
|
|
501
518
|
// Clean up any lingering timers to prevent resource leaks
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import EnableAuditLogOn from "../BaseDatabase/EnableAuditLogOn";
|
|
2
|
+
import GenericFunction from "../GenericFunction";
|
|
3
|
+
|
|
4
|
+
export default (
|
|
5
|
+
enableAuditLogOn: EnableAuditLogOn = {
|
|
6
|
+
create: true,
|
|
7
|
+
update: true,
|
|
8
|
+
delete: true,
|
|
9
|
+
},
|
|
10
|
+
) => {
|
|
11
|
+
return (ctr: GenericFunction) => {
|
|
12
|
+
ctr.prototype.enableAuditLogOn = {
|
|
13
|
+
create: enableAuditLogOn.create ?? true,
|
|
14
|
+
update: enableAuditLogOn.update ?? true,
|
|
15
|
+
delete: enableAuditLogOn.delete ?? true,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -4,4 +4,10 @@ export default interface ReturnResult {
|
|
|
4
4
|
returnValue: any;
|
|
5
5
|
logMessages: string[];
|
|
6
6
|
capturedMetrics: CapturedMetric[];
|
|
7
|
+
/**
|
|
8
|
+
* Populated when user-supplied code threw (or timed out). The runner still
|
|
9
|
+
* returns collected side-channel data (logs, metrics, and any host-realm
|
|
10
|
+
* context objects the caller passed in) so partial state survives the throw.
|
|
11
|
+
*/
|
|
12
|
+
scriptError?: Error | undefined;
|
|
7
13
|
}
|