8000 Client API v2 CLI client: provide help and autocomplete based on Keycloak version of the logged session by michalvavrik · Pull Request #47525 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
8000
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public static void main(String[] args) {
KcAdmV2Completer.complete(stripArgs(v2Args, COMPLETE_FLAG),
new PrintWriter(System.out, true));
} else {
Globals.main(v2Args, new KcAdmV2Cmd(), CMD, DEFAULT_CONFIG_FILE_STRING);
Globals.main(v2Args, new KcAdmV2Cmd(v2Args), CMD, DEFAULT_CONFIG_FILE_STRING);
}
}

Expand Down
29 changes: 24 additions & 5 deletions .../client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import picocli.CommandLine.Command;

import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.KcAdmMain.V2_FLAG;


/**
Expand All @@ -36,6 +37,16 @@ public class ConfigCmd extends AbstractAuthOptionsCmd {

public static final String NAME = "config";

private final boolean v2;

public ConfigCmd() {
this.v2 = false;
}

public ConfigCmd(boolean v2) {
this.v2 = v2;
}

@Override
protected void process() {

Expand All @@ -48,19 +59,27 @@ protected boolean nothingToDo() {

@Override
protected String help() {
return usage();
return usage(v2);
}

public static String usage() {
return usage(false);
}

private static String usage(boolean v2) {
String cmd = v2 ? CMD + " " + V2_FLAG : CMD;
String subcommands = v2 ? "'credentials', 'truststore', 'openapi'" : "'credentials', 'truststore'";
String helpHint = v2 ? cmd + " config SUB_COMMAND --help" : CMD + " help config SUB_COMMAND";

StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " config SUB_COMMAND [ARGUMENTS]");
out.println("Usage: " + cmd + " config SUB_COMMAND [ARGUMENTS]");
out.println();
out.println("Where SUB_COMMAND is one of: 'credentials', 'truststore'");
out.println("Where SUB_COMMAND is one of: " + subcommands);
out.println();
out.println();
out.println("Use '" + CMD + " help config SUB_COMMAND' for more info.");
out.println("Use '" + CMD + " help' for general information and a list of commands.");
out.println("Use '" + helpHint + "' for more info.");
out.println("Use '" + cmd + " help' for general information and a list of commands.");
return sb.toString();
}
}
8000
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package org.keycloak.client.admin.cli.v2;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.Path;

import org.keycloak.client.admin.cli.KcAdmMain;
import org.keycloak.client.admin.cli.commands.ConfigCmd;
import org.keycloak.client.cli.common.BaseGlobalOptionsCmd;
import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.util.JsonSerialization;

import picocli.CommandLine;
import picocli.CommandLine.Command;
Expand All @@ -17,15 +23,41 @@
import static org.keycloak.client.cli.util.OsUtil.PROMPT;

@Command(name = "kcadm",
description = "%nCOMMAND [ARGUMENTS]"
description = "%nCOMMAND [ARGUMENTS]",
footer = {"%nEnable tab completion:%n source <(kcadm.sh --v2 completion)"}
)
public class KcAdmV2Cmd extends BaseGlobalOptionsCmd {

private static final String BUNDLED_DESCRIPTOR = "/kcadm-v2-commands.json";
private static final String CONFIG_FILE_NAME = Path.of(KcAdmMain.DEFAULT_CONFIG_FILE_PATH).getFileName().toString();
private static final String CONFIG_OPTION = "--config";

private static final Path DEFAULT_CACHE_DIR =
Path.of(KcAdmMain.DEFAULT_CONFIG_FILE_PATH).getParent().resolve("command-descriptors").resolve("v2");

private final Path cacheDir;
private final String configFilePath;

@Spec
CommandSpec spec;

public KcAdmV2Cmd() {
this(DEFAULT_CACHE_DIR);
}

public KcAdmV2Cmd(Path cacheDir) {
this(cacheDir, null);
}

public KcAdmV2Cmd(String[] args) {
this(DEFAULT_CACHE_DIR, args);
}

public KcAdmV2Cmd(Path cacheDir, String[] args) {
this.cacheDir = cacheDir;
this.configFilePath = findConfigPath(args);
}

@Override
protected boolean nothingToDo() {
return true;
Expand Down Expand Up @@ -57,20 +89,72 @@ protected void printHelpIfNeeded() {
@Override
protected void configureCommandLine(CommandLine cli) {
cli.getCommandSpec().name(CMD + " " + V2_FLAG);
CommandLine configCmd = new CommandLine(new ConfigCmd());
CommandLine configCmd = new CommandLine(new ConfigCmd(true));
configCmd.getCommandSpec().usageMessage().description("Configuration management");
configCmd.getCommandSpec().removeSubcommand("credentials");
configCmd.addSubcommand("credentials", new CommandLine(new KcAdmV2ConfigCredentialsCmd(cacheDir)));
configCmd.addSubcommand("openapi", new CommandLine(new KcAdmV2ConfigOpenApiCmd(cacheDir)));
cli.addSubcommand(configCmd);
KcAdmV2CommandDescriptor descriptor = loadDescriptor();
KcAdmV2CommandBuilder.addCommands(cli, descriptor);
}

private KcAdmV2CommandDescriptor loadDescriptor() {
// TODO: fetch and cache server-specific descriptor (follow-up PR)
KcAdmV2DescriptorCache cache = new KcAdmV2DescriptorCache(cacheDir);

String serverUrl = readServerUrlFromConfig();
if (serverUrl != null) {
KcAdmV2CommandDescriptor cached = cache.loadForServer(serverUrl);
if (cached != null) {
return cached;
}
}

return loadBundledDescriptor();
}

private KcAdmV2CommandDescriptor loadBundledDescriptor() {
try (InputStream is = getClass().getResourceAsStream(BUNDLED_DESCRIPTOR)) {
private String readServerUrlFromConfig() {
if (configFilePath != null) {
String fromConfig = readServerUrlFrom(configFilePath);
if (fromConfig != null) {
return fromConfig;
}
}
String fromCacheDir = readServerUrlFrom(cacheDir.resolve(CONFIG_FILE_NAME).toString());
if (fromCacheDir != null) {
return fromCacheDir;
}
return readServerUrlFrom(KcAdmMain.DEFAULT_CONFIG_FILE_PATH);
}

static String readServerUrlFrom(String configFilePath) {
try {
File configFile = new File(configFilePath);
if (!configFile.isFile()) {
return null;
}
try (FileInputStream is = new FileInputStream(configFile)) {
ConfigData config = JsonSerialization.readValue(is, ConfigData.class);
return config.getServerUrl();
}
} catch (Exception e) {
return null;
}
}

private static String findConfigPath(String[] args) {
if (args != null) {
for (int i = 0; i < args.length - 1; i++) {
if (CONFIG_OPTION.equals(args[i])) {
return args[i + 1];
}
}
}
return null;
}

public static KcAdmV2CommandDescriptor loadBundledDescriptor() {
try (InputStream is = KcAdmV2Cmd.class.getResourceAsStream(BUNDLED_DESCRIPTOR)) {
if (is == null) {
throw new RuntimeException("Bundled command descriptor not found: " + BUNDLED_DESCRIPTOR);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.keycloak.client.admin.cli.v2;

import java.io.PrintWriter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -16,10 +17,20 @@ public class KcAdmV2Completer {
private static final String LONG_OPTION_PREFIX = "--";

public static void complete(String[] args, PrintWriter out) {
KcAdmV2Cmd rootCmd = new KcAdmV2Cmd();
completeWith(buildCli(new KcAdmV2Cmd(args)), args, out);
}

public static void complete(String[] args, PrintWriter out, Path cacheDir) {
completeWith(buildCli(new KcAdmV2Cmd(cacheDir, args)), args, out);
}

private static CommandLine buildCli(KcAdmV2Cmd rootCmd) {
CommandLine cli = new CommandLine(rootCmd);
rootCmd.configureCommandLine(cli);
return cli;
}

private static void completeWith(CommandLine cli, String[] args, PrintWriter out) {
String partial = args.length > 0 ? args[args.length - 1] : "";

if (LONG_OPTION_PREFIX.equals(partial)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.keycloak.client.admin.cli.v2;

import java.io.PrintWriter;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;

import org.keycloak.client.admin.cli.commands.ConfigCredentialsCmd;

import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;

import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.KcAdmMain.V2_FLAG;

@Command(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
class KcAdmV2ConfigCredentialsCmd extends ConfigCredentialsCmd {

private static final int DEFAULT_MANAGEMENT_PORT = 9000;
private static final String OPENAPI_PATH = "/openapi";

@Option(names = "--openapi-url", description = "URL of the OpenAPI endpoint for auto-fetching the server descriptor " +
"(default: <server-protocol>://<server-host>:" + DEFAULT_MANAGEMENT_PORT + OPENAPI_PATH + ")")
String openApiUrl;

@Spec
CommandSpec spec;

private final Path cacheDir;

KcAdmV2ConfigCredentialsCmd(Path cacheDir) {
this.cacheDir = cacheDir;
}

@Override
protected String getCommand() {
return CMD + " " + V2_FLAG;
}

@Override
protected void printExtraOptions(PrintWriter out) {
out.println(" --openapi-url URL URL of the OpenAPI endpoint for auto-fetching the server descriptor");
out.println(" (default: <server-protocol>://<server-host>:" + DEFAULT_MANAGEMENT_PORT + OPENAPI_PATH + ")");
}

@Override
public void process() {
super.process();
tryAutoFetchOpenApi();
}

private void tryAutoFetchOpenApi() {
if (server == null) {
// --status without --server — no login happened, nothing to fetch
return;
}

String url = openApiUrl;
if (url == null) {
try {
url = deriveDefaultOpenApiUrl();
} catch (Exception e) {
warnAutoFetchFailed("could not determine OpenAPI URL from server " + server
+ (e.getMessage() != null ? ": " + e.getMessage() : ""));
return;
}
}

try {
KcAdmV2CommandDescriptor descriptor = KcAdmV2ConfigOpenApiCmd.fetchDescriptorFromUrl(url);
KcAdmV2DescriptorCache cache = new KcAdmV2DescriptorCache(cacheDir);
cache.save(server, descriptor);
printToErr("OpenAPI descriptor fetched from " + url + " and cached for " + server
+ " (version " + descriptor.getVersion() + ")");
} catch (Exception e) {
warnAutoFetchFailed(url + (e.getMessage() != null ? ": " + e.getMessage() : ""));
}
}

private void warnAutoFetchFailed(String detail) {
printToErr("Failed to fetch OpenAPI descriptor from " + detail + ". "
+ "CLI commands may not match your server version. "
+ "You can use 'config openapi' to manually load the descriptor.");
}

private void printToErr(String message) {
spec.commandLine().getErr().println(message);
}

private String deriveDefaultOpenApiUrl() throws Exception {
URL serverParsed = new URL(server);
URI managementUri = new URI(serverParsed.getProtocol(), null,
serverParsed.getHost(), DEFAULT_MANAGEMENT_PORT, OPENAPI_PATH, null, null);
return managementUri.toString();
}
Comment thread
michalvavrik marked this conversation as resolved.
Resolved
}
Loading
Loading
0