Keycloak config persistance implemented (to test)

This commit is contained in:
Joseph Garrone
2024-12-14 14:36:11 +01:00
parent 8d59fe7b67
commit 9185740d35
17 changed files with 465 additions and 228 deletions

View File

@ -3,11 +3,14 @@ import { assert, type Equals } from "tsafe/assert";
import { is } from "tsafe/is";
import { id } from "tsafe/id";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type ParsedRealmJson = {
name: string;
loginTheme?: string;
accountTheme?: string;
adminTheme?: string;
emailTheme?: string;
eventsListeners: string[];
users: {
id: string;
email: string;
@ -52,6 +55,11 @@ export function readRealmJsonFile(params: {
const zTargetType = z.object({
name: z.string(),
loginTheme: z.string().optional(),
accountTheme: z.string().optional(),
adminTheme: z.string().optional(),
emailTheme: z.string().optional(),
eventsListeners: z.array(z.string()),
users: z.array(
z.object({
id: z.string(),
@ -105,19 +113,3 @@ export function readRealmJsonFile(params: {
return parsedRealmJson;
}
export function getDefaultConfig(params: {
keycloakMajorVersionNumber: number;
}): ParsedRealmJson {
const { keycloakMajorVersionNumber } = params;
const realmJsonFilePath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
`myrealm-realm-${keycloakMajorVersionNumber}.json`
);
return readRealmJsonFile({ realmJsonFilePath });
}

View File

@ -0,0 +1,74 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import { getThisCodebaseRootDirPath } from "../../../tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import { type ParsedRealmJson, readRealmJsonFile } from "../ParsedRealmJson";
export function getDefaultRealmJsonFilePath(params: {
keycloakMajorVersionNumber: number;
}) {
const { keycloakMajorVersionNumber } = params;
return pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
"realmConfig",
"defaultConfig",
`realm-kc-${keycloakMajorVersionNumber}.json`
);
}
export const { getSupportedKeycloakMajorVersions } = (() => {
let cache: number[] | undefined = undefined;
function getSupportedKeycloakMajorVersions(): number[] {
if (cache !== undefined) {
return cache;
}
cache = fs
.readdirSync(
pathDirname(
getDefaultRealmJsonFilePath({ keycloakMajorVersionNumber: 0 })
)
)
.map(fileBasename => {
const match = fileBasename.match(/^realm-kc-(\d+)\.json$/);
if (match === null) {
return undefined;
}
const n = parseInt(match[1]);
assert(!isNaN(n));
return n;
})
.filter(exclude(undefined));
return cache;
}
return { getSupportedKeycloakMajorVersions };
})();
export function getDefaultConfig(params: {
keycloakMajorVersionNumber: number;
}): ParsedRealmJson {
const { keycloakMajorVersionNumber } = params;
assert(
getSupportedKeycloakMajorVersions().includes(keycloakMajorVersionNumber),
`We do not have a default config for Keycloak ${keycloakMajorVersionNumber}`
);
return readRealmJsonFile({
realmJsonFilePath: getDefaultRealmJsonFilePath({
keycloakMajorVersionNumber
})
});
}

View File

@ -0,0 +1 @@
export * from "./defaultConfig";

View File

@ -1,4 +1,3 @@
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
import { CONTAINER_NAME } from "../../shared/constants";
import child_process from "child_process";
import { join as pathJoin } from "path";
@ -6,7 +5,7 @@ import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred";
import { assert, is } from "tsafe/assert";
import type { BuildContext } from "../../shared/buildContext";
import * as fs from "fs/promises";
import { type ParsedRealmJson, readRealmJsonFile } from "./ParsedRealmJson";
export type BuildContextLike = {
cacheDirPath: string;
@ -17,15 +16,9 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export async function dumpContainerConfig(params: {
realmName: string;
keycloakMajorVersionNumber: number;
targetRealmConfigJsonFilePath: string;
buildContext: BuildContextLike;
}) {
const {
realmName,
keycloakMajorVersionNumber,
targetRealmConfigJsonFilePath,
buildContext
} = params;
}): Promise<ParsedRealmJson> {
const { realmName, keycloakMajorVersionNumber, buildContext } = params;
{
// https://github.com/keycloak/keycloak/issues/33800
@ -148,20 +141,7 @@ export async function dumpContainerConfig(params: {
await dCompleted.pr;
}
let sourceCode = (await fs.readFile(targetRealmConfigJsonFilePath_tmp)).toString(
"utf8"
);
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
sourceCode = await runPrettier({
filePath: targetRealmConfigJsonFilePath,
sourceCode: sourceCode
});
}
await fs.writeFile(targetRealmConfigJsonFilePath, Buffer.from(sourceCode, "utf8"));
return readRealmJsonFile({
realmJsonFilePath: targetRealmConfigJsonFilePath_tmp
});
}

View File

@ -0,0 +1 @@
export * from "./realmConfig";

View File

@ -1,15 +1,26 @@
import { assert } from "tsafe/assert";
import { getDefaultConfig, type ParsedRealmJson } from "./ParsedRealmJson";
import type { ParsedRealmJson } from "./ParsedRealmJson";
import { getDefaultConfig } from "./defaultConfig";
import type { BuildContext } from "../../shared/buildContext";
import { objectKeys } from "tsafe/objectKeys";
export type BuildContextLike = {
themeNames: BuildContext["themeNames"];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
};
assert<BuildContext extends BuildContextLike ? true : false>;
export function prepareRealmConfig(params: {
parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number;
buildContext: BuildContextLike;
}): {
realmName: string;
clientName: string;
username: string;
} {
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params;
const { username } = addOrEditTestUser({
parsedRealmJson,
@ -23,6 +34,22 @@ export function prepareRealmConfig(params: {
editAccountConsoleAndSecurityAdminConsole({ parsedRealmJson });
enableCustomThemes({
parsedRealmJson,
themeName: buildContext.themeNames[0],
implementedThemeTypes: buildContext.implementedThemeTypes
});
enable_custom_events_listeners: {
const name = "keycloakify-logging";
if (parsedRealmJson.eventsListeners.includes(name)) {
break enable_custom_events_listeners;
}
parsedRealmJson.eventsListeners.push(name);
}
return {
realmName: parsedRealmJson.name,
clientName: clientId,
@ -30,6 +57,21 @@ export function prepareRealmConfig(params: {
};
}
function enableCustomThemes(params: {
parsedRealmJson: ParsedRealmJson;
themeName: string;
implementedThemeTypes: BuildContextLike["implementedThemeTypes"];
}) {
const { parsedRealmJson, themeName, implementedThemeTypes } = params;
for (const themeType of objectKeys(implementedThemeTypes)) {
parsedRealmJson[`${themeType}Theme` as const] = implementedThemeTypes[themeType]
.isImplemented
? themeName
: "";
}
}
function addOrEditTestUser(params: {
parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number;

View File

@ -0,0 +1,108 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
import { getDefaultConfig } from "./defaultConfig";
import {
prepareRealmConfig,
type BuildContextLike as BuildContextLike_prepareRealmConfig
} from "./prepareRealmConfig";
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { existsAsync } from "../../tools/fs.existsAsync";
import { readRealmJsonFile, type ParsedRealmJson } from "./ParsedRealmJson";
import {
dumpContainerConfig,
type BuildContextLike as BuildContextLike_dumpContainerConfig
} from "./dumpContainerConfig";
export type BuildContextLike = BuildContextLike_dumpContainerConfig &
BuildContextLike_prepareRealmConfig & {
projectDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>;
export async function getRealmConfig(params: {
keycloakMajorVersionNumber: number;
realmJsonFilePath_userProvided: string | undefined;
buildContext: BuildContextLike;
}): Promise<{
realmJsonFilePath: string;
clientName: string;
realmName: string;
username: string;
onRealmConfigChange: () => Promise<void>;
}> {
const { keycloakMajorVersionNumber, realmJsonFilePath_userProvided, buildContext } =
params;
const realmJsonFilePath = pathJoin(
buildContext.projectDirPath,
`realm-kc-${keycloakMajorVersionNumber}.json`
);
const parsedRealmJson = await (async () => {
if (realmJsonFilePath_userProvided !== undefined) {
return readRealmJsonFile({
realmJsonFilePath: realmJsonFilePath_userProvided
});
}
if (await existsAsync(realmJsonFilePath)) {
return readRealmJsonFile({
realmJsonFilePath
});
}
return getDefaultConfig({ keycloakMajorVersionNumber });
})();
const { clientName, realmName, username } = prepareRealmConfig({
parsedRealmJson,
buildContext,
keycloakMajorVersionNumber
});
{
const dirPath = pathDirname(realmJsonFilePath);
if (!(await existsAsync(dirPath))) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
const writeRealmJsonFile = async (params: { parsedRealmJson: ParsedRealmJson }) => {
const { parsedRealmJson } = params;
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode,
filePath: realmJsonFilePath
});
}
fs.writeFileSync(realmJsonFilePath, sourceCode);
};
await writeRealmJsonFile({ parsedRealmJson });
async function onRealmConfigChange() {
const parsedRealmJson = await dumpContainerConfig({
buildContext,
realmName,
keycloakMajorVersionNumber
});
await writeRealmJsonFile({ parsedRealmJson });
}
return {
realmJsonFilePath,
clientName,
realmName,
username,
onRealmConfigChange
};
}