Keycloak config persistance implemented (to test)
This commit is contained in:
parent
8d59fe7b67
commit
9185740d35
89
src/bin/start-keycloak/getQuayIoKeycloakDockerImageTags.ts
Normal file
89
src/bin/start-keycloak/getQuayIoKeycloakDockerImageTags.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import fetch from "make-fetch-happen";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { SemVer } from "../tools/SemVer";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
fetchOptions: BuildContext["fetchOptions"];
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>;
|
||||||
|
|
||||||
|
let cache: string[] | undefined = undefined;
|
||||||
|
|
||||||
|
export async function getKeycloakDockerImageLatestSemVerTagsForEveryMajors(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}) {
|
||||||
|
if (cache !== undefined) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const { tags } = await fetch(
|
||||||
|
"https://quay.io/v2/keycloak/keycloak/tags/list",
|
||||||
|
buildContext.fetchOptions
|
||||||
|
)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(j =>
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
tags: z.array(z.string())
|
||||||
|
})
|
||||||
|
.parse(j)
|
||||||
|
);
|
||||||
|
|
||||||
|
const arr = tags
|
||||||
|
.map(tag => ({
|
||||||
|
tag,
|
||||||
|
version: (() => {
|
||||||
|
if (tag.includes("-")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let version: SemVer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
version = SemVer.parse(tag);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
})()
|
||||||
|
}))
|
||||||
|
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
|
||||||
|
.filter(exclude(undefined));
|
||||||
|
|
||||||
|
const versionByMajor: Record<number, SemVer | undefined> = {};
|
||||||
|
|
||||||
|
for (const { version } of arr) {
|
||||||
|
const version_current = versionByMajor[version.major];
|
||||||
|
|
||||||
|
if (
|
||||||
|
version_current === undefined ||
|
||||||
|
SemVer.compare(version_current, version) === -1
|
||||||
|
) {
|
||||||
|
versionByMajor[version.major] = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
|
||||||
|
|
||||||
|
cache = Object.values(versionByMajor)
|
||||||
|
.map(version => {
|
||||||
|
assert(version !== undefined);
|
||||||
|
|
||||||
|
if (!supportedKeycloakMajorVersions.includes(version.major)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SemVer.stringify(version);
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined));
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
@ -3,11 +3,14 @@ import { assert, type Equals } from "tsafe/assert";
|
|||||||
import { is } from "tsafe/is";
|
import { is } from "tsafe/is";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
|
||||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
|
||||||
|
|
||||||
export type ParsedRealmJson = {
|
export type ParsedRealmJson = {
|
||||||
name: string;
|
name: string;
|
||||||
|
loginTheme?: string;
|
||||||
|
accountTheme?: string;
|
||||||
|
adminTheme?: string;
|
||||||
|
emailTheme?: string;
|
||||||
|
eventsListeners: string[];
|
||||||
users: {
|
users: {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -52,6 +55,11 @@ export function readRealmJsonFile(params: {
|
|||||||
|
|
||||||
const zTargetType = z.object({
|
const zTargetType = z.object({
|
||||||
name: z.string(),
|
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(
|
users: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@ -105,19 +113,3 @@ export function readRealmJsonFile(params: {
|
|||||||
|
|
||||||
return parsedRealmJson;
|
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 });
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./defaultConfig";
|
@ -1,4 +1,3 @@
|
|||||||
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
|
|
||||||
import { CONTAINER_NAME } from "../../shared/constants";
|
import { CONTAINER_NAME } from "../../shared/constants";
|
||||||
import child_process from "child_process";
|
import child_process from "child_process";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
@ -6,7 +5,7 @@ import chalk from "chalk";
|
|||||||
import { Deferred } from "evt/tools/Deferred";
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
import { assert, is } from "tsafe/assert";
|
import { assert, is } from "tsafe/assert";
|
||||||
import type { BuildContext } from "../../shared/buildContext";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import * as fs from "fs/promises";
|
import { type ParsedRealmJson, readRealmJsonFile } from "./ParsedRealmJson";
|
||||||
|
|
||||||
export type BuildContextLike = {
|
export type BuildContextLike = {
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
@ -17,15 +16,9 @@ assert<BuildContext extends BuildContextLike ? true : false>();
|
|||||||
export async function dumpContainerConfig(params: {
|
export async function dumpContainerConfig(params: {
|
||||||
realmName: string;
|
realmName: string;
|
||||||
keycloakMajorVersionNumber: number;
|
keycloakMajorVersionNumber: number;
|
||||||
targetRealmConfigJsonFilePath: string;
|
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
}) {
|
}): Promise<ParsedRealmJson> {
|
||||||
const {
|
const { realmName, keycloakMajorVersionNumber, buildContext } = params;
|
||||||
realmName,
|
|
||||||
keycloakMajorVersionNumber,
|
|
||||||
targetRealmConfigJsonFilePath,
|
|
||||||
buildContext
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
// https://github.com/keycloak/keycloak/issues/33800
|
// https://github.com/keycloak/keycloak/issues/33800
|
||||||
@ -148,20 +141,7 @@ export async function dumpContainerConfig(params: {
|
|||||||
await dCompleted.pr;
|
await dCompleted.pr;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sourceCode = (await fs.readFile(targetRealmConfigJsonFilePath_tmp)).toString(
|
return readRealmJsonFile({
|
||||||
"utf8"
|
realmJsonFilePath: targetRealmConfigJsonFilePath_tmp
|
||||||
);
|
});
|
||||||
|
|
||||||
run_prettier: {
|
|
||||||
if (!(await getIsPrettierAvailable())) {
|
|
||||||
break run_prettier;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceCode = await runPrettier({
|
|
||||||
filePath: targetRealmConfigJsonFilePath,
|
|
||||||
sourceCode: sourceCode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(targetRealmConfigJsonFilePath, Buffer.from(sourceCode, "utf8"));
|
|
||||||
}
|
}
|
||||||
|
1
src/bin/start-keycloak/realmConfig/index.ts
Normal file
1
src/bin/start-keycloak/realmConfig/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./realmConfig";
|
@ -1,15 +1,26 @@
|
|||||||
import { assert } from "tsafe/assert";
|
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: {
|
export function prepareRealmConfig(params: {
|
||||||
parsedRealmJson: ParsedRealmJson;
|
parsedRealmJson: ParsedRealmJson;
|
||||||
keycloakMajorVersionNumber: number;
|
keycloakMajorVersionNumber: number;
|
||||||
|
buildContext: BuildContextLike;
|
||||||
}): {
|
}): {
|
||||||
realmName: string;
|
realmName: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
username: string;
|
username: string;
|
||||||
} {
|
} {
|
||||||
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
|
const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params;
|
||||||
|
|
||||||
const { username } = addOrEditTestUser({
|
const { username } = addOrEditTestUser({
|
||||||
parsedRealmJson,
|
parsedRealmJson,
|
||||||
@ -23,6 +34,22 @@ export function prepareRealmConfig(params: {
|
|||||||
|
|
||||||
editAccountConsoleAndSecurityAdminConsole({ parsedRealmJson });
|
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 {
|
return {
|
||||||
realmName: parsedRealmJson.name,
|
realmName: parsedRealmJson.name,
|
||||||
clientName: clientId,
|
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: {
|
function addOrEditTestUser(params: {
|
||||||
parsedRealmJson: ParsedRealmJson;
|
parsedRealmJson: ParsedRealmJson;
|
||||||
keycloakMajorVersionNumber: number;
|
keycloakMajorVersionNumber: number;
|
||||||
|
108
src/bin/start-keycloak/realmConfig/realmConfig.ts
Normal file
108
src/bin/start-keycloak/realmConfig/realmConfig.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import { exclude } from "tsafe/exclude";
|
import { exclude } from "tsafe/exclude";
|
||||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
|
||||||
import {
|
import {
|
||||||
CONTAINER_NAME,
|
CONTAINER_NAME,
|
||||||
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
||||||
@ -13,8 +12,7 @@ import {
|
|||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
relative as pathRelative,
|
relative as pathRelative,
|
||||||
sep as pathSep,
|
sep as pathSep,
|
||||||
basename as pathBasename,
|
basename as pathBasename
|
||||||
dirname as pathDirname
|
|
||||||
} from "path";
|
} from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@ -32,6 +30,9 @@ import { existsAsync } from "../tools/fs.existsAsync";
|
|||||||
import { rm } from "../tools/fs.rm";
|
import { rm } from "../tools/fs.rm";
|
||||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||||
import { startViteDevServer } from "./startViteDevServer";
|
import { startViteDevServer } from "./startViteDevServer";
|
||||||
|
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
||||||
|
import { getKeycloakDockerImageLatestSemVerTagsForEveryMajors } from "./getQuayIoKeycloakDockerImageTags";
|
||||||
|
import { getRealmConfig } from "./realmConfig";
|
||||||
|
|
||||||
export async function command(params: {
|
export async function command(params: {
|
||||||
buildContext: BuildContext;
|
buildContext: BuildContext;
|
||||||
@ -95,9 +96,32 @@ export async function command(params: {
|
|||||||
|
|
||||||
const { cliCommandOptions, buildContext } = params;
|
const { cliCommandOptions, buildContext } = params;
|
||||||
|
|
||||||
|
const availableTags = await getKeycloakDockerImageLatestSemVerTagsForEveryMajors({
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
const { dockerImageTag } = await (async () => {
|
const { dockerImageTag } = await (async () => {
|
||||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||||
return { dockerImageTag: cliCommandOptions.keycloakVersion };
|
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
|
||||||
|
|
||||||
|
const tag = availableTags.find(tag =>
|
||||||
|
tag.startsWith(cliCommandOptions_keycloakVersion)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tag === undefined) {
|
||||||
|
console.log(
|
||||||
|
chalk.red(
|
||||||
|
[
|
||||||
|
`We could not find a Keycloak Docker image for ${cliCommandOptions_keycloakVersion}`,
|
||||||
|
`Example of valid values: --keycloak-version 26, --keycloak-version 26.0.7`
|
||||||
|
].join("\n")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dockerImageTag: tag };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
|
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
|
||||||
@ -112,50 +136,81 @@ export async function command(params: {
|
|||||||
"On which version of Keycloak do you want to test your theme?"
|
"On which version of Keycloak do you want to test your theme?"
|
||||||
),
|
),
|
||||||
chalk.gray(
|
chalk.gray(
|
||||||
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 25.0.2` (or any other version)"
|
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 26` (or any other version)"
|
||||||
)
|
)
|
||||||
].join("\n")
|
].join("\n")
|
||||||
);
|
);
|
||||||
|
|
||||||
const { keycloakVersion } = await promptKeycloakVersion({
|
const { value: tag } = await cliSelect<string>({
|
||||||
startingFromMajor: 18,
|
values: availableTags
|
||||||
excludeMajorVersions: [22],
|
}).catch(() => {
|
||||||
doOmitPatch: true,
|
process.exit(-1);
|
||||||
buildContext
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`→ ${keycloakVersion}`);
|
console.log(`→ ${tag}`);
|
||||||
|
|
||||||
return { dockerImageTag: keycloakVersion };
|
return { dockerImageTag: tag };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const keycloakMajorVersionNumber = (() => {
|
const keycloakMajorVersionNumber = (() => {
|
||||||
if (buildContext.startKeycloakOptions.dockerImage === undefined) {
|
const [wrap] = getSupportedKeycloakMajorVersions()
|
||||||
return SemVer.parse(dockerImageTag).major;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tag } = buildContext.startKeycloakOptions.dockerImage;
|
|
||||||
|
|
||||||
const [wrap] = [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
|
|
||||||
.map(majorVersionNumber => ({
|
.map(majorVersionNumber => ({
|
||||||
majorVersionNumber,
|
majorVersionNumber,
|
||||||
index: tag.indexOf(`${majorVersionNumber}`)
|
index: dockerImageTag.indexOf(`${majorVersionNumber}`)
|
||||||
}))
|
}))
|
||||||
.filter(({ index }) => index !== -1)
|
.filter(({ index }) => index !== -1)
|
||||||
.sort((a, b) => a.index - b.index);
|
.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
if (wrap === undefined) {
|
if (wrap === undefined) {
|
||||||
console.warn(
|
try {
|
||||||
chalk.yellow(
|
const version = SemVer.parse(dockerImageTag);
|
||||||
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25`
|
|
||||||
)
|
console.error(
|
||||||
);
|
chalk.yellow(
|
||||||
return 25;
|
`Keycloak version ${version.major} is not supported, supported versions are ${getSupportedKeycloakMajorVersions().join(", ")}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
`Could not determine the major Keycloak version number from the docker image tag ${dockerImageTag}. Assuming 26`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return 26;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrap.majorVersionNumber;
|
return wrap.majorVersionNumber;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
|
||||||
|
await getRealmConfig({
|
||||||
|
keycloakMajorVersionNumber,
|
||||||
|
realmJsonFilePath_userProvided: await (async () => {
|
||||||
|
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
||||||
|
return getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: cliCommandOptions.realmJsonFilePath,
|
||||||
|
cwd: process.cwd()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
||||||
|
assert(
|
||||||
|
await existsAsync(
|
||||||
|
buildContext.startKeycloakOptions.realmJsonFilePath
|
||||||
|
),
|
||||||
|
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
||||||
|
);
|
||||||
|
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})(),
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
const { isAppBuildSuccess } = await appBuild({
|
const { isAppBuildSuccess } = await appBuild({
|
||||||
buildContext
|
buildContext
|
||||||
@ -193,156 +248,39 @@ export async function command(params: {
|
|||||||
|
|
||||||
assert(jarFilePath !== undefined);
|
assert(jarFilePath !== undefined);
|
||||||
|
|
||||||
const extensionJarFilePaths = await Promise.all(
|
const extensionJarFilePaths = [
|
||||||
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
|
pathJoin(
|
||||||
switch (extensionJar.type) {
|
getThisCodebaseRootDirPath(),
|
||||||
case "path": {
|
"src",
|
||||||
assert(
|
"bin",
|
||||||
await existsAsync(extensionJar.path),
|
"start-keycloak",
|
||||||
`${extensionJar.path} does not exist`
|
KEYCLOAKIFY_LOGIN_JAR_BASENAME
|
||||||
);
|
),
|
||||||
return extensionJar.path;
|
...(await Promise.all(
|
||||||
|
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
|
||||||
|
switch (extensionJar.type) {
|
||||||
|
case "path": {
|
||||||
|
assert(
|
||||||
|
await existsAsync(extensionJar.path),
|
||||||
|
`${extensionJar.path} does not exist`
|
||||||
|
);
|
||||||
|
return extensionJar.path;
|
||||||
|
}
|
||||||
|
case "url": {
|
||||||
|
const { archiveFilePath } = await downloadAndExtractArchive({
|
||||||
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
|
fetchOptions: buildContext.fetchOptions,
|
||||||
|
url: extensionJar.url,
|
||||||
|
uniqueIdOfOnArchiveFile: "no extraction",
|
||||||
|
onArchiveFile: async () => {}
|
||||||
|
});
|
||||||
|
return archiveFilePath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "url": {
|
assert<Equals<typeof extensionJar, never>>(false);
|
||||||
const { archiveFilePath } = await downloadAndExtractArchive({
|
})
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
))
|
||||||
fetchOptions: buildContext.fetchOptions,
|
];
|
||||||
url: extensionJar.url,
|
|
||||||
uniqueIdOfOnArchiveFile: "no extraction",
|
|
||||||
onArchiveFile: async () => {}
|
|
||||||
});
|
|
||||||
return archiveFilePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert<Equals<typeof extensionJar, never>>(false);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const thisDirPath = pathJoin(
|
|
||||||
getThisCodebaseRootDirPath(),
|
|
||||||
"src",
|
|
||||||
"bin",
|
|
||||||
"start-keycloak"
|
|
||||||
);
|
|
||||||
|
|
||||||
extensionJarFilePaths.unshift(pathJoin(thisDirPath, KEYCLOAKIFY_LOGIN_JAR_BASENAME));
|
|
||||||
|
|
||||||
const getRealmJsonFilePath_defaultForKeycloakMajor = (
|
|
||||||
keycloakMajorVersionNumber: number
|
|
||||||
) => pathJoin(thisDirPath, `myrealm-realm-${keycloakMajorVersionNumber}.json`);
|
|
||||||
|
|
||||||
const realmJsonFilePath = await (async () => {
|
|
||||||
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
|
||||||
if (cliCommandOptions.realmJsonFilePath === "none") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return getAbsoluteAndInOsFormatPath({
|
|
||||||
pathIsh: cliCommandOptions.realmJsonFilePath,
|
|
||||||
cwd: process.cwd()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
|
||||||
assert(
|
|
||||||
await existsAsync(buildContext.startKeycloakOptions.realmJsonFilePath),
|
|
||||||
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
|
||||||
);
|
|
||||||
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const internalFilePath = await (async () => {
|
|
||||||
const defaultFilePath = getRealmJsonFilePath_defaultForKeycloakMajor(
|
|
||||||
keycloakMajorVersionNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(defaultFilePath)) {
|
|
||||||
return defaultFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${chalk.yellow(
|
|
||||||
`Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.`
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(chalk.cyan("Select what configuration to use:"));
|
|
||||||
|
|
||||||
const dirPath = pathDirname(defaultFilePath);
|
|
||||||
|
|
||||||
const { value } = await cliSelect<string>({
|
|
||||||
values: [
|
|
||||||
...fs
|
|
||||||
.readdirSync(dirPath)
|
|
||||||
.filter(fileBasename => fileBasename.endsWith(".json")),
|
|
||||||
"none"
|
|
||||||
]
|
|
||||||
}).catch(() => {
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (value === "none") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathJoin(dirPath, value);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (internalFilePath === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = pathJoin(
|
|
||||||
buildContext.cacheDirPath,
|
|
||||||
pathBasename(internalFilePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
filePath,
|
|
||||||
Buffer.from(
|
|
||||||
fs
|
|
||||||
.readFileSync(internalFilePath)
|
|
||||||
.toString("utf8")
|
|
||||||
.replace(/keycloakify\-starter/g, buildContext.themeNames[0])
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
return filePath;
|
|
||||||
})();
|
|
||||||
|
|
||||||
add_test_user_if_missing: {
|
|
||||||
if (realmJsonFilePath === undefined) {
|
|
||||||
break add_test_user_if_missing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const realm: Record<string, unknown> = JSON.parse(
|
|
||||||
fs.readFileSync(realmJsonFilePath).toString("utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (realm.users !== undefined) {
|
|
||||||
break add_test_user_if_missing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const realmJsonFilePath_internal = (() => {
|
|
||||||
const filePath = getRealmJsonFilePath_defaultForKeycloakMajor(
|
|
||||||
keycloakMajorVersionNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
return getRealmJsonFilePath_defaultForKeycloakMajor(25);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filePath;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const users = JSON.parse(
|
|
||||||
fs.readFileSync(realmJsonFilePath_internal).toString("utf8")
|
|
||||||
).users;
|
|
||||||
|
|
||||||
realm.users = users;
|
|
||||||
|
|
||||||
fs.writeFileSync(realmJsonFilePath, JSON.stringify(realm, null, 2), "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractThemeResourcesFromJar() {
|
async function extractThemeResourcesFromJar() {
|
||||||
await extractArchive({
|
await extractArchive({
|
||||||
@ -382,9 +320,7 @@ export async function command(params: {
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const DEFAULT_PORT = 8080;
|
const port = cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080;
|
||||||
const port =
|
|
||||||
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
|
|
||||||
|
|
||||||
const doStartDevServer = (() => {
|
const doStartDevServer = (() => {
|
||||||
const hasSpaUi =
|
const hasSpaUi =
|
||||||
@ -457,7 +393,7 @@ export async function command(params: {
|
|||||||
...(realmJsonFilePath === undefined
|
...(realmJsonFilePath === undefined
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json`
|
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/${realmName}-realm.json`
|
||||||
]),
|
]),
|
||||||
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
|
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
|
||||||
...extensionJarFilePaths.map(
|
...extensionJarFilePaths.map(
|
||||||
@ -532,7 +468,14 @@ export async function command(params: {
|
|||||||
{ shell: true }
|
{ shell: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
child.stdout.on("data", data => process.stdout.write(data));
|
child.stdout.on("data", async data => {
|
||||||
|
if (data.toString("utf8").includes("keycloakify-logging: REALM_CONFIG_CHANGED")) {
|
||||||
|
await onRealmConfigChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
child.stderr.on("data", data => process.stderr.write(data));
|
child.stderr.on("data", data => process.stderr.write(data));
|
||||||
|
|
||||||
@ -581,7 +524,7 @@ export async function command(params: {
|
|||||||
(() => {
|
(() => {
|
||||||
const url = new URL("https://my-theme.keycloakify.dev");
|
const url = new URL("https://my-theme.keycloakify.dev");
|
||||||
|
|
||||||
if (port !== DEFAULT_PORT) {
|
if (port !== 8080) {
|
||||||
url.searchParams.set("port", `${port}`);
|
url.searchParams.set("port", `${port}`);
|
||||||
}
|
}
|
||||||
if (kcHttpRelativePath !== undefined) {
|
if (kcHttpRelativePath !== undefined) {
|
||||||
@ -590,13 +533,20 @@ export async function command(params: {
|
|||||||
kcHttpRelativePath
|
kcHttpRelativePath
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (realmName !== "myrealm") {
|
||||||
|
url.searchParams.set("realm", realmName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientName !== "myclient") {
|
||||||
|
url.searchParams.set("client", clientName);
|
||||||
|
}
|
||||||
|
|
||||||
return url.href;
|
return url.href;
|
||||||
})()
|
})()
|
||||||
)}`,
|
)}`,
|
||||||
"",
|
"",
|
||||||
"You can login with the following credentials:",
|
"You can login with the following credentials:",
|
||||||
`- username: ${chalk.cyan.bold("testuser")}`,
|
`- username: ${chalk.cyan.bold(username)}`,
|
||||||
`- password: ${chalk.cyan.bold("password123")}`,
|
`- password: ${chalk.cyan.bold("password123")}`,
|
||||||
"",
|
"",
|
||||||
`Watching for changes in ${chalk.bold(
|
`Watching for changes in ${chalk.bold(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user