Replace development server with nightly version of Keycloak (#2395)

This commit is contained in:
Jon Koops 2022-04-08 19:32:44 +02:00 committed by GitHub
parent 4a7c0436cc
commit cabbe95557
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 989 additions and 182 deletions

View file

@ -1,43 +1,60 @@
# Keycloak Admin Console V2
# Keycloak Admin UI
This project is the next generation of the Keycloak Administration Console. It is written with React and [PatternFly 4][1].
This project is the next generation of the Keycloak Administration UI. It is written with React and [PatternFly 4](https://www.patternfly.org/v4/).
## Development Instructions
## Development
For development on this project you will need a running Keycloak server listening on port 8180.
### Prerequisites
1. Install modules
Make sure that you have Node.js version 16 (or later) installed on your system. If you do not have Node.js installed we recommend using [Node Version Manager](https://github.com/nvm-sh/nvm) to install it.
```bash
$> npm install
```
You can find out which version of Node.js you are using by running the following command:
1. Start Keycloak
* Download and run with one command
```bash
node --version
```
```bash
$> ./start.mjs
```
In order to run the Keycloak server you will also have to install the Java Development Kit (JDK). We recommend that you use the same version of the JDK as [required by the Keycloak server]((https://github.com/keycloak/keycloak/blob/main/docs/building.md#building-from-source)).
* or download Keycloak server from [keycloak downloads page][2] unpack and run it like:
### Running the Keycloak server
```bash
$> cd <unpacked download folder>/bin
$> standalone -Djboss.socket.binding.port-offset=100
```
First, ensure that all dependencies are installed locally using NPM by running:
1. Go to the clients section of the existing Keycloak Admin Console and add the client
* like this:
![realm settings](images/realm-settings.png "Realm Settings")
* or click on the "Select file" button and import `security-admin-console-v2.json`
* or run `$> ./import.mjs`
```bash
npm install
```
1. Install dependencies and run:
After the dependencies are installed we can start the Keycloak server by running the following command:
```bash
$> npm install
$> npm run start
```
```bash
npm run server: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).
Wait for the Keycloak server to be up and running and run the following command in a new terminal:
```bash
npm run server:import-client
```
You'll only have to run this command once, unless you remove the server directory or Keycloak server data.
### Running the development server
Now that the Keycloak sever is running it's time to run the development server for the Admin UI. This server is used to build the Admin UI in a manner that it can be iterated on quickly in a browser, using features such as [Hot Module Replacement (HMR) and Fast Refresh](https://www.snowpack.dev/concepts/hot-module-replacement).
To start the development server run the following command:
```bash
npm run start
```
Once the process of optimization is done your browser will automatically open your local host on port `8080`. From here you will be redirected to the Keycloak server to authenticate, which you can do with the default username and password (`admin`).
You can now start making changes to the source code, and they will be reflected in your browser.
## Building as a Keycloak theme
@ -159,6 +176,3 @@ Read more about [how to write tests](./cypress/WRITING_TESTS.md)
## License
* [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
[1]: https://www.patternfly.org/v4/
[2]: https://www.keycloak.org/downloads

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View file

@ -1,27 +0,0 @@
#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import KcAdminClient from "@keycloak/keycloak-admin-client";
const consoleClientConfig = JSON.parse(
await readFile(new URL("./security-admin-console-v2.json", import.meta.url))
);
const adminClient = new KcAdminClient.default({
baseUrl: "http://localhost:8180",
realmName: "master",
});
await adminClient.auth({
username: "admin",
password: "admin",
grantType: "password",
clientId: "admin-cli",
});
const adminConsoleClient = await adminClient.clients.find({
clientId: "security-admin-console-v2",
});
if (adminConsoleClient.length === 0) {
adminClient.clients.create(consoleClientConfig);
}

805
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,9 @@
"test": "jest",
"start:cypress": "cypress open",
"start:cypress-tests": "cypress run --browser chrome",
"prepare": "husky install"
"prepare": "husky install",
"server:start": "./scripts/start-server.mjs",
"server:import-client": "./scripts/import-client.mjs"
},
"dependencies": {
"@keycloak/keycloak-admin-client": "^18.0.0-dev.10",
@ -48,6 +50,7 @@
"@cypress/webpack-batteries-included-preprocessor": "^2.2.3",
"@cypress/webpack-preprocessor": "^5.11.1",
"@jest/types": "^27.5.1",
"@octokit/rest": "^18.12.0",
"@snowpack/app-scripts-react": "^2.0.1",
"@snowpack/plugin-postcss": "^1.4.3",
"@snowpack/plugin-react-refresh": "^2.5.0",
@ -59,12 +62,14 @@
"@types/dagre": "^0.7.47",
"@types/file-saver": "^2.0.5",
"@types/flat": "^5.0.2",
"@types/gunzip-maybe": "^1.4.0",
"@types/lodash-es": "^4.17.6",
"@types/node": "^17.0.23",
"@types/react": "^17.0.43",
"@types/react-dom": "^17.0.14",
"@types/react-router-dom": "^5.3.3",
"@types/snowpack-env": "^2.3.4",
"@types/tar-fs": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"cli-progress": "^3.10.0",
@ -79,16 +84,19 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.4",
"fork-ts-checker-webpack-plugin": "^7.2.3",
"gunzip-maybe": "^1.4.2",
"http2-proxy": "^5.0.53",
"husky": "^7.0.4",
"jest": "^27.5.1",
"lint-staged": "^12.3.7",
"node-fetch": "^3.2.3",
"postcss": "^8.4.12",
"postcss-import": "^14.1.0",
"postcss-svg": "^3.0.0",
"prettier": "^2.6.2",
"progress-promise": "^0.0.6",
"snowpack": "^3.8.6",
"tar-fs": "^2.1.1",
"ts-jest": "^27.1.4",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"

43
scripts/import-client.mjs Executable file
View file

@ -0,0 +1,43 @@
#!/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.default({
baseUrl: "http://localhost: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.");
}

87
scripts/start-server.mjs Executable file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env node
// @ts-check
import { Octokit } from "@octokit/rest";
import gunzip from "gunzip-maybe";
import fetch from "node-fetch";
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=admin2,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-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));
}

123
start.mjs
View file

@ -1,123 +0,0 @@
#!/usr/bin/env node
import http from "node:https";
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import decompress from "decompress";
import ProgressPromise from "progress-promise";
import cliProgress from "cli-progress";
import colors from "colors";
const args = process.argv.slice(2);
const version = args[0] && !args[0].startsWith("-") ? args[0] : "17.0.0";
const folder = "server";
const fileName = path.join(folder, `keycloak-${version}.tar.gz`);
const serverPath = path.join(folder, `keycloak-${version}`);
const extension = process.platform === "win32" ? ".bat" : ".sh";
if (!fs.existsSync(folder)) {
fs.mkdirSync(folder);
}
const progressTick = () => {
return new ProgressPromise((resolve, _, progress) => {
for (let i = 0; i < 10; i++) {
setTimeout(() => progress(i * 10), i * 1500);
}
setTimeout(resolve, 15000);
});
};
async function decompressKeycloak() {
try {
const progressBar = new cliProgress.Bar({
format:
"Decompress |" +
colors.cyan("{bar}") +
"| {percentage}% || ETA: {eta}s ",
barCompleteChar: "\u2588",
barIncompleteChar: "\u2591",
});
progressBar.start(100);
await Promise.all([
decompress(fileName, folder),
progressTick()
.progress((value) => progressBar.update(value))
.then(() => {
progressBar.update(100);
progressBar.stop();
console.log("\nFiles decompressed");
}),
]);
} catch (error) {
console.error(error);
}
}
const run = () => {
const proc = spawn(
path.join(serverPath, "bin", `kc${extension}`),
[
"start-dev",
"--http-port=8180",
"--features=admin2,admin-fine-grained-authz,declarative-user-profile",
...args,
],
{
env: {
KEYCLOAK_ADMIN: "admin",
KEYCLOAK_ADMIN_PASSWORD: "admin",
},
}
);
proc.stdout.on("data", (data) => {
console.log(data.toString());
});
};
const request = (url, file, progressBar) => {
http.get(url, (response) => {
if (response.statusCode === 302) {
request(response.headers.location, file, progressBar);
} else if (response.statusCode === 404) {
throw new Error(`version not found '${version}'`);
} else {
let data = 0;
progressBar.start(parseInt(response.headers["content-length"]), 0);
response.pipe(file);
response.on("data", (chunk) => {
progressBar.update(data);
data += chunk.length;
});
response.on("end", () => {
console.log("\nDownloaded keycloak");
decompressKeycloak().then(() => run());
});
}
});
};
if (!fs.existsSync(fileName)) {
const file = fs.createWriteStream(fileName);
const progressBar = new cliProgress.Bar({
format:
"Download |" +
colors.cyan("{bar}") +
"| {percentage}% || {value}/{total} Chunks",
barCompleteChar: "\u2588",
barIncompleteChar: "\u2591",
});
request(
`https://github.com/keycloak/keycloak/releases/download/${version}/keycloak-${version}.tar.gz`,
file,
progressBar
);
} else if (!fs.existsSync(serverPath)) {
decompressKeycloak().then(() => run());
} else {
run();
}