From 2b04008b097770a6a5a20587beabc299dabaf8b6 Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Thu, 20 Jul 2023 12:10:40 +0200 Subject: [PATCH] Automatically import dev client when starting Keycloak server (#21844) --- js/apps/keycloak-server/README.md | 9 +- js/apps/keycloak-server/package.json | 8 +- .../keycloak-server/scripts/import-client.mjs | 43 ----- .../keycloak-server/scripts/start-server.js | 156 ++++++++++++++++++ .../keycloak-server/scripts/start-server.mjs | 86 ---------- 5 files changed, 161 insertions(+), 141 deletions(-) delete mode 100755 js/apps/keycloak-server/scripts/import-client.mjs create mode 100755 js/apps/keycloak-server/scripts/start-server.js delete mode 100755 js/apps/keycloak-server/scripts/start-server.mjs diff --git a/js/apps/keycloak-server/README.md b/js/apps/keycloak-server/README.md index 542d855ed7..06359bd8cb 100644 --- a/js/apps/keycloak-server/README.md +++ b/js/apps/keycloak-server/README.md @@ -18,12 +18,5 @@ pnpm run start This will download the [Nightly version](https://github.com/keycloak/keycloak/releases/tag/nightly) of the Keycloak server and run it locally on port `8180`. If a previously downloaded version was found in the `server/` directory then that one will be used instead. If you want to download the latest Nightly version you can remove the server directory before running the command to start the server. -In order for the development version of the Admin UI to work you will have to import a custom client to the Keycloak server. This is only required during development as the development server for the Admin UI runs on a different port (more on that later). +In order for the development version of the Admin UI to work you will have to import a custom client to the Keycloak server. This is only required during development as the development server for the Admin UI runs on a different port. This client will be imported automatically under the name `security-admin-console-v2` when the Keycloak server starts. -Wait for the Keycloak server to be up and running and run the following command in a new terminal: - -```bash -pnpm run import-client -``` - -You'll only have to run this command once, unless you remove the server directory or Keycloak server data. diff --git a/js/apps/keycloak-server/package.json b/js/apps/keycloak-server/package.json index 6aaa1cc3b2..932ae7fc8b 100644 --- a/js/apps/keycloak-server/package.json +++ b/js/apps/keycloak-server/package.json @@ -1,13 +1,13 @@ { "name": "keycloak-server", + "type": "module", "scripts": { - "start": "./scripts/start-server.mjs", - "import-client": "wireit", + "start": "wireit", "clear-data": "rm -r ./server/data" }, "wireit": { - "import-client": { - "command": "./scripts/import-client.mjs", + "start": { + "command": "./scripts/start-server.js", "dependencies": [ "../../libs/keycloak-admin-client:build" ] diff --git a/js/apps/keycloak-server/scripts/import-client.mjs b/js/apps/keycloak-server/scripts/import-client.mjs deleted file mode 100755 index 5cbc001a4a..0000000000 --- a/js/apps/keycloak-server/scripts/import-client.mjs +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -import KcAdminClient from "@keycloak/keycloak-admin-client"; -import { readFile } from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const DIR_NAME = path.dirname(fileURLToPath(import.meta.url)); -const ADMIN_USERNAME = "admin"; -const ADMIN_PASSWORD = "admin"; - -await importClient(); - -async function importClient() { - const adminClient = new KcAdminClient({ - baseUrl: "http://127.0.0.1:8180", - realmName: "master", - }); - - await adminClient.auth({ - username: ADMIN_USERNAME, - password: ADMIN_PASSWORD, - grantType: "password", - clientId: "admin-cli", - }); - - const adminConsoleClient = await adminClient.clients.find({ - clientId: "security-admin-console-v2", - }); - - if (adminConsoleClient.length > 0) { - console.info("Client already exists, skipping import."); - return; - } - - console.info("Importing client…"); - - const configPath = path.join(DIR_NAME, "security-admin-console-v2.json"); - const config = JSON.parse(await readFile(configPath, "utf-8")); - - await adminClient.clients.create(config); - - console.info("Client imported successfully."); -} diff --git a/js/apps/keycloak-server/scripts/start-server.js b/js/apps/keycloak-server/scripts/start-server.js new file mode 100755 index 0000000000..19bb6cf366 --- /dev/null +++ b/js/apps/keycloak-server/scripts/start-server.js @@ -0,0 +1,156 @@ +#!/usr/bin/env node +import KcAdminClient from "@keycloak/keycloak-admin-client"; +import { Octokit } from "@octokit/rest"; +import gunzip from "gunzip-maybe"; +import { spawn } from "node:child_process"; +import fs from "node:fs"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { pipeline } from "node:stream/promises"; +import { fileURLToPath } from "node:url"; +import { extract } from "tar-fs"; + +const DIR_NAME = path.dirname(fileURLToPath(import.meta.url)); +const SERVER_DIR = path.resolve(DIR_NAME, "../server"); +const SCRIPT_EXTENSION = process.platform === "win32" ? ".bat" : ".sh"; +const ADMIN_USERNAME = "admin"; +const ADMIN_PASSWORD = "admin"; +const AUTH_DELAY = 5000; +const AUTH_RETRY_LIMIT = 3; + +await startServer(); + +async function startServer() { + await downloadServer(); + + console.info("Starting server…"); + + const args = process.argv.slice(2); + const child = spawn( + path.join(SERVER_DIR, `bin/kc${SCRIPT_EXTENSION}`), + [ + "start-dev", + "--http-port=8180", + "--features=account3,admin-fine-grained-authz,declarative-user-profile", + ...args, + ], + { + env: { + KEYCLOAK_ADMIN: ADMIN_USERNAME, + KEYCLOAK_ADMIN_PASSWORD: ADMIN_PASSWORD, + ...process.env, + }, + }, + ); + + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stderr); + + await wait(AUTH_DELAY); + await importClient(); +} + +async function downloadServer() { + const directoryExists = fs.existsSync(SERVER_DIR); + + if (directoryExists) { + console.info("Server installation found, skipping download."); + return; + } + + console.info("Downloading and extracting server…"); + + const nightlyAsset = await getNightlyAsset(); + const assetStream = await getAssetAsStream(nightlyAsset); + + await extractTarball(assetStream, SERVER_DIR, { strip: 1 }); +} + +async function importClient() { + const adminClient = new KcAdminClient({ + baseUrl: "http://127.0.0.1:8180", + realmName: "master", + }); + + await authenticateAdminClient(adminClient); + + console.info("Checking if client already exists…"); + + const adminConsoleClient = await adminClient.clients.find({ + clientId: "security-admin-console-v2", + }); + + if (adminConsoleClient.length > 0) { + console.info("Client already exists, skipping import."); + return; + } + + console.info("Importing client…"); + + const configPath = path.join(DIR_NAME, "security-admin-console-v2.json"); + const config = JSON.parse(await readFile(configPath, "utf-8")); + + await adminClient.clients.create(config); + + console.info("Client imported successfully."); +} + +async function getNightlyAsset() { + const api = new Octokit(); + const release = await api.repos.getReleaseByTag({ + owner: "keycloak", + repo: "keycloak", + tag: "nightly", + }); + + return release.data.assets.find( + ({ name }) => name === "keycloak-999.0.0-SNAPSHOT.tar.gz", + ); +} + +async function getAssetAsStream(asset) { + const response = await fetch(asset.browser_download_url); + + if (!response.ok) { + throw new Error("Something went wrong requesting the nightly release."); + } + + return response.body; +} + +function extractTarball(stream, path, options) { + return pipeline(stream, gunzip(), extract(path, options)); +} + +async function authenticateAdminClient( + adminClient, + numRetries = AUTH_RETRY_LIMIT, +) { + console.log("Authenticating admin client…"); + + try { + await adminClient.auth({ + username: ADMIN_USERNAME, + password: ADMIN_PASSWORD, + grantType: "password", + clientId: "admin-cli", + }); + } catch (error) { + if (numRetries === 0) { + throw error; + } + + console.info( + `Authentication failed, retrying in ${AUTH_DELAY / 1000} seconds.`, + ); + + await wait(AUTH_DELAY); + await authenticateAdminClient(adminClient, numRetries - 1); + } + + console.log("Admin client authenticated successfully."); +} + +async function wait(delay) { + return new Promise((resolve) => setTimeout(() => resolve(), delay)); +} diff --git a/js/apps/keycloak-server/scripts/start-server.mjs b/js/apps/keycloak-server/scripts/start-server.mjs deleted file mode 100755 index 23c7b55337..0000000000 --- a/js/apps/keycloak-server/scripts/start-server.mjs +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env node -// @ts-check -import { Octokit } from "@octokit/rest"; -import gunzip from "gunzip-maybe"; -import { spawn } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import { pipeline } from "node:stream/promises"; -import { fileURLToPath } from "node:url"; -import tar from "tar-fs"; - -const DIR_NAME = path.dirname(fileURLToPath(import.meta.url)); -const SERVER_DIR = path.resolve(DIR_NAME, "../server"); -const SCRIPT_EXTENSION = process.platform === "win32" ? ".bat" : ".sh"; - -await startServer(); - -async function startServer() { - await downloadServer(); - - console.info("Starting server…"); - - const args = process.argv.slice(2); - const child = spawn( - path.join(SERVER_DIR, `bin/kc${SCRIPT_EXTENSION}`), - [ - "start-dev", - "--http-port=8180", - "--features=account3,admin-fine-grained-authz,declarative-user-profile", - ...args, - ], - { - env: { - KEYCLOAK_ADMIN: "admin", - KEYCLOAK_ADMIN_PASSWORD: "admin", - ...process.env, - }, - } - ); - - child.stdout.pipe(process.stdout); - child.stderr.pipe(process.stderr); -} - -async function downloadServer() { - const directoryExists = fs.existsSync(SERVER_DIR); - - if (directoryExists) { - console.info("Server installation found, skipping download."); - return; - } - - console.info("Downloading and extracting server…"); - - const nightlyAsset = await getNightlyAsset(); - const assetStream = await getAssetAsStream(nightlyAsset); - - await extractTarball(assetStream, SERVER_DIR, { strip: 1 }); -} - -async function getNightlyAsset() { - const api = new Octokit(); - const release = await api.repos.getReleaseByTag({ - owner: "keycloak", - repo: "keycloak", - tag: "nightly", - }); - - return release.data.assets.find( - ({ name }) => name === "keycloak-999.0.0-SNAPSHOT.tar.gz" - ); -} - -async function getAssetAsStream(asset) { - const response = await fetch(asset.browser_download_url); - - if (!response.ok) { - throw new Error("Something went wrong requesting the nightly release."); - } - - return response.body; -} - -function extractTarball(stream, path, options) { - return pipeline(stream, gunzip(), tar.extract(path, options)); -}