From a8fb73256074218f4bd491cf0edfb4435f3586e0 Mon Sep 17 00:00:00 2001 From: Subhash Khileri Date: Sun, 21 Dec 2025 22:39:19 +0530 Subject: [PATCH 1/4] keycloak helper --- package.json | 7 +- .../keycloak/config/keycloak-values.yaml | 94 +++ src/deployment/keycloak/constants.ts | 87 +++ src/deployment/keycloak/deployment.ts | 547 ++++++++++++++++++ src/deployment/keycloak/index.ts | 1 + src/deployment/keycloak/types.ts | 64 ++ .../rhdh/config/app-config-rhdh.yaml | 22 + .../rhdh/config/dynamic-plugins.yaml | 3 + src/deployment/rhdh/config/rhdh-secrets.yaml | 6 + src/deployment/rhdh/deployment.ts | 57 +- src/eslint/base.config.ts | 2 - src/playwright/base-config.ts | 14 +- src/playwright/fixtures/test.ts | 4 +- src/playwright/global-setup.ts | 43 +- src/playwright/helpers/common.ts | 5 +- src/utils/kubernetes-client.ts | 75 ++- yarn.lock | 322 ++++++++--- 17 files changed, 1212 insertions(+), 141 deletions(-) create mode 100644 src/deployment/keycloak/config/keycloak-values.yaml create mode 100644 src/deployment/keycloak/constants.ts create mode 100644 src/deployment/keycloak/deployment.ts create mode 100644 src/deployment/keycloak/index.ts create mode 100644 src/deployment/keycloak/types.ts diff --git a/package.json b/package.json index d00bfab..e96f64e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,10 @@ "./pages": { "types": "./dist/playwright/pages/index.d.ts", "default": "./dist/playwright/pages/index.js" + }, + "./keycloak": { + "types": "./dist/deployment/keycloak/index.d.ts", + "default": "./dist/deployment/keycloak/index.js" } }, "files": [ @@ -42,7 +46,7 @@ "tsconfig.base.json" ], "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json && cp -r src/deployment/rhdh/config src/deployment/rhdh/helm src/deployment/rhdh/operator dist/deployment/rhdh/", + "build": "yarn clean && tsc -p tsconfig.build.json && cp -r src/deployment/rhdh/config src/deployment/rhdh/helm src/deployment/rhdh/operator dist/deployment/rhdh/ && cp -r src/deployment/keycloak/config dist/deployment/keycloak/", "check": "yarn typecheck && yarn lint:check && yarn prettier:check", "clean": "rm -rf dist", "lint:check": "eslint . --ignore-pattern dist --ignore-pattern README.md", @@ -76,6 +80,7 @@ "dependencies": { "@axe-core/playwright": "^4.11.0", "@eslint/js": "^9.39.1", + "@keycloak/keycloak-admin-client": "^26.0.0", "@kubernetes/client-node": "^1.4.0", "boxen": "^8.0.1", "eslint": "^9.39.1", diff --git a/src/deployment/keycloak/config/keycloak-values.yaml b/src/deployment/keycloak/config/keycloak-values.yaml new file mode 100644 index 0000000..4921cc1 --- /dev/null +++ b/src/deployment/keycloak/config/keycloak-values.yaml @@ -0,0 +1,94 @@ +global: + security: + allowInsecureImages: true + +replicaCount: 1 + +# Use Bitnami legacy repository (Bitnami images moved to bitnamilegacy as of Aug 2025) +# Note: Legacy images are not updated/maintained. Consider migrating to official Keycloak image for long-term. +image: + registry: docker.io + repository: bitnamilegacy/keycloak + tag: "26.3.3-debian-12-r0" + pullPolicy: IfNotPresent + +auth: + adminUser: admin + adminPassword: admin123 + +service: + type: ClusterIP + port: 8080 + +# OpenShift Route configuration +route: + enabled: true + host: "" # Will be auto-generated by OpenShift + tls: + enabled: false + +ingress: + enabled: false + +postgresql: + enabled: true + image: + registry: docker.io + repository: bitnamilegacy/postgresql + tag: "17.6.0-debian-12-r4" + pullPolicy: IfNotPresent + auth: + postgresPassword: postgres123 + username: keycloak + password: keycloak123 + database: keycloak + primary: + resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 100m + memory: 256Mi + persistence: + enabled: true + size: 1Gi + +resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 100m + memory: 256Mi + +extraEnvVars: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin123 + - name: KC_HTTP_ENABLED + value: "true" + - name: KC_PROXY_HEADERS + value: "xforwarded" + - name: KC_HOSTNAME_STRICT + value: "false" + - name: JAVA_OPTS_APPEND + value: "-Djava.net.preferIPv4Stack=true -Xms256m -Xmx512m" + +# Increase probe timeouts for slower startup on resource-constrained clusters +livenessProbe: + enabled: true + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +readinessProbe: + enabled: true + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 diff --git a/src/deployment/keycloak/constants.ts b/src/deployment/keycloak/constants.ts new file mode 100644 index 0000000..b83918b --- /dev/null +++ b/src/deployment/keycloak/constants.ts @@ -0,0 +1,87 @@ +import path from "path"; +import type { KeycloakClientConfig } from "./types.js"; + +// Navigate from dist/deployment/keycloak/ to package root +const PACKAGE_ROOT = path.resolve(import.meta.dirname, "../../.."); + +export const DEFAULT_KEYCLOAK_CONFIG = { + namespace: "rhdh-keycloak", + releaseName: "keycloak", + adminUser: "admin", + adminPassword: "admin123", + realm: "rhdh", +}; + +export const DEFAULT_CONFIG_PATHS = { + valuesFile: path.join( + PACKAGE_ROOT, + "dist/deployment/keycloak/config/keycloak-values.yaml", + ), +}; + +export const BITNAMI_CHART_REPO = "https://charts.bitnami.com/bitnami"; +export const BITNAMI_CHART_NAME = "bitnami/keycloak"; + +export const DEFAULT_RHDH_CLIENT: KeycloakClientConfig = { + clientId: "rhdh-client", + clientSecret: "rhdh-client-secret", + name: "RHDH Client", + redirectUris: ["*"], + webOrigins: ["*"], + standardFlowEnabled: true, + implicitFlowEnabled: true, + directAccessGrantsEnabled: true, + serviceAccountsEnabled: true, + authorizationServicesEnabled: true, + publicClient: false, + defaultClientScopes: [ + "service_account", + "web-origins", + "roles", + "profile", + "basic", + "email", + ], + optionalClientScopes: [ + "address", + "phone", + "offline_access", + "microprofile-jwt", + ], +}; + +export const DEFAULT_GROUPS = [ + { name: "developers" }, + { name: "admins" }, + { name: "viewers" }, +]; + +export const DEFAULT_USERS = [ + { + username: "test1", + email: "test1@example.com", + firstName: "Test", + lastName: "User1", + enabled: true, + emailVerified: true, + password: "test1@123", + groups: ["developers"], + }, + { + username: "test2", + email: "test2@example.com", + firstName: "Test", + lastName: "User2", + enabled: true, + emailVerified: true, + password: "test2@123", + groups: ["developers"], + }, +]; + +// Service account roles required for RHDH integration +export const SERVICE_ACCOUNT_ROLES = [ + "view-authorization", + "manage-authorization", + "view-users", +]; diff --git a/src/deployment/keycloak/deployment.ts b/src/deployment/keycloak/deployment.ts new file mode 100644 index 0000000..640f3ae --- /dev/null +++ b/src/deployment/keycloak/deployment.ts @@ -0,0 +1,547 @@ +import KeycloakAdminClient from "@keycloak/keycloak-admin-client"; +import { KubernetesClientHelper } from "../../utils/kubernetes-client.js"; +import { $ } from "../../utils/bash.js"; +import { + DEFAULT_KEYCLOAK_CONFIG, + BITNAMI_CHART_REPO, + BITNAMI_CHART_NAME, + DEFAULT_CONFIG_PATHS, + DEFAULT_RHDH_CLIENT, + SERVICE_ACCOUNT_ROLES, + DEFAULT_USERS, + DEFAULT_GROUPS, +} from "./constants.js"; +import type { + KeycloakDeploymentOptions, + KeycloakDeploymentConfig, + KeycloakClientConfig, + KeycloakUserConfig, + KeycloakGroupConfig, + KeycloakRealmConfig, + KeycloakConnectionConfig, +} from "./types.js"; + +export class KeycloakHelper { + public k8sClient = new KubernetesClientHelper(); + public deploymentConfig: KeycloakDeploymentConfig; + public keycloakUrl: string = ""; + public realm: string = ""; + public clientId: string = ""; + public clientSecret: string = ""; + private _adminClient: KeycloakAdminClient | null = null; + + constructor(options: KeycloakDeploymentOptions = {}) { + this.deploymentConfig = this._buildDeploymentConfig(options); + } + + /** + * Deploy Keycloak using Helm and configure it for RHDH. + */ + async deploy(): Promise { + this._log("Starting Keycloak deployment..."); + + await this.k8sClient.createNamespaceIfNotExists( + this.deploymentConfig.namespace, + ); + + await this._deployWithHelm(); + await this._createRoute(); + await this._waitForKeycloak(); + await this._initializeAdminClient(); + } + + /** + * Check if Keycloak is already running + */ + async isRunning(): Promise { + try { + this.keycloakUrl = await this.getRouteLocation(); + const response = await fetch(`${this.keycloakUrl}/realms/master`); + return response.ok; + } catch { + return false; + } + } + + /** + * Configure Keycloak with realm, client, groups, and users for RHDH + */ + async configureForRHDH(options?: { + realm?: string; + client?: Partial; + groups?: KeycloakGroupConfig[]; + users?: KeycloakUserConfig[]; + }): Promise { + this._log("Configuring Keycloak for RHDH..."); + + await this._ensureAdminClient(); + + const realmName = options?.realm ?? DEFAULT_KEYCLOAK_CONFIG.realm; + + // Create realm + await this.createRealm({ realm: realmName, enabled: true }); + + // Create client + const clientConfig = { + ...DEFAULT_RHDH_CLIENT, + ...options?.client, + }; + await this.createClient(realmName, clientConfig); + + // Store realm and client info for external access + this.realm = realmName; + this.clientId = clientConfig.clientId; + this.clientSecret = clientConfig.clientSecret; + + // Assign service account roles + await this._assignServiceAccountRoles(realmName, clientConfig.clientId); + + // Create groups + const groups = options?.groups ?? DEFAULT_GROUPS; + for (const group of groups) { + await this.createGroup(realmName, group); + } + + // Create users + const users = options?.users ?? DEFAULT_USERS; + for (const user of users) { + await this.createUser(realmName, user); + } + } + + /** + * Connect to an existing Keycloak instance + */ + async connect(config: KeycloakConnectionConfig): Promise { + this.keycloakUrl = config.baseUrl; + this._adminClient = new KeycloakAdminClient({ + baseUrl: config.baseUrl, + realmName: config.realm ?? "master", + }); + + if (config.username && config.password) { + await this._adminClient.auth({ + username: config.username, + password: config.password, + grantType: "password", + clientId: config.clientId ?? "admin-cli", + }); + } else if (config.clientId && config.clientSecret) { + await this._adminClient.auth({ + grantType: "client_credentials", + clientId: config.clientId, + clientSecret: config.clientSecret, + }); + } + } + + /** + * Create a new realm + */ + async createRealm(config: KeycloakRealmConfig): Promise { + await this._ensureAdminClient(); + + try { + await this._adminClient!.realms.create({ + realm: config.realm, + displayName: config.displayName ?? config.realm, + enabled: config.enabled ?? true, + }); + this._log(`Created realm: ${config.realm}`); + } catch (error) { + if (this._isConflictError(error)) { + this._log(`Realm ${config.realm} already exists`); + } else { + throw error; + } + } + } + + /** + * Create a new client in a realm + */ + async createClient( + realm: string, + config: KeycloakClientConfig, + ): Promise { + await this._ensureAdminClient(); + + try { + this._adminClient!.setConfig({ realmName: realm }); + + await this._adminClient!.clients.create({ + clientId: config.clientId, + secret: config.clientSecret, + name: config.name ?? config.clientId, + description: config.description ?? "", + redirectUris: config.redirectUris ?? ["*"], + webOrigins: config.webOrigins ?? ["*"], + standardFlowEnabled: config.standardFlowEnabled ?? true, + implicitFlowEnabled: config.implicitFlowEnabled ?? true, + directAccessGrantsEnabled: config.directAccessGrantsEnabled ?? true, + serviceAccountsEnabled: config.serviceAccountsEnabled ?? true, + authorizationServicesEnabled: + config.authorizationServicesEnabled ?? true, + publicClient: config.publicClient ?? false, + enabled: true, + protocol: "openid-connect", + fullScopeAllowed: true, + attributes: config.attributes, + defaultClientScopes: config.defaultClientScopes, + optionalClientScopes: config.optionalClientScopes, + }); + this._log(`Created client: ${config.clientId}`); + } catch (error) { + if (this._isConflictError(error)) { + this._log(`Client ${config.clientId} already exists`); + } else { + throw error; + } + } + } + + /** + * Create a group in a realm + */ + async createGroup(realm: string, config: KeycloakGroupConfig): Promise { + await this._ensureAdminClient(); + + try { + this._adminClient!.setConfig({ realmName: realm }); + await this._adminClient!.groups.create({ + name: config.name, + }); + this._log(`Created group: ${config.name}`); + } catch (error) { + if (this._isConflictError(error)) { + this._log(`Group ${config.name} already exists`); + } else { + throw error; + } + } + } + + /** + * Create a user in a realm with optional group membership + */ + async createUser(realm: string, config: KeycloakUserConfig): Promise { + await this._ensureAdminClient(); + + try { + this._adminClient!.setConfig({ realmName: realm }); + + // Create user + const createResponse = await this._adminClient!.users.create({ + username: config.username, + email: config.email, + firstName: config.firstName, + lastName: config.lastName, + enabled: config.enabled ?? true, + emailVerified: config.emailVerified ?? true, + }); + this._log(`Created user: ${config.username}`); + + const userId = createResponse.id; + + // Set password if provided + if (config.password) { + await this._adminClient!.users.resetPassword({ + id: userId, + credential: { + type: "password", + value: config.password, + temporary: config.temporary ?? false, + }, + }); + } + + // Add to groups if specified + if (config.groups?.length) { + for (const groupName of config.groups) { + await this._addUserToGroup(realm, userId, groupName); + } + } + } catch (error) { + if (this._isConflictError(error)) { + this._log(`User ${config.username} already exists`); + } else { + throw error; + } + } + } + + /** + * Get all users in a realm + */ + async getUsers(realm: string): Promise { + await this._ensureAdminClient(); + this._adminClient!.setConfig({ realmName: realm }); + + const users = await this._adminClient!.users.find(); + return users.map((u) => ({ + username: u.username!, + email: u.email, + firstName: u.firstName, + lastName: u.lastName, + enabled: u.enabled, + emailVerified: u.emailVerified, + })); + } + + /** + * Get all groups in a realm + */ + async getGroups(realm: string): Promise { + await this._ensureAdminClient(); + this._adminClient!.setConfig({ realmName: realm }); + + const groups = await this._adminClient!.groups.find(); + return groups.map((g) => ({ name: g.name! })); + } + + /** + * Delete a user from a realm + */ + async deleteUser(realm: string, username: string): Promise { + await this._ensureAdminClient(); + this._adminClient!.setConfig({ realmName: realm }); + + const users = await this._adminClient!.users.find({ username }); + if (users.length > 0) { + await this._adminClient!.users.del({ id: users[0].id! }); + this._log(`Deleted user: ${username}`); + } + } + + /** + * Delete a group from a realm + */ + async deleteGroup(realm: string, groupName: string): Promise { + await this._ensureAdminClient(); + this._adminClient!.setConfig({ realmName: realm }); + + const groups = await this._adminClient!.groups.find({ search: groupName }); + const group = groups.find((g) => g.name === groupName); + if (group) { + await this._adminClient!.groups.del({ id: group.id! }); + this._log(`Deleted group: ${groupName}`); + } + } + + /** + * Delete a realm + */ + async deleteRealm(realm: string): Promise { + await this._ensureAdminClient(); + + try { + await this._adminClient!.realms.del({ realm }); + this._log(`Deleted realm: ${realm}`); + } catch (error) { + this._log(`Failed to delete realm ${realm}: ${error}`); + } + } + + /** + * Teardown Keycloak deployment + */ + async teardown(): Promise { + await this.k8sClient.deleteNamespace(this.deploymentConfig.namespace); + this._log(`Keycloak deployment torn down`); + } + + /** + * Wait for Keycloak to be ready + */ + async waitUntilReady(timeout: number = 300): Promise { + this._log(`Waiting for Keycloak to be ready...`); + await this.k8sClient.waitForStatefulSetReady( + this.deploymentConfig.namespace, + this.deploymentConfig.releaseName, + timeout, + ); + } + + // Private methods + + private _buildDeploymentConfig( + options: KeycloakDeploymentOptions, + ): KeycloakDeploymentConfig { + return { + namespace: options.namespace ?? DEFAULT_KEYCLOAK_CONFIG.namespace, + releaseName: options.releaseName ?? DEFAULT_KEYCLOAK_CONFIG.releaseName, + valuesFile: options.valuesFile ?? DEFAULT_CONFIG_PATHS.valuesFile, + adminUser: options.adminUser ?? DEFAULT_KEYCLOAK_CONFIG.adminUser, + adminPassword: + options.adminPassword ?? DEFAULT_KEYCLOAK_CONFIG.adminPassword, + }; + } + + private async _deployWithHelm(): Promise { + await $`helm repo add bitnami ${BITNAMI_CHART_REPO} || true`; + await $`helm repo update > /dev/null 2>&1`; + + await $`helm upgrade --install ${this.deploymentConfig.releaseName} ${BITNAMI_CHART_NAME} \ + --namespace ${this.deploymentConfig.namespace} \ + --values ${this.deploymentConfig.valuesFile} > /dev/null 2>&1`; + + await this.waitUntilReady(); + } + + private async _createRoute(): Promise { + // Use TLS edge termination with Allow policy to support both HTTP and HTTPS + const routeManifest = ` +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: ${this.deploymentConfig.releaseName} + namespace: ${this.deploymentConfig.namespace} + labels: + app.kubernetes.io/name: keycloak + app.kubernetes.io/instance: ${this.deploymentConfig.releaseName} +spec: + to: + kind: Service + name: ${this.deploymentConfig.releaseName} + weight: 100 + port: + targetPort: http + tls: + termination: edge + insecureEdgeTerminationPolicy: Allow + wildcardPolicy: None +`; + + await $`echo ${routeManifest} | kubectl apply -f -`; + } + + async getRouteLocation(): Promise { + return await this.k8sClient.getRouteLocation( + this.deploymentConfig.namespace, + this.deploymentConfig.releaseName, + ); + } + + private async _waitForKeycloak(): Promise { + this._log("Waiting for Keycloak API to be ready..."); + + const timeout = 300; + const startTime = Date.now(); + + while (true) { + if (await this.isRunning()) { + break; + } + + if ((Date.now() - startTime) / 1000 >= timeout) { + throw new Error("Keycloak API not ready after 5 minutes"); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + this._log(" Waiting for Keycloak API to be ready..."); + } + } + + private async _initializeAdminClient(): Promise { + this._adminClient = new KeycloakAdminClient({ + baseUrl: this.keycloakUrl, + realmName: "master", + }); + + await this._adminClient.auth({ + username: this.deploymentConfig.adminUser, + password: this.deploymentConfig.adminPassword, + grantType: "password", + clientId: "admin-cli", + }); + } + + private async _ensureAdminClient(): Promise { + if (!this._adminClient) { + throw new Error( + "Admin client not initialized. Call deploy() or connect() first.", + ); + } + } + + private async _assignServiceAccountRoles( + realm: string, + clientId: string, + ): Promise { + await this._ensureAdminClient(); + this._adminClient!.setConfig({ realmName: realm }); + + // Get service account user + const clients = await this._adminClient!.clients.find({ clientId }); + if (clients.length === 0) { + throw new Error(`Client ${clientId} not found`); + } + const client = clients[0]; + + const serviceAccountUser = + await this._adminClient!.clients.getServiceAccountUser({ + id: client.id!, + }); + + // Get realm-management client + const realmMgmtClients = await this._adminClient!.clients.find({ + clientId: "realm-management", + }); + if (realmMgmtClients.length === 0) { + throw new Error("realm-management client not found"); + } + const realmMgmtClient = realmMgmtClients[0]; + + // Get roles + const allRoles = await this._adminClient!.clients.listRoles({ + id: realmMgmtClient.id!, + }); + const rolesToAssign = allRoles.filter((r) => + SERVICE_ACCOUNT_ROLES.includes(r.name!), + ); + + if (rolesToAssign.length > 0) { + await this._adminClient!.users.addClientRoleMappings({ + id: serviceAccountUser.id!, + clientUniqueId: realmMgmtClient.id!, + roles: rolesToAssign.map((r) => ({ + id: r.id!, + name: r.name!, + })), + }); + this._log( + `Assigned service account roles: ${rolesToAssign.map((r) => r.name).join(", ")}`, + ); + } + } + + private async _addUserToGroup( + realm: string, + userId: string, + groupName: string, + ): Promise { + this._adminClient!.setConfig({ realmName: realm }); + + const groups = await this._adminClient!.groups.find({ search: groupName }); + const group = groups.find((g) => g.name === groupName); + + if (group) { + await this._adminClient!.users.addToGroup({ + id: userId, + groupId: group.id!, + }); + this._log(` Added user to group: ${groupName}`); + } else { + this._log(` Warning: Group ${groupName} not found`); + } + } + + private _isConflictError(error: unknown): boolean { + const err = error as { response?: { status?: number }; status?: number }; + return err.response?.status === 409 || err.status === 409; + } + + private _log(...args: unknown[]): void { + console.log("[Keycloak]", ...args); + } +} diff --git a/src/deployment/keycloak/index.ts b/src/deployment/keycloak/index.ts new file mode 100644 index 0000000..93ede1f --- /dev/null +++ b/src/deployment/keycloak/index.ts @@ -0,0 +1 @@ +export { KeycloakHelper } from "./deployment.js"; diff --git a/src/deployment/keycloak/types.ts b/src/deployment/keycloak/types.ts new file mode 100644 index 0000000..2ea5f3d --- /dev/null +++ b/src/deployment/keycloak/types.ts @@ -0,0 +1,64 @@ +export type KeycloakDeploymentOptions = { + namespace?: string; + releaseName?: string; + valuesFile?: string; + adminUser?: string; + adminPassword?: string; +}; + +export type KeycloakDeploymentConfig = { + namespace: string; + releaseName: string; + valuesFile: string; + adminUser: string; + adminPassword: string; +}; + +export type KeycloakClientConfig = { + clientId: string; + clientSecret: string; + name?: string; + description?: string; + redirectUris?: string[]; + webOrigins?: string[]; + standardFlowEnabled?: boolean; + implicitFlowEnabled?: boolean; + directAccessGrantsEnabled?: boolean; + serviceAccountsEnabled?: boolean; + authorizationServicesEnabled?: boolean; + publicClient?: boolean; + attributes?: Record; + defaultClientScopes?: string[]; + optionalClientScopes?: string[]; +}; + +export type KeycloakUserConfig = { + username: string; + email?: string; + firstName?: string; + lastName?: string; + enabled?: boolean; + emailVerified?: boolean; + password?: string; + temporary?: boolean; + groups?: string[]; +}; + +export type KeycloakGroupConfig = { + name: string; +}; + +export type KeycloakRealmConfig = { + realm: string; + displayName?: string; + enabled?: boolean; +}; + +export type KeycloakConnectionConfig = { + baseUrl: string; + realm?: string; + clientId?: string; + clientSecret?: string; + username?: string; + password?: string; +}; diff --git a/src/deployment/rhdh/config/app-config-rhdh.yaml b/src/deployment/rhdh/config/app-config-rhdh.yaml index 0271f15..cba2b93 100644 --- a/src/deployment/rhdh/config/app-config-rhdh.yaml +++ b/src/deployment/rhdh/config/app-config-rhdh.yaml @@ -4,3 +4,25 @@ backend: baseUrl: "${RHDH_BASE_URL}" cors: origin: "${RHDH_BASE_URL}" + +auth: + environment: development + session: + secret: superSecretSecret + providers: + guest: + dangerouslyAllowOutsideDevelopment: true + oidc: + development: + metadataUrl: "${KEYCLOAK_METADATA_URL}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + prompt: auto + callbackUrl: "${RHDH_BASE_URL}/api/auth/oidc/handler/frame" + signIn: + resolvers: + - resolver: emailLocalPartMatchingUserEntityName +signInPage: oidc +catalog: + rules: + - allow: [User, Group] diff --git a/src/deployment/rhdh/config/dynamic-plugins.yaml b/src/deployment/rhdh/config/dynamic-plugins.yaml index cc9134c..294aa17 100644 --- a/src/deployment/rhdh/config/dynamic-plugins.yaml +++ b/src/deployment/rhdh/config/dynamic-plugins.yaml @@ -1,2 +1,5 @@ includes: - dynamic-plugins.default.yaml +plugins: + - package: ./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic + disabled: false diff --git a/src/deployment/rhdh/config/rhdh-secrets.yaml b/src/deployment/rhdh/config/rhdh-secrets.yaml index 274f623..8f216d2 100644 --- a/src/deployment/rhdh/config/rhdh-secrets.yaml +++ b/src/deployment/rhdh/config/rhdh-secrets.yaml @@ -5,3 +5,9 @@ metadata: type: Opaque stringData: RHDH_BASE_URL: $RHDH_BASE_URL + KEYCLOAK_BASE_URL: $KEYCLOAK_BASE_URL + KEYCLOAK_METADATA_URL: $KEYCLOAK_METADATA_URL + KEYCLOAK_CLIENT_ID: $KEYCLOAK_CLIENT_ID + KEYCLOAK_CLIENT_SECRET: $KEYCLOAK_CLIENT_SECRET + KEYCLOAK_REALM: $KEYCLOAK_REALM + KEYCLOAK_LOGIN_REALM: $KEYCLOAK_LOGIN_REALM diff --git a/src/deployment/rhdh/deployment.ts b/src/deployment/rhdh/deployment.ts index ee5d608..52b4c48 100644 --- a/src/deployment/rhdh/deployment.ts +++ b/src/deployment/rhdh/deployment.ts @@ -41,6 +41,7 @@ export class RHDHDeployment { if (this.deploymentConfig.method === "helm") { await this._deployWithHelm(this.deploymentConfig.valueFile); + await this.scaleDownAndRestart(); // Restart as helm does not monitor config changes } else { await this._applyDynamicPlugins(); await this._deployWithOperator(this.deploymentConfig.subscription); @@ -53,13 +54,7 @@ export class RHDHDeployment { DEFAULT_CONFIG_PATHS.appConfig, this.deploymentConfig.appConfig, ]); - console.log( - boxen(yaml.dump(appConfigYaml), { - title: "App Config", - padding: 1, - align: "left", - }), - ); + this._logBoxen("App Config", appConfigYaml); await this.k8sClient.applyConfigMapFromObject( "app-config-rhdh", @@ -86,13 +81,7 @@ export class RHDHDeployment { DEFAULT_CONFIG_PATHS.dynamicPlugins, this.deploymentConfig.dynamicPlugins, ]); - console.log( - boxen(yaml.dump(dynamicPluginsYaml), { - title: "Dynamic Plugins", - padding: 1, - align: "left", - }), - ); + this._logBoxen("Dynamic Plugins", dynamicPluginsYaml); await this.k8sClient.applyConfigMapFromObject( "dynamic-plugins", dynamicPluginsYaml, @@ -110,13 +99,7 @@ export class RHDHDeployment { valueFile, ])) as Record>; - console.log( - boxen(yaml.dump(valueFileObject), { - title: "Value File", - padding: 1, - align: "left", - }), - ); + this._logBoxen("Value File", valueFileObject); // Merge dynamic plugins into the values file if (!valueFileObject.global) { @@ -127,13 +110,7 @@ export class RHDHDeployment { this.deploymentConfig.dynamicPlugins, ]); - console.log( - boxen(yaml.dump(valueFileObject.global.dynamic), { - title: "Dynamic Plugins", - padding: 1, - align: "left", - }), - ); + this._logBoxen("Dynamic Plugins", valueFileObject.global.dynamic); fs.writeFileSync( `/tmp/${this.deploymentConfig.namespace}-value-file.yaml`, @@ -155,13 +132,7 @@ export class RHDHDeployment { DEFAULT_CONFIG_PATHS.operator.subscription, subscription, ]); - console.log( - boxen(yaml.dump(subscriptionObject), { - title: "Subscription", - padding: 1, - align: "left", - }), - ); + this._logBoxen("Subscription", subscriptionObject); fs.writeFileSync( `/tmp/${this.deploymentConfig.namespace}-subscription.yaml`, yaml.dump(subscriptionObject), @@ -195,6 +166,18 @@ export class RHDHDeployment { await this.waitUntilReady(); } + /** + * Performs a clean restart by scaling down to 0 first, waiting for pods to terminate, + * then scaling back up. This prevents MigrationLocked errors by ensuring no pods + * hold database locks when new pods start. + */ + async scaleDownAndRestart(): Promise { + const namespace = this.deploymentConfig.namespace; + await $`oc scale deployment -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' --replicas=0 -n ${namespace}`; + await $`oc wait --for=delete pod -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub),app.kubernetes.io/name!=postgresql' -n ${namespace} --timeout=120s || true`; + await $`oc scale deployment -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' --replicas=1 -n ${namespace}`; + } + async waitUntilReady(timeout: number = 300): Promise { this._log( `Waiting for RHDH deployment to be ready in namespace ${this.deploymentConfig.namespace}...`, @@ -307,4 +290,8 @@ export class RHDHDeployment { private _log(...args: unknown[]): void { console.log("[RHDHDeployment]", ...args); } + + private _logBoxen(title: string, data: unknown): void { + console.log(boxen(yaml.dump(data), { title, padding: 1 })); + } } diff --git a/src/eslint/base.config.ts b/src/eslint/base.config.ts index c00395f..331cfc6 100644 --- a/src/eslint/base.config.ts +++ b/src/eslint/base.config.ts @@ -93,8 +93,6 @@ export function createEslintConfig(tsconfigRootDir: string): Linter.Config[] { "@typescript-eslint/no-misused-promises": "error", // Allow any type in tests (for mocking, test data) "@typescript-eslint/no-explicit-any": "warn", - // Modern import style - "@typescript-eslint/consistent-type-imports": "error", // Prefer modern syntax "@typescript-eslint/prefer-optional-chain": "error", // Allow unused vars starting with underscore diff --git a/src/playwright/base-config.ts b/src/playwright/base-config.ts index 8678407..3e61285 100644 --- a/src/playwright/base-config.ts +++ b/src/playwright/base-config.ts @@ -1,4 +1,7 @@ -import type { PlaywrightTestConfig } from "@playwright/test"; +import { + defineConfig as baseDefineConfig, + PlaywrightTestConfig, +} from "@playwright/test"; import { resolve } from "path"; /** @@ -36,16 +39,17 @@ export const baseConfig: PlaywrightTestConfig = { }; /** - * Creates a workspace-specific config by merging with base config. + * Defines a workspace-specific config by merging with base config. * Only allows overriding the projects configuration. * @param overrides - Object containing projects to override * @returns Merged Playwright configuration */ -export function createPlaywrightConfig( + +export function defineConfig( overrides: Pick = {}, ): PlaywrightTestConfig { - return { + return baseDefineConfig({ ...baseConfig, projects: overrides.projects, - }; + }); } diff --git a/src/playwright/fixtures/test.ts b/src/playwright/fixtures/test.ts index ed737b4..17cea3a 100644 --- a/src/playwright/fixtures/test.ts +++ b/src/playwright/fixtures/test.ts @@ -12,6 +12,8 @@ type RHDHDeploymentWorkerFixtures = { rhdhDeploymentWorker: RHDHDeployment; }; +export * from "@playwright/test"; + export const test = base.extend< RHDHDeploymentTestFixtures, RHDHDeploymentWorkerFixtures @@ -64,5 +66,3 @@ export const test = base.extend< { scope: "test" }, ] as const, }); - -export { expect } from "@playwright/test"; diff --git a/src/playwright/global-setup.ts b/src/playwright/global-setup.ts index 6bef55f..0b7ce2c 100644 --- a/src/playwright/global-setup.ts +++ b/src/playwright/global-setup.ts @@ -5,6 +5,11 @@ import { KubernetesClientHelper } from "../utils/kubernetes-client.js"; import { $ } from "../utils/bash.js"; +import { KeycloakHelper } from "../deployment/keycloak/index.js"; +import { + DEFAULT_KEYCLOAK_CONFIG, + DEFAULT_RHDH_CLIENT, +} from "../deployment/keycloak/constants.js"; const REQUIRED_BINARIES = ["oc", "kubectl", "helm"] as const; @@ -13,7 +18,7 @@ async function checkRequiredBinaries(): Promise { for (const binary of REQUIRED_BINARIES) { try { - await $`which ${binary}`; + await $`command -v ${binary} > /dev/null 2>&1`; } catch { missingBinaries.push(binary); } @@ -33,8 +38,44 @@ async function setClusterRouterBaseEnv(): Promise { console.log(`Cluster router base: ${process.env.K8S_CLUSTER_ROUTER_BASE}`); } +async function deployKeycloak(): Promise { + if (process.env.SKIP_KEYCLOAK_DEPLOYMENT) { + console.log("Skipping Keycloak deployment"); + return; + } + + const keycloak = new KeycloakHelper({ namespace: "rhdh-keycloak" }); + + // Check if Keycloak is already running + if (await keycloak.isRunning()) { + console.log("Keycloak is already running, skipping deployment"); + } else { + await keycloak.deploy(); + await keycloak.configureForRHDH(); + } + + // Set environment variables for RHDH integration + const realm = DEFAULT_KEYCLOAK_CONFIG.realm; + process.env.KEYCLOAK_CLIENT_SECRET = DEFAULT_RHDH_CLIENT.clientSecret; + process.env.KEYCLOAK_CLIENT_ID = DEFAULT_RHDH_CLIENT.clientId; + process.env.KEYCLOAK_REALM = realm; + process.env.KEYCLOAK_LOGIN_REALM = realm; + process.env.KEYCLOAK_METADATA_URL = `${keycloak.keycloakUrl}/realms/${realm}`; + process.env.KEYCLOAK_BASE_URL = keycloak.keycloakUrl; + + console.table({ + keycloakURL: keycloak.keycloakUrl, + adminUser: keycloak.deploymentConfig.adminUser, + adminPassword: keycloak.deploymentConfig.adminPassword, + testUsername: "test1", + testPassword: "test1@123", + }); +} + export default async function globalSetup(): Promise { console.log("Running global setup..."); await checkRequiredBinaries(); await setClusterRouterBaseEnv(); + await deployKeycloak(); + console.log("Global setup completed successfully"); } diff --git a/src/playwright/helpers/common.ts b/src/playwright/helpers/common.ts index 2fcd084..ea2fcd1 100644 --- a/src/playwright/helpers/common.ts +++ b/src/playwright/helpers/common.ts @@ -6,6 +6,7 @@ import { SETTINGS_PAGE_COMPONENTS } from "../page-objects/page-obj.js"; import { UI_HELPER_ELEMENTS } from "../page-objects/global-obj.js"; import * as path from "path"; import * as fs from "fs"; +import { DEFAULT_USERS } from "../../deployment/keycloak/constants.js"; export class LoginHelper { page: Page; @@ -84,8 +85,8 @@ export class LoginHelper { } async loginAsKeycloakUser( - userid: string = process.env.GH_USER_ID as string, - password: string = process.env.GH_USER_PASS as string, + userid: string = DEFAULT_USERS[0].username, + password: string = DEFAULT_USERS[0].password, ) { await this.page.goto("/"); await this.uiHelper.waitForLoad(240000); diff --git a/src/utils/kubernetes-client.ts b/src/utils/kubernetes-client.ts index 21e666f..2e2a182 100644 --- a/src/utils/kubernetes-client.ts +++ b/src/utils/kubernetes-client.ts @@ -12,13 +12,41 @@ $.verbose = true; class KubernetesClientHelper { private _kc: k8s.KubeConfig; private _k8sApi: k8s.CoreV1Api; + private _appsApi: k8s.AppsV1Api; private _customObjectsApi: k8s.CustomObjectsApi; constructor() { this._kc = new k8s.KubeConfig(); this._kc.loadFromDefault(); - this._k8sApi = this._kc.makeApiClient(k8s.CoreV1Api); - this._customObjectsApi = this._kc.makeApiClient(k8s.CustomObjectsApi); + + try { + this._k8sApi = this._kc.makeApiClient(k8s.CoreV1Api); + this._appsApi = this._kc.makeApiClient(k8s.AppsV1Api); + this._customObjectsApi = this._kc.makeApiClient(k8s.CustomObjectsApi); + } catch (error) { + if ( + error instanceof Error && + error.message.includes("No active cluster") + ) { + const currentContext = this._kc.getCurrentContext(); + const contexts = this._kc.getContexts().map((c) => c.name); + + throw new Error( + `No active Kubernetes cluster found.\n\n` + + `The kubeconfig was loaded but no cluster is configured or the current context is invalid.\n\n` + + `Current context: ${currentContext || "(none)"}\n` + + `Available contexts: ${contexts.length > 0 ? contexts.join(", ") : "(none)"}\n\n` + + `To fix this:\n` + + ` 1. Log in to your k8s cluster: oc login or kubectl login\n` + + ` 2. Or set a valid context: kubectl config use-context \n` + + ` 3. Verify your connection: oc whoami && oc cluster-info\n\n` + + `Kubeconfig locations checked:\n` + + ` - KUBECONFIG env: ${process.env.KUBECONFIG || "(not set)"}\n` + + ` - Default: ~/.kube/config`, + ); + } + throw error; + } } /** @@ -313,6 +341,49 @@ class KubernetesClientHelper { } } } + + /** + * Check if a StatefulSet is ready (all replicas are available) + */ + async isStatefulSetReady(namespace: string, name: string): Promise { + try { + const statefulSet = await this._appsApi.readNamespacedStatefulSet({ + name, + namespace, + }); + const replicas = statefulSet.spec?.replicas ?? 1; + const readyReplicas = statefulSet.status?.readyReplicas ?? 0; + return readyReplicas >= replicas; + } catch { + return false; + } + } + + /** + * Wait for a StatefulSet to be ready (all replicas available) + */ + async waitForStatefulSetReady( + namespace: string, + name: string, + timeoutSeconds: number = 300, + pollIntervalMs: number = 5000, + ): Promise { + const startTime = Date.now(); + const timeoutMs = timeoutSeconds * 1000; + + while (Date.now() - startTime < timeoutMs) { + if (await this.isStatefulSetReady(namespace, name)) { + console.log(`✓ StatefulSet ${name} is ready`); + return true; + } + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + } + + throw new Error( + `StatefulSet ${name} in namespace ${namespace} not ready after ${timeoutSeconds}s`, + ); + } + /** * Get the cluster's ingress domain from OpenShift config * Equivalent to: oc get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}' diff --git a/yarn.lock b/yarn.lock index 8017201..438e32c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -109,10 +109,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.39.1, @eslint/js@npm:^9.39.1": - version: 9.39.1 - resolution: "@eslint/js@npm:9.39.1" - checksum: b651930aec03a5aef97bc144627aebb05070afec5364cd3c5fd7c5dbb97f4fd82faf1b200b3be17572d5ebb7f8805211b655f463be96f2b02202ec7250868048 +"@eslint/js@npm:9.39.2, @eslint/js@npm:^9.39.1": + version: 9.39.2 + resolution: "@eslint/js@npm:9.39.2" + checksum: 362aa447266fa5717e762b2b252f177345cb0d7b2954113db9773b3a28898f7cbbc807e07f8078995e6da3f62791f7c5fa2c03517b7170a8e76613cf7fd83c92 languageName: node linkType: hard @@ -207,6 +207,16 @@ __metadata: languageName: node linkType: hard +"@keycloak/keycloak-admin-client@npm:^26.0.0": + version: 26.4.7 + resolution: "@keycloak/keycloak-admin-client@npm:26.4.7" + dependencies: + camelize-ts: ^3.0.0 + url-template: ^3.1.1 + checksum: 973af8c11fb61d648fac7e7b767d52849dd409d1fcee33b36e72c7096e266943cbd2f8f00ebcb47bca59b2a3c106e7853ba7179c730c3eb429cbea39f7c53171 + languageName: node + linkType: hard + "@kubernetes/client-node@npm:^1.4.0": version: 1.4.0 resolution: "@kubernetes/client-node@npm:1.4.0" @@ -379,20 +389,20 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 25.0.1 - resolution: "@types/node@npm:25.0.1" + version: 25.0.3 + resolution: "@types/node@npm:25.0.3" dependencies: undici-types: ~7.16.0 - checksum: f9dc7f9b083c1044b5f687db4e577417b670701625d2b6743e6be212d73fda2660073226339bdf55dee519256dafa91116f3d29deb59f9341147640c76465d5f + checksum: b5e0146eafe208e2f1c1167fd6078a460ace823ad1da61967ec70b8d7521bd6dd26f3cd945796effac48ef4b3df4d8d57d03e9eefd5f2903f6c1d6daf84a9a79 languageName: node linkType: hard "@types/node@npm:^24.0.0, @types/node@npm:^24.10.1": - version: 24.10.3 - resolution: "@types/node@npm:24.10.3" + version: 24.10.4 + resolution: "@types/node@npm:24.10.4" dependencies: undici-types: ~7.16.0 - checksum: 823345eafe5d38a98389a76481bdcfe277286b6fdb4c82c12dc549822f11159956061b75caa4d689e9164c641068a44c1237b190f42da7ed40f62af046e67852 + checksum: 27db63085116aec2b92a36405ab4e8838eafd361ab05ba043e16b70e58b41572145b8078244aa5fd51b1f80076b2e7422c848c31c5a0df0dc5e20053e24720d3 languageName: node linkType: hard @@ -405,105 +415,105 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.49.0" +"@typescript-eslint/eslint-plugin@npm:8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.50.0" dependencies: "@eslint-community/regexpp": ^4.10.0 - "@typescript-eslint/scope-manager": 8.49.0 - "@typescript-eslint/type-utils": 8.49.0 - "@typescript-eslint/utils": 8.49.0 - "@typescript-eslint/visitor-keys": 8.49.0 + "@typescript-eslint/scope-manager": 8.50.0 + "@typescript-eslint/type-utils": 8.50.0 + "@typescript-eslint/utils": 8.50.0 + "@typescript-eslint/visitor-keys": 8.50.0 ignore: ^7.0.0 natural-compare: ^1.4.0 ts-api-utils: ^2.1.0 peerDependencies: - "@typescript-eslint/parser": ^8.49.0 + "@typescript-eslint/parser": ^8.50.0 eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 0bae18dda8e8c86d8da311c382642e4e321e708ca7bad1ae86e43981b1679e99e7d9bd4e32d4874e8016cbe2e39f5a255a71f16cc2c64ec3471b23161e51afec + checksum: c113589c8d912237f0e7e5f9a0e3b9a09697b932eb365c663a86d99e5466284d3bab58fb940aeec4c2ce16e0bd6930641d0fb5cb7365867c4a032aea97f92e12 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/parser@npm:8.49.0" +"@typescript-eslint/parser@npm:8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/parser@npm:8.50.0" dependencies: - "@typescript-eslint/scope-manager": 8.49.0 - "@typescript-eslint/types": 8.49.0 - "@typescript-eslint/typescript-estree": 8.49.0 - "@typescript-eslint/visitor-keys": 8.49.0 + "@typescript-eslint/scope-manager": 8.50.0 + "@typescript-eslint/types": 8.50.0 + "@typescript-eslint/typescript-estree": 8.50.0 + "@typescript-eslint/visitor-keys": 8.50.0 debug: ^4.3.4 peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 27a157372fec09d72b9d3b266ca18cc6d4db040df6d507c5c9d30f97375e0be373d5fde9d02bcd997e40f21738edcc7a2e51d5a56e3cdd600147637bc96d920b + checksum: 75ece2a5ba968672ce9125c9d72aaf4082983460aa9ef095a5fa76d779dd9f1c4114a8b3e8b6d07864aeec5833d0f1f4b86d5179132432862940ddb4b7ef5f7a languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/project-service@npm:8.49.0" +"@typescript-eslint/project-service@npm:8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/project-service@npm:8.50.0" dependencies: - "@typescript-eslint/tsconfig-utils": ^8.49.0 - "@typescript-eslint/types": ^8.49.0 + "@typescript-eslint/tsconfig-utils": ^8.50.0 + "@typescript-eslint/types": ^8.50.0 debug: ^4.3.4 peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 378cd7e6982820aa0bb1dfe78a8cf133dc8192ad68b4e2a3ed1615a1a1b4542a1a20da08de6f5dee2a5804192aeceabe06e6c16a0453a8aaa43e495527e6af6a + checksum: e9804c3dfa80013700d2e37b8f60e32872217af85dfce9dadefe19f86b782514ff9bec52a3c3e285974040e1885e86218857d775ff91d04ca7336f970c334bc8 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/scope-manager@npm:8.49.0" +"@typescript-eslint/scope-manager@npm:8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/scope-manager@npm:8.50.0" dependencies: - "@typescript-eslint/types": 8.49.0 - "@typescript-eslint/visitor-keys": 8.49.0 - checksum: 85aae146729547df03a2ffdb4e447a10023e7c71b426a2a5d7eb3b2a82ec1bbd8ba214d619363994c500a4cf742fbb3f3743723aa13784649e0b9e909ab4529f + "@typescript-eslint/types": 8.50.0 + "@typescript-eslint/visitor-keys": 8.50.0 + checksum: 787e636483a9b05dcb61eba6fd14e73933c8d4c19b6463d14d21c5043b42651cd0b549206da6420845daab45a8b1503290122fa3956dc63f1822f77b5bc2baa2 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.49.0, @typescript-eslint/tsconfig-utils@npm:^8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.49.0" +"@typescript-eslint/tsconfig-utils@npm:8.50.0, @typescript-eslint/tsconfig-utils@npm:^8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.50.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: be26283df8cf05a3a8d17596ac52e51ec27017f27ec5588e2fa3b804c31758864732a24e1ab777ac3e3567dda9b55de5b18d318b6a6e56025baa4f117f371804 + checksum: 1c6e47673e0f701dca9075f1c29cb45dbdc330d9319c8dc906843899ebc22c2b6bb2037f61b5aaae129784bf285aa993a872abb43118b1a4a7f399601f11e1c7 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/type-utils@npm:8.49.0" +"@typescript-eslint/type-utils@npm:8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/type-utils@npm:8.50.0" dependencies: - "@typescript-eslint/types": 8.49.0 - "@typescript-eslint/typescript-estree": 8.49.0 - "@typescript-eslint/utils": 8.49.0 + "@typescript-eslint/types": 8.50.0 + "@typescript-eslint/typescript-estree": 8.50.0 + "@typescript-eslint/utils": 8.50.0 debug: ^4.3.4 ts-api-utils: ^2.1.0 peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: ce5795464be57b0a1cf5970103547a148e8971fe7cf1aafb9a62b40251c670fd1b03535edfc4622c520112705cd6ee5efd88124a7432d2fbbcfc5be54fbf131f + checksum: 79afb8939c3fdbf2104049962731ca0fbb4d185d50b2c12bd05a27618f9111430531b059ebc0ce769afdd8620e0f23b0b960c5f3363eb7c888a60490f3145335 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.49.0, @typescript-eslint/types@npm:^8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/types@npm:8.49.0" - checksum: e604e27f9ff7dd4c7ae0060db5f506338b64cc302563841e729f4da7730a1e94176db8ae1f1c4c0c0c8df5086f127408dc050f27595a36d412f60ed0e09f5a64 +"@typescript-eslint/types@npm:8.50.0, @typescript-eslint/types@npm:^8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/types@npm:8.50.0" + checksum: 1ce987036c0f5e8b5b05afa8897556680f0a07f37bb67c618057427a719444613cca60cfb34a6bc2af68d6c7d68029ce19d3ee5306ee5d311c850c1f9855250a languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.49.0" +"@typescript-eslint/typescript-estree@npm:8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.50.0" dependencies: - "@typescript-eslint/project-service": 8.49.0 - "@typescript-eslint/tsconfig-utils": 8.49.0 - "@typescript-eslint/types": 8.49.0 - "@typescript-eslint/visitor-keys": 8.49.0 + "@typescript-eslint/project-service": 8.50.0 + "@typescript-eslint/tsconfig-utils": 8.50.0 + "@typescript-eslint/types": 8.50.0 + "@typescript-eslint/visitor-keys": 8.50.0 debug: ^4.3.4 minimatch: ^9.0.4 semver: ^7.6.0 @@ -511,32 +521,32 @@ __metadata: ts-api-utils: ^2.1.0 peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: a03545eefdf2487172602930fdd27c8810dc775bdfa4d9c3a45651c5f5465c5e1fc652f318c61ece7f4f35425231961434e96d4ffca84f10149fca111e1fc520 + checksum: 658e20d1faf91ee7db99889b9ed7033feac6b232b5b292b78b41befea2480bce197a96c05d6d9e0b1437277f1ea5818793a25a1194ecda6293bd83b2f0b1f1fd languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/utils@npm:8.49.0" +"@typescript-eslint/utils@npm:8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/utils@npm:8.50.0" dependencies: "@eslint-community/eslint-utils": ^4.7.0 - "@typescript-eslint/scope-manager": 8.49.0 - "@typescript-eslint/types": 8.49.0 - "@typescript-eslint/typescript-estree": 8.49.0 + "@typescript-eslint/scope-manager": 8.50.0 + "@typescript-eslint/types": 8.50.0 + "@typescript-eslint/typescript-estree": 8.50.0 peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: be1bdf2e4a8bb56bb0c39ba8b8a5f1fc187fb17a53af0ef4d50be95914027076dfac385b54d969fdaa2a42fa8a95f31d105457a3768875054a5507ebe6f6257a + checksum: 0ab7e8b5b25c043f33b1af7865fcb35f793f0616f57998ad419fc82c59515e1f7f82dc1bf62b28008d44b2e5e4740152148488f981b33be2edad9ba21d5eff9a languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.49.0": - version: 8.49.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.49.0" +"@typescript-eslint/visitor-keys@npm:8.50.0": + version: 8.50.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.50.0" dependencies: - "@typescript-eslint/types": 8.49.0 + "@typescript-eslint/types": 8.50.0 eslint-visitor-keys: ^4.2.1 - checksum: 446d6345d9702bcdf8713a47561ea52657bbec1c8170b1559d9462e1d815b122adff35f1cc778ecb94f4459d51ac7aac7cafe9ec8d8319b2c7d7984a0edee6ba + checksum: ee3fbacf34051cdc5cdf3df76986d106a550e4bd92f1e4b9ec7f2e177b1eb482284fdddef5116a26827c1f7616ad9bfe8cfb48622ebc95fcf566fe6027412b96 languageName: node linkType: hard @@ -848,6 +858,13 @@ __metadata: languageName: node linkType: hard +"camelize-ts@npm:^3.0.0": + version: 3.0.0 + resolution: "camelize-ts@npm:3.0.0" + checksum: 835f7f79ddec6e6e0364c6a8294ce82586bca5d9443001f28077169181801cb126d8bc608c85504aa6c877de6fe5f7c9533f80996dc81117d865ff92c676d680 + languageName: node + linkType: hard + "chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -858,7 +875,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.3.0": +"chalk@npm:^5.3.0, chalk@npm:^5.6.2": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 4ee2d47a626d79ca27cb5299ecdcce840ef5755e287412536522344db0fc51ca0f6d6433202332c29e2288c6a90a2b31f3bd626bc8c14743b6b6ee28abd3b796 @@ -879,6 +896,22 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^5.0.0": + version: 5.0.0 + resolution: "cli-cursor@npm:5.0.0" + dependencies: + restore-cursor: ^5.0.0 + checksum: 1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 + languageName: node + linkType: hard + +"cli-spinners@npm:^3.2.0": + version: 3.3.0 + resolution: "cli-spinners@npm:3.3.0" + checksum: c3b9c31d96c9158f4d7140557fffb8c1caea2169d7b895374dd3c2f159267aa0db3b72f36bfcc3bbe3532a7ed162d07dc5c0dc3117e1c0dfe4d387e1d723d616 + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -1095,8 +1128,8 @@ __metadata: linkType: hard "eslint@npm:^9.39.1": - version: 9.39.1 - resolution: "eslint@npm:9.39.1" + version: 9.39.2 + resolution: "eslint@npm:9.39.2" dependencies: "@eslint-community/eslint-utils": ^4.8.0 "@eslint-community/regexpp": ^4.12.1 @@ -1104,7 +1137,7 @@ __metadata: "@eslint/config-helpers": ^0.4.2 "@eslint/core": ^0.17.0 "@eslint/eslintrc": ^3.3.1 - "@eslint/js": 9.39.1 + "@eslint/js": 9.39.2 "@eslint/plugin-kit": ^0.4.1 "@humanfs/node": ^0.16.6 "@humanwhocodes/module-importer": ^1.0.1 @@ -1139,7 +1172,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 35583d4d93f431ea2716e18c912e0b10980e27377a89d2c644a3a755921e42a2665dfd7367b8e9b54c7e4e9f193dea4126ce503c866f5795b170934ffd3f1dd9 + checksum: bfa288fe6b19b6e7f8868e1434d8e469603203d6259e4451b8be4e2172de3172f3b07ed8943ba3904f3545c7c546062c0d656774baa0a10a54483f3907c525e3 languageName: node linkType: hard @@ -1308,13 +1341,13 @@ __metadata: linkType: hard "fs-extra@npm:^11.3.2": - version: 11.3.2 - resolution: "fs-extra@npm:11.3.2" + version: 11.3.3 + resolution: "fs-extra@npm:11.3.3" dependencies: graceful-fs: ^4.2.0 jsonfile: ^6.0.1 universalify: ^2.0.0 - checksum: 24a7a6e09668add7f74bf6884086b860ce39c7883d94f564623d4ca5c904ff9e5e33fa6333bd3efbf3528333cdedf974e49fa0723e9debf952f0882e6553d81e + checksum: fb2acabbd1e04bcaca90eadfe98e6ffba1523b8009afbb9f4c0aae5efbca0bd0bf6c9a6831df5af5aaacb98d3e499898be848fb0c03d31ae7b9d1b053e81c151 languageName: node linkType: hard @@ -1360,7 +1393,7 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0": +"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0": version: 1.4.0 resolution: "get-east-asian-width@npm:1.4.0" checksum: 1d9a81a8004f4217ebef5d461875047d269e4b57e039558fd65130877cd4da8e3f61e1c4eada0c8b10e2816c7baf7d5fddb7006f561da13bc6f6dd19c1e964a4 @@ -1582,6 +1615,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^2.0.0": + version: 2.0.0 + resolution: "is-interactive@npm:2.0.0" + checksum: e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c + languageName: node + linkType: hard + "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -1589,6 +1629,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -1741,6 +1788,16 @@ __metadata: languageName: node linkType: hard +"log-symbols@npm:^7.0.1": + version: 7.0.1 + resolution: "log-symbols@npm:7.0.1" + dependencies: + is-unicode-supported: ^2.0.0 + yoctocolors: ^2.1.1 + checksum: 0862313d84826b551582e39659b8586c56b65130c5f4f976420e2c23985228334f2a26fc4251ac22bf0a5b415d9430e86bf332557d934c10b036f9a549d63a09 + languageName: node + linkType: hard + "lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": version: 11.2.4 resolution: "lru-cache@npm:11.2.4" @@ -1800,6 +1857,13 @@ __metadata: languageName: node linkType: hard +"mimic-function@npm:^5.0.0": + version: 5.0.1 + resolution: "mimic-function@npm:5.0.1" + checksum: eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c + languageName: node + linkType: hard + "minimatch@npm:^10.1.1": version: 10.1.1 resolution: "minimatch@npm:10.1.1" @@ -1985,6 +2049,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^7.0.0": + version: 7.0.0 + resolution: "onetime@npm:7.0.0" + dependencies: + mimic-function: ^5.0.0 + checksum: eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c + languageName: node + linkType: hard + "openid-client@npm:^6.1.3": version: 6.8.1 resolution: "openid-client@npm:6.8.1" @@ -2009,6 +2082,23 @@ __metadata: languageName: node linkType: hard +"ora@npm:^9.0.0": + version: 9.0.0 + resolution: "ora@npm:9.0.0" + dependencies: + chalk: ^5.6.2 + cli-cursor: ^5.0.0 + cli-spinners: ^3.2.0 + is-interactive: ^2.0.0 + is-unicode-supported: ^2.1.0 + log-symbols: ^7.0.1 + stdin-discarder: ^0.2.2 + string-width: ^8.1.0 + strip-ansi: ^7.1.2 + checksum: 4efc9c3caa45b552bae4c9755c586eb4f39b824e456d77ec380539529439ca95f3acf9626119131f1ca76618c176088996023e257f5b82e1b86a509990f4545a + languageName: node + linkType: hard + "otplib@npm:12.0.1": version: 12.0.1 resolution: "otplib@npm:12.0.1" @@ -2180,6 +2270,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^5.0.0": + version: 5.1.0 + resolution: "restore-cursor@npm:5.1.0" + dependencies: + onetime: ^7.0.0 + signal-exit: ^4.1.0 + checksum: 838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -2201,6 +2301,7 @@ __metadata: "@axe-core/playwright": ^4.11.0 "@backstage/catalog-model": 1.7.5 "@eslint/js": ^9.39.1 + "@keycloak/keycloak-admin-client": ^26.0.0 "@kubernetes/client-node": ^1.4.0 "@playwright/test": ^1.57.0 "@types/fs-extra": ^11.0.4 @@ -2214,6 +2315,7 @@ __metadata: fs-extra: ^11.3.2 js-yaml: ^4.1.1 lodash.mergewith: ^4.6.2 + ora: ^9.0.0 otplib: 12.0.1 prettier: ^3.7.4 typescript: ^5.9.3 @@ -2265,6 +2367,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -2302,6 +2411,13 @@ __metadata: languageName: node linkType: hard +"stdin-discarder@npm:^0.2.2": + version: 0.2.2 + resolution: "stdin-discarder@npm:0.2.2" + checksum: 642ffd05bd5b100819d6b24a613d83c6e3857c6de74eb02fc51506fa61dc1b0034665163831873868157c4538d71e31762bcf319be86cea04c3aba5336470478 + languageName: node + linkType: hard + "stream-buffers@npm:^3.0.2": version: 3.0.3 resolution: "stream-buffers@npm:3.0.3" @@ -2342,6 +2458,16 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^8.1.0": + version: 8.1.0 + resolution: "string-width@npm:8.1.0" + dependencies: + get-east-asian-width: ^1.3.0 + strip-ansi: ^7.1.0 + checksum: 51ee97c4ffee7b94f8a2ee785fac14f81ec9809b9fcec9a4db44e25c717c263af0cc4387c111aef76195c0718dc43766f3678c07fb542294fb0244f7bfbde883 + languageName: node + linkType: hard + "strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -2351,7 +2477,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.1.0": +"strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": version: 7.1.2 resolution: "strip-ansi@npm:7.1.2" dependencies: @@ -2492,17 +2618,17 @@ __metadata: linkType: hard "typescript-eslint@npm:^8.48.1": - version: 8.49.0 - resolution: "typescript-eslint@npm:8.49.0" + version: 8.50.0 + resolution: "typescript-eslint@npm:8.50.0" dependencies: - "@typescript-eslint/eslint-plugin": 8.49.0 - "@typescript-eslint/parser": 8.49.0 - "@typescript-eslint/typescript-estree": 8.49.0 - "@typescript-eslint/utils": 8.49.0 + "@typescript-eslint/eslint-plugin": 8.50.0 + "@typescript-eslint/parser": 8.50.0 + "@typescript-eslint/typescript-estree": 8.50.0 + "@typescript-eslint/utils": 8.50.0 peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: fd91cffcf3c5de73a9ead2253dcb8516ed664fc9179d26c019e6be53f4d4429e280dd5c783c68789a4a2db34712e569468a6c9c7613fc918a310687ca53b91b1 + checksum: 91c983ae43f917d621224c126347e263e09a0ed43f944e6c5c5ba3343412eef42d925baf07b8727697019d267ebe352026536ced0b513e4eb3376cb7267e941a languageName: node linkType: hard @@ -2567,6 +2693,13 @@ __metadata: languageName: node linkType: hard +"url-template@npm:^3.1.1": + version: 3.1.1 + resolution: "url-template@npm:3.1.1" + checksum: ac09daaeaec55a6b070b838ed161d66b050a46fc12ac251cb2db1ce356e786cfb117ee4391d943aaaa757971c509a0142b3cd83dfd8cc3d7b6d90a99d001a5f9 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -2676,6 +2809,13 @@ __metadata: languageName: node linkType: hard +"yoctocolors@npm:^2.1.1": + version: 2.1.2 + resolution: "yoctocolors@npm:2.1.2" + checksum: 6ee42d665a4cc161c7de3f015b2a65d6c65d2808bfe3b99e228bd2b1b784ef1e54d1907415c025fc12b400f26f372bfc1b71966c6c738d998325ca422eb39363 + languageName: node + linkType: hard + "zx@npm:^8.8.5": version: 8.8.5 resolution: "zx@npm:8.8.5" From e9bafc82e3dbbc14146c0521ce27070c12ee3515 Mon Sep 17 00:00:00 2001 From: Subhash Khileri Date: Sat, 27 Dec 2025 17:11:09 +0530 Subject: [PATCH 2/4] Refactor RHDH deployment to support modular auth configurations --- package.json | 2 +- .../rhdh/config/auth/guest/app-config.yaml | 6 + .../keycloak/app-config.yaml} | 13 +-- .../{ => auth/keycloak}/dynamic-plugins.yaml | 2 - .../keycloak/secrets.yaml} | 1 - .../rhdh/config/common/app-config-rhdh.yaml | 6 + .../rhdh/config/common/dynamic-plugins.yaml | 3 + .../rhdh/config/common/rhdh-secrets.yaml | 8 ++ .../rhdh/{ => config}/helm/value_file.yaml | 0 .../{ => config}/operator/subscription.yaml | 0 src/deployment/rhdh/constants.ts | 39 ++++++- src/deployment/rhdh/deployment.ts | 31 ++++-- src/deployment/rhdh/index.ts | 8 -- src/deployment/rhdh/types.ts | 5 +- src/playwright/fixtures/test.ts | 7 +- src/playwright/global-setup.ts | 10 +- src/utils/merge-yamls.ts | 105 ++++++++++++++++-- 17 files changed, 191 insertions(+), 55 deletions(-) create mode 100644 src/deployment/rhdh/config/auth/guest/app-config.yaml rename src/deployment/rhdh/config/{app-config-rhdh.yaml => auth/keycloak/app-config.yaml} (67%) rename src/deployment/rhdh/config/{ => auth/keycloak}/dynamic-plugins.yaml (75%) rename src/deployment/rhdh/config/{rhdh-secrets.yaml => auth/keycloak/secrets.yaml} (91%) create mode 100644 src/deployment/rhdh/config/common/app-config-rhdh.yaml create mode 100644 src/deployment/rhdh/config/common/dynamic-plugins.yaml create mode 100644 src/deployment/rhdh/config/common/rhdh-secrets.yaml rename src/deployment/rhdh/{ => config}/helm/value_file.yaml (100%) rename src/deployment/rhdh/{ => config}/operator/subscription.yaml (100%) diff --git a/package.json b/package.json index e96f64e..f0c9913 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "tsconfig.base.json" ], "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json && cp -r src/deployment/rhdh/config src/deployment/rhdh/helm src/deployment/rhdh/operator dist/deployment/rhdh/ && cp -r src/deployment/keycloak/config dist/deployment/keycloak/", + "build": "yarn clean && tsc -p tsconfig.build.json && cp -r src/deployment/rhdh/config dist/deployment/rhdh/ && cp -r src/deployment/keycloak/config dist/deployment/keycloak/", "check": "yarn typecheck && yarn lint:check && yarn prettier:check", "clean": "rm -rf dist", "lint:check": "eslint . --ignore-pattern dist --ignore-pattern README.md", diff --git a/src/deployment/rhdh/config/auth/guest/app-config.yaml b/src/deployment/rhdh/config/auth/guest/app-config.yaml new file mode 100644 index 0000000..5c33528 --- /dev/null +++ b/src/deployment/rhdh/config/auth/guest/app-config.yaml @@ -0,0 +1,6 @@ +auth: + environment: development + providers: + guest: + dangerouslyAllowOutsideDevelopment: true + diff --git a/src/deployment/rhdh/config/app-config-rhdh.yaml b/src/deployment/rhdh/config/auth/keycloak/app-config.yaml similarity index 67% rename from src/deployment/rhdh/config/app-config-rhdh.yaml rename to src/deployment/rhdh/config/auth/keycloak/app-config.yaml index cba2b93..71b2705 100644 --- a/src/deployment/rhdh/config/app-config-rhdh.yaml +++ b/src/deployment/rhdh/config/auth/keycloak/app-config.yaml @@ -1,19 +1,10 @@ -app: - baseUrl: "${RHDH_BASE_URL}" -backend: - baseUrl: "${RHDH_BASE_URL}" - cors: - origin: "${RHDH_BASE_URL}" - auth: - environment: development + environment: production session: secret: superSecretSecret providers: - guest: - dangerouslyAllowOutsideDevelopment: true oidc: - development: + production: metadataUrl: "${KEYCLOAK_METADATA_URL}" clientId: "${KEYCLOAK_CLIENT_ID}" clientSecret: "${KEYCLOAK_CLIENT_SECRET}" diff --git a/src/deployment/rhdh/config/dynamic-plugins.yaml b/src/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml similarity index 75% rename from src/deployment/rhdh/config/dynamic-plugins.yaml rename to src/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml index 294aa17..51bc2ad 100644 --- a/src/deployment/rhdh/config/dynamic-plugins.yaml +++ b/src/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml @@ -1,5 +1,3 @@ -includes: - - dynamic-plugins.default.yaml plugins: - package: ./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic disabled: false diff --git a/src/deployment/rhdh/config/rhdh-secrets.yaml b/src/deployment/rhdh/config/auth/keycloak/secrets.yaml similarity index 91% rename from src/deployment/rhdh/config/rhdh-secrets.yaml rename to src/deployment/rhdh/config/auth/keycloak/secrets.yaml index 8f216d2..7d2bc18 100644 --- a/src/deployment/rhdh/config/rhdh-secrets.yaml +++ b/src/deployment/rhdh/config/auth/keycloak/secrets.yaml @@ -4,7 +4,6 @@ metadata: name: rhdh-secrets type: Opaque stringData: - RHDH_BASE_URL: $RHDH_BASE_URL KEYCLOAK_BASE_URL: $KEYCLOAK_BASE_URL KEYCLOAK_METADATA_URL: $KEYCLOAK_METADATA_URL KEYCLOAK_CLIENT_ID: $KEYCLOAK_CLIENT_ID diff --git a/src/deployment/rhdh/config/common/app-config-rhdh.yaml b/src/deployment/rhdh/config/common/app-config-rhdh.yaml new file mode 100644 index 0000000..0271f15 --- /dev/null +++ b/src/deployment/rhdh/config/common/app-config-rhdh.yaml @@ -0,0 +1,6 @@ +app: + baseUrl: "${RHDH_BASE_URL}" +backend: + baseUrl: "${RHDH_BASE_URL}" + cors: + origin: "${RHDH_BASE_URL}" diff --git a/src/deployment/rhdh/config/common/dynamic-plugins.yaml b/src/deployment/rhdh/config/common/dynamic-plugins.yaml new file mode 100644 index 0000000..e0d486d --- /dev/null +++ b/src/deployment/rhdh/config/common/dynamic-plugins.yaml @@ -0,0 +1,3 @@ +includes: + - dynamic-plugins.default.yaml +plugins: [] diff --git a/src/deployment/rhdh/config/common/rhdh-secrets.yaml b/src/deployment/rhdh/config/common/rhdh-secrets.yaml new file mode 100644 index 0000000..04c388d --- /dev/null +++ b/src/deployment/rhdh/config/common/rhdh-secrets.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: rhdh-secrets +type: Opaque +stringData: + RHDH_BASE_URL: $RHDH_BASE_URL + diff --git a/src/deployment/rhdh/helm/value_file.yaml b/src/deployment/rhdh/config/helm/value_file.yaml similarity index 100% rename from src/deployment/rhdh/helm/value_file.yaml rename to src/deployment/rhdh/config/helm/value_file.yaml diff --git a/src/deployment/rhdh/operator/subscription.yaml b/src/deployment/rhdh/config/operator/subscription.yaml similarity index 100% rename from src/deployment/rhdh/operator/subscription.yaml rename to src/deployment/rhdh/config/operator/subscription.yaml diff --git a/src/deployment/rhdh/constants.ts b/src/deployment/rhdh/constants.ts index 098b5ca..6cff7bb 100644 --- a/src/deployment/rhdh/constants.ts +++ b/src/deployment/rhdh/constants.ts @@ -1,4 +1,5 @@ import path from "path"; +import type { AuthProvider } from "./types.js"; // Navigate from dist/deployment/rhdh/ to package root const PACKAGE_ROOT = path.resolve(import.meta.dirname, "../../.."); @@ -6,26 +7,54 @@ const PACKAGE_ROOT = path.resolve(import.meta.dirname, "../../.."); export const DEFAULT_CONFIG_PATHS = { appConfig: path.join( PACKAGE_ROOT, - "dist/deployment/rhdh/config/app-config-rhdh.yaml", + "dist/deployment/rhdh/config/common/app-config-rhdh.yaml", ), secrets: path.join( PACKAGE_ROOT, - "dist/deployment/rhdh/config/rhdh-secrets.yaml", + "dist/deployment/rhdh/config/common/rhdh-secrets.yaml", ), dynamicPlugins: path.join( PACKAGE_ROOT, - "dist/deployment/rhdh/config/dynamic-plugins.yaml", + "dist/deployment/rhdh/config/common/dynamic-plugins.yaml", ), helm: { valueFile: path.join( PACKAGE_ROOT, - "src/deployment/rhdh/helm/value_file.yaml", + "dist/deployment/rhdh/config/helm/value_file.yaml", ), }, operator: { subscription: path.join( PACKAGE_ROOT, - "src/deployment/rhdh/operator/subscription.yaml", + "dist/deployment/rhdh/config/operator/subscription.yaml", + ), + }, +}; + +export const AUTH_CONFIG_PATHS: Record< + AuthProvider, + { appConfig: string; secrets: string; dynamicPlugins: string } +> = { + guest: { + appConfig: path.join( + PACKAGE_ROOT, + "dist/deployment/rhdh/config/auth/guest/app-config.yaml", + ), + secrets: "", + dynamicPlugins: "", + }, + keycloak: { + appConfig: path.join( + PACKAGE_ROOT, + "dist/deployment/rhdh/config/auth/keycloak/app-config.yaml", + ), + secrets: path.join( + PACKAGE_ROOT, + "dist/deployment/rhdh/config/auth/keycloak/secrets.yaml", + ), + dynamicPlugins: path.join( + PACKAGE_ROOT, + "dist/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml", ), }, }; diff --git a/src/deployment/rhdh/deployment.ts b/src/deployment/rhdh/deployment.ts index 52b4c48..24b146b 100644 --- a/src/deployment/rhdh/deployment.ts +++ b/src/deployment/rhdh/deployment.ts @@ -6,7 +6,7 @@ import { mergeYamlFilesIfExists } from "../../utils/merge-yamls.js"; import { envsubst } from "../../utils/common.js"; import fs from "fs-extra"; import boxen from "boxen"; -import { DEFAULT_CONFIG_PATHS, CHART_URL } from "./constants.js"; +import { DEFAULT_CONFIG_PATHS, AUTH_CONFIG_PATHS, CHART_URL } from "./constants.js"; import type { DeploymentOptions, DeploymentConfig, @@ -19,12 +19,13 @@ export class RHDHDeployment { public rhdhUrl: string; public deploymentConfig: DeploymentConfig; - constructor(deploymentOptions: DeploymentOptions) { - this.deploymentConfig = this._buildDeploymentConfig(deploymentOptions); + constructor(namespace: string) { + this.deploymentConfig = this._buildDeploymentConfig({ namespace }); this.rhdhUrl = this._buildBaseUrl(); this._log( `RHDH deployment initialized (namespace: ${this.deploymentConfig.namespace})`, ); + this._log("RHDH Base URL: " + this.rhdhUrl); console.table(this.deploymentConfig); } @@ -50,8 +51,10 @@ export class RHDHDeployment { } private async _applyAppConfig(): Promise { + const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth]; const appConfigYaml = await mergeYamlFilesIfExists([ DEFAULT_CONFIG_PATHS.appConfig, + authConfig.appConfig, this.deploymentConfig.appConfig, ]); this._logBoxen("App Config", appConfigYaml); @@ -64,8 +67,10 @@ export class RHDHDeployment { } private async _applySecrets(): Promise { + const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth]; const secretsYaml = await mergeYamlFilesIfExists([ DEFAULT_CONFIG_PATHS.secrets, + authConfig.secrets, this.deploymentConfig.secrets, ]); @@ -77,10 +82,12 @@ export class RHDHDeployment { } private async _applyDynamicPlugins(): Promise { + const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth]; const dynamicPluginsYaml = await mergeYamlFilesIfExists([ DEFAULT_CONFIG_PATHS.dynamicPlugins, + authConfig.dynamicPlugins, this.deploymentConfig.dynamicPlugins, - ]); + ], { arrayMergeStrategy: { byKey: "package" } }); this._logBoxen("Dynamic Plugins", dynamicPluginsYaml); await this.k8sClient.applyConfigMapFromObject( "dynamic-plugins", @@ -101,14 +108,16 @@ export class RHDHDeployment { this._logBoxen("Value File", valueFileObject); - // Merge dynamic plugins into the values file + // Merge dynamic plugins into the values file (including auth-specific plugins) + const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth]; if (!valueFileObject.global) { valueFileObject.global = {}; } valueFileObject.global.dynamic = await mergeYamlFilesIfExists([ DEFAULT_CONFIG_PATHS.dynamicPlugins, + authConfig.dynamicPlugins, this.deploymentConfig.dynamicPlugins, - ]); + ], { arrayMergeStrategy: { byKey: "package" } }); this._logBoxen("Dynamic Plugins", valueFileObject.global.dynamic); @@ -188,10 +197,13 @@ export class RHDHDeployment { `RHDH deployment is ready in namespace ${this.deploymentConfig.namespace}`, ); } catch (error) { - this._log( + console.log("----------------------------------------------------------------"); + console.log("Deployment Failed Logs"); + console.log("----------------------------------------------------------------"); + await $`oc logs -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' -n ${this.deploymentConfig.namespace} --tail=100` + throw new Error( `Error waiting for RHDH deployment to be ready in timeout ${timeout}s in namespace ${this.deploymentConfig.namespace}: ${error}`, ); - throw error; } } @@ -239,7 +251,8 @@ export class RHDHDeployment { const base: DeploymentConfigBase = { version, - namespace: input.namespace, + namespace: input.namespace ?? this.deploymentConfig.namespace, + auth: input.auth ?? "keycloak", appConfig: input.appConfig ?? `tests/config/app-config-rhdh.yaml`, secrets: input.secrets ?? `tests/config/rhdh-secrets.yaml`, dynamicPlugins: diff --git a/src/deployment/rhdh/index.ts b/src/deployment/rhdh/index.ts index fc47a87..8e83699 100644 --- a/src/deployment/rhdh/index.ts +++ b/src/deployment/rhdh/index.ts @@ -1,9 +1 @@ export { RHDHDeployment } from "./deployment.js"; -export type { - DeploymentOptions, - DeploymentConfig, - DeploymentConfigBase, - DeploymentMethod, - HelmDeploymentConfig, - OperatorDeploymentConfig, -} from "./types.js"; diff --git a/src/deployment/rhdh/types.ts b/src/deployment/rhdh/types.ts index 363667c..6f56033 100644 --- a/src/deployment/rhdh/types.ts +++ b/src/deployment/rhdh/types.ts @@ -1,8 +1,10 @@ export type DeploymentMethod = "helm" | "operator"; +export type AuthProvider = "guest" | "keycloak"; export type DeploymentOptions = { version?: string; - namespace: string; + namespace?: string; + auth?: AuthProvider; appConfig?: string; secrets?: string; dynamicPlugins?: string; @@ -24,6 +26,7 @@ export type OperatorDeploymentConfig = { export type DeploymentConfigBase = { version: string; namespace: string; + auth: AuthProvider; appConfig: string; secrets: string; dynamicPlugins: string; diff --git a/src/playwright/fixtures/test.ts b/src/playwright/fixtures/test.ts index 17cea3a..61e8eee 100644 --- a/src/playwright/fixtures/test.ts +++ b/src/playwright/fixtures/test.ts @@ -25,11 +25,12 @@ export const test = base.extend< `Deploying rhdh for plugin ${workerInfo.project.name} in namespace ${workerInfo.project.name}`, ); - const rhdhDeployment = new RHDHDeployment({ - namespace: workerInfo.project.name, - }); + const rhdhDeployment = new RHDHDeployment( + workerInfo.project.name, + ); try { + await rhdhDeployment.configure(); await use(rhdhDeployment); } finally { if (process.env.CI) { diff --git a/src/playwright/global-setup.ts b/src/playwright/global-setup.ts index 0b7ce2c..271f25c 100644 --- a/src/playwright/global-setup.ts +++ b/src/playwright/global-setup.ts @@ -9,6 +9,7 @@ import { KeycloakHelper } from "../deployment/keycloak/index.js"; import { DEFAULT_KEYCLOAK_CONFIG, DEFAULT_RHDH_CLIENT, + DEFAULT_USERS, } from "../deployment/keycloak/constants.js"; const REQUIRED_BINARIES = ["oc", "kubectl", "helm"] as const; @@ -39,10 +40,13 @@ async function setClusterRouterBaseEnv(): Promise { } async function deployKeycloak(): Promise { - if (process.env.SKIP_KEYCLOAK_DEPLOYMENT) { + if (process.env.SKIP_KEYCLOAK_DEPLOYMENT === "true") { console.log("Skipping Keycloak deployment"); return; } + console.log( + "Set SKIP_KEYCLOAK_DEPLOYMENT=true if test doesn't require keycloak/oidc as auth provider", + ); const keycloak = new KeycloakHelper({ namespace: "rhdh-keycloak" }); @@ -67,8 +71,8 @@ async function deployKeycloak(): Promise { keycloakURL: keycloak.keycloakUrl, adminUser: keycloak.deploymentConfig.adminUser, adminPassword: keycloak.deploymentConfig.adminPassword, - testUsername: "test1", - testPassword: "test1@123", + testUsername: DEFAULT_USERS[0].username, + testPassword: DEFAULT_USERS[0].password, }); } diff --git a/src/utils/merge-yamls.ts b/src/utils/merge-yamls.ts index ae5ba0b..22352f7 100644 --- a/src/utils/merge-yamls.ts +++ b/src/utils/merge-yamls.ts @@ -2,36 +2,114 @@ import fs from "fs-extra"; import yaml from "js-yaml"; import mergeWith from "lodash.mergewith"; +/** + * Array merge strategy options for YAML merging. + */ +export type ArrayMergeStrategy = + | "replace" // Replace arrays entirely (default, Kustomize-style) + | "concat" // Concatenate arrays + | { byKey: string }; // Merge arrays of objects by a specific key + +/** + * Options for YAML merging. + */ +export interface MergeOptions { + /** + * Strategy for merging arrays. + * - "replace": Replace arrays entirely (default) + * - "concat": Concatenate arrays + * - { byKey: "keyName" }: Merge arrays of objects by a specific key + */ + arrayMergeStrategy?: ArrayMergeStrategy; +} + +/** + * Merges two arrays of objects by a specific key. + * Objects with matching keys are deeply merged, new objects are appended. + */ +function mergeArraysByKey( + target: unknown[], + source: unknown[], + key: string, + mergeOptions: MergeOptions, +): unknown[] { + const result = [...target]; + + for (const srcItem of source) { + if (typeof srcItem === "object" && srcItem !== null && key in srcItem) { + const srcKeyValue = (srcItem as Record)[key]; + const existingIndex = result.findIndex( + (item) => + typeof item === "object" && + item !== null && + (item as Record)[key] === srcKeyValue, + ); + + if (existingIndex !== -1) { + // Merge existing object with source object + result[existingIndex] = deepMerge( + result[existingIndex] as Record, + srcItem as Record, + mergeOptions, + ); + } else { + // Append new object + result.push(srcItem); + } + } else { + // Non-object or missing key, append as-is + result.push(srcItem); + } + } + + return result; +} + /** * Deeply merges two YAML-compatible objects. - * Arrays are replaced (not concatenated) — this mimics Kustomize-style merging. + * Array handling is controlled by the arrayMergeStrategy option. */ function deepMerge( target: Record, source: Record, + options: MergeOptions = {}, ): Record { - return mergeWith(target, source, (objValue: unknown, srcValue: unknown) => { - if (Array.isArray(objValue) && Array.isArray(srcValue)) { - return srcValue; // Replace arrays instead of merging - } - }); + const strategy = options.arrayMergeStrategy ?? "replace"; + + return mergeWith( + { ...target }, + source, + (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + if (strategy === "replace") { + return srcValue; + } else if (strategy === "concat") { + return [...objValue, ...srcValue]; + } else if (typeof strategy === "object" && "byKey" in strategy) { + return mergeArraysByKey(objValue, srcValue, strategy.byKey, options); + } + } + }, + ); } /** * Merge multiple YAML files into one object. * * @param paths List of YAML file paths (base first, overlays last) + * @param options Optional merge options (e.g., arrayMergeStrategy) * @returns Merged YAML object */ export async function mergeYamlFiles( paths: string[], + options: MergeOptions = {}, ): Promise> { let merged: Record = {}; for (const path of paths) { const content = await fs.readFile(path, "utf8"); const parsed = (yaml.load(content) || {}) as Record; - merged = deepMerge(merged, parsed); + merged = deepMerge(merged, parsed, options); } return merged; @@ -41,10 +119,12 @@ export async function mergeYamlFiles( * Merge multiple YAML files if they exist. * * @param paths List of YAML file paths + * @param options Optional merge options (e.g., arrayMergeStrategy) * @returns Merged YAML object */ export async function mergeYamlFilesIfExists( paths: string[], + options: MergeOptions = {}, ): Promise> { return await mergeYamlFiles( paths.filter((path) => { @@ -52,6 +132,7 @@ export async function mergeYamlFilesIfExists( if (!exists) console.log(`YAML file ${path} does not exist`); return exists; }), + options, ); } @@ -60,15 +141,17 @@ export async function mergeYamlFilesIfExists( * * @param inputPaths List of input YAML files * @param outputPath Output YAML file path - * @param options Optional dump formatting + * @param dumpOptions Optional dump formatting + * @param mergeOptions Optional merge options (e.g., arrayMergeStrategy) */ export async function mergeYamlFilesToFile( inputPaths: string[], outputPath: string, - options: yaml.DumpOptions = { lineWidth: -1 }, + dumpOptions: yaml.DumpOptions = { lineWidth: -1 }, + mergeOptions: MergeOptions = {}, ): Promise { - const merged = await mergeYamlFiles(inputPaths); - const yamlString = yaml.dump(merged, options); + const merged = await mergeYamlFiles(inputPaths, mergeOptions); + const yamlString = yaml.dump(merged, dumpOptions); await fs.outputFile(outputPath, yamlString); console.log(`Merged ${inputPaths.length} YAML files into ${outputPath}`); } From e594ec44a963a20c38815c9e7c8e8a7cd724b9c6 Mon Sep 17 00:00:00 2001 From: Subhash Khileri Date: Sat, 27 Dec 2025 17:15:38 +0530 Subject: [PATCH 3/4] Refactor RHDH deployment to support modular auth configurations --- .../rhdh/config/auth/guest/app-config.yaml | 1 - .../rhdh/config/common/rhdh-secrets.yaml | 1 - src/deployment/rhdh/deployment.ts | 42 ++++++++++++------- src/playwright/fixtures/test.ts | 4 +- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/deployment/rhdh/config/auth/guest/app-config.yaml b/src/deployment/rhdh/config/auth/guest/app-config.yaml index 5c33528..a867aee 100644 --- a/src/deployment/rhdh/config/auth/guest/app-config.yaml +++ b/src/deployment/rhdh/config/auth/guest/app-config.yaml @@ -3,4 +3,3 @@ auth: providers: guest: dangerouslyAllowOutsideDevelopment: true - diff --git a/src/deployment/rhdh/config/common/rhdh-secrets.yaml b/src/deployment/rhdh/config/common/rhdh-secrets.yaml index 04c388d..274f623 100644 --- a/src/deployment/rhdh/config/common/rhdh-secrets.yaml +++ b/src/deployment/rhdh/config/common/rhdh-secrets.yaml @@ -5,4 +5,3 @@ metadata: type: Opaque stringData: RHDH_BASE_URL: $RHDH_BASE_URL - diff --git a/src/deployment/rhdh/deployment.ts b/src/deployment/rhdh/deployment.ts index 24b146b..207a496 100644 --- a/src/deployment/rhdh/deployment.ts +++ b/src/deployment/rhdh/deployment.ts @@ -6,7 +6,11 @@ import { mergeYamlFilesIfExists } from "../../utils/merge-yamls.js"; import { envsubst } from "../../utils/common.js"; import fs from "fs-extra"; import boxen from "boxen"; -import { DEFAULT_CONFIG_PATHS, AUTH_CONFIG_PATHS, CHART_URL } from "./constants.js"; +import { + DEFAULT_CONFIG_PATHS, + AUTH_CONFIG_PATHS, + CHART_URL, +} from "./constants.js"; import type { DeploymentOptions, DeploymentConfig, @@ -83,11 +87,14 @@ export class RHDHDeployment { private async _applyDynamicPlugins(): Promise { const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth]; - const dynamicPluginsYaml = await mergeYamlFilesIfExists([ - DEFAULT_CONFIG_PATHS.dynamicPlugins, - authConfig.dynamicPlugins, - this.deploymentConfig.dynamicPlugins, - ], { arrayMergeStrategy: { byKey: "package" } }); + const dynamicPluginsYaml = await mergeYamlFilesIfExists( + [ + DEFAULT_CONFIG_PATHS.dynamicPlugins, + authConfig.dynamicPlugins, + this.deploymentConfig.dynamicPlugins, + ], + { arrayMergeStrategy: { byKey: "package" } }, + ); this._logBoxen("Dynamic Plugins", dynamicPluginsYaml); await this.k8sClient.applyConfigMapFromObject( "dynamic-plugins", @@ -113,11 +120,14 @@ export class RHDHDeployment { if (!valueFileObject.global) { valueFileObject.global = {}; } - valueFileObject.global.dynamic = await mergeYamlFilesIfExists([ - DEFAULT_CONFIG_PATHS.dynamicPlugins, - authConfig.dynamicPlugins, - this.deploymentConfig.dynamicPlugins, - ], { arrayMergeStrategy: { byKey: "package" } }); + valueFileObject.global.dynamic = await mergeYamlFilesIfExists( + [ + DEFAULT_CONFIG_PATHS.dynamicPlugins, + authConfig.dynamicPlugins, + this.deploymentConfig.dynamicPlugins, + ], + { arrayMergeStrategy: { byKey: "package" } }, + ); this._logBoxen("Dynamic Plugins", valueFileObject.global.dynamic); @@ -197,10 +207,14 @@ export class RHDHDeployment { `RHDH deployment is ready in namespace ${this.deploymentConfig.namespace}`, ); } catch (error) { - console.log("----------------------------------------------------------------"); + console.log( + "----------------------------------------------------------------", + ); console.log("Deployment Failed Logs"); - console.log("----------------------------------------------------------------"); - await $`oc logs -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' -n ${this.deploymentConfig.namespace} --tail=100` + console.log( + "----------------------------------------------------------------", + ); + await $`oc logs -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' -n ${this.deploymentConfig.namespace} --tail=100`; throw new Error( `Error waiting for RHDH deployment to be ready in timeout ${timeout}s in namespace ${this.deploymentConfig.namespace}: ${error}`, ); diff --git a/src/playwright/fixtures/test.ts b/src/playwright/fixtures/test.ts index 61e8eee..f7c783d 100644 --- a/src/playwright/fixtures/test.ts +++ b/src/playwright/fixtures/test.ts @@ -25,9 +25,7 @@ export const test = base.extend< `Deploying rhdh for plugin ${workerInfo.project.name} in namespace ${workerInfo.project.name}`, ); - const rhdhDeployment = new RHDHDeployment( - workerInfo.project.name, - ); + const rhdhDeployment = new RHDHDeployment(workerInfo.project.name); try { await rhdhDeployment.configure(); From 7c4cebe5eba40d45928c045c328189fd8d0a16e6 Mon Sep 17 00:00:00 2001 From: Subhash Khileri Date: Sat, 27 Dec 2025 20:28:21 +0530 Subject: [PATCH 4/4] Refactor RHDH deployment to support modular auth configurations --- yarn.lock | 121 ++---------------------------------------------------- 1 file changed, 3 insertions(+), 118 deletions(-) diff --git a/yarn.lock b/yarn.lock index 438e32c..ac280ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -875,7 +875,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.3.0, chalk@npm:^5.6.2": +"chalk@npm:^5.3.0": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 4ee2d47a626d79ca27cb5299ecdcce840ef5755e287412536522344db0fc51ca0f6d6433202332c29e2288c6a90a2b31f3bd626bc8c14743b6b6ee28abd3b796 @@ -896,22 +896,6 @@ __metadata: languageName: node linkType: hard -"cli-cursor@npm:^5.0.0": - version: 5.0.0 - resolution: "cli-cursor@npm:5.0.0" - dependencies: - restore-cursor: ^5.0.0 - checksum: 1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 - languageName: node - linkType: hard - -"cli-spinners@npm:^3.2.0": - version: 3.3.0 - resolution: "cli-spinners@npm:3.3.0" - checksum: c3b9c31d96c9158f4d7140557fffb8c1caea2169d7b895374dd3c2f159267aa0db3b72f36bfcc3bbe3532a7ed162d07dc5c0dc3117e1c0dfe4d387e1d723d616 - languageName: node - linkType: hard - "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -1393,7 +1377,7 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0": +"get-east-asian-width@npm:^1.0.0": version: 1.4.0 resolution: "get-east-asian-width@npm:1.4.0" checksum: 1d9a81a8004f4217ebef5d461875047d269e4b57e039558fd65130877cd4da8e3f61e1c4eada0c8b10e2816c7baf7d5fddb7006f561da13bc6f6dd19c1e964a4 @@ -1615,13 +1599,6 @@ __metadata: languageName: node linkType: hard -"is-interactive@npm:^2.0.0": - version: 2.0.0 - resolution: "is-interactive@npm:2.0.0" - checksum: e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c - languageName: node - linkType: hard - "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -1629,13 +1606,6 @@ __metadata: languageName: node linkType: hard -"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": - version: 2.1.0 - resolution: "is-unicode-supported@npm:2.1.0" - checksum: f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -1788,16 +1758,6 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:^7.0.1": - version: 7.0.1 - resolution: "log-symbols@npm:7.0.1" - dependencies: - is-unicode-supported: ^2.0.0 - yoctocolors: ^2.1.1 - checksum: 0862313d84826b551582e39659b8586c56b65130c5f4f976420e2c23985228334f2a26fc4251ac22bf0a5b415d9430e86bf332557d934c10b036f9a549d63a09 - languageName: node - linkType: hard - "lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": version: 11.2.4 resolution: "lru-cache@npm:11.2.4" @@ -1857,13 +1817,6 @@ __metadata: languageName: node linkType: hard -"mimic-function@npm:^5.0.0": - version: 5.0.1 - resolution: "mimic-function@npm:5.0.1" - checksum: eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c - languageName: node - linkType: hard - "minimatch@npm:^10.1.1": version: 10.1.1 resolution: "minimatch@npm:10.1.1" @@ -2049,15 +2002,6 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^7.0.0": - version: 7.0.0 - resolution: "onetime@npm:7.0.0" - dependencies: - mimic-function: ^5.0.0 - checksum: eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c - languageName: node - linkType: hard - "openid-client@npm:^6.1.3": version: 6.8.1 resolution: "openid-client@npm:6.8.1" @@ -2082,23 +2026,6 @@ __metadata: languageName: node linkType: hard -"ora@npm:^9.0.0": - version: 9.0.0 - resolution: "ora@npm:9.0.0" - dependencies: - chalk: ^5.6.2 - cli-cursor: ^5.0.0 - cli-spinners: ^3.2.0 - is-interactive: ^2.0.0 - is-unicode-supported: ^2.1.0 - log-symbols: ^7.0.1 - stdin-discarder: ^0.2.2 - string-width: ^8.1.0 - strip-ansi: ^7.1.2 - checksum: 4efc9c3caa45b552bae4c9755c586eb4f39b824e456d77ec380539529439ca95f3acf9626119131f1ca76618c176088996023e257f5b82e1b86a509990f4545a - languageName: node - linkType: hard - "otplib@npm:12.0.1": version: 12.0.1 resolution: "otplib@npm:12.0.1" @@ -2270,16 +2197,6 @@ __metadata: languageName: node linkType: hard -"restore-cursor@npm:^5.0.0": - version: 5.1.0 - resolution: "restore-cursor@npm:5.1.0" - dependencies: - onetime: ^7.0.0 - signal-exit: ^4.1.0 - checksum: 838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c - languageName: node - linkType: hard - "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -2315,7 +2232,6 @@ __metadata: fs-extra: ^11.3.2 js-yaml: ^4.1.1 lodash.mergewith: ^4.6.2 - ora: ^9.0.0 otplib: 12.0.1 prettier: ^3.7.4 typescript: ^5.9.3 @@ -2367,13 +2283,6 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.1.0": - version: 4.1.0 - resolution: "signal-exit@npm:4.1.0" - checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 - languageName: node - linkType: hard - "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -2411,13 +2320,6 @@ __metadata: languageName: node linkType: hard -"stdin-discarder@npm:^0.2.2": - version: 0.2.2 - resolution: "stdin-discarder@npm:0.2.2" - checksum: 642ffd05bd5b100819d6b24a613d83c6e3857c6de74eb02fc51506fa61dc1b0034665163831873868157c4538d71e31762bcf319be86cea04c3aba5336470478 - languageName: node - linkType: hard - "stream-buffers@npm:^3.0.2": version: 3.0.3 resolution: "stream-buffers@npm:3.0.3" @@ -2458,16 +2360,6 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^8.1.0": - version: 8.1.0 - resolution: "string-width@npm:8.1.0" - dependencies: - get-east-asian-width: ^1.3.0 - strip-ansi: ^7.1.0 - checksum: 51ee97c4ffee7b94f8a2ee785fac14f81ec9809b9fcec9a4db44e25c717c263af0cc4387c111aef76195c0718dc43766f3678c07fb542294fb0244f7bfbde883 - languageName: node - linkType: hard - "strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -2477,7 +2369,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": +"strip-ansi@npm:^7.1.0": version: 7.1.2 resolution: "strip-ansi@npm:7.1.2" dependencies: @@ -2809,13 +2701,6 @@ __metadata: languageName: node linkType: hard -"yoctocolors@npm:^2.1.1": - version: 2.1.2 - resolution: "yoctocolors@npm:2.1.2" - checksum: 6ee42d665a4cc161c7de3f015b2a65d6c65d2808bfe3b99e228bd2b1b784ef1e54d1907415c025fc12b400f26f372bfc1b71966c6c738d998325ca422eb39363 - languageName: node - linkType: hard - "zx@npm:^8.8.5": version: 8.8.5 resolution: "zx@npm:8.8.5"