Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2d289d4
fix(e2e): resolve multiple ocp-operator-nightly test failures
zdrapela Apr 2, 2026
129763f
fix(e2e): improve RBAC test reliability with API-based cleanup and he…
zdrapela Apr 2, 2026
066a2b7
fix(e2e): fix subsequent CI failures from PR feedback
zdrapela Apr 2, 2026
1bf66c7
fix(e2e): healer-driven fixes for adoption-insights and bulk-import
zdrapela Apr 2, 2026
7e849dc
fix(e2e): handle Promise.race losing promise to prevent unhandled rej…
zdrapela Apr 2, 2026
43098a9
fix(e2e): restore original adoption-insights setup with targeted fixes
zdrapela Apr 2, 2026
86f0090
fix(e2e): increase role update success message timeout to 15s
zdrapela Apr 2, 2026
2e5af4e
fix(e2e): fixme RBAC tests with broken CSV policy loading, fix bulk-i…
zdrapela Apr 8, 2026
811b162
fix(e2e): fix RBAC conditional policies not loaded in operator deploy…
zdrapela Apr 8, 2026
7d233b8
revert(e2e): revert all bulk-import changes — fix was ineffective
zdrapela Apr 10, 2026
af10494
fix(e2e): verify '3 Permissions' heading in RBAC test
zdrapela Apr 10, 2026
7cea0b1
fix(e2e): revert Choose button fallback to upstream behavior
zdrapela Apr 10, 2026
1dfb22b
fix(e2e): revert unnecessary RBAC config changes
zdrapela Apr 10, 2026
a885139
fix(e2e): address review — remove dead code and fix API context leak
zdrapela Apr 10, 2026
ce6dab3
fix(e2e): narrow condition-read audit status to by-id/404 only
zdrapela Apr 10, 2026
b537cc8
fix(e2e): remove ineffective hideQuickstart and toBeEnabled checks
zdrapela Apr 13, 2026
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
1 change: 0 additions & 1 deletion .ci/pipelines/cluster/aks/aks-operator-deployment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ initiate_rbac_aks_operator_deployment() {

namespace::configure "${namespace}"
# deploy_test_backstage_customization_provider "${namespace}" # Doesn't work on K8s
config::create_conditional_policies_operator /tmp/conditional-policies.yaml
config::prepare_operator_app_config "${DIR}/resources/config_map/app-config-rhdh-rbac.yaml"
apply_yaml_files "${DIR}" "${namespace}" "${rhdh_base_url}"

Expand Down
1 change: 0 additions & 1 deletion .ci/pipelines/cluster/eks/eks-operator-deployment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ initiate_rbac_eks_operator_deployment() {

namespace::configure "${namespace}"
# deploy_test_backstage_customization_provider "${namespace}" # Doesn't work on K8s
config::create_conditional_policies_operator /tmp/conditional-policies.yaml
config::prepare_operator_app_config "${DIR}/resources/config_map/app-config-rhdh-rbac.yaml"
apply_yaml_files "${DIR}" "${namespace}" "${rhdh_base_url}"

Expand Down
1 change: 0 additions & 1 deletion .ci/pipelines/cluster/gke/gke-operator-deployment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ initiate_rbac_gke_operator_deployment() {

namespace::configure "${namespace}"
# deploy_test_backstage_customization_provider "${namespace}" # Doesn't work on K8s
config::create_conditional_policies_operator /tmp/conditional-policies.yaml
config::prepare_operator_app_config "${DIR}/resources/config_map/app-config-rhdh-rbac.yaml"
apply_yaml_files "${DIR}" "${namespace}" "${rhdh_base_url}"
apply_gke_frontend_config "${namespace}"
Expand Down
2 changes: 0 additions & 2 deletions .ci/pipelines/jobs/ocp-operator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ initiate_operator_deployments() {
log::warn "Skipping orchestrator plugins and workflows deployment on Operator $NAME_SPACE deployment"

namespace::configure "${NAME_SPACE_RBAC}"
config::create_conditional_policies_operator /tmp/conditional-policies.yaml
config::prepare_operator_app_config "${DIR}/resources/config_map/app-config-rhdh-rbac.yaml"
local rbac_rhdh_base_url="https://backstage-${RELEASE_NAME_RBAC}-${NAME_SPACE_RBAC}.${K8S_CLUSTER_ROUTER_BASE}"
apply_yaml_files "${DIR}" "${NAME_SPACE_RBAC}" "${rbac_rhdh_base_url}"
Expand Down Expand Up @@ -66,7 +65,6 @@ initiate_operator_deployments_osd_gcp() {
log::warn "Skipping orchestrator plugins and workflows deployment on OSD-GCP environment"

namespace::configure "${NAME_SPACE_RBAC}"
config::create_conditional_policies_operator /tmp/conditional-policies.yaml
config::prepare_operator_app_config "${DIR}/resources/config_map/app-config-rhdh-rbac.yaml"
local rbac_rhdh_base_url="https://backstage-${RELEASE_NAME_RBAC}-${NAME_SPACE_RBAC}.${K8S_CLUSTER_ROUTER_BASE}"
apply_yaml_files "${DIR}" "${NAME_SPACE_RBAC}" "${rbac_rhdh_base_url}"
Expand Down
3 changes: 1 addition & 2 deletions .ci/pipelines/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ Functions: `namespace::configure`, `namespace::delete`, `namespace::force_delete
Configuration management for ConfigMaps, dynamic plugins, and app configuration.

Functions: `config::create_app_config_map`, `config::select_config_map_file`,
`config::create_dynamic_plugins_config`, `config::create_conditional_policies_operator`,
`config::prepare_operator_app_config`
`config::create_dynamic_plugins_config`, `config::prepare_operator_app_config`

### `testing.sh`

Expand Down
21 changes: 0 additions & 21 deletions .ci/pipelines/lib/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,27 +102,6 @@ EOF
# Operator Configuration
# ==============================================================================

# Create conditional policies file for RBAC operator deployment
# Args:
# $1 - destination_file: Path for the generated policies file
# Returns:
# 0 - Success
config::create_conditional_policies_operator() {
local destination_file=$1

if [[ -z "$destination_file" ]]; then
log::error "Missing required parameter: destination_file"
log::info "Usage: config::create_conditional_policies_operator <destination_file>"
return 1
fi

yq '.upstream.backstage.initContainers[0].command[2]' "${DIR}/value_files/values_showcase-rbac.yaml" \
| head -n -4 \
| tail -n +2 > "$destination_file"
common::sed_inplace 's/\\\$/\$/g' "$destination_file"
return $?
}

# Prepare app configuration for operator deployment with RBAC
# Args:
# $1 - config_file: Path to the app configuration file to modify
Expand Down
12 changes: 3 additions & 9 deletions .ci/pipelines/utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,9 @@ apply_yaml_files() {
common::create_configmap_from_file "dynamic-plugins-config" "$project" \
"dynamic-plugins-config.yaml" "$dir/resources/config_map/dynamic-plugins-config.yaml"

if [[ "$JOB_NAME" == *operator* ]] && [[ "${project}" == *rbac* ]]; then
common::create_configmap_from_files "rbac-policy" "$project" \
"rbac-policy.csv=$dir/resources/config_map/rbac-policy.csv" \
"conditional-policies.yaml=/tmp/conditional-policies.yaml"
else
common::create_configmap_from_files "rbac-policy" "$project" \
"rbac-policy.csv=$dir/resources/config_map/rbac-policy.csv" \
"conditional-policies.yaml=$dir/resources/config_map/conditional-policies.yaml"
fi
common::create_configmap_from_files "rbac-policy" "$project" \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that all 6 call sites of config::create_conditional_policies_operator are removed, the function definition itself is dead code (.ci/pipelines/lib/config.sh:110-124). Its mention in .ci/pipelines/lib/README.md:64 should be cleaned up too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — removed the config::create_conditional_policies_operator function definition from config.sh and cleaned up the reference in README.md. Done in bfd155b.

"rbac-policy.csv=$dir/resources/config_map/rbac-policy.csv" \
"conditional-policies.yaml=$dir/resources/config_map/conditional-policies.yaml"

# configuration for testing global floating action button.
common::create_configmap_from_file "dynamic-global-floating-action-button-config" "$project" \
Expand Down
16 changes: 14 additions & 2 deletions e2e-tests/playwright/e2e/audit-log/auditor-rbac.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from "@playwright/test";
import { test, expect } from "@playwright/test";
import { Common, setupBrowser } from "../../utils/common";
import {
RBAC_API,
Expand Down Expand Up @@ -240,12 +240,24 @@

for (const s of conditionRead) {
test(`condition-read → ${s.name}`, async () => {
await s.call();
const response = await s.call();
let status: "succeeded" | "failed";
if (s.name === "by-id" && response.status() === 404) {

Check warning on line 245 in e2e-tests/playwright/e2e/audit-log/auditor-rbac.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint, ShellCheck and Prettier

Avoid having conditionals in tests

Check warning on line 245 in e2e-tests/playwright/e2e/audit-log/auditor-rbac.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint, ShellCheck and Prettier

Avoid having conditionals in tests
// Condition by-id may return 404 if no conditions exist (e.g.,
// empty conditional-policies.yaml). The audit log still records the
// event but with status "failed" instead of "succeeded".
status = "failed";
} else {
expect(response.ok()).toBe(true);

Check warning on line 251 in e2e-tests/playwright/e2e/audit-log/auditor-rbac.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint, ShellCheck and Prettier

Avoid calling `expect` conditionally
status = "succeeded";
}
await validateRbacLogEvent(
"condition-read",
USER_ENTITY_REF,
{ method: "GET", url: s.url },
s.meta,
undefined,
status,
);
});
}
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/playwright/e2e/audit-log/log-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export class LogUtils {
retryDelay: number = 2000,
): Promise<string> {
const deploySelector = getBackstageDeploySelector();
const tailNumber = 100;
const tailNumber = 500;

// Resolve the deployment by its metadata labels, then fetch logs from it.
// This works for both Helm and Operator since both set app.kubernetes.io/name
Expand Down
19 changes: 15 additions & 4 deletions e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,8 @@ test.describe("Test RBAC", () => {
await saveButton.click();
await uiHelper.verifyText(
"Role role:default/test-role updated successfully",
true,
15000,
);

await page.getByPlaceholder("Filter").waitFor({
Expand Down Expand Up @@ -453,18 +455,23 @@ test.describe("Test RBAC", () => {
await uiHelper.clickButton("Save");
await uiHelper.verifyText(
"Role role:default/test-role1 updated successfully",
true,
15000,
);
await uiHelper.verifyHeading(rbacPo.regexpShortUsersAndGroups(1, 1));

await page
.getByTestId(ROLE_OVERVIEW_COMPONENTS_TEST_ID.updatePolicies)
.click();
// Wait for the permissions section update button to be available
const updatePoliciesButton = page.getByTestId(
ROLE_OVERVIEW_COMPONENTS_TEST_ID.updatePolicies,
);
await expect(updatePoliciesButton).toBeVisible({ timeout: 15000 });
await updatePoliciesButton.click();
await uiHelper.verifyHeading("Edit Role");
await rbacPo.selectPluginsCombobox.click();
await rbacPo.selectOption("scaffolder");

// Close the plugins dropdown to access the permissions table
await page.getByRole("button", { name: "Close" }).click();
await page.keyboard.press("Escape");

// Expand the Scaffolder row to access its permissions
await page
Expand All @@ -483,6 +490,8 @@ test.describe("Test RBAC", () => {
await uiHelper.clickButton("Save");
await uiHelper.verifyText(
"Role role:default/test-role1 updated successfully",
true,
15000,
);
await uiHelper.verifyHeading("2 permissions");

Expand Down Expand Up @@ -873,6 +882,8 @@ test.describe("Test RBAC", () => {
await saveButton.click();
await uiHelper.verifyText(
"Role role:default/test-role updated successfully",
true,
15000,
);

await page.getByPlaceholder("Filter").waitFor({
Expand Down
10 changes: 9 additions & 1 deletion e2e-tests/playwright/support/api/rbac-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default class RhdhRbacApi {
Authorization: string;
};
private myContext: APIRequestContext;
private readonly roleRegex = /^[a-zA-Z]+\/[a-zA-Z_]+$/;
private readonly roleRegex = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/;

private constructor(private readonly token: string) {
this.authHeader = {
Expand Down Expand Up @@ -113,6 +113,14 @@ export default class RhdhRbacApi {
return await this.myContext.get(`roles/conditions/${id}`);
}

public async deleteConditionById(id: number): Promise<APIResponse> {
return await this.myContext.delete(`roles/conditions/${id}`);
}

public async dispose(): Promise<void> {
await this.myContext.dispose();
}

private checkRoleFormat(role: string) {
if (!this.roleRegex.test(role))
throw Error(
Expand Down
101 changes: 82 additions & 19 deletions e2e-tests/playwright/support/page-objects/rbac-po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ROLES_PAGE_COMPONENTS,
} from "./page-obj";
import { type RoleBasedPolicy } from "@backstage-community/plugin-rbac-common";
import RhdhRbacApi from "../api/rbac-api";

type PermissionPolicyType = "anyOf" | "not";

Expand Down Expand Up @@ -281,23 +282,40 @@ export class RbacPo extends PageObject {
await this.verifyPermissionPoliciesHeader(policies.length);
await this.create();

// Check for error alert first
// Wait for either success message or error alert.
// Wrap both waitFor calls so the losing promise cannot reject unhandled.
const successLocator = this.page
.getByText(`Role role:default/${name} created successfully`, {
exact: true,
})
.first();
const errorAlert = this.page
.getByRole("alert")
.filter({ hasText: /error/i });
const errorCount = await errorAlert.count();

if (errorCount > 0) {
const outcome = await Promise.race([
successLocator
.waitFor({ state: "visible", timeout: 30000 })
.then(() => "success" as const)
.catch(() => "success_timeout" as const),
errorAlert
.waitFor({ state: "visible", timeout: 30000 })
.then(() => "error" as const)
.catch(() => "error_timeout" as const),
]);

if (outcome === "error") {
const errorMessage = await errorAlert.textContent();
throw new Error(
`Failed to create role: ${errorMessage}. This may indicate insufficient permissions.`,
`Failed to create role: ${errorMessage}. This may indicate insufficient permissions or a leftover role from a previous test run.`,
);
}

// Wait for success message before proceeding to roles list
await this.uiHelper.verifyText(
`Role role:default/${name} created successfully`,
);
if (outcome !== "success") {
throw new Error(
`Role creation timed out: neither success message nor error alert appeared within 30s.`,
);
}

// Now we should be on the roles list page
await this.page.getByPlaceholder("Filter").waitFor({ state: "visible" });
Expand Down Expand Up @@ -364,6 +382,8 @@ export class RbacPo extends PageObject {
await this.uiHelper.clickButton("Create");
await this.uiHelper.verifyText(
`Role role:default/${name} created successfully`,
true,
15000,
);
} else if (permissionPolicyType === "not") {
// Conditional Scenario 2: Permission policies using Not
Expand Down Expand Up @@ -391,18 +411,61 @@ export class RbacPo extends PageObject {
}

async tryDeleteRole(name: string): Promise<void> {
await this.page.goto("/rbac");
await this.uiHelper.searchInputAriaLabel(name);
const deleteButton = this.page.locator(
ROLES_PAGE_COMPONENTS.deleteRole(name),
);
if ((await deleteButton.count()) > 0) {
await deleteButton.click();
await this.uiHelper.verifyHeading("Delete this role?");
await this.page.fill(DELETE_ROLE_COMPONENTS.roleName, name);
await this.uiHelper.clickButton("Delete");
await this.uiHelper.verifyText(`Role ${name} deleted successfully`);
// Use the RBAC REST API for reliable cleanup — the UI-based approach
// can silently fail if the page hasn't fully loaded or the filter
// doesn't match, leaving a leftover role that blocks recreation.
const rbacApi = await RhdhRbacApi.buildRbacApi(this.page);
try {
// name is fully qualified like "role:default/test-role1"
// The API expects just "default/test-role1"
const apiRoleName = name.replace(/^role:/, "");

// Delete policies associated with the role first
const policiesResponse = await rbacApi.getPoliciesByRole(apiRoleName);
if (policiesResponse.ok()) {
const policies = await policiesResponse.json();
if (policies.length > 0) {
await rbacApi.deletePolicy(apiRoleName, policies);
console.log(
`Deleted ${policies.length} leftover policies for ${name} via API`,
);
}
}

// Delete conditions associated with the role
const conditionsResponse = await rbacApi.getConditionByQuery({
roleEntityRef: name,
});
if (conditionsResponse.ok()) {
const conditions = await conditionsResponse.json();
for (const condition of conditions) {
const delResponse = await rbacApi.deleteConditionById(condition.id);
if (delResponse.ok()) {
console.log(
`Deleted leftover condition ${condition.id} for ${name} via API`,
);
}
}
}

// Delete the role itself
const response = await rbacApi.deleteRole(apiRoleName);
if (response.ok()) {
console.log(`Successfully deleted leftover role ${name} via API`);
} else if (response.status() === 404) {
console.log(`Role ${name} does not exist, no cleanup needed`);
} else {
console.warn(
`Unexpected status ${response.status()} when deleting role ${name} via API`,
);
}
} catch (error) {
console.warn(`API cleanup of role ${name} failed: ${error}`);
} finally {
await rbacApi.dispose();
}
// Navigate to RBAC page for the subsequent test steps
await this.page.goto("/rbac");
}

async deleteRole(name: string, header: string = "All roles (0)") {
Expand Down
Loading
Loading