rhdh-e2e-test-utils 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +652 -56
- package/dist/deployment/keycloak/config/keycloak-values.yaml +94 -0
- package/dist/deployment/keycloak/constants.d.ts +29 -0
- package/dist/deployment/keycloak/constants.d.ts.map +1 -0
- package/dist/deployment/keycloak/constants.js +75 -0
- package/dist/deployment/keycloak/deployment.d.ts +89 -0
- package/dist/deployment/keycloak/deployment.d.ts.map +1 -0
- package/dist/deployment/keycloak/deployment.js +437 -0
- package/dist/deployment/keycloak/index.d.ts +2 -0
- package/dist/deployment/keycloak/index.d.ts.map +1 -0
- package/dist/deployment/keycloak/index.js +1 -0
- package/dist/deployment/keycloak/types.d.ts +59 -0
- package/dist/deployment/keycloak/types.d.ts.map +1 -0
- package/dist/deployment/keycloak/types.js +1 -0
- package/dist/deployment/rhdh/config/auth/guest/app-config.yaml +5 -0
- package/dist/deployment/rhdh/config/auth/keycloak/app-config.yaml +19 -0
- package/dist/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml +3 -0
- package/dist/deployment/rhdh/config/auth/keycloak/secrets.yaml +12 -0
- package/dist/deployment/rhdh/config/common/app-config-rhdh.yaml +6 -0
- package/dist/deployment/rhdh/config/common/dynamic-plugins.yaml +3 -0
- package/dist/deployment/rhdh/config/common/rhdh-secrets.yaml +7 -0
- package/dist/deployment/rhdh/config/helm/value_file.yaml +7 -0
- package/dist/deployment/rhdh/config/operator/subscription.yaml +21 -0
- package/dist/deployment/rhdh/constants.d.ts +6 -0
- package/dist/deployment/rhdh/constants.d.ts.map +1 -1
- package/dist/deployment/rhdh/constants.js +17 -5
- package/dist/deployment/rhdh/deployment.d.ts +8 -1
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +47 -39
- package/dist/deployment/rhdh/index.d.ts +0 -1
- package/dist/deployment/rhdh/index.d.ts.map +1 -1
- package/dist/deployment/rhdh/types.d.ts +4 -1
- package/dist/deployment/rhdh/types.d.ts.map +1 -1
- package/dist/eslint/base.config.d.ts.map +1 -1
- package/dist/eslint/base.config.js +9 -2
- package/dist/playwright/base-config.d.ts +3 -3
- package/dist/playwright/base-config.d.ts.map +1 -1
- package/dist/playwright/base-config.js +5 -4
- package/dist/playwright/fixtures/test.d.ts +4 -1
- package/dist/playwright/fixtures/test.d.ts.map +1 -1
- package/dist/playwright/fixtures/test.js +16 -4
- package/dist/playwright/global-setup.d.ts.map +1 -1
- package/dist/playwright/global-setup.js +36 -1
- package/dist/playwright/helpers/accessibility.d.ts +13 -0
- package/dist/playwright/helpers/accessibility.d.ts.map +1 -0
- package/dist/playwright/helpers/accessibility.js +24 -0
- package/dist/playwright/helpers/api-endpoints.d.ts +11 -0
- package/dist/playwright/helpers/api-endpoints.d.ts.map +1 -0
- package/dist/playwright/helpers/api-endpoints.js +15 -0
- package/dist/playwright/helpers/api-helper.d.ts +77 -0
- package/dist/playwright/helpers/api-helper.d.ts.map +1 -0
- package/dist/playwright/helpers/api-helper.js +285 -0
- package/dist/playwright/helpers/common.d.ts +31 -0
- package/dist/playwright/helpers/common.d.ts.map +1 -0
- package/dist/playwright/helpers/common.js +342 -0
- package/dist/playwright/helpers/index.d.ts +5 -0
- package/dist/playwright/helpers/index.d.ts.map +1 -0
- package/dist/playwright/helpers/index.js +4 -0
- package/dist/playwright/helpers/navbar.d.ts +2 -0
- package/dist/playwright/helpers/navbar.d.ts.map +1 -0
- package/dist/playwright/helpers/navbar.js +1 -0
- package/dist/playwright/helpers/ui-helper.d.ts +106 -0
- package/dist/playwright/helpers/ui-helper.d.ts.map +1 -0
- package/dist/playwright/helpers/ui-helper.js +439 -0
- package/dist/playwright/page-objects/global-obj.d.ts +25 -0
- package/dist/playwright/page-objects/global-obj.d.ts.map +1 -0
- package/dist/playwright/page-objects/global-obj.js +24 -0
- package/dist/playwright/page-objects/page-obj.d.ts +41 -0
- package/dist/playwright/page-objects/page-obj.d.ts.map +1 -0
- package/dist/playwright/page-objects/page-obj.js +40 -0
- package/dist/playwright/pages/catalog-import.d.ts +31 -0
- package/dist/playwright/pages/catalog-import.d.ts.map +1 -0
- package/dist/playwright/pages/catalog-import.js +65 -0
- package/dist/playwright/pages/catalog.d.ts +14 -0
- package/dist/playwright/pages/catalog.d.ts.map +1 -0
- package/dist/playwright/pages/catalog.js +37 -0
- package/dist/playwright/pages/extensions.d.ts +38 -0
- package/dist/playwright/pages/extensions.d.ts.map +1 -0
- package/dist/playwright/pages/extensions.js +110 -0
- package/dist/playwright/pages/home-page.d.ts +10 -0
- package/dist/playwright/pages/home-page.d.ts.map +1 -0
- package/dist/playwright/pages/home-page.js +46 -0
- package/dist/playwright/pages/index.d.ts +6 -0
- package/dist/playwright/pages/index.d.ts.map +1 -0
- package/dist/playwright/pages/index.js +5 -0
- package/dist/playwright/pages/notifications.d.ts +24 -0
- package/dist/playwright/pages/notifications.d.ts.map +1 -0
- package/dist/playwright/pages/notifications.js +112 -0
- package/dist/utils/kubernetes-client.d.ts +9 -0
- package/dist/utils/kubernetes-client.d.ts.map +1 -1
- package/dist/utils/kubernetes-client.js +57 -2
- package/dist/utils/merge-yamls.d.ts +25 -4
- package/dist/utils/merge-yamls.d.ts.map +1 -1
- package/dist/utils/merge-yamls.js +52 -12
- package/package.json +19 -6
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
|
2
|
+
import { KubernetesClientHelper } from "../../utils/kubernetes-client.js";
|
|
3
|
+
import { $ } from "../../utils/bash.js";
|
|
4
|
+
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";
|
|
5
|
+
export class KeycloakHelper {
|
|
6
|
+
k8sClient = new KubernetesClientHelper();
|
|
7
|
+
deploymentConfig;
|
|
8
|
+
keycloakUrl = "";
|
|
9
|
+
realm = "";
|
|
10
|
+
clientId = "";
|
|
11
|
+
clientSecret = "";
|
|
12
|
+
_adminClient = null;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.deploymentConfig = this._buildDeploymentConfig(options);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Deploy Keycloak using Helm and configure it for RHDH.
|
|
18
|
+
*/
|
|
19
|
+
async deploy() {
|
|
20
|
+
this._log("Starting Keycloak deployment...");
|
|
21
|
+
await this.k8sClient.createNamespaceIfNotExists(this.deploymentConfig.namespace);
|
|
22
|
+
await this._deployWithHelm();
|
|
23
|
+
await this._createRoute();
|
|
24
|
+
await this._waitForKeycloak();
|
|
25
|
+
await this._initializeAdminClient();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if Keycloak is already running
|
|
29
|
+
*/
|
|
30
|
+
async isRunning() {
|
|
31
|
+
try {
|
|
32
|
+
this.keycloakUrl = await this.getRouteLocation();
|
|
33
|
+
const response = await fetch(`${this.keycloakUrl}/realms/master`);
|
|
34
|
+
return response.ok;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Configure Keycloak with realm, client, groups, and users for RHDH
|
|
42
|
+
*/
|
|
43
|
+
async configureForRHDH(options) {
|
|
44
|
+
this._log("Configuring Keycloak for RHDH...");
|
|
45
|
+
await this._ensureAdminClient();
|
|
46
|
+
const realmName = options?.realm ?? DEFAULT_KEYCLOAK_CONFIG.realm;
|
|
47
|
+
// Create realm
|
|
48
|
+
await this.createRealm({ realm: realmName, enabled: true });
|
|
49
|
+
// Create client
|
|
50
|
+
const clientConfig = {
|
|
51
|
+
...DEFAULT_RHDH_CLIENT,
|
|
52
|
+
...options?.client,
|
|
53
|
+
};
|
|
54
|
+
await this.createClient(realmName, clientConfig);
|
|
55
|
+
// Store realm and client info for external access
|
|
56
|
+
this.realm = realmName;
|
|
57
|
+
this.clientId = clientConfig.clientId;
|
|
58
|
+
this.clientSecret = clientConfig.clientSecret;
|
|
59
|
+
// Assign service account roles
|
|
60
|
+
await this._assignServiceAccountRoles(realmName, clientConfig.clientId);
|
|
61
|
+
// Create groups
|
|
62
|
+
const groups = options?.groups ?? DEFAULT_GROUPS;
|
|
63
|
+
for (const group of groups) {
|
|
64
|
+
await this.createGroup(realmName, group);
|
|
65
|
+
}
|
|
66
|
+
// Create users
|
|
67
|
+
const users = options?.users ?? DEFAULT_USERS;
|
|
68
|
+
for (const user of users) {
|
|
69
|
+
await this.createUser(realmName, user);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Connect to an existing Keycloak instance
|
|
74
|
+
*/
|
|
75
|
+
async connect(config) {
|
|
76
|
+
this.keycloakUrl = config.baseUrl;
|
|
77
|
+
this._adminClient = new KeycloakAdminClient({
|
|
78
|
+
baseUrl: config.baseUrl,
|
|
79
|
+
realmName: config.realm ?? "master",
|
|
80
|
+
});
|
|
81
|
+
if (config.username && config.password) {
|
|
82
|
+
await this._adminClient.auth({
|
|
83
|
+
username: config.username,
|
|
84
|
+
password: config.password,
|
|
85
|
+
grantType: "password",
|
|
86
|
+
clientId: config.clientId ?? "admin-cli",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (config.clientId && config.clientSecret) {
|
|
90
|
+
await this._adminClient.auth({
|
|
91
|
+
grantType: "client_credentials",
|
|
92
|
+
clientId: config.clientId,
|
|
93
|
+
clientSecret: config.clientSecret,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create a new realm
|
|
99
|
+
*/
|
|
100
|
+
async createRealm(config) {
|
|
101
|
+
await this._ensureAdminClient();
|
|
102
|
+
try {
|
|
103
|
+
await this._adminClient.realms.create({
|
|
104
|
+
realm: config.realm,
|
|
105
|
+
displayName: config.displayName ?? config.realm,
|
|
106
|
+
enabled: config.enabled ?? true,
|
|
107
|
+
});
|
|
108
|
+
this._log(`Created realm: ${config.realm}`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (this._isConflictError(error)) {
|
|
112
|
+
this._log(`Realm ${config.realm} already exists`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Create a new client in a realm
|
|
121
|
+
*/
|
|
122
|
+
async createClient(realm, config) {
|
|
123
|
+
await this._ensureAdminClient();
|
|
124
|
+
try {
|
|
125
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
126
|
+
await this._adminClient.clients.create({
|
|
127
|
+
clientId: config.clientId,
|
|
128
|
+
secret: config.clientSecret,
|
|
129
|
+
name: config.name ?? config.clientId,
|
|
130
|
+
description: config.description ?? "",
|
|
131
|
+
redirectUris: config.redirectUris ?? ["*"],
|
|
132
|
+
webOrigins: config.webOrigins ?? ["*"],
|
|
133
|
+
standardFlowEnabled: config.standardFlowEnabled ?? true,
|
|
134
|
+
implicitFlowEnabled: config.implicitFlowEnabled ?? true,
|
|
135
|
+
directAccessGrantsEnabled: config.directAccessGrantsEnabled ?? true,
|
|
136
|
+
serviceAccountsEnabled: config.serviceAccountsEnabled ?? true,
|
|
137
|
+
authorizationServicesEnabled: config.authorizationServicesEnabled ?? true,
|
|
138
|
+
publicClient: config.publicClient ?? false,
|
|
139
|
+
enabled: true,
|
|
140
|
+
protocol: "openid-connect",
|
|
141
|
+
fullScopeAllowed: true,
|
|
142
|
+
attributes: config.attributes,
|
|
143
|
+
defaultClientScopes: config.defaultClientScopes,
|
|
144
|
+
optionalClientScopes: config.optionalClientScopes,
|
|
145
|
+
});
|
|
146
|
+
this._log(`Created client: ${config.clientId}`);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (this._isConflictError(error)) {
|
|
150
|
+
this._log(`Client ${config.clientId} already exists`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create a group in a realm
|
|
159
|
+
*/
|
|
160
|
+
async createGroup(realm, config) {
|
|
161
|
+
await this._ensureAdminClient();
|
|
162
|
+
try {
|
|
163
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
164
|
+
await this._adminClient.groups.create({
|
|
165
|
+
name: config.name,
|
|
166
|
+
});
|
|
167
|
+
this._log(`Created group: ${config.name}`);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (this._isConflictError(error)) {
|
|
171
|
+
this._log(`Group ${config.name} already exists`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Create a user in a realm with optional group membership
|
|
180
|
+
*/
|
|
181
|
+
async createUser(realm, config) {
|
|
182
|
+
await this._ensureAdminClient();
|
|
183
|
+
try {
|
|
184
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
185
|
+
// Create user
|
|
186
|
+
const createResponse = await this._adminClient.users.create({
|
|
187
|
+
username: config.username,
|
|
188
|
+
email: config.email,
|
|
189
|
+
firstName: config.firstName,
|
|
190
|
+
lastName: config.lastName,
|
|
191
|
+
enabled: config.enabled ?? true,
|
|
192
|
+
emailVerified: config.emailVerified ?? true,
|
|
193
|
+
});
|
|
194
|
+
this._log(`Created user: ${config.username}`);
|
|
195
|
+
const userId = createResponse.id;
|
|
196
|
+
// Set password if provided
|
|
197
|
+
if (config.password) {
|
|
198
|
+
await this._adminClient.users.resetPassword({
|
|
199
|
+
id: userId,
|
|
200
|
+
credential: {
|
|
201
|
+
type: "password",
|
|
202
|
+
value: config.password,
|
|
203
|
+
temporary: config.temporary ?? false,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Add to groups if specified
|
|
208
|
+
if (config.groups?.length) {
|
|
209
|
+
for (const groupName of config.groups) {
|
|
210
|
+
await this._addUserToGroup(realm, userId, groupName);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (this._isConflictError(error)) {
|
|
216
|
+
this._log(`User ${config.username} already exists`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get all users in a realm
|
|
225
|
+
*/
|
|
226
|
+
async getUsers(realm) {
|
|
227
|
+
await this._ensureAdminClient();
|
|
228
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
229
|
+
const users = await this._adminClient.users.find();
|
|
230
|
+
return users.map((u) => ({
|
|
231
|
+
username: u.username,
|
|
232
|
+
email: u.email,
|
|
233
|
+
firstName: u.firstName,
|
|
234
|
+
lastName: u.lastName,
|
|
235
|
+
enabled: u.enabled,
|
|
236
|
+
emailVerified: u.emailVerified,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get all groups in a realm
|
|
241
|
+
*/
|
|
242
|
+
async getGroups(realm) {
|
|
243
|
+
await this._ensureAdminClient();
|
|
244
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
245
|
+
const groups = await this._adminClient.groups.find();
|
|
246
|
+
return groups.map((g) => ({ name: g.name }));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Delete a user from a realm
|
|
250
|
+
*/
|
|
251
|
+
async deleteUser(realm, username) {
|
|
252
|
+
await this._ensureAdminClient();
|
|
253
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
254
|
+
const users = await this._adminClient.users.find({ username });
|
|
255
|
+
if (users.length > 0) {
|
|
256
|
+
await this._adminClient.users.del({ id: users[0].id });
|
|
257
|
+
this._log(`Deleted user: ${username}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Delete a group from a realm
|
|
262
|
+
*/
|
|
263
|
+
async deleteGroup(realm, groupName) {
|
|
264
|
+
await this._ensureAdminClient();
|
|
265
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
266
|
+
const groups = await this._adminClient.groups.find({ search: groupName });
|
|
267
|
+
const group = groups.find((g) => g.name === groupName);
|
|
268
|
+
if (group) {
|
|
269
|
+
await this._adminClient.groups.del({ id: group.id });
|
|
270
|
+
this._log(`Deleted group: ${groupName}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Delete a realm
|
|
275
|
+
*/
|
|
276
|
+
async deleteRealm(realm) {
|
|
277
|
+
await this._ensureAdminClient();
|
|
278
|
+
try {
|
|
279
|
+
await this._adminClient.realms.del({ realm });
|
|
280
|
+
this._log(`Deleted realm: ${realm}`);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
this._log(`Failed to delete realm ${realm}: ${error}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Teardown Keycloak deployment
|
|
288
|
+
*/
|
|
289
|
+
async teardown() {
|
|
290
|
+
await this.k8sClient.deleteNamespace(this.deploymentConfig.namespace);
|
|
291
|
+
this._log(`Keycloak deployment torn down`);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Wait for Keycloak to be ready
|
|
295
|
+
*/
|
|
296
|
+
async waitUntilReady(timeout = 300) {
|
|
297
|
+
this._log(`Waiting for Keycloak to be ready...`);
|
|
298
|
+
await this.k8sClient.waitForStatefulSetReady(this.deploymentConfig.namespace, this.deploymentConfig.releaseName, timeout);
|
|
299
|
+
}
|
|
300
|
+
// Private methods
|
|
301
|
+
_buildDeploymentConfig(options) {
|
|
302
|
+
return {
|
|
303
|
+
namespace: options.namespace ?? DEFAULT_KEYCLOAK_CONFIG.namespace,
|
|
304
|
+
releaseName: options.releaseName ?? DEFAULT_KEYCLOAK_CONFIG.releaseName,
|
|
305
|
+
valuesFile: options.valuesFile ?? DEFAULT_CONFIG_PATHS.valuesFile,
|
|
306
|
+
adminUser: options.adminUser ?? DEFAULT_KEYCLOAK_CONFIG.adminUser,
|
|
307
|
+
adminPassword: options.adminPassword ?? DEFAULT_KEYCLOAK_CONFIG.adminPassword,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async _deployWithHelm() {
|
|
311
|
+
await $ `helm repo add bitnami ${BITNAMI_CHART_REPO} || true`;
|
|
312
|
+
await $ `helm repo update > /dev/null 2>&1`;
|
|
313
|
+
await $ `helm upgrade --install ${this.deploymentConfig.releaseName} ${BITNAMI_CHART_NAME} \
|
|
314
|
+
--namespace ${this.deploymentConfig.namespace} \
|
|
315
|
+
--values ${this.deploymentConfig.valuesFile} > /dev/null 2>&1`;
|
|
316
|
+
await this.waitUntilReady();
|
|
317
|
+
}
|
|
318
|
+
async _createRoute() {
|
|
319
|
+
// Use TLS edge termination with Allow policy to support both HTTP and HTTPS
|
|
320
|
+
const routeManifest = `
|
|
321
|
+
apiVersion: route.openshift.io/v1
|
|
322
|
+
kind: Route
|
|
323
|
+
metadata:
|
|
324
|
+
name: ${this.deploymentConfig.releaseName}
|
|
325
|
+
namespace: ${this.deploymentConfig.namespace}
|
|
326
|
+
labels:
|
|
327
|
+
app.kubernetes.io/name: keycloak
|
|
328
|
+
app.kubernetes.io/instance: ${this.deploymentConfig.releaseName}
|
|
329
|
+
spec:
|
|
330
|
+
to:
|
|
331
|
+
kind: Service
|
|
332
|
+
name: ${this.deploymentConfig.releaseName}
|
|
333
|
+
weight: 100
|
|
334
|
+
port:
|
|
335
|
+
targetPort: http
|
|
336
|
+
tls:
|
|
337
|
+
termination: edge
|
|
338
|
+
insecureEdgeTerminationPolicy: Allow
|
|
339
|
+
wildcardPolicy: None
|
|
340
|
+
`;
|
|
341
|
+
await $ `echo ${routeManifest} | kubectl apply -f -`;
|
|
342
|
+
}
|
|
343
|
+
async getRouteLocation() {
|
|
344
|
+
return await this.k8sClient.getRouteLocation(this.deploymentConfig.namespace, this.deploymentConfig.releaseName);
|
|
345
|
+
}
|
|
346
|
+
async _waitForKeycloak() {
|
|
347
|
+
this._log("Waiting for Keycloak API to be ready...");
|
|
348
|
+
const timeout = 300;
|
|
349
|
+
const startTime = Date.now();
|
|
350
|
+
while (true) {
|
|
351
|
+
if (await this.isRunning()) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
if ((Date.now() - startTime) / 1000 >= timeout) {
|
|
355
|
+
throw new Error("Keycloak API not ready after 5 minutes");
|
|
356
|
+
}
|
|
357
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
358
|
+
this._log(" Waiting for Keycloak API to be ready...");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async _initializeAdminClient() {
|
|
362
|
+
this._adminClient = new KeycloakAdminClient({
|
|
363
|
+
baseUrl: this.keycloakUrl,
|
|
364
|
+
realmName: "master",
|
|
365
|
+
});
|
|
366
|
+
await this._adminClient.auth({
|
|
367
|
+
username: this.deploymentConfig.adminUser,
|
|
368
|
+
password: this.deploymentConfig.adminPassword,
|
|
369
|
+
grantType: "password",
|
|
370
|
+
clientId: "admin-cli",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async _ensureAdminClient() {
|
|
374
|
+
if (!this._adminClient) {
|
|
375
|
+
throw new Error("Admin client not initialized. Call deploy() or connect() first.");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async _assignServiceAccountRoles(realm, clientId) {
|
|
379
|
+
await this._ensureAdminClient();
|
|
380
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
381
|
+
// Get service account user
|
|
382
|
+
const clients = await this._adminClient.clients.find({ clientId });
|
|
383
|
+
if (clients.length === 0) {
|
|
384
|
+
throw new Error(`Client ${clientId} not found`);
|
|
385
|
+
}
|
|
386
|
+
const client = clients[0];
|
|
387
|
+
const serviceAccountUser = await this._adminClient.clients.getServiceAccountUser({
|
|
388
|
+
id: client.id,
|
|
389
|
+
});
|
|
390
|
+
// Get realm-management client
|
|
391
|
+
const realmMgmtClients = await this._adminClient.clients.find({
|
|
392
|
+
clientId: "realm-management",
|
|
393
|
+
});
|
|
394
|
+
if (realmMgmtClients.length === 0) {
|
|
395
|
+
throw new Error("realm-management client not found");
|
|
396
|
+
}
|
|
397
|
+
const realmMgmtClient = realmMgmtClients[0];
|
|
398
|
+
// Get roles
|
|
399
|
+
const allRoles = await this._adminClient.clients.listRoles({
|
|
400
|
+
id: realmMgmtClient.id,
|
|
401
|
+
});
|
|
402
|
+
const rolesToAssign = allRoles.filter((r) => SERVICE_ACCOUNT_ROLES.includes(r.name));
|
|
403
|
+
if (rolesToAssign.length > 0) {
|
|
404
|
+
await this._adminClient.users.addClientRoleMappings({
|
|
405
|
+
id: serviceAccountUser.id,
|
|
406
|
+
clientUniqueId: realmMgmtClient.id,
|
|
407
|
+
roles: rolesToAssign.map((r) => ({
|
|
408
|
+
id: r.id,
|
|
409
|
+
name: r.name,
|
|
410
|
+
})),
|
|
411
|
+
});
|
|
412
|
+
this._log(`Assigned service account roles: ${rolesToAssign.map((r) => r.name).join(", ")}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async _addUserToGroup(realm, userId, groupName) {
|
|
416
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
417
|
+
const groups = await this._adminClient.groups.find({ search: groupName });
|
|
418
|
+
const group = groups.find((g) => g.name === groupName);
|
|
419
|
+
if (group) {
|
|
420
|
+
await this._adminClient.users.addToGroup({
|
|
421
|
+
id: userId,
|
|
422
|
+
groupId: group.id,
|
|
423
|
+
});
|
|
424
|
+
this._log(` Added user to group: ${groupName}`);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
this._log(` Warning: Group ${groupName} not found`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
_isConflictError(error) {
|
|
431
|
+
const err = error;
|
|
432
|
+
return err.response?.status === 409 || err.status === 409;
|
|
433
|
+
}
|
|
434
|
+
_log(...args) {
|
|
435
|
+
console.log("[Keycloak]", ...args);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/deployment/keycloak/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { KeycloakHelper } from "./deployment.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type KeycloakDeploymentOptions = {
|
|
2
|
+
namespace?: string;
|
|
3
|
+
releaseName?: string;
|
|
4
|
+
valuesFile?: string;
|
|
5
|
+
adminUser?: string;
|
|
6
|
+
adminPassword?: string;
|
|
7
|
+
};
|
|
8
|
+
export type KeycloakDeploymentConfig = {
|
|
9
|
+
namespace: string;
|
|
10
|
+
releaseName: string;
|
|
11
|
+
valuesFile: string;
|
|
12
|
+
adminUser: string;
|
|
13
|
+
adminPassword: string;
|
|
14
|
+
};
|
|
15
|
+
export type KeycloakClientConfig = {
|
|
16
|
+
clientId: string;
|
|
17
|
+
clientSecret: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
redirectUris?: string[];
|
|
21
|
+
webOrigins?: string[];
|
|
22
|
+
standardFlowEnabled?: boolean;
|
|
23
|
+
implicitFlowEnabled?: boolean;
|
|
24
|
+
directAccessGrantsEnabled?: boolean;
|
|
25
|
+
serviceAccountsEnabled?: boolean;
|
|
26
|
+
authorizationServicesEnabled?: boolean;
|
|
27
|
+
publicClient?: boolean;
|
|
28
|
+
attributes?: Record<string, string>;
|
|
29
|
+
defaultClientScopes?: string[];
|
|
30
|
+
optionalClientScopes?: string[];
|
|
31
|
+
};
|
|
32
|
+
export type KeycloakUserConfig = {
|
|
33
|
+
username: string;
|
|
34
|
+
email?: string;
|
|
35
|
+
firstName?: string;
|
|
36
|
+
lastName?: string;
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
emailVerified?: boolean;
|
|
39
|
+
password?: string;
|
|
40
|
+
temporary?: boolean;
|
|
41
|
+
groups?: string[];
|
|
42
|
+
};
|
|
43
|
+
export type KeycloakGroupConfig = {
|
|
44
|
+
name: string;
|
|
45
|
+
};
|
|
46
|
+
export type KeycloakRealmConfig = {
|
|
47
|
+
realm: string;
|
|
48
|
+
displayName?: string;
|
|
49
|
+
enabled?: boolean;
|
|
50
|
+
};
|
|
51
|
+
export type KeycloakConnectionConfig = {
|
|
52
|
+
baseUrl: string;
|
|
53
|
+
realm?: string;
|
|
54
|
+
clientId?: string;
|
|
55
|
+
clientSecret?: string;
|
|
56
|
+
username?: string;
|
|
57
|
+
password?: string;
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/deployment/keycloak/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
auth:
|
|
2
|
+
environment: production
|
|
3
|
+
session:
|
|
4
|
+
secret: superSecretSecret
|
|
5
|
+
providers:
|
|
6
|
+
oidc:
|
|
7
|
+
production:
|
|
8
|
+
metadataUrl: "${KEYCLOAK_METADATA_URL}"
|
|
9
|
+
clientId: "${KEYCLOAK_CLIENT_ID}"
|
|
10
|
+
clientSecret: "${KEYCLOAK_CLIENT_SECRET}"
|
|
11
|
+
prompt: auto
|
|
12
|
+
callbackUrl: "${RHDH_BASE_URL}/api/auth/oidc/handler/frame"
|
|
13
|
+
signIn:
|
|
14
|
+
resolvers:
|
|
15
|
+
- resolver: emailLocalPartMatchingUserEntityName
|
|
16
|
+
signInPage: oidc
|
|
17
|
+
catalog:
|
|
18
|
+
rules:
|
|
19
|
+
- allow: [User, Group]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Secret
|
|
3
|
+
metadata:
|
|
4
|
+
name: rhdh-secrets
|
|
5
|
+
type: Opaque
|
|
6
|
+
stringData:
|
|
7
|
+
KEYCLOAK_BASE_URL: $KEYCLOAK_BASE_URL
|
|
8
|
+
KEYCLOAK_METADATA_URL: $KEYCLOAK_METADATA_URL
|
|
9
|
+
KEYCLOAK_CLIENT_ID: $KEYCLOAK_CLIENT_ID
|
|
10
|
+
KEYCLOAK_CLIENT_SECRET: $KEYCLOAK_CLIENT_SECRET
|
|
11
|
+
KEYCLOAK_REALM: $KEYCLOAK_REALM
|
|
12
|
+
KEYCLOAK_LOGIN_REALM: $KEYCLOAK_LOGIN_REALM
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
apiVersion: rhdh.redhat.com/v1alpha3
|
|
2
|
+
kind: Backstage
|
|
3
|
+
metadata:
|
|
4
|
+
name: developer-hub
|
|
5
|
+
spec:
|
|
6
|
+
application:
|
|
7
|
+
appConfig:
|
|
8
|
+
configMaps:
|
|
9
|
+
- name: app-config-rhdh
|
|
10
|
+
mountPath: /opt/app-root/src
|
|
11
|
+
extraFiles:
|
|
12
|
+
mountPath: /opt/app-root/src
|
|
13
|
+
replicas: 1
|
|
14
|
+
route:
|
|
15
|
+
enabled: true
|
|
16
|
+
dynamicPluginsConfigMapName: dynamic-plugins
|
|
17
|
+
extraEnvs:
|
|
18
|
+
secrets:
|
|
19
|
+
- name: rhdh-secrets
|
|
20
|
+
database:
|
|
21
|
+
enableLocalDb: true
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuthProvider } from "./types.js";
|
|
1
2
|
export declare const DEFAULT_CONFIG_PATHS: {
|
|
2
3
|
appConfig: string;
|
|
3
4
|
secrets: string;
|
|
@@ -9,5 +10,10 @@ export declare const DEFAULT_CONFIG_PATHS: {
|
|
|
9
10
|
subscription: string;
|
|
10
11
|
};
|
|
11
12
|
};
|
|
13
|
+
export declare const AUTH_CONFIG_PATHS: Record<AuthProvider, {
|
|
14
|
+
appConfig: string;
|
|
15
|
+
secrets: string;
|
|
16
|
+
dynamicPlugins: string;
|
|
17
|
+
}>;
|
|
12
18
|
export declare const CHART_URL = "oci://quay.io/rhdh/chart";
|
|
13
19
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/constants.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/constants.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,eAAO,MAAM,oBAAoB;;;;;;;;;;CAyBhC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,MAAM,CACpC,YAAY,EACZ;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAwB/D,CAAC;AAEF,eAAO,MAAM,SAAS,6BAA6B,CAAC"}
|
|
@@ -2,14 +2,26 @@ import path from "path";
|
|
|
2
2
|
// Navigate from dist/deployment/rhdh/ to package root
|
|
3
3
|
const PACKAGE_ROOT = path.resolve(import.meta.dirname, "../../..");
|
|
4
4
|
export const DEFAULT_CONFIG_PATHS = {
|
|
5
|
-
appConfig: path.join(PACKAGE_ROOT, "
|
|
6
|
-
secrets: path.join(PACKAGE_ROOT, "
|
|
7
|
-
dynamicPlugins: path.join(PACKAGE_ROOT, "
|
|
5
|
+
appConfig: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/common/app-config-rhdh.yaml"),
|
|
6
|
+
secrets: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/common/rhdh-secrets.yaml"),
|
|
7
|
+
dynamicPlugins: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/common/dynamic-plugins.yaml"),
|
|
8
8
|
helm: {
|
|
9
|
-
valueFile: path.join(PACKAGE_ROOT, "
|
|
9
|
+
valueFile: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/helm/value_file.yaml"),
|
|
10
10
|
},
|
|
11
11
|
operator: {
|
|
12
|
-
subscription: path.join(PACKAGE_ROOT, "
|
|
12
|
+
subscription: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/operator/subscription.yaml"),
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export const AUTH_CONFIG_PATHS = {
|
|
16
|
+
guest: {
|
|
17
|
+
appConfig: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/auth/guest/app-config.yaml"),
|
|
18
|
+
secrets: "",
|
|
19
|
+
dynamicPlugins: "",
|
|
20
|
+
},
|
|
21
|
+
keycloak: {
|
|
22
|
+
appConfig: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/auth/keycloak/app-config.yaml"),
|
|
23
|
+
secrets: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/auth/keycloak/secrets.yaml"),
|
|
24
|
+
dynamicPlugins: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml"),
|
|
13
25
|
},
|
|
14
26
|
};
|
|
15
27
|
export const CHART_URL = "oci://quay.io/rhdh/chart";
|