keycloak_theme/src/bin/start-keycloak/start-keycloak.ts

494 lines
16 KiB
TypeScript
Raw Normal View History

2024-05-20 15:34:07 +02:00
import { readBuildOptions } from "../shared/buildOptions";
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { readMetaInfKeycloakThemes } from "../shared/metaInfKeycloakThemes";
2024-05-20 15:48:51 +02:00
import {
accountV1ThemeName,
skipBuildJarsEnvName,
containerName
} from "../shared/constants";
2024-05-20 15:34:07 +02:00
import { SemVer } from "../tools/SemVer";
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange";
import { getJarFileBasename } from "../shared/getJarFileBasename";
2024-05-17 05:13:41 +02:00
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs";
2024-05-20 15:48:51 +02:00
import {
join as pathJoin,
relative as pathRelative,
sep as pathSep,
posix as pathPosix
} from "path";
2024-05-17 05:13:41 +02:00
import * as child_process from "child_process";
2024-05-18 10:02:14 +02:00
import chalk from "chalk";
2024-05-20 02:27:40 +02:00
import chokidar from "chokidar";
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
2024-05-20 15:34:07 +02:00
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
2024-05-20 02:27:40 +02:00
import { Deferred } from "evt/tools/Deferred";
2024-05-20 15:34:07 +02:00
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import cliSelect from "cli-select";
import { isInside } from "../tools/isInside";
2024-05-20 19:30:04 +02:00
import * as runExclusive from "run-exclusive";
2024-05-18 11:40:09 +02:00
export type CliCommandOptions = CliCommandOptions_common & {
port: number;
keycloakVersion: string | undefined;
2024-05-20 15:34:07 +02:00
realmJsonFilePath: string | undefined;
2024-05-18 11:40:09 +02:00
};
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
2024-05-18 11:40:09 +02:00
exit_if_docker_not_installed: {
2024-05-18 11:09:04 +02:00
let commandOutput: Buffer | undefined = undefined;
try {
2024-05-20 15:48:51 +02:00
commandOutput = child_process.execSync("docker --version", {
stdio: ["ignore", "pipe", "ignore"]
});
2024-05-18 11:09:04 +02:00
} catch {}
2024-05-18 11:40:09 +02:00
if (commandOutput?.toString("utf8").includes("Docker")) {
break exit_if_docker_not_installed;
2024-05-18 11:09:04 +02:00
}
console.log(
[
`${chalk.red("Docker required.")}`,
2024-05-20 15:48:51 +02:00
`Install it with Docker Desktop: ${chalk.bold.underline(
"https://www.docker.com/products/docker-desktop/"
)}`,
2024-05-18 11:09:04 +02:00
`(or any other way)`
].join(" ")
);
process.exit(1);
}
2024-05-18 11:40:09 +02:00
exit_if_docker_not_running: {
2024-05-18 11:09:04 +02:00
let isDockerRunning: boolean;
try {
2024-05-20 15:48:51 +02:00
child_process.execSync("docker info", { stdio: "ignore" });
2024-05-18 11:09:04 +02:00
isDockerRunning = true;
} catch {
isDockerRunning = false;
}
if (isDockerRunning) {
2024-05-18 11:40:09 +02:00
break exit_if_docker_not_running;
2024-05-18 11:09:04 +02:00
}
2024-05-20 15:48:51 +02:00
console.log(
[
`${chalk.red("Docker daemon is not running.")}`,
`Please start Docker Desktop and try again.`
].join(" ")
);
2024-05-18 11:40:09 +02:00
process.exit(1);
2024-05-18 11:09:04 +02:00
}
const { cliCommandOptions } = params;
const buildOptions = readBuildOptions({ cliCommandOptions });
2024-05-18 11:40:09 +02:00
exit_if_theme_not_built: {
if (fs.existsSync(buildOptions.keycloakifyBuildDirPath)) {
break exit_if_theme_not_built;
}
console.log(
2024-05-20 15:48:51 +02:00
[
`${chalk.red("The theme has not been built.")}`,
`Please run ${chalk.bold("npx vite && npx keycloakify build")} first.`
].join(" ")
2024-05-18 11:40:09 +02:00
);
process.exit(1);
}
2024-05-17 05:13:41 +02:00
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({
2024-05-20 15:48:51 +02:00
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
2024-05-17 05:13:41 +02:00
});
2024-05-20 15:48:51 +02:00
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some(
({ name }) => name === accountV1ThemeName
);
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } =
await (async function getKeycloakMajor(): Promise<{
keycloakVersion: string;
keycloakMajorNumber: number;
}> {
if (cliCommandOptions.keycloakVersion !== undefined) {
return {
keycloakVersion: cliCommandOptions.keycloakVersion,
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion)
.major
};
}
2024-05-18 11:40:09 +02:00
2024-05-20 15:48:51 +02:00
console.log(
chalk.cyan("On which version of Keycloak do you want to test your theme?")
);
2024-05-18 11:40:09 +02:00
2024-05-20 15:48:51 +02:00
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 17,
cacheDirPath: buildOptions.cacheDirPath
});
2024-05-17 05:13:41 +02:00
2024-05-20 15:48:51 +02:00
console.log(`${keycloakVersion}`);
2024-05-20 02:27:40 +02:00
2024-05-20 15:48:51 +02:00
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
2024-05-17 05:13:41 +02:00
2024-05-20 15:48:51 +02:00
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
console.log(
[
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
"Please select any other Keycloak version"
].join(" ")
);
return getKeycloakMajor();
}
2024-05-17 05:13:41 +02:00
2024-05-20 15:48:51 +02:00
return { keycloakVersion, keycloakMajorNumber };
})();
2024-05-17 05:13:41 +02:00
const keycloakVersionRange: KeycloakVersionRange = (() => {
if (doesImplementAccountTheme) {
const keycloakVersionRange = (() => {
2024-05-20 15:34:07 +02:00
if (keycloakMajorVersionNumber <= 21) {
2024-05-17 05:13:41 +02:00
return "21-and-below" as const;
}
2024-05-20 15:34:07 +02:00
assert(keycloakMajorVersionNumber !== 22);
2024-05-17 05:13:41 +02:00
2024-05-20 15:34:07 +02:00
if (keycloakMajorVersionNumber === 23) {
2024-05-17 05:13:41 +02:00
return "23" as const;
}
return "24-and-above" as const;
})();
2024-05-20 15:48:51 +02:00
assert<
Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>
>();
2024-05-17 05:13:41 +02:00
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
2024-05-20 15:34:07 +02:00
if (keycloakMajorVersionNumber <= 21) {
2024-05-17 05:13:41 +02:00
return "21-and-below" as const;
}
return "22-and-above" as const;
})();
2024-05-20 15:48:51 +02:00
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme
>
>();
2024-05-17 05:13:41 +02:00
return keycloakVersionRange;
}
})();
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
2024-05-20 02:42:57 +02:00
console.log(`Using Keycloak ${chalk.bold(jarFileBasename)}`);
2024-05-17 05:13:41 +02:00
const mountTargets = buildOptions.themeNames
.map(themeName => {
2024-05-20 15:48:51 +02:00
const themeEntry = metaInfKeycloakThemes.themes.find(
({ name }) => name === themeName
);
2024-05-17 05:13:41 +02:00
assert(themeEntry !== undefined);
return themeEntry.types
.map(themeType => {
const localPathDirname = pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
themeType
);
return fs
.readdirSync(localPathDirname)
2024-05-20 15:48:51 +02:00
.filter(
fileOrDirectoryBasename =>
!fileOrDirectoryBasename.endsWith(".properties")
)
2024-05-17 05:13:41 +02:00
.map(fileOrDirectoryBasename => ({
2024-05-20 15:48:51 +02:00
localPath: pathJoin(
localPathDirname,
fileOrDirectoryBasename
),
containerPath: pathPosix.join(
"/",
"opt",
"keycloak",
"themes",
themeName,
themeType,
fileOrDirectoryBasename
)
2024-05-17 05:13:41 +02:00
}));
})
.flat();
})
.flat();
try {
2024-05-20 15:48:51 +02:00
child_process.execSync(`docker rm --force ${containerName}`, {
stdio: "ignore"
});
2024-05-17 05:13:41 +02:00
} catch {}
2024-05-20 15:34:07 +02:00
const realmJsonFilePath = await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) {
2024-05-20 15:48:51 +02:00
console.log(
chalk.green(
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
)
);
2024-05-20 15:34:07 +02:00
return getAbsoluteAndInOsFormatPath({
2024-05-20 15:48:51 +02:00
pathIsh: cliCommandOptions.realmJsonFilePath,
cwd: process.cwd()
2024-05-20 15:34:07 +02:00
});
}
2024-05-20 15:48:51 +02:00
const dirPath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak"
);
2024-05-20 15:34:07 +02:00
2024-05-20 15:48:51 +02:00
const filePath = pathJoin(
dirPath,
`myrealm-realm-${keycloakMajorVersionNumber}.json`
);
2024-05-20 15:34:07 +02:00
if (fs.existsSync(filePath)) {
return filePath;
}
2024-05-20 15:48:51 +02:00
console.log(
`${chalk.yellow(
`Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.`
)}`
);
2024-05-20 15:34:07 +02:00
console.log(chalk.cyan("Select what configuration to use:"));
const { value } = await cliSelect<string>({
2024-05-20 15:48:51 +02:00
values: [
...fs
.readdirSync(dirPath)
.filter(fileBasename => fileBasename.endsWith(".json")),
"none"
]
2024-05-20 15:34:07 +02:00
}).catch(() => {
process.exit(-1);
});
if (value === "none") {
return undefined;
}
return pathJoin(dirPath, value);
})();
const spawnArgs = [
2024-05-17 05:13:41 +02:00
"docker",
[
"run",
2024-05-18 11:40:09 +02:00
...["-p", `${cliCommandOptions.port}:8080`],
2024-05-17 05:13:41 +02:00
...["--name", containerName],
...["-e", "KEYCLOAK_ADMIN=admin"],
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
2024-05-20 15:48:51 +02:00
...(realmJsonFilePath === undefined
? []
: [
"-v",
`${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json`
]),
...[
"-v",
`${pathJoin(
buildOptions.keycloakifyBuildDirPath,
jarFileBasename
)}:/opt/keycloak/providers/keycloak-theme.jar`
],
...(keycloakMajorVersionNumber <= 20
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
: []),
...mountTargets
.map(({ localPath, containerPath }) => [
"-v",
`${localPath}:${containerPath}:rw`
])
.flat(),
2024-05-18 07:53:06 +02:00
`quay.io/keycloak/keycloak:${keycloakVersion}`,
2024-05-17 05:13:41 +02:00
"start-dev",
2024-05-20 15:48:51 +02:00
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
? ["--features=declarative-user-profile"]
: []),
2024-05-20 15:34:07 +02:00
...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
2024-05-17 05:13:41 +02:00
],
{
2024-05-20 15:48:51 +02:00
cwd: buildOptions.keycloakifyBuildDirPath
2024-05-17 05:13:41 +02:00
}
2024-05-20 02:27:40 +02:00
] as const;
2024-05-20 15:34:07 +02:00
console.log(JSON.stringify(spawnArgs, null, 2));
const child = child_process.spawn(...spawnArgs);
2024-05-17 05:13:41 +02:00
2024-05-18 07:53:06 +02:00
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
2024-05-17 05:13:41 +02:00
2024-05-20 02:27:40 +02:00
child.on("exit", process.exit);
const srcDirPath = pathJoin(buildOptions.reactAppRootDirPath, "src");
2024-05-20 02:27:40 +02:00
2024-05-18 10:02:14 +02:00
{
const handler = async (data: Buffer) => {
if (!data.toString("utf8").includes("Listening on: http://0.0.0.0:8080")) {
return;
}
child.stdout.off("data", handler);
await new Promise(resolve => setTimeout(resolve, 1_000));
console.log(
[
"",
`${chalk.green("Your theme is accessible at:")}`,
2024-05-20 15:48:51 +02:00
`${chalk.green("➜")} ${chalk.cyan.bold(
"https://my-theme.keycloakify.dev/"
)}`,
"",
"You can login with the following credentials:",
`- username: ${chalk.cyan.bold("testuser")}`,
`- password: ${chalk.cyan.bold("password123")}`,
2024-05-20 02:27:40 +02:00
"",
2024-05-20 15:48:51 +02:00
`Keycloak Admin console: ${chalk.cyan.bold(
`http://localhost:${cliCommandOptions.port}`
)}`,
`- user: ${chalk.cyan.bold("admin")}`,
2024-05-20 02:27:40 +02:00
`- password: ${chalk.cyan.bold("admin")}`,
"",
2024-05-20 15:48:51 +02:00
`Watching for changes in ${chalk.bold(
`.${pathSep}${pathRelative(process.cwd(), srcDirPath)}`
)}`
2024-05-18 10:02:14 +02:00
].join("\n")
);
};
child.stdout.on("data", handler);
}
2024-05-20 02:27:40 +02:00
{
2024-05-20 19:30:04 +02:00
const runBuildKeycloakTheme = runExclusive.build(async () => {
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
2024-05-20 15:34:07 +02:00
2024-05-20 19:30:04 +02:00
{
const dResult = new Deferred<{ isSuccess: boolean }>();
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
const child = child_process.spawn("npx", ["vite", "build"], {
cwd: buildOptions.reactAppRootDirPath,
env: process.env
});
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
child.stdout.on("data", data => {
if (data.toString("utf8").includes("gzip:")) {
return;
}
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
process.stdout.write(data);
});
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
child.stderr.on("data", data => process.stderr.write(data));
2024-05-20 15:34:07 +02:00
2024-05-20 19:30:04 +02:00
child.on("exit", code => dResult.resolve({ isSuccess: code === 0 }));
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
const { isSuccess } = await dResult.pr;
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
if (!isSuccess) {
return;
2024-05-20 15:48:51 +02:00
}
2024-05-20 19:30:04 +02:00
}
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
{
const dResult = new Deferred<{ isSuccess: boolean }>();
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
const child = child_process.spawn("npx", ["keycloakify", "build"], {
cwd: buildOptions.reactAppRootDirPath,
env: {
...process.env,
[skipBuildJarsEnvName]: "true"
}
});
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
child.on("exit", code => {
if (code !== 0) {
console.log(chalk.yellow("Theme not updated, build failed"));
return;
}
2024-05-20 02:27:40 +02:00
2024-05-20 19:30:04 +02:00
console.log(chalk.green("Rebuild done"));
});
2024-05-20 02:42:57 +02:00
2024-05-20 19:30:04 +02:00
child.on("exit", code => dResult.resolve({ isSuccess: code === 0 }));
const { isSuccess } = await dResult.pr;
if (!isSuccess) {
return;
}
}
});
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
chokidar
.watch([srcDirPath, getThisCodebaseRootDirPath()], {
ignoreInitial: true
})
.on("all", async (...[, filePath]) => {
for (const dir1 of ["src", "."]) {
for (const dir2 of ["bin", "vite-plugin"]) {
if (
isInside({
dirPath: pathJoin(
getThisCodebaseRootDirPath(),
dir1,
dir2
),
filePath
})
) {
2024-05-20 15:48:51 +02:00
return;
}
2024-05-20 19:30:04 +02:00
}
2024-05-20 15:48:51 +02:00
}
2024-05-20 19:30:04 +02:00
await waitForDebounce();
runBuildKeycloakTheme();
2024-05-20 15:48:51 +02:00
});
2024-05-20 02:27:40 +02:00
}
}