@red-hat-developer-hub/e2e-test-utils 1.1.10
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/LICENSE +201 -0
- package/README.md +6 -0
- package/dist/deployment/keycloak/config/keycloak-values.yaml +96 -0
- package/dist/deployment/keycloak/constants.d.ts +29 -0
- package/dist/deployment/keycloak/constants.d.ts.map +1 -0
- package/dist/deployment/keycloak/constants.js +75 -0
- package/dist/deployment/keycloak/deployment.d.ts +89 -0
- package/dist/deployment/keycloak/deployment.d.ts.map +1 -0
- package/dist/deployment/keycloak/deployment.js +435 -0
- package/dist/deployment/keycloak/index.d.ts +2 -0
- package/dist/deployment/keycloak/index.d.ts.map +1 -0
- package/dist/deployment/keycloak/index.js +1 -0
- package/dist/deployment/keycloak/types.d.ts +59 -0
- package/dist/deployment/keycloak/types.d.ts.map +1 -0
- package/dist/deployment/keycloak/types.js +1 -0
- package/dist/deployment/rhdh/config/auth/guest/app-config.yaml +5 -0
- package/dist/deployment/rhdh/config/auth/keycloak/app-config.yaml +19 -0
- package/dist/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml +3 -0
- package/dist/deployment/rhdh/config/auth/keycloak/secrets.yaml +12 -0
- package/dist/deployment/rhdh/config/common/app-config-rhdh.yaml +6 -0
- package/dist/deployment/rhdh/config/common/dynamic-plugins.yaml +3 -0
- package/dist/deployment/rhdh/config/common/rhdh-secrets.yaml +7 -0
- package/dist/deployment/rhdh/config/helm/value_file.yaml +7 -0
- package/dist/deployment/rhdh/config/operator/subscription.yaml +21 -0
- package/dist/deployment/rhdh/constants.d.ts +19 -0
- package/dist/deployment/rhdh/constants.d.ts.map +1 -0
- package/dist/deployment/rhdh/constants.js +27 -0
- package/dist/deployment/rhdh/deployment.d.ts +40 -0
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -0
- package/dist/deployment/rhdh/deployment.js +303 -0
- package/dist/deployment/rhdh/index.d.ts +2 -0
- package/dist/deployment/rhdh/index.d.ts.map +1 -0
- package/dist/deployment/rhdh/index.js +1 -0
- package/dist/deployment/rhdh/types.d.ts +31 -0
- package/dist/deployment/rhdh/types.d.ts.map +1 -0
- package/dist/deployment/rhdh/types.js +1 -0
- package/dist/eslint/base.config.d.ts +10 -0
- package/dist/eslint/base.config.d.ts.map +1 -0
- package/dist/eslint/base.config.js +213 -0
- package/dist/playwright/base-config.d.ts +14 -0
- package/dist/playwright/base-config.d.ts.map +1 -0
- package/dist/playwright/base-config.js +47 -0
- package/dist/playwright/fixtures/test.d.ts +13 -0
- package/dist/playwright/fixtures/test.d.ts.map +1 -0
- package/dist/playwright/fixtures/test.js +48 -0
- package/dist/playwright/global-setup.d.ts +6 -0
- package/dist/playwright/global-setup.d.ts.map +1 -0
- package/dist/playwright/global-setup.js +67 -0
- package/dist/playwright/helpers/accessibility.d.ts +13 -0
- package/dist/playwright/helpers/accessibility.d.ts.map +1 -0
- package/dist/playwright/helpers/accessibility.js +24 -0
- package/dist/playwright/helpers/api-endpoints.d.ts +11 -0
- package/dist/playwright/helpers/api-endpoints.d.ts.map +1 -0
- package/dist/playwright/helpers/api-endpoints.js +15 -0
- package/dist/playwright/helpers/api-helper.d.ts +77 -0
- package/dist/playwright/helpers/api-helper.d.ts.map +1 -0
- package/dist/playwright/helpers/api-helper.js +285 -0
- package/dist/playwright/helpers/common.d.ts +31 -0
- package/dist/playwright/helpers/common.d.ts.map +1 -0
- package/dist/playwright/helpers/common.js +342 -0
- package/dist/playwright/helpers/index.d.ts +5 -0
- package/dist/playwright/helpers/index.d.ts.map +1 -0
- package/dist/playwright/helpers/index.js +4 -0
- package/dist/playwright/helpers/navbar.d.ts +2 -0
- package/dist/playwright/helpers/navbar.d.ts.map +1 -0
- package/dist/playwright/helpers/navbar.js +1 -0
- package/dist/playwright/helpers/ui-helper.d.ts +106 -0
- package/dist/playwright/helpers/ui-helper.d.ts.map +1 -0
- package/dist/playwright/helpers/ui-helper.js +439 -0
- package/dist/playwright/page-objects/global-obj.d.ts +25 -0
- package/dist/playwright/page-objects/global-obj.d.ts.map +1 -0
- package/dist/playwright/page-objects/global-obj.js +24 -0
- package/dist/playwright/page-objects/page-obj.d.ts +41 -0
- package/dist/playwright/page-objects/page-obj.d.ts.map +1 -0
- package/dist/playwright/page-objects/page-obj.js +40 -0
- package/dist/playwright/pages/catalog-import.d.ts +31 -0
- package/dist/playwright/pages/catalog-import.d.ts.map +1 -0
- package/dist/playwright/pages/catalog-import.js +65 -0
- package/dist/playwright/pages/catalog.d.ts +14 -0
- package/dist/playwright/pages/catalog.d.ts.map +1 -0
- package/dist/playwright/pages/catalog.js +37 -0
- package/dist/playwright/pages/extensions.d.ts +38 -0
- package/dist/playwright/pages/extensions.d.ts.map +1 -0
- package/dist/playwright/pages/extensions.js +110 -0
- package/dist/playwright/pages/home-page.d.ts +10 -0
- package/dist/playwright/pages/home-page.d.ts.map +1 -0
- package/dist/playwright/pages/home-page.js +46 -0
- package/dist/playwright/pages/index.d.ts +6 -0
- package/dist/playwright/pages/index.d.ts.map +1 -0
- package/dist/playwright/pages/index.js +5 -0
- package/dist/playwright/pages/notifications.d.ts +24 -0
- package/dist/playwright/pages/notifications.d.ts.map +1 -0
- package/dist/playwright/pages/notifications.js +112 -0
- package/dist/utils/bash.d.ts +3 -0
- package/dist/utils/bash.d.ts.map +1 -0
- package/dist/utils/bash.js +4 -0
- package/dist/utils/common.d.ts +3 -0
- package/dist/utils/common.d.ts.map +1 -0
- package/dist/utils/common.js +8 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/kubernetes-client.d.ts +97 -0
- package/dist/utils/kubernetes-client.d.ts.map +1 -0
- package/dist/utils/kubernetes-client.js +558 -0
- package/dist/utils/merge-yamls.d.ts +50 -0
- package/dist/utils/merge-yamls.d.ts.map +1 -0
- package/dist/utils/merge-yamls.js +96 -0
- package/dist/utils/plugin-metadata.d.ts +116 -0
- package/dist/utils/plugin-metadata.d.ts.map +1 -0
- package/dist/utils/plugin-metadata.js +330 -0
- package/package.json +108 -0
- package/tsconfig.base.json +16 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { $ } from "./bash.js";
|
|
2
|
+
import * as k8s from "@kubernetes/client-node";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as yaml from "js-yaml";
|
|
6
|
+
$.verbose = true;
|
|
7
|
+
/**
|
|
8
|
+
* Kubernetes client wrapper with proper abstraction
|
|
9
|
+
*/
|
|
10
|
+
class KubernetesClientHelper {
|
|
11
|
+
_kc;
|
|
12
|
+
_k8sApi;
|
|
13
|
+
_appsApi;
|
|
14
|
+
_customObjectsApi;
|
|
15
|
+
constructor() {
|
|
16
|
+
this._kc = new k8s.KubeConfig();
|
|
17
|
+
this._kc.loadFromDefault();
|
|
18
|
+
try {
|
|
19
|
+
this._k8sApi = this._kc.makeApiClient(k8s.CoreV1Api);
|
|
20
|
+
this._appsApi = this._kc.makeApiClient(k8s.AppsV1Api);
|
|
21
|
+
this._customObjectsApi = this._kc.makeApiClient(k8s.CustomObjectsApi);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof Error &&
|
|
25
|
+
error.message.includes("No active cluster")) {
|
|
26
|
+
const currentContext = this._kc.getCurrentContext();
|
|
27
|
+
const contexts = this._kc.getContexts().map((c) => c.name);
|
|
28
|
+
throw new Error(`No active Kubernetes cluster found.\n\n` +
|
|
29
|
+
`The kubeconfig was loaded but no cluster is configured or the current context is invalid.\n\n` +
|
|
30
|
+
`Current context: ${currentContext || "(none)"}\n` +
|
|
31
|
+
`Available contexts: ${contexts.length > 0 ? contexts.join(", ") : "(none)"}\n\n` +
|
|
32
|
+
`To fix this:\n` +
|
|
33
|
+
` 1. Log in to your k8s cluster: oc login or kubectl login\n` +
|
|
34
|
+
` 2. Or set a valid context: kubectl config use-context <context-name>\n` +
|
|
35
|
+
` 3. Verify your connection: oc whoami && oc cluster-info\n\n` +
|
|
36
|
+
`Kubeconfig locations checked:\n` +
|
|
37
|
+
` - KUBECONFIG env: ${process.env.KUBECONFIG || "(not set)"}\n` +
|
|
38
|
+
` - Default: ~/.kube/config`);
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create or update a ConfigMap from a file
|
|
45
|
+
*/
|
|
46
|
+
async createOrUpdateConfigMap(name, namespace, configFilePath, dataKey) {
|
|
47
|
+
try {
|
|
48
|
+
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
49
|
+
const key = dataKey || path.basename(configFilePath);
|
|
50
|
+
const configMap = {
|
|
51
|
+
apiVersion: "v1",
|
|
52
|
+
kind: "ConfigMap",
|
|
53
|
+
metadata: {
|
|
54
|
+
name,
|
|
55
|
+
namespace,
|
|
56
|
+
},
|
|
57
|
+
data: {
|
|
58
|
+
[key]: fileContent,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
// Check if ConfigMap exists first
|
|
62
|
+
try {
|
|
63
|
+
await this._k8sApi.readNamespacedConfigMap({ name, namespace });
|
|
64
|
+
// Exists, so update it
|
|
65
|
+
const response = await this._k8sApi.replaceNamespacedConfigMap({
|
|
66
|
+
name,
|
|
67
|
+
namespace,
|
|
68
|
+
body: configMap,
|
|
69
|
+
});
|
|
70
|
+
console.log(`✓ Updated ConfigMap ${name} in namespace ${namespace}`);
|
|
71
|
+
return response;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Doesn't exist, create it
|
|
75
|
+
const response = await this._k8sApi.createNamespacedConfigMap({
|
|
76
|
+
namespace,
|
|
77
|
+
body: configMap,
|
|
78
|
+
});
|
|
79
|
+
console.log(`✓ Created ConfigMap ${name} in namespace ${namespace}`);
|
|
80
|
+
return response;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(`✗ Failed to create/update ConfigMap ${name}:`, error instanceof Error ? error.message : error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a namespace if it doesn't exist
|
|
90
|
+
*/
|
|
91
|
+
async createNamespaceIfNotExists(namespace) {
|
|
92
|
+
if (!namespace?.trim())
|
|
93
|
+
throw new Error("Namespace is required");
|
|
94
|
+
try {
|
|
95
|
+
const response = await this._k8sApi.readNamespace({ name: namespace });
|
|
96
|
+
console.log(`✓ Namespace ${namespace} already exists`);
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// If read fails (likely 404), try to create
|
|
101
|
+
try {
|
|
102
|
+
const namespaceObj = {
|
|
103
|
+
apiVersion: "v1",
|
|
104
|
+
kind: "Namespace",
|
|
105
|
+
metadata: {
|
|
106
|
+
name: namespace,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const response = await this._k8sApi.createNamespace({
|
|
110
|
+
body: namespaceObj,
|
|
111
|
+
});
|
|
112
|
+
console.log(`✓ Created namespace ${namespace}`);
|
|
113
|
+
return response;
|
|
114
|
+
}
|
|
115
|
+
catch (createError) {
|
|
116
|
+
console.error(`✗ Failed to create namespace ${namespace}:`, createError instanceof Error ? createError.message : createError);
|
|
117
|
+
throw createError;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Apply a Kubernetes manifest from a YAML file
|
|
123
|
+
// */
|
|
124
|
+
// async applyManifest(filePath: string, namespace: string): Promise<void> {
|
|
125
|
+
// try {
|
|
126
|
+
// const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
127
|
+
// const docs = yaml.loadAll(fileContent) as any[];
|
|
128
|
+
// for (const doc of docs) {
|
|
129
|
+
// if (!doc || !doc.kind) continue;
|
|
130
|
+
// doc.metadata = doc.metadata || {};
|
|
131
|
+
// doc.metadata.namespace = namespace;
|
|
132
|
+
// await this.applyResource(doc, namespace);
|
|
133
|
+
// }
|
|
134
|
+
// } catch (error: any) {
|
|
135
|
+
// console.error(`✗ Failed to apply manifest ${filePath}:`, error.message);
|
|
136
|
+
// throw error;
|
|
137
|
+
// }
|
|
138
|
+
// }
|
|
139
|
+
/**
|
|
140
|
+
* Apply a Kubernetes resource dynamically
|
|
141
|
+
*/
|
|
142
|
+
// private async applyResource(resource: any, namespace: string): Promise<void> {
|
|
143
|
+
// const kind = resource.kind;
|
|
144
|
+
// const name = resource.metadata.name;
|
|
145
|
+
// try {
|
|
146
|
+
// switch (kind) {
|
|
147
|
+
// case "Secret":
|
|
148
|
+
// await this.applySecret(resource, namespace);
|
|
149
|
+
// break;
|
|
150
|
+
// case "ConfigMap":
|
|
151
|
+
// await this.applyConfigMap(resource, namespace);
|
|
152
|
+
// break;
|
|
153
|
+
// default:
|
|
154
|
+
// console.warn(`⚠ Skipping unsupported resource type: ${kind}`);
|
|
155
|
+
// }
|
|
156
|
+
// } catch (error: any) {
|
|
157
|
+
// console.error(`✗ Failed to apply ${kind} ${name}:`, error.message);
|
|
158
|
+
// throw error;
|
|
159
|
+
// }
|
|
160
|
+
// }
|
|
161
|
+
/**
|
|
162
|
+
* Create or update a Secret
|
|
163
|
+
*/
|
|
164
|
+
async _applySecret(secret, namespace) {
|
|
165
|
+
const name = secret.metadata.name;
|
|
166
|
+
try {
|
|
167
|
+
await this._k8sApi.replaceNamespacedSecret({
|
|
168
|
+
name,
|
|
169
|
+
namespace,
|
|
170
|
+
body: secret,
|
|
171
|
+
});
|
|
172
|
+
console.log(`✓ Updated Secret ${name} in namespace ${namespace}`);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// If replace fails (likely 404), try to create
|
|
176
|
+
try {
|
|
177
|
+
await this._k8sApi.createNamespacedSecret({
|
|
178
|
+
namespace,
|
|
179
|
+
body: secret,
|
|
180
|
+
});
|
|
181
|
+
console.log(`✓ Created Secret ${name} in namespace ${namespace}`);
|
|
182
|
+
}
|
|
183
|
+
catch (createError) {
|
|
184
|
+
console.error(`✗ Failed to create/update Secret ${name} in namespace ${namespace}:`, createError instanceof Error ? createError.message : createError);
|
|
185
|
+
throw createError;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Create or update a ConfigMap from a plain object
|
|
191
|
+
*/
|
|
192
|
+
async applyConfigMapFromObject(name, data, namespace) {
|
|
193
|
+
// Convert the data object to a YAML string
|
|
194
|
+
const yamlContent = yaml.dump(data);
|
|
195
|
+
// Create proper ConfigMap structure
|
|
196
|
+
const fullConfigMap = {
|
|
197
|
+
apiVersion: "v1",
|
|
198
|
+
kind: "ConfigMap",
|
|
199
|
+
metadata: {
|
|
200
|
+
name,
|
|
201
|
+
namespace,
|
|
202
|
+
},
|
|
203
|
+
data: {
|
|
204
|
+
[name + ".yaml"]: yamlContent,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
try {
|
|
208
|
+
await this._k8sApi.replaceNamespacedConfigMap({
|
|
209
|
+
name,
|
|
210
|
+
namespace,
|
|
211
|
+
body: fullConfigMap,
|
|
212
|
+
});
|
|
213
|
+
console.log(`✓ Updated ConfigMap ${name} in namespace ${namespace}`);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Check for 404 status in different possible error structures
|
|
217
|
+
try {
|
|
218
|
+
await this._k8sApi.createNamespacedConfigMap({
|
|
219
|
+
namespace,
|
|
220
|
+
body: fullConfigMap,
|
|
221
|
+
});
|
|
222
|
+
console.log(`✓ Created ConfigMap ${name} in namespace ${namespace}`);
|
|
223
|
+
}
|
|
224
|
+
catch (createError) {
|
|
225
|
+
console.error(`✗ Failed to create/update ConfigMap ${name} in namespace ${namespace}:`, createError instanceof Error ? createError.message : createError);
|
|
226
|
+
throw createError;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Create or update a Secret from a plain object
|
|
232
|
+
*/
|
|
233
|
+
async applySecretFromObject(name, data, namespace) {
|
|
234
|
+
// Create proper Secret structure
|
|
235
|
+
const fullSecret = {
|
|
236
|
+
apiVersion: "v1",
|
|
237
|
+
kind: "Secret",
|
|
238
|
+
metadata: {
|
|
239
|
+
name,
|
|
240
|
+
namespace,
|
|
241
|
+
},
|
|
242
|
+
stringData: data.stringData,
|
|
243
|
+
};
|
|
244
|
+
try {
|
|
245
|
+
await this._k8sApi.replaceNamespacedSecret({
|
|
246
|
+
name,
|
|
247
|
+
namespace,
|
|
248
|
+
body: fullSecret,
|
|
249
|
+
});
|
|
250
|
+
console.log(`✓ Updated Secret ${name} in namespace ${namespace}`);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// If replace fails (likely 404), try to create
|
|
254
|
+
try {
|
|
255
|
+
await this._k8sApi.createNamespacedSecret({
|
|
256
|
+
namespace,
|
|
257
|
+
body: fullSecret,
|
|
258
|
+
});
|
|
259
|
+
console.log(`✓ Created Secret ${name} in namespace ${namespace}`);
|
|
260
|
+
}
|
|
261
|
+
catch (createError) {
|
|
262
|
+
console.error(`✗ Failed to create/update Secret ${name} in namespace ${namespace}:`, createError instanceof Error ? createError.message : createError);
|
|
263
|
+
throw createError;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Delete a namespace and wait for it to be fully terminated
|
|
269
|
+
*/
|
|
270
|
+
async deleteNamespace(namespace, waitForDeletion = true, timeoutSeconds = 180) {
|
|
271
|
+
try {
|
|
272
|
+
await this._k8sApi.deleteNamespace({ name: namespace });
|
|
273
|
+
console.log(`[K8sHelper] Deleting namespace ${namespace}...`);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
// Ignore if namespace doesn't exist (already deleted), but throw other errors
|
|
277
|
+
if (this._isNotFoundError(error)) {
|
|
278
|
+
console.log(`✓ Namespace ${namespace} already deleted or doesn't exist`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
console.error(`✗ Failed to delete namespace ${namespace}:`, error instanceof Error ? error.message : error);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (waitForDeletion) {
|
|
287
|
+
await this._waitForNamespaceDeletion(namespace, timeoutSeconds);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Wait for a namespace to be fully deleted
|
|
292
|
+
*/
|
|
293
|
+
async _waitForNamespaceDeletion(namespace, timeoutSeconds = 180) {
|
|
294
|
+
const startTime = Date.now();
|
|
295
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
296
|
+
const pollIntervalMs = 3000;
|
|
297
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
298
|
+
try {
|
|
299
|
+
const ns = await this._k8sApi.readNamespace({ name: namespace });
|
|
300
|
+
const phase = ns.status?.phase;
|
|
301
|
+
// Namespace still exists, wait and retry
|
|
302
|
+
if (phase === "Terminating") {
|
|
303
|
+
// Only log occasionally to avoid spam
|
|
304
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
305
|
+
if (elapsed % 10 === 0) {
|
|
306
|
+
console.log(`[K8sHelper] Namespace ${namespace} still terminating (${elapsed}s)...`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
// Check for 404 in various error formats from different k8s client versions
|
|
313
|
+
if (this._isNotFoundError(error)) {
|
|
314
|
+
console.log(`✓ Namespace ${namespace} fully deleted`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`Timeout waiting for namespace ${namespace} to be deleted after ${timeoutSeconds}s`);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Check if an error is a "not found" (404) error.
|
|
324
|
+
* Handles different error formats from various k8s client versions.
|
|
325
|
+
*/
|
|
326
|
+
_isNotFoundError(error) {
|
|
327
|
+
if (!error)
|
|
328
|
+
return false;
|
|
329
|
+
// Check error message for "404" or "not found"
|
|
330
|
+
if (error instanceof Error) {
|
|
331
|
+
const msg = error.message.toLowerCase();
|
|
332
|
+
if (msg.includes("404") || msg.includes("not found")) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Check various object properties for 404 status codes
|
|
337
|
+
const err = error;
|
|
338
|
+
return (err.body?.code === 404 ||
|
|
339
|
+
err.response?.statusCode === 404 ||
|
|
340
|
+
err.statusCode === 404 ||
|
|
341
|
+
err.code === 404);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Check if a StatefulSet is ready (all replicas are available)
|
|
345
|
+
*/
|
|
346
|
+
async isStatefulSetReady(namespace, name) {
|
|
347
|
+
try {
|
|
348
|
+
const statefulSet = await this._appsApi.readNamespacedStatefulSet({
|
|
349
|
+
name,
|
|
350
|
+
namespace,
|
|
351
|
+
});
|
|
352
|
+
const replicas = statefulSet.spec?.replicas ?? 1;
|
|
353
|
+
const readyReplicas = statefulSet.status?.readyReplicas ?? 0;
|
|
354
|
+
return readyReplicas >= replicas;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Wait for a StatefulSet to be ready (all replicas available)
|
|
362
|
+
*/
|
|
363
|
+
async waitForStatefulSetReady(namespace, name, timeoutSeconds = 300, pollIntervalMs = 5000) {
|
|
364
|
+
const startTime = Date.now();
|
|
365
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
366
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
367
|
+
if (await this.isStatefulSetReady(namespace, name)) {
|
|
368
|
+
console.log(`✓ StatefulSet ${name} is ready`);
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
372
|
+
}
|
|
373
|
+
throw new Error(`StatefulSet ${name} in namespace ${namespace} not ready after ${timeoutSeconds}s`);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get the cluster's ingress domain from OpenShift config
|
|
377
|
+
* Equivalent to: oc get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}'
|
|
378
|
+
*/
|
|
379
|
+
async getClusterIngressDomain() {
|
|
380
|
+
try {
|
|
381
|
+
const ingress = await this._customObjectsApi.getClusterCustomObject({
|
|
382
|
+
group: "config.openshift.io",
|
|
383
|
+
version: "v1",
|
|
384
|
+
plural: "ingresses",
|
|
385
|
+
name: "cluster",
|
|
386
|
+
});
|
|
387
|
+
const domain = ingress.spec?.domain;
|
|
388
|
+
if (!domain) {
|
|
389
|
+
throw new Error("Ingress domain not found in cluster config");
|
|
390
|
+
}
|
|
391
|
+
return domain;
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
throw new Error(`Failed to get cluster ingress domain: ${error instanceof Error ? error.message : error}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get the URL/location of an OpenShift Route by name
|
|
399
|
+
*
|
|
400
|
+
* @param namespace - The namespace to search in
|
|
401
|
+
* @param name - The route name
|
|
402
|
+
* @returns The route URL (e.g., https://myapp.apps.cluster.example.com)
|
|
403
|
+
*/
|
|
404
|
+
async getRouteLocation(namespace, name) {
|
|
405
|
+
try {
|
|
406
|
+
const route = await this._customObjectsApi.getNamespacedCustomObject({
|
|
407
|
+
group: "route.openshift.io",
|
|
408
|
+
version: "v1",
|
|
409
|
+
namespace,
|
|
410
|
+
plural: "routes",
|
|
411
|
+
name,
|
|
412
|
+
});
|
|
413
|
+
return this._extractRouteUrl(route, name);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
throw new Error(`Failed to get route ${name} in namespace ${namespace}: ${error instanceof Error ? error.message : error}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Extract the URL from a route object
|
|
421
|
+
*/
|
|
422
|
+
_extractRouteUrl(route, routeName) {
|
|
423
|
+
const routeObj = route;
|
|
424
|
+
// Try to get host from spec first, then from status
|
|
425
|
+
const host = routeObj.spec?.host || routeObj.status?.ingress?.[0]?.host;
|
|
426
|
+
if (!host) {
|
|
427
|
+
throw new Error(`Route ${routeName} does not have a host configured`);
|
|
428
|
+
}
|
|
429
|
+
// Determine protocol based on TLS configuration
|
|
430
|
+
const protocol = routeObj.spec?.tls ? "https" : "http";
|
|
431
|
+
return `${protocol}://${host}`;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Failure states that indicate a pod will not recover without intervention
|
|
435
|
+
*/
|
|
436
|
+
static failureReasons = new Set([
|
|
437
|
+
"CrashLoopBackOff",
|
|
438
|
+
"Error",
|
|
439
|
+
"ImagePullBackOff",
|
|
440
|
+
"ErrImagePull",
|
|
441
|
+
"InvalidImageName",
|
|
442
|
+
"CreateContainerConfigError",
|
|
443
|
+
"CreateContainerError",
|
|
444
|
+
]);
|
|
445
|
+
/**
|
|
446
|
+
* Wait for pods matching a label selector to be ready, with early failure detection.
|
|
447
|
+
* Fails fast when it detects unrecoverable states like CrashLoopBackOff.
|
|
448
|
+
*
|
|
449
|
+
* @param namespace - Namespace to watch
|
|
450
|
+
* @param labelSelector - Label selector (e.g., "app=myapp")
|
|
451
|
+
* @param timeoutSeconds - Maximum time to wait (default: 300)
|
|
452
|
+
* @param pollIntervalMs - How often to check pod status (default: 5000)
|
|
453
|
+
*/
|
|
454
|
+
async waitForPodsWithFailureDetection(namespace, labelSelector, timeoutSeconds = 500, pollIntervalMs = 5000) {
|
|
455
|
+
const startTime = Date.now();
|
|
456
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
457
|
+
console.log(`[K8sHelper] Waiting for pods (${labelSelector}) in ${namespace}...`);
|
|
458
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
459
|
+
let pods;
|
|
460
|
+
try {
|
|
461
|
+
pods = (await this._k8sApi.listNamespacedPod({ namespace, labelSelector })).items;
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
console.log(`[K8sHelper] API error, retrying: ${err}`);
|
|
465
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (pods.length === 0) {
|
|
469
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
for (const pod of pods) {
|
|
473
|
+
const podName = pod.metadata?.name || "unknown";
|
|
474
|
+
const failure = this._checkPodFailure(pod);
|
|
475
|
+
if (failure) {
|
|
476
|
+
console.log(`[K8sHelper] Pod ${podName} failed: ${failure.reason}`);
|
|
477
|
+
try {
|
|
478
|
+
if (failure.container) {
|
|
479
|
+
await $ `oc logs ${podName} -n ${namespace} -c ${failure.container} --tail=100`;
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
await $ `oc logs ${podName} -n ${namespace} --tail=100`;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// Ignore log fetch errors
|
|
487
|
+
}
|
|
488
|
+
throw new Error(`Pod ${podName} failed: ${failure.reason}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Check if all pods are ready
|
|
492
|
+
const allReady = pods.every((pod) => {
|
|
493
|
+
const ready = pod.status?.conditions?.find((c) => c.type === "Ready");
|
|
494
|
+
return ready?.status === "True";
|
|
495
|
+
});
|
|
496
|
+
if (allReady) {
|
|
497
|
+
console.log(`[K8sHelper] All ${pods.length} pod(s) ready in ${namespace}`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// Log pod status every 20 seconds
|
|
501
|
+
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
|
502
|
+
if (elapsedSec > 0 && elapsedSec % 20 === 0) {
|
|
503
|
+
try {
|
|
504
|
+
await $ `oc get pods -n ${namespace} -l ${labelSelector}`;
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Ignore errors
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
511
|
+
}
|
|
512
|
+
// Timeout reached - collect diagnostic info before throwing
|
|
513
|
+
console.log(`\n[K8sHelper] ═══ Pod Diagnostics (timeout reached) ═══`);
|
|
514
|
+
try {
|
|
515
|
+
console.log(`\n[K8sHelper] ─── Pod Status ───`);
|
|
516
|
+
await $ `oc get pods -n ${namespace} -l ${labelSelector} -o wide`;
|
|
517
|
+
console.log(`\n[K8sHelper] ─── Pod Details ───`);
|
|
518
|
+
await $ `oc describe pods -n ${namespace} -l ${labelSelector}`;
|
|
519
|
+
console.log(`\n[K8sHelper] ─── Namespace Events ───`);
|
|
520
|
+
await $ `oc get events -n ${namespace} --sort-by='.lastTimestamp'`;
|
|
521
|
+
console.log(`\n[K8sHelper] ─── Pod Logs ───`);
|
|
522
|
+
await $ `oc logs -n ${namespace} -l ${labelSelector} --all-containers --tail=100 2>&1 || true`;
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// Ignore errors from diagnostic commands
|
|
526
|
+
}
|
|
527
|
+
console.log(`\n[K8sHelper] ═══ End Pod Diagnostics ═══\n`);
|
|
528
|
+
throw new Error(`Timeout waiting for pods (${labelSelector}) after ${timeoutSeconds}s`);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Check if a pod is in a failure state. Returns failure info or null if healthy.
|
|
532
|
+
*/
|
|
533
|
+
_checkPodFailure(pod) {
|
|
534
|
+
// Check init containers first
|
|
535
|
+
for (const cs of pod.status?.initContainerStatuses || []) {
|
|
536
|
+
const reason = cs.state?.waiting?.reason;
|
|
537
|
+
if (reason && KubernetesClientHelper.failureReasons.has(reason)) {
|
|
538
|
+
return { reason: `Init:${reason}`, container: cs.name };
|
|
539
|
+
}
|
|
540
|
+
if (cs.state?.terminated?.exitCode &&
|
|
541
|
+
cs.state.terminated.exitCode !== 0) {
|
|
542
|
+
return {
|
|
543
|
+
reason: `Init:Error (exit ${cs.state.terminated.exitCode})`,
|
|
544
|
+
container: cs.name,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Check main containers
|
|
549
|
+
for (const cs of pod.status?.containerStatuses || []) {
|
|
550
|
+
const reason = cs.state?.waiting?.reason;
|
|
551
|
+
if (reason && KubernetesClientHelper.failureReasons.has(reason)) {
|
|
552
|
+
return { reason, container: cs.name };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
export { KubernetesClientHelper };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Array merge strategy options for YAML merging.
|
|
4
|
+
*/
|
|
5
|
+
export type ArrayMergeStrategy = "replace" | "concat" | {
|
|
6
|
+
byKey: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Options for YAML merging.
|
|
10
|
+
*/
|
|
11
|
+
export interface MergeOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Strategy for merging arrays.
|
|
14
|
+
* - "replace": Replace arrays entirely (default)
|
|
15
|
+
* - "concat": Concatenate arrays
|
|
16
|
+
* - { byKey: "keyName" }: Merge arrays of objects by a specific key
|
|
17
|
+
*/
|
|
18
|
+
arrayMergeStrategy?: ArrayMergeStrategy;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Deeply merges two YAML-compatible objects.
|
|
22
|
+
* Array handling is controlled by the arrayMergeStrategy option.
|
|
23
|
+
*/
|
|
24
|
+
export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>, options?: MergeOptions): Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* Merge multiple YAML files into one object.
|
|
27
|
+
*
|
|
28
|
+
* @param paths List of YAML file paths (base first, overlays last)
|
|
29
|
+
* @param options Optional merge options (e.g., arrayMergeStrategy)
|
|
30
|
+
* @returns Merged YAML object
|
|
31
|
+
*/
|
|
32
|
+
export declare function mergeYamlFiles(paths: string[], options?: MergeOptions): Promise<Record<string, unknown>>;
|
|
33
|
+
/**
|
|
34
|
+
* Merge multiple YAML files if they exist.
|
|
35
|
+
*
|
|
36
|
+
* @param paths List of YAML file paths
|
|
37
|
+
* @param options Optional merge options (e.g., arrayMergeStrategy)
|
|
38
|
+
* @returns Merged YAML object
|
|
39
|
+
*/
|
|
40
|
+
export declare function mergeYamlFilesIfExists(paths: string[], options?: MergeOptions): Promise<Record<string, unknown>>;
|
|
41
|
+
/**
|
|
42
|
+
* Merge multiple YAML files and write the result to an output file.
|
|
43
|
+
*
|
|
44
|
+
* @param inputPaths List of input YAML files
|
|
45
|
+
* @param outputPath Output YAML file path
|
|
46
|
+
* @param dumpOptions Optional dump formatting
|
|
47
|
+
* @param mergeOptions Optional merge options (e.g., arrayMergeStrategy)
|
|
48
|
+
*/
|
|
49
|
+
export declare function mergeYamlFilesToFile(inputPaths: string[], outputPath: string, dumpOptions?: yaml.DumpOptions, mergeOptions?: MergeOptions): Promise<void>;
|
|
50
|
+
//# sourceMappingURL=merge-yamls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-yamls.d.ts","sourceRoot":"","sources":["../../src/utils/merge-yamls.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,SAAS,CAAC;AAG3B;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,QAAQ,GACR;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtB;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AA4CD;;;GAGG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,GAAE,YAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBzB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAUlC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CASlC;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,EAClB,WAAW,GAAE,IAAI,CAAC,WAA+B,EACjD,YAAY,GAAE,YAAiB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAKf"}
|