keycloak start command options support in config

This commit is contained in:
Joseph Garrone 2024-08-17 23:20:52 +02:00
parent 66623e3324
commit 02f2124126
7 changed files with 254 additions and 100 deletions

View File

@ -314,7 +314,7 @@ export async function generateResourcesForMainTheme(params: {
} }
const { extractedDirPath } = await downloadAndExtractArchive({ const { extractedDirPath } = await downloadAndExtractArchive({
url: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar", urlOrPath: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar",
cacheDirPath: buildContext.cacheDirPath, cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions, fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages", uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages",

View File

@ -61,6 +61,18 @@ export type BuildContext = {
keycloakVersionRange: KeycloakVersionRange; keycloakVersionRange: KeycloakVersionRange;
jarFileBasename: string; jarFileBasename: string;
}[]; }[];
startKeycloakOptions: {
dockerImage:
| {
reference: string;
tag: string;
}
| undefined;
dockerExtraArgs: string[];
keycloakExtraArgs: string[];
extensionJars: ({ type: "path"; path: string } | { type: "url"; url: string })[];
realmJsonFilePath: string | undefined;
};
}; };
assert<Equals<keyof BuildContext["implementedThemeTypes"], ThemeType | "email">>(); assert<Equals<keyof BuildContext["implementedThemeTypes"], ThemeType | "email">>();
@ -75,6 +87,13 @@ export type BuildOptions = {
loginThemeResourcesFromKeycloakVersion?: string; loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string; keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: string; kcContextExclusionsFtl?: string;
startKeycloakOptions?: {
dockerImage?: string;
dockerExtraArgs?: string[];
keycloakExtraArgs?: string[];
extensionJars?: string[];
realmJsonFilePath?: string;
};
} & BuildOptions.AccountThemeImplAndKeycloakVersionTargets; } & BuildOptions.AccountThemeImplAndKeycloakVersionTargets;
export namespace BuildOptions { export namespace BuildOptions {
@ -301,6 +320,22 @@ export function getBuildContext(params: {
return id<z.ZodType<TargetType>>(zTargetType); return id<z.ZodType<TargetType>>(zTargetType);
})(); })();
const zStartKeycloakOptions = (() => {
type TargetType = NonNullable<BuildOptions["startKeycloakOptions"]>;
const zTargetType = z.object({
dockerImage: z.string().optional(),
extensionJars: z.array(z.string()).optional(),
realmJsonFilePath: z.string().optional(),
dockerExtraArgs: z.array(z.string()).optional(),
keycloakExtraArgs: z.array(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const zBuildOptions = (() => { const zBuildOptions = (() => {
type TargetType = BuildOptions; type TargetType = BuildOptions;
@ -321,7 +356,8 @@ export function getBuildContext(params: {
groupId: z.string().optional(), groupId: z.string().optional(),
loginThemeResourcesFromKeycloakVersion: z.string().optional(), loginThemeResourcesFromKeycloakVersion: z.string().optional(),
keycloakifyBuildDirPath: z.string().optional(), keycloakifyBuildDirPath: z.string().optional(),
kcContextExclusionsFtl: z.string().optional() kcContextExclusionsFtl: z.string().optional(),
startKeycloakOptions: zStartKeycloakOptions.optional()
}), }),
zAccountThemeImplAndKeycloakVersionTargets zAccountThemeImplAndKeycloakVersionTargets
); );
@ -891,6 +927,47 @@ export function getBuildContext(params: {
} }
return jarTargets; return jarTargets;
})() })(),
startKeycloakOptions: {
dockerImage: (() => {
if (buildOptions.startKeycloakOptions?.dockerImage === undefined) {
return undefined;
}
const [reference, tag, ...rest] =
buildOptions.startKeycloakOptions.dockerImage.split(":");
assert(
reference !== undefined && tag !== undefined && rest.length === 0,
`Invalid docker image: ${buildOptions.startKeycloakOptions.dockerImage}`
);
return { reference, tag };
})(),
dockerExtraArgs: buildOptions.startKeycloakOptions?.dockerExtraArgs ?? [],
keycloakExtraArgs: buildOptions.startKeycloakOptions?.keycloakExtraArgs ?? [],
extensionJars: (buildOptions.startKeycloakOptions?.extensionJars ?? []).map(
urlOrPath => {
if (/^https?:\/\//.test(urlOrPath)) {
return { type: "url", url: urlOrPath };
}
return {
type: "path",
path: getAbsoluteAndInOsFormatPath({
pathIsh: urlOrPath,
cwd: projectDirPath
})
};
}
),
realmJsonFilePath:
buildOptions.startKeycloakOptions?.realmJsonFilePath === undefined
? undefined
: getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
cwd: projectDirPath
})
}
}; };
} }

View File

@ -21,7 +21,7 @@ export async function downloadKeycloakDefaultTheme(params: {
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined; let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
const { extractedDirPath } = await downloadAndExtractArchive({ const { extractedDirPath } = await downloadAndExtractArchive({
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, urlOrPath: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath: buildContext.cacheDirPath, cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions, fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme", uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme",

View File

@ -40,7 +40,7 @@ async function appBuild_vite(params: {
const dIsSuccess = new Deferred<boolean>(); const dIsSuccess = new Deferred<boolean>();
console.log(chalk.blue("Running: 'npx vite build'")); console.log(chalk.blue("$ npx vite build"));
const child = child_process.spawn("npx", ["vite", "build"], { const child = child_process.spawn("npx", ["vite", "build"], {
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,
@ -145,7 +145,7 @@ async function appBuild_webpack(params: {
continue; continue;
} }
console.log(chalk.blue(`Running: '${subCommand}'`)); console.log(chalk.blue(`$ ${subCommand}`));
const child = child_process.spawn(command, args, { const child = child_process.spawn(command, args, {
cwd: commandCwd, cwd: commandCwd,

View File

@ -20,7 +20,7 @@ export async function keycloakifyBuild(params: {
const dResult = new Deferred<{ isSuccess: boolean }>(); const dResult = new Deferred<{ isSuccess: boolean }>();
console.log(chalk.blue("Running: 'npx keycloakify build'")); console.log(chalk.blue("$ npx keycloakify build"));
const child = child_process.spawn("npx", ["keycloakify", "build"], { const child = child_process.spawn("npx", ["keycloakify", "build"], {
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,

View File

@ -4,7 +4,7 @@ import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { ACCOUNT_V1_THEME_NAME, CONTAINER_NAME } from "../shared/constants"; import { ACCOUNT_V1_THEME_NAME, CONTAINER_NAME } from "../shared/constants";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
import { assert } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs"; import * as fs from "fs";
import { import {
join as pathJoin, join as pathJoin,
@ -26,6 +26,7 @@ import { keycloakifyBuild } from "./keycloakifyBuild";
import { isInside } from "../tools/isInside"; import { isInside } from "../tools/isInside";
import { existsAsync } from "../tools/fs.existsAsync"; import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm"; import { rm } from "../tools/fs.rm";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
export type CliCommandOptions = CliCommandOptions_common & { export type CliCommandOptions = CliCommandOptions_common & {
port: number; port: number;
@ -88,11 +89,14 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildContext = getBuildContext({ cliCommandOptions }); const buildContext = getBuildContext({ cliCommandOptions });
const { keycloakVersion } = await (async () => { const { dockerImageTag } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) { if (cliCommandOptions.keycloakVersion !== undefined) {
return { dockerImageTag: cliCommandOptions.keycloakVersion };
}
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
return { return {
keycloakVersion: cliCommandOptions.keycloakVersion, dockerImageTag: buildContext.startKeycloakOptions.dockerImage.tag
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion).major
}; };
} }
@ -115,10 +119,35 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(`${keycloakVersion}`); console.log(`${keycloakVersion}`);
return { keycloakVersion }; return { dockerImageTag: keycloakVersion };
})(); })();
const keycloakMajorVersionNumber = SemVer.parse(keycloakVersion).major; const keycloakMajorVersionNumber = (() => {
if (buildContext.startKeycloakOptions.dockerImage === undefined) {
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 => ({
majorVersionNumber,
index: tag.indexOf(`${majorVersionNumber}`)
}))
.filter(({ index }) => index !== -1)
.sort((a, b) => a.index - b.index);
if (wrap === undefined) {
console.warn(
chalk.yellow(
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25`
)
);
return 25;
}
return wrap.majorVersionNumber;
})();
{ {
const { isAppBuildSuccess } = await appBuild({ const { isAppBuildSuccess } = await appBuild({
@ -157,26 +186,50 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
assert(jarFilePath !== undefined); assert(jarFilePath !== undefined);
console.log(`Using ${chalk.bold(pathBasename(jarFilePath))}`); const extensionJarFilePaths = 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,
urlOrPath: extensionJar.url,
uniqueIdOfOnArchiveFile: "no extraction",
onArchiveFile: async () => {}
});
return archiveFilePath;
}
}
assert<Equals<typeof extensionJar, never>>(false);
})
);
const realmJsonFilePath = await (async () => { const realmJsonFilePath = await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) { if (cliCommandOptions.realmJsonFilePath !== undefined) {
if (cliCommandOptions.realmJsonFilePath === "none") { if (cliCommandOptions.realmJsonFilePath === "none") {
return undefined; return undefined;
} }
console.log(
chalk.green(
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
)
);
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: cliCommandOptions.realmJsonFilePath, pathIsh: cliCommandOptions.realmJsonFilePath,
cwd: process.cwd() 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 internalFilePath = await (async () => {
const dirPath = pathJoin( const dirPath = pathJoin(
getThisCodebaseRootDirPath(), getThisCodebaseRootDirPath(),
@ -281,26 +334,23 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}); });
} catch {} } catch {}
const spawnArgs = [ const dockerRunArgs: string[] = [
"docker", `-p ${cliCommandOptions.port}:8080`,
[ `--name ${CONTAINER_NAME}`,
"run", `-e KEYCLOAK_ADMIN=admin`,
...["-p", `${cliCommandOptions.port}:8080`], `-e KEYCLOAK_ADMIN_PASSWORD=admin`,
...["--name", CONTAINER_NAME],
...["-e", "KEYCLOAK_ADMIN=admin"],
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
...(realmJsonFilePath === undefined ...(realmJsonFilePath === undefined
? [] ? []
: [ : [
"-v", `-v ".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
`"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json`
]), ]),
...[ `-v "./${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
"-v", ...extensionJarFilePaths.map(
`"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar` jarFilePath =>
], `-v ".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
),
...(keycloakMajorVersionNumber <= 20 ...(keycloakMajorVersionNumber <= 20
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"] ? ["-e JAVA_OPTS=-Dkeycloak.profile=preview"]
: []), : []),
...[ ...[
...buildContext.themeNames, ...buildContext.themeNames,
@ -322,36 +372,46 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
), ),
containerDirPath: `/opt/keycloak/themes/${themeName}` containerDirPath: `/opt/keycloak/themes/${themeName}`
})) }))
.map(({ localDirPath, containerDirPath }) => [ .map(
"-v", ({ localDirPath, containerDirPath }) =>
`"${localDirPath}":${containerDirPath}:rw` `-v ".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
]) ),
.flat(),
...buildContext.environmentVariables ...buildContext.environmentVariables
.map(({ name }) => ({ name, envValue: process.env[name] })) .map(({ name }) => ({ name, envValue: process.env[name] }))
.map(({ name, envValue }) => .map(({ name, envValue }) =>
envValue === undefined ? undefined : { name, envValue } envValue === undefined ? undefined : { name, envValue }
) )
.filter(exclude(undefined)) .filter(exclude(undefined))
.map(({ name, envValue }) => [ .map(
"--env", ({ name, envValue }) =>
`${name}='${envValue.replace(/'/g, "'\\''")}'` `--env ${name}='${envValue.replace(/'/g, "'\\''")}'`
]) ),
.flat(), ...buildContext.startKeycloakOptions.dockerExtraArgs,
`quay.io/keycloak/keycloak:${keycloakVersion}`, `${buildContext.startKeycloakOptions.dockerImage?.reference ?? "quay.io/keycloak/keycloak"}:${dockerImageTag}`,
"start-dev", "start-dev",
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24 ...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
? ["--features=declarative-user-profile"] ? ["--features=declarative-user-profile"]
: []), : []),
...(realmJsonFilePath === undefined ? [] : ["--import-realm"]) ...(realmJsonFilePath === undefined ? [] : ["--import-realm"]),
], ...buildContext.startKeycloakOptions.keycloakExtraArgs
{ ];
cwd: buildContext.keycloakifyBuildDirPath,
shell: true
}
] as const;
const child = child_process.spawn(...spawnArgs); console.log(
chalk.blue(
[
`$ docker run \\`,
...dockerRunArgs.map(
(line, i, arr) => ` ${line}${arr.length - 1 === i ? "" : " \\"}`
)
].join("\n")
)
);
const child = child_process.spawn(
"docker",
["run", ...dockerRunArgs.map(line => line.split(" ")).flat()],
{ shell: true }
);
child.stdout.on("data", data => process.stdout.write(data)); child.stdout.on("data", data => process.stdout.write(data));

View File

@ -1,15 +1,15 @@
import fetch, { type FetchOptions } from "make-fetch-happen"; import fetch, { type FetchOptions } from "make-fetch-happen";
import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises"; import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises";
import { dirname as pathDirname, join as pathJoin } from "path"; import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { extractArchive } from "./extractArchive"; import { extractArchive } from "./extractArchive";
import { existsAsync } from "./fs.existsAsync"; import { existsAsync } from "./fs.existsAsync";
import * as crypto from "crypto"; import * as crypto from "crypto";
import { rm } from "./fs.rm"; import { rm } from "./fs.rm";
import * as fsPr from "fs/promises";
export async function downloadAndExtractArchive(params: { export async function downloadAndExtractArchive(params: {
url: string; urlOrPath: string;
uniqueIdOfOnArchiveFile: string; uniqueIdOfOnArchiveFile: string;
onArchiveFile: (params: { onArchiveFile: (params: {
fileRelativePath: string; fileRelativePath: string;
@ -21,15 +21,34 @@ export async function downloadAndExtractArchive(params: {
}) => Promise<void>; }) => Promise<void>;
cacheDirPath: string; cacheDirPath: string;
fetchOptions: FetchOptions | undefined; fetchOptions: FetchOptions | undefined;
}): Promise<{ extractedDirPath: string }> { }): Promise<{ extractedDirPath: string; archiveFilePath: string; }> {
const { url, uniqueIdOfOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } = const {
params; urlOrPath,
uniqueIdOfOnArchiveFile,
onArchiveFile,
cacheDirPath,
fetchOptions
} = params;
const archiveFileBasename = url.split("?")[0].split("/").reverse()[0]; const isUrl = /^https?:\/\//.test(urlOrPath);
const archiveFileBasename = isUrl
? urlOrPath.split("?")[0].split("/").reverse()[0]
: pathBasename(urlOrPath);
const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename); const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename);
download: { download: {
await mkdir(pathDirname(archiveFilePath), { recursive: true });
if (!isUrl) {
await fsPr.copyFile(urlOrPath, archiveFilePath);
break download;
}
const url = urlOrPath;
if (await existsAsync(archiveFilePath)) { if (await existsAsync(archiveFilePath)) {
const isDownloaded = await SuccessTracker.getIsDownloaded({ const isDownloaded = await SuccessTracker.getIsDownloaded({
cacheDirPath, cacheDirPath,
@ -48,8 +67,6 @@ export async function downloadAndExtractArchive(params: {
}); });
} }
await mkdir(pathDirname(archiveFilePath), { recursive: true });
const response = await fetch(url, fetchOptions); const response = await fetch(url, fetchOptions);
response.body?.setMaxListeners(Number.MAX_VALUE); response.body?.setMaxListeners(Number.MAX_VALUE);
@ -136,7 +153,7 @@ export async function downloadAndExtractArchive(params: {
}); });
} }
return { extractedDirPath }; return { extractedDirPath, archiveFilePath };
} }
type SuccessTracker = { type SuccessTracker = {