forked from redhat-developer/rhdh
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelper.ts
More file actions
301 lines (275 loc) · 10.2 KB
/
Copy pathhelper.ts
File metadata and controls
301 lines (275 loc) · 10.2 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 { execFile as execFileCb } from "child_process";
import fs from "fs";
import { type Page, type Locator } from "@playwright/test";
import {
BACKSTAGE_DEPLOY_SELECTOR,
type JobNamePattern,
type JobNameRegexPattern,
type JobTypePattern,
type IsOpenShiftValue,
} from "./constants";
function execFileAsync(
cmd: string,
args: string[],
options: { maxBuffer?: number; timeout?: number },
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
execFileCb(cmd, args, options, (error, stdout, stderr) => {
if (error !== null) {
const err = error instanceof Error ? error : new Error(`execFile failed: ${cmd}`);
reject(err);
return;
}
resolve({ stdout, stderr });
});
});
}
export async function downloadAndReadFile(
page: Page,
locator: Locator,
): Promise<string | undefined> {
const [download] = await Promise.all([page.waitForEvent("download"), locator.click()]);
const filePath = await download.path();
if (filePath) {
return fs.readFileSync(filePath, "utf-8");
}
console.error("Download failed or path is not available");
return undefined;
}
/**
* Helper function to skip tests based on JOB_NAME environment variable
* Use this to detect specific job configurations (e.g., "osd-gcp", "helm", "operator", "nightly")
*
* @param jobNamePattern - Pattern to match in JOB_NAME (use JOB_NAME_PATTERNS constants)
* @returns boolean - true if test should be skipped
*
* @example
* import { JOB_NAME_PATTERNS } from "./constants";
* test.skip(() => skipIfJobName(JOB_NAME_PATTERNS.OSD_GCP));
*
* @see https://prow.ci.openshift.org/configured-jobs/redhat-developer/rhdh
*/
export function skipIfJobName(jobNamePattern: JobNamePattern): boolean {
return process.env.JOB_NAME?.includes(jobNamePattern) ?? false;
}
/**
* Helper function to skip tests based on JOB_NAME environment variable using regex patterns
* Use this for flexible pattern matching (e.g., OCP version patterns like "ocp-v4.15-*")
*
* @param jobNameRegexPattern - Regex pattern to match in JOB_NAME (use JOB_NAME_REGEX_PATTERNS constants)
* @returns boolean - true if test should be skipped
*
* @example
* import { JOB_NAME_REGEX_PATTERNS } from "./constants";
* // Skip if running on any OCP version (e.g., ocp-v4.15-*, ocp-v4.16-*)
* test.skip(() => skipIfJobNameRegex(JOB_NAME_REGEX_PATTERNS.OCP_VERSION));
*
* @see https://prow.ci.openshift.org/configured-jobs/redhat-developer/rhdh
*/
export function skipIfJobNameRegex(jobNameRegexPattern: JobNameRegexPattern): boolean {
const jobName = process.env.JOB_NAME;
if (jobName === undefined || jobName === "") {
return false;
}
return jobNameRegexPattern.test(jobName);
}
/**
* Helper function to skip tests based on JOB_TYPE environment variable
* Use this to detect job execution type (e.g., "presubmit", "periodic", "postsubmit")
*
* @param jobTypePattern - Pattern to match in JOB_TYPE (use JOB_TYPE_PATTERNS constants)
* @returns boolean - true if test should be skipped
*
* @example
* import { JOB_TYPE_PATTERNS } from "./constants";
* test.skip(() => skipIfJobType(JOB_TYPE_PATTERNS.PRESUBMIT));
*
* @see https://docs.prow.k8s.io/docs/jobs/#job-environment-variables
*/
export function skipIfJobType(jobTypePattern: JobTypePattern): boolean {
return process.env.JOB_TYPE?.includes(jobTypePattern) ?? false;
}
/**
* Helper function to skip tests based on IS_OPENSHIFT environment variable
* Use this to detect if running on OpenShift vs other platforms (e.g., AKS, EKS, GKE)
*
* Note: IS_OPENSHIFT is a custom project variable (different from OPENSHIFT_CI).
* It is set in the CI scripts for specific jobs (e.g., OSD-GCP is OpenShift but doesn't have "ocp" in its JOB_NAME).
*
* @param isOpenShiftValue - Value to match IS_OPENSHIFT against (use IS_OPENSHIFT_VALUES constants)
* @returns boolean - true if test should be skipped
*
* @example
* import { IS_OPENSHIFT_VALUES } from "./constants";
* // Skip if running on OpenShift
* test.skip(() => skipIfIsOpenShift(IS_OPENSHIFT_VALUES.TRUE));
* // Skip if NOT running on OpenShift
* test.skip(() => skipIfIsOpenShift(IS_OPENSHIFT_VALUES.FALSE));
*/
export function skipIfIsOpenShift(isOpenShiftValue: IsOpenShiftValue): boolean {
return process.env.IS_OPENSHIFT === isOpenShiftValue;
}
/**
* Canonical install method detection. Checks INSTALL_METHOD env var first,
* falls back to JOB_NAME pattern matching.
*/
export function resolveInstallMethod(): "helm" | "operator" {
if (process.env.INSTALL_METHOD === "operator") return "operator";
if (process.env.INSTALL_METHOD === "helm") return "helm";
const job = process.env.JOB_NAME ?? "";
return job.includes("operator") ? "operator" : "helm";
}
/**
* Canonical release name resolution. Returns the RELEASE_NAME env var if set
* and non-empty, otherwise defaults to "rhdh".
*
* Note: the explicit check is used instead of `||` because oxlint's
* strict-boolean-expressions and prefer-nullish-coalescing rules
* (pedantic category) reject `||` on string operands.
*/
export function getReleaseName(): string {
return process.env.RELEASE_NAME !== undefined && process.env.RELEASE_NAME !== ""
? process.env.RELEASE_NAME
: "rhdh";
}
/** Base64-encode a string. */
export function base64Encode(value: string): string {
return Buffer.from(value).toString("base64");
}
/** Base64-decode a string. */
export function base64Decode(value: string): string {
return Buffer.from(value, "base64").toString("utf-8");
}
/**
* Returns whether the current job is an Operator deployment.
*/
export function isOperatorDeployment(): boolean {
return resolveInstallMethod() === "operator";
}
/**
* Returns the deployment-level label selector for the backstage Deployment.
* Works with `oc get deploy -l` or `listNamespacedDeployment` to resolve the
* deployment, then target pods via `oc logs deployment/<name>`.
*
* Generalizes the auth-providers pattern from rhdh-deployment.ts which queries
* deployments (not pods) by `app.kubernetes.io/name` + `app.kubernetes.io/instance`.
*
* @returns The appropriate deployment label selector string
*/
export function getBackstageDeploySelector(): string {
return isOperatorDeployment()
? BACKSTAGE_DEPLOY_SELECTOR.OPERATOR
: BACKSTAGE_DEPLOY_SELECTOR.HELM;
}
// ─── Shell command execution ─────────────────────────────────────────────────
/**
* Run a shell command and return stdout. Throws on non-zero exit.
*/
export async function run(
cmd: string,
args: string[],
options?: { timeout?: number },
): Promise<string> {
const timeout = options?.timeout ?? 300_000;
console.log(` $ ${cmd} ${args.join(" ")}`);
const { stdout, stderr } = await execFileAsync(cmd, args, {
maxBuffer: 10 * 1024 * 1024,
timeout,
});
if (stderr) {
// Helm and oc print warnings to stderr that are not errors
for (const line of stderr.split("\n").filter(Boolean)) {
console.log(` (stderr) ${line}`);
}
}
return stdout.trim();
}
// ─── OpenShift cluster discovery ─────────────────────────────────────────────
/**
* Discover the cluster router base from the OpenShift console route.
* Falls back to K8S_CLUSTER_ROUTER_BASE env var if set.
*/
export async function discoverRouterBase(): Promise<string> {
try {
const output = await run("oc", [
"get",
"route",
"console",
"-n",
"openshift-console",
"-o",
"jsonpath={.spec.host}",
]);
return output.replace(/^console-openshift-console\./u, "");
} catch {
throw new Error("K8S_CLUSTER_ROUTER_BASE not set and could not discover from cluster");
}
}
// ─── Image reference utilities ───────────────────────────────────────────────
/** Parsed image reference with registry, repository, and tag or digest. */
export interface ImageRef {
registry: string;
repository: string;
/** Tag value (e.g. "1.10") or digest (e.g. "sha256:abc123"). */
tag: string;
/** ":" for tag references, "@" for digest references. */
separator: ":" | "@";
}
/** Reconstruct a full image reference from its parsed components. */
export function imageRefToString(ref: ImageRef): string {
return `${ref.registry}/${ref.repository}${ref.separator}${ref.tag}`;
}
/**
* Build an ImageRef from individual registry, repository, and tag/digest values.
* Detects digest references (tag starting with "sha256:") and sets the
* separator accordingly.
*/
export function buildImageRef(registry: string, repository: string, tag: string): ImageRef {
return {
registry,
repository,
tag,
separator: tag.startsWith("sha256:") ? "@" : ":",
};
}
/**
* Decompose a full image reference into registry / repository / tag.
*
* Handles both tag references (quay.io/rhdh/image:1.10) and digest
* references (quay.io/rhdh/image@sha256:abc123).
*/
export function parseCatalogIndexImage(imageRef: string): ImageRef {
// Handle @sha256: digest references (e.g. quay.io/rhdh/image@sha256:abc123)
const atIdx = imageRef.indexOf("@");
if (atIdx !== -1) {
const digest = imageRef.slice(atIdx + 1);
const withoutDigest = imageRef.slice(0, atIdx);
const slashIdx = withoutDigest.indexOf("/");
if (slashIdx === -1) {
throw new Error(`Invalid CATALOG_INDEX_IMAGE (no registry separator '/'): ${imageRef}`);
}
return {
registry: withoutDigest.slice(0, slashIdx),
repository: withoutDigest.slice(slashIdx + 1),
tag: digest,
separator: "@",
};
}
// Handle tag references (e.g. quay.io/rhdh/image:1.10)
const colonIdx = imageRef.lastIndexOf(":");
if (colonIdx === -1) {
throw new Error(`Invalid CATALOG_INDEX_IMAGE (no tag separator ':'): ${imageRef}`);
}
const tag = imageRef.slice(colonIdx + 1);
const withoutTag = imageRef.slice(0, colonIdx);
const slashIdx = withoutTag.indexOf("/");
if (slashIdx === -1) {
throw new Error(`Invalid CATALOG_INDEX_IMAGE (no registry separator '/'): ${imageRef}`);
}
return {
registry: withoutTag.slice(0, slashIdx),
repository: withoutTag.slice(slashIdx + 1),
tag,
separator: ":",
};
}