Automatically import dev client when starting Keycloak server (#21844)

This commit is contained in:
Jon Koops 2023-07-20 12:10:40 +02:00 committed by GitHub
parent 776bcbcbd4
commit 2b04008b09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 141 deletions

View file

@ -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.

View file

@ -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"
]

View file

@ -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.");
}

View file

@ -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));
}

View file

@ -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));
}