Skip to content

Commit

Permalink
Automatically import dev client when starting Keycloak server (keyclo…
Browse files Browse the repository at this point in the history
  • Loading branch information
jonkoops authored Jul 20, 2023
1 parent 776bcbc commit 2b04008
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 141 deletions.
9 changes: 1 addition & 8 deletions js/apps/keycloak-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 4 additions & 4 deletions js/apps/keycloak-server/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
Expand Down
43 changes: 0 additions & 43 deletions js/apps/keycloak-server/scripts/import-client.mjs

This file was deleted.

156 changes: 156 additions & 0 deletions js/apps/keycloak-server/scripts/start-server.js
Original file line number Diff line number Diff line change
@@ -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));
}
86 changes: 0 additions & 86 deletions js/apps/keycloak-server/scripts/start-server.mjs

This file was deleted.

0 comments on commit 2b04008

Please sign in to comment.