Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions frontend/e2e/clients/kubernetes-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ export default class KubernetesClient {
}
}

async patchSecret(name: string, namespace: string, patch: object[]): Promise<void> {
await this.k8sApi.patchNamespacedSecret({ name, namespace, body: patch });
}

async deleteSecret(name: string, namespace: string): Promise<void> {
try {
await this.k8sApi.deleteNamespacedSecret({ name, namespace });
Expand Down Expand Up @@ -471,4 +475,24 @@ export default class KubernetesClient {
const response = await this.k8sApi.listNamespacedPod({ namespace });
return response.items || [];
}

async createPod(pod: k8s.V1Pod): Promise<void> {
if (!pod.metadata?.namespace) {
throw new Error('createPod: pod.metadata.namespace is required');
}
await this.k8sApi.createNamespacedPod({
namespace: pod.metadata.namespace,
body: pod,
});
Comment thread
rhamilto marked this conversation as resolved.
}

async deletePod(name: string, namespace: string): Promise<void> {
try {
await this.k8sApi.deleteNamespacedPod({ name, namespace });
} catch (err) {
if (!isNotFound(err)) {
throw err;
}
}
}
}
124 changes: 124 additions & 0 deletions frontend/e2e/pages/alertmanager-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { expect } from '@playwright/test';
import yaml from 'js-yaml';

import BasePage from './base-page';

type AlertmanagerConfig = {
global?: Record<string, any>;
receivers?: AlertmanagerReceiver[];
route?: any;
inhibit_rules?: any[];
};

type AlertmanagerReceiver = {
name: string;
[key: string]: any;
};

export class AlertmanagerPage extends BasePage {
private readonly createReceiverButton = this.page.getByTestId('create-receiver');
private readonly receiverNameInput = this.page.getByTestId('receiver-name');
private readonly receiverTypeDropdown = this.page.getByTestId('receiver-type');
private readonly saveChangesButton = this.page.getByTestId('save-changes');
private readonly advancedConfigButton = this.page.getByTestId('advanced-configuration');

async navigateToAlertmanager(): Promise<void> {
await this.goTo('/settings/cluster/alertmanagerconfig');
await this.createReceiverButton.waitFor({ state: 'visible' });
}

async navigateToYAMLPage(): Promise<void> {
await this.goTo('/settings/cluster/alertmanageryaml');
// Wait for editor toolbar to load (indicates editor is ready)
await this.page.getByRole('button', { name: 'Copy code to clipboard' }).waitFor();
}

async navigateToEditReceiver(receiverName: string): Promise<void> {
await this.goTo(`/settings/cluster/alertmanagerconfig/receivers/${receiverName}/edit`);
await this.saveChangesButton.waitFor({ state: 'visible' });
}

async createReceiver(receiverName: string, receiverTypeConfig: string): Promise<void> {
await this.robustClick(this.createReceiverButton);
await this.receiverNameInput.fill(receiverName);

// Open receiver type dropdown and select
await this.robustClick(this.receiverTypeDropdown);
const typeOption = this.page.getByTestId(`receiver-type-${receiverTypeConfig}`);
await this.robustClick(typeOption);
}

async save(): Promise<void> {
await expect(this.saveChangesButton).toBeEnabled();
await this.robustClick(this.saveChangesButton);
// Wait for the save to complete and redirect back to the receiver list
await this.createReceiverButton.waitFor({ state: 'visible', timeout: 30_000 });
}

async showAdvancedConfiguration(): Promise<void> {
const button = this.advancedConfigButton.locator('button');
await this.robustClick(button);
}

async getYAMLContent(): Promise<string> {
// Get content from Monaco editor
const content = await this.page.evaluate(() => {
const monacoEditor = (window as any).monaco?.editor?.getModels()?.[0];
return monacoEditor?.getValue() || '';
});

return content;
}

async setYAMLContent(content: string): Promise<void> {
await this.page.evaluate((text) => {
const monacoEditor = (window as any).monaco?.editor?.getModels()?.[0];
monacoEditor?.setValue(text);
}, content);
}

async validateReceiverInList(receiverName: string): Promise<void> {
// Navigate to list page and wait for the receiver to appear.
// The alertmanager config propagation can take a few seconds after the secret
// is patched, so retry navigation until the receiver row is visible.
await expect(async () => {
await this.navigateToAlertmanager();
await expect(this.page.getByRole('row', { name: new RegExp(receiverName) })).toBeVisible({
timeout: 5_000,
});
}).toPass({ intervals: [2_000, 3_000, 5_000], timeout: 30_000 });

// Check that integration type cell is visible
const integrationTypeCell = this.page.getByTestId(
`data-view-cell-${receiverName}-integration-types`,
);
await expect(integrationTypeCell).toBeVisible();

// Check that routing labels cell is visible
const routingLabelsCell = this.page.getByTestId(
`data-view-cell-${receiverName}-routing-labels`,
);
await expect(routingLabelsCell).toBeVisible();
}
}

export function getGlobalsAndReceiverConfig(
receiverName: string,
configName: string,
yamlContent: string,
): {
globals: any;
receiverConfig: any;
} {
const parsed = yaml.load(yamlContent);
const config: AlertmanagerConfig =
typeof parsed === 'object' && parsed !== null ? (parsed as AlertmanagerConfig) : ({} as AlertmanagerConfig);
const receiver: AlertmanagerReceiver | undefined = config.receivers?.find(
(r) => r.name === receiverName,
);

return {
globals: config.global || {},
receiverConfig: receiver?.[configName]?.[0] || {},
};
}
66 changes: 66 additions & 0 deletions frontend/e2e/pages/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Page } from '@playwright/test';

/**
* Helper class for navigating using the primary navigation menu
*/
export class Navigation {
constructor(private page: Page) {}

/**
* Navigate using the primary nav by expanding a nav section and clicking a link
* @param section - The nav section to expand (e.g., "Administration", "Workloads")
* @param link - The link to click within that section (e.g., "CustomResourceDefinitions", "Pods")
*/
async navigateViaNav(section: string, link: string): Promise<void> {
// Navigate to home first to ensure app is loaded
await this.page.goto('/');
const sectionButton = this.page.getByRole('button', { name: section });
await sectionButton.waitFor({ state: 'visible' });

await sectionButton.click();
await this.page.getByRole('link', { name: link }).click();
Comment thread
rhamilto marked this conversation as resolved.
await this.page.waitForLoadState('domcontentloaded');
}
Comment thread
rhamilto marked this conversation as resolved.

/**
* Navigate to CustomResourceDefinitions via Administration nav
*/
async navigateToCRDs(): Promise<void> {
await this.navigateViaNav('Administration', 'CustomResourceDefinitions');
}

/**
* Navigate to a specific page via Administration nav
*/
async navigateToAdministration(link: string): Promise<void> {
await this.navigateViaNav('Administration', link);
}

/**
* Navigate to a specific page via Workloads nav
*/
async navigateToWorkloads(link: string): Promise<void> {
await this.navigateViaNav('Workloads', link);
}

/**
* Navigate to a specific page via Compute nav
*/
async navigateToCompute(link: string): Promise<void> {
await this.navigateViaNav('Compute', link);
}

/**
* Navigate to a specific page via Storage nav
*/
async navigateToStorage(link: string): Promise<void> {
await this.navigateViaNav('Storage', link);
}

/**
* Navigate to a specific page via User Management nav
*/
async navigateToUserManagement(link: string): Promise<void> {
await this.navigateViaNav('User Management', link);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import KubernetesClient from '../../../../clients/kubernetes-client';

export const DEFAULT_ALERTMANAGER_YAML = `global:
resolve_timeout: 5m
inhibit_rules:
- equal:
- namespace
- alertname
source_match:
severity: critical
target_match_re:
severity: warning|info
- equal:
- namespace
- alertname
source_match:
severity: warning
target_match_re:
severity: info
receivers:
- name: Default
- name: Watchdog
- name: Critical
route:
group_by:
- namespace
group_interval: 5m
group_wait: 30s
receiver: Default
repeat_interval: 12h
routes:
- matchers:
- alertname = Watchdog
receiver: Watchdog
- matchers:
- severity = critical
receiver: Critical`;

export async function resetAlertmanagerConfig(k8sClient: KubernetesClient): Promise<void> {
await k8sClient.patchSecret('alertmanager-main', 'openshift-monitoring', [
{
op: 'replace',
path: '/data/alertmanager.yaml',
value: Buffer.from(DEFAULT_ALERTMANAGER_YAML).toString('base64'),
},
]);
}
Loading