Skip to content

Commit e4256c4

Browse files
Cragsmannclaude
andcommitted
Merge upstream/main into CONSOLE-5279 branch
Resolve conflict in kubernetes-client.ts by keeping both our createSecret/getSecret methods and upstream's patchSecret method. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents ca19bef + 07a33c7 commit e4256c4

622 files changed

Lines changed: 6933 additions & 21145 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/gen-rtl-test/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,9 @@ it('renders')
719719

720720
### Rule 18: Avoid Snapshot Tests
721721

722-
**DO NOT** use `toMatchSnapshot()`. Snapshot tests are brittle, give false security, and test implementation details.
722+
**DO NOT** use `toMatchSnapshot()`, `toMatchInlineSnapshot()`, or error snapshot matchers. Snapshot tests are brittle, give false security, and test implementation details. Prefer **`toStrictEqual`**, **`toMatchObject`**, or RTL queries on user-visible output.
723+
724+
**Enforcement:** `jest/no-restricted-matchers` from `eslint-plugin-console` **errors** on these matchers for paths matched by `plugin:console/testing-library-tests` (the same `**/*spec*` / `**/__tests__**` globs used for RTL lint).
723725

724726
### Rule 19: Render in Each Test by Default
725727

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
global.ResizeObserver = class {
2+
constructor() {}
3+
disconnect() {}
4+
observe() {}
5+
unobserve() {}
6+
};

frontend/e2e/clients/kubernetes-client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,10 @@ export default class KubernetesClient {
396396
return this.k8sApi.readNamespacedSecret({ name, namespace });
397397
}
398398

399+
async patchSecret(name: string, namespace: string, patch: object[]): Promise<void> {
400+
await this.k8sApi.patchNamespacedSecret({ name, namespace, body: patch });
401+
}
402+
399403
async deleteSecret(name: string, namespace: string): Promise<void> {
400404
try {
401405
await this.k8sApi.deleteNamespacedSecret({ name, namespace });
@@ -578,4 +582,24 @@ export default class KubernetesClient {
578582
const response = await this.k8sApi.listNamespacedPod({ namespace });
579583
return response.items || [];
580584
}
585+
586+
async createPod(pod: k8s.V1Pod): Promise<void> {
587+
if (!pod.metadata?.namespace) {
588+
throw new Error('createPod: pod.metadata.namespace is required');
589+
}
590+
await this.k8sApi.createNamespacedPod({
591+
namespace: pod.metadata.namespace,
592+
body: pod,
593+
});
594+
}
595+
596+
async deletePod(name: string, namespace: string): Promise<void> {
597+
try {
598+
await this.k8sApi.deleteNamespacedPod({ name, namespace });
599+
} catch (err) {
600+
if (!isNotFound(err)) {
601+
throw err;
602+
}
603+
}
604+
}
581605
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { expect } from '@playwright/test';
2+
import yaml from 'js-yaml';
3+
4+
import BasePage from './base-page';
5+
6+
type AlertmanagerConfig = {
7+
global?: Record<string, any>;
8+
receivers?: AlertmanagerReceiver[];
9+
route?: any;
10+
inhibit_rules?: any[];
11+
};
12+
13+
type AlertmanagerReceiver = {
14+
name: string;
15+
[key: string]: any;
16+
};
17+
18+
export class AlertmanagerPage extends BasePage {
19+
private readonly createReceiverButton = this.page.getByTestId('create-receiver');
20+
private readonly receiverNameInput = this.page.getByTestId('receiver-name');
21+
private readonly receiverTypeDropdown = this.page.getByTestId('receiver-type');
22+
private readonly saveChangesButton = this.page.getByTestId('save-changes');
23+
private readonly advancedConfigButton = this.page.getByTestId('advanced-configuration');
24+
25+
async navigateToAlertmanager(): Promise<void> {
26+
await this.goTo('/settings/cluster/alertmanagerconfig');
27+
await this.createReceiverButton.waitFor({ state: 'visible' });
28+
}
29+
30+
async navigateToYAMLPage(): Promise<void> {
31+
await this.goTo('/settings/cluster/alertmanageryaml');
32+
// Wait for editor toolbar to load (indicates editor is ready)
33+
await this.page.getByRole('button', { name: 'Copy code to clipboard' }).waitFor();
34+
}
35+
36+
async navigateToEditReceiver(receiverName: string): Promise<void> {
37+
await this.goTo(`/settings/cluster/alertmanagerconfig/receivers/${receiverName}/edit`);
38+
await this.saveChangesButton.waitFor({ state: 'visible' });
39+
}
40+
41+
async createReceiver(receiverName: string, receiverTypeConfig: string): Promise<void> {
42+
await this.robustClick(this.createReceiverButton);
43+
await this.receiverNameInput.fill(receiverName);
44+
45+
// Open receiver type dropdown and select
46+
await this.robustClick(this.receiverTypeDropdown);
47+
const typeOption = this.page.getByTestId(`receiver-type-${receiverTypeConfig}`);
48+
await this.robustClick(typeOption);
49+
}
50+
51+
async save(): Promise<void> {
52+
await expect(this.saveChangesButton).toBeEnabled();
53+
await this.robustClick(this.saveChangesButton);
54+
// Wait for the save to complete and redirect back to the receiver list
55+
await this.createReceiverButton.waitFor({ state: 'visible', timeout: 30_000 });
56+
}
57+
58+
async showAdvancedConfiguration(): Promise<void> {
59+
const button = this.advancedConfigButton.locator('button');
60+
await this.robustClick(button);
61+
}
62+
63+
async getYAMLContent(): Promise<string> {
64+
// Get content from Monaco editor
65+
const content = await this.page.evaluate(() => {
66+
const monacoEditor = (window as any).monaco?.editor?.getModels()?.[0];
67+
return monacoEditor?.getValue() || '';
68+
});
69+
70+
return content;
71+
}
72+
73+
async setYAMLContent(content: string): Promise<void> {
74+
await this.page.evaluate((text) => {
75+
const monacoEditor = (window as any).monaco?.editor?.getModels()?.[0];
76+
monacoEditor?.setValue(text);
77+
}, content);
78+
}
79+
80+
async validateReceiverInList(receiverName: string): Promise<void> {
81+
// Navigate to list page and wait for the receiver to appear.
82+
// The alertmanager config propagation can take a few seconds after the secret
83+
// is patched, so retry navigation until the receiver row is visible.
84+
await expect(async () => {
85+
await this.navigateToAlertmanager();
86+
await expect(this.page.getByRole('row', { name: new RegExp(receiverName) })).toBeVisible({
87+
timeout: 5_000,
88+
});
89+
}).toPass({ intervals: [2_000, 3_000, 5_000], timeout: 30_000 });
90+
91+
// Check that integration type cell is visible
92+
const integrationTypeCell = this.page.getByTestId(
93+
`data-view-cell-${receiverName}-integration-types`,
94+
);
95+
await expect(integrationTypeCell).toBeVisible();
96+
97+
// Check that routing labels cell is visible
98+
const routingLabelsCell = this.page.getByTestId(
99+
`data-view-cell-${receiverName}-routing-labels`,
100+
);
101+
await expect(routingLabelsCell).toBeVisible();
102+
}
103+
}
104+
105+
export function getGlobalsAndReceiverConfig(
106+
receiverName: string,
107+
configName: string,
108+
yamlContent: string,
109+
): {
110+
globals: any;
111+
receiverConfig: any;
112+
} {
113+
const parsed = yaml.load(yamlContent);
114+
const config: AlertmanagerConfig =
115+
typeof parsed === 'object' && parsed !== null ? (parsed as AlertmanagerConfig) : ({} as AlertmanagerConfig);
116+
const receiver: AlertmanagerReceiver | undefined = config.receivers?.find(
117+
(r) => r.name === receiverName,
118+
);
119+
120+
return {
121+
globals: config.global || {},
122+
receiverConfig: receiver?.[configName]?.[0] || {},
123+
};
124+
}

frontend/e2e/pages/navigation.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Page } from '@playwright/test';
2+
3+
/**
4+
* Helper class for navigating using the primary navigation menu
5+
*/
6+
export class Navigation {
7+
constructor(private page: Page) {}
8+
9+
/**
10+
* Navigate using the primary nav by expanding a nav section and clicking a link
11+
* @param section - The nav section to expand (e.g., "Administration", "Workloads")
12+
* @param link - The link to click within that section (e.g., "CustomResourceDefinitions", "Pods")
13+
*/
14+
async navigateViaNav(section: string, link: string): Promise<void> {
15+
// Navigate to home first to ensure app is loaded
16+
await this.page.goto('/');
17+
const sectionButton = this.page.getByRole('button', { name: section });
18+
await sectionButton.waitFor({ state: 'visible' });
19+
20+
await sectionButton.click();
21+
await this.page.getByRole('link', { name: link }).click();
22+
await this.page.waitForLoadState('domcontentloaded');
23+
}
24+
25+
/**
26+
* Navigate to CustomResourceDefinitions via Administration nav
27+
*/
28+
async navigateToCRDs(): Promise<void> {
29+
await this.navigateViaNav('Administration', 'CustomResourceDefinitions');
30+
}
31+
32+
/**
33+
* Navigate to a specific page via Administration nav
34+
*/
35+
async navigateToAdministration(link: string): Promise<void> {
36+
await this.navigateViaNav('Administration', link);
37+
}
38+
39+
/**
40+
* Navigate to a specific page via Workloads nav
41+
*/
42+
async navigateToWorkloads(link: string): Promise<void> {
43+
await this.navigateViaNav('Workloads', link);
44+
}
45+
46+
/**
47+
* Navigate to a specific page via Compute nav
48+
*/
49+
async navigateToCompute(link: string): Promise<void> {
50+
await this.navigateViaNav('Compute', link);
51+
}
52+
53+
/**
54+
* Navigate to a specific page via Storage nav
55+
*/
56+
async navigateToStorage(link: string): Promise<void> {
57+
await this.navigateViaNav('Storage', link);
58+
}
59+
60+
/**
61+
* Navigate to a specific page via User Management nav
62+
*/
63+
async navigateToUserManagement(link: string): Promise<void> {
64+
await this.navigateViaNav('User Management', link);
65+
}
66+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import KubernetesClient from '../../../../clients/kubernetes-client';
2+
3+
export const DEFAULT_ALERTMANAGER_YAML = `global:
4+
resolve_timeout: 5m
5+
inhibit_rules:
6+
- equal:
7+
- namespace
8+
- alertname
9+
source_match:
10+
severity: critical
11+
target_match_re:
12+
severity: warning|info
13+
- equal:
14+
- namespace
15+
- alertname
16+
source_match:
17+
severity: warning
18+
target_match_re:
19+
severity: info
20+
receivers:
21+
- name: Default
22+
- name: Watchdog
23+
- name: Critical
24+
route:
25+
group_by:
26+
- namespace
27+
group_interval: 5m
28+
group_wait: 30s
29+
receiver: Default
30+
repeat_interval: 12h
31+
routes:
32+
- matchers:
33+
- alertname = Watchdog
34+
receiver: Watchdog
35+
- matchers:
36+
- severity = critical
37+
receiver: Critical`;
38+
39+
export async function resetAlertmanagerConfig(k8sClient: KubernetesClient): Promise<void> {
40+
await k8sClient.patchSecret('alertmanager-main', 'openshift-monitoring', [
41+
{
42+
op: 'replace',
43+
path: '/data/alertmanager.yaml',
44+
value: Buffer.from(DEFAULT_ALERTMANAGER_YAML).toString('base64'),
45+
},
46+
]);
47+
}

0 commit comments

Comments
 (0)