Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Config CLI #41203

Merged
merged 1 commit into from
Jul 2, 2024
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
13 changes: 9 additions & 4 deletions devtools/cli/src/main/java/io/quarkus/cli/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@
import java.util.List;
import java.util.concurrent.Callable;

import io.quarkus.cli.common.HelpOption;
import io.quarkus.cli.common.OutputOptionMixin;
import io.quarkus.cli.config.Encrypt;
import io.quarkus.cli.config.RemoveConfig;
import io.quarkus.cli.config.SetConfig;
import picocli.CommandLine;
import picocli.CommandLine.Command;

@Command(name = "config", header = "Manage Quarkus configuration", subcommands = { SetConfig.class, Encrypt.class })
@Command(name = "config", header = "Manage Quarkus configuration", subcommands = { SetConfig.class, RemoveConfig.class,
Encrypt.class })
public class Config implements Callable<Integer> {
@CommandLine.Mixin(name = "output")
protected OutputOptionMixin output;

@CommandLine.Mixin
protected HelpOption helpOption;

@CommandLine.Spec
protected CommandLine.Model.CommandSpec spec;

Expand All @@ -22,8 +28,7 @@ public class Config implements Callable<Integer> {

@Override
public Integer call() throws Exception {
CommandLine.ParseResult result = spec.commandLine().getParseResult();
CommandLine appCommand = spec.subcommands().get("set");
return appCommand.execute(result.originalArgs().stream().filter(x -> !"config".equals(x)).toArray(String[]::new));
spec.commandLine().usage(System.out);
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;

import io.quarkus.cli.common.HelpOption;
import io.quarkus.cli.common.OutputOptionMixin;
import io.smallrye.config.ConfigValue;
import picocli.CommandLine;

public class BaseConfigCommand {
@CommandLine.Mixin(name = "output")
protected OutputOptionMixin output;

@CommandLine.Mixin
protected HelpOption helpOption;

@CommandLine.Spec
protected CommandLine.Model.CommandSpec spec;

Expand All @@ -29,4 +35,15 @@ protected Path projectRoot() {
protected String encodeToString(byte[] data) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
}

protected ConfigValue findKey(List<String> lines, String name) {
ConfigValue configValue = ConfigValue.builder().withName(name).build();
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (line.startsWith(configValue.getName() + "=")) {
return configValue.withValue(line.substring(name.length() + 1)).withLineNumber(i);
}
}
return configValue;
}
}
27 changes: 19 additions & 8 deletions devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.cli.config;

import static io.quarkus.devtools.messagewriter.MessageIcons.SUCCESS_ICON;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
Expand All @@ -15,16 +17,17 @@

import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

@Command(name = "encrypt", aliases = "enc", header = "Encrypt Secrets using AES/GCM/NoPadding algorithm by default")
@Command(name = "encrypt", aliases = "enc", header = "Encrypt Secrets", description = "Encrypt a Secret value using the AES/GCM/NoPadding algorithm as a default. The encryption key is generated unless a specific key is set with the --key option.")
public class Encrypt extends BaseConfigCommand implements Callable<Integer> {
@Option(required = true, names = { "-s", "--secret" }, description = "Secret")
@Parameters(index = "0", paramLabel = "SECRET", description = "The Secret value to encrypt")
String secret;

@Option(names = { "-k", "--key" }, description = "Encryption Key")
@Option(names = { "-k", "--key" }, description = "The Encryption Key")
String encryptionKey;

@Option(names = { "-f", "--format" }, description = "Encryption Key Format (base64 / plain)", defaultValue = "base64")
@Option(names = { "-f", "--format" }, description = "The Encryption Key Format (base64 / plain)", defaultValue = "base64")
KeyFormat encryptionKeyFormat;

@Option(hidden = true, names = { "-a", "--algorithm" }, description = "Algorithm", defaultValue = "AES")
Expand All @@ -33,7 +36,7 @@ public class Encrypt extends BaseConfigCommand implements Callable<Integer> {
@Option(hidden = true, names = { "-m", "--mode" }, description = "Mode", defaultValue = "GCM")
String mode;

@Option(hidden = true, names = { "-p", "--padding" }, description = "Algorithm", defaultValue = "NoPadding")
@Option(hidden = true, names = { "-p", "--padding" }, description = "Padding", defaultValue = "NoPadding")
String padding;

@Option(hidden = true, names = { "-q", "--quiet" }, defaultValue = "false")
Expand All @@ -43,8 +46,10 @@ public class Encrypt extends BaseConfigCommand implements Callable<Integer> {

@Override
public Integer call() throws Exception {
boolean generatedKey = false;
if (encryptionKey == null) {
encryptionKey = encodeToString(generateEncryptionKey().getEncoded());
generatedKey = true;
} else {
if (encryptionKeyFormat.equals(KeyFormat.base64)) {
encryptionKey = encodeToString(encryptionKey.getBytes());
Expand All @@ -67,8 +72,13 @@ public Integer call() throws Exception {

this.encryptedSecret = Base64.getUrlEncoder().withoutPadding().encodeToString((message.array()));
if (!quiet) {
System.out.println("Encrypted Secret: " + encryptedSecret);
System.out.println("Encryption Key: " + encryptionKey);
String success = SUCCESS_ICON + " The secret @|bold " + secret + "|@ was encrypted to @|bold " + encryptedSecret
+ "|@";
if (generatedKey) {
success = success + " with the generated encryption key (" + encryptionKeyFormat + "): @|bold " + encryptionKey
+ "|@";
}
output.info(success);
}

return 0;
Expand All @@ -78,7 +88,8 @@ private SecretKey generateEncryptionKey() {
try {
return KeyGenerator.getInstance(algorithm).generateKey();
} catch (Exception e) {
System.err.println("Error while generating the encryption key: " + e);
output.error("Error while generating the encryption key: ");
output.printStackTrace(e);
System.exit(-1);
}
return null;
Expand Down
47 changes: 47 additions & 0 deletions devtools/cli/src/main/java/io/quarkus/cli/config/RemoveConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.quarkus.cli.config;

import static io.quarkus.devtools.messagewriter.MessageIcons.SUCCESS_ICON;

import java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.Callable;

import io.smallrye.config.ConfigValue;
import picocli.CommandLine;

@CommandLine.Command(name = "remove", header = "Removes a configuration from application.properties")
public class RemoveConfig extends BaseConfigCommand implements Callable<Integer> {
@CommandLine.Parameters(index = "0", arity = "1", paramLabel = "NAME", description = "Configuration name")
String name;

@Override
public Integer call() throws Exception {
Path properties = projectRoot().resolve("src/main/resources/application.properties");
if (!properties.toFile().exists()) {
output.error("Could not find an application.properties file");
return -1;
}

List<String> lines = Files.readAllLines(properties);

ConfigValue configValue = findKey(lines, name);
if (configValue.getLineNumber() != -1) {
output.info(SUCCESS_ICON + " Removing configuration @|bold " + name + "|@");
lines.remove(configValue.getLineNumber());
} else {
output.error("Could not find configuration " + name);
return -1;
}

try (BufferedWriter writer = Files.newBufferedWriter(properties)) {
for (String i : lines) {
writer.write(i);
writer.newLine();
}
}

return CommandLine.ExitCode.OK;
}
}
61 changes: 23 additions & 38 deletions devtools/cli/src/main/java/io/quarkus/cli/config/SetConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.cli.config;

import static io.quarkus.devtools.messagewriter.MessageIcons.SUCCESS_ICON;

import java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -10,22 +12,28 @@
import io.smallrye.config.ConfigValue;
import io.smallrye.config.Converters;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

@CommandLine.Command(name = "set")
@Command(name = "set", header = "Sets a configuration in application.properties")
public class SetConfig extends BaseConfigCommand implements Callable<Integer> {
@CommandLine.Option(required = true, names = { "-n", "--name" }, description = "Configuration name")
@Parameters(index = "0", arity = "1", paramLabel = "NAME", description = "Configuration name")
String name;
@CommandLine.Option(names = { "-a", "--value" }, description = "Configuration value")
@Parameters(index = "1", arity = "0..1", paramLabel = "VALUE", description = "Configuration value")
String value;
@CommandLine.Option(names = { "-k", "--encrypt" }, description = "Encrypt value")
@Option(names = { "-k", "--encrypt" }, description = "Encrypt the configuration value")
boolean encrypt;

@Override
public Integer call() throws Exception {
Path properties = projectRoot().resolve("src/main/resources/application.properties");
if (!properties.toFile().exists()) {
System.out.println("Could not find an application.properties file");
return 0;
output.warn("Could not find an application.properties file, creating one now!");
Path resources = projectRoot().resolve("src/main/resources");
Files.createDirectories(resources);
Files.createFile(resources.resolve("application.properties"));
output.info(SUCCESS_ICON + " @|bold application.properties|@ file created in @|bold src/main/resources|@");
}

List<String> lines = Files.readAllLines(properties);
Expand All @@ -37,9 +45,9 @@ public Integer call() throws Exception {
if (value == null) {
value = findKey(lines, name).getValue();
}
args.add("--secret=" + value);
args.add(value);
if (value == null || value.length() == 0) {
System.out.println("Cannot encrypt an empty value");
output.error("Cannot encrypt an empty value");
return -1;
}

Expand All @@ -64,25 +72,13 @@ public Integer call() throws Exception {
}
}

int nameLineNumber = -1;
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (line.startsWith(name + "=")) {
nameLineNumber = i;
break;
}
}

if (nameLineNumber != -1) {
if (value != null) {
System.out.println("Setting " + name + " to " + value);
lines.set(nameLineNumber, name + "=" + value);
} else {
System.out.println("Removing " + name);
lines.remove(nameLineNumber);
}
ConfigValue configValue = findKey(lines, name);
String actualValue = value != null ? value : "empty value";
if (configValue.getLineNumber() != -1) {
output.info(SUCCESS_ICON + " Setting configuration @|bold " + name + "|@ to value @|bold " + actualValue + "|@");
lines.set(configValue.getLineNumber(), name + "=" + (value != null ? value : ""));
} else {
System.out.println("Adding " + name + " with " + value);
output.info(SUCCESS_ICON + " Adding configuration @|bold " + name + "|@ with value @|bold " + actualValue + "|@");
lines.add(name + "=" + (value != null ? value : ""));
}

Expand All @@ -93,17 +89,6 @@ public Integer call() throws Exception {
}
}

return 0;
}

public static ConfigValue findKey(List<String> lines, String name) {
ConfigValue configValue = ConfigValue.builder().withName(name).build();
for (int i = 0; i < lines.size(); i++) {
final String line = lines.get(i);
if (line.startsWith(configValue.getName() + "=")) {
return configValue.withValue(line.substring(name.length() + 1)).withLineNumber(i);
}
}
return configValue;
return CommandLine.ExitCode.OK;
}
}
23 changes: 16 additions & 7 deletions devtools/cli/src/test/java/io/quarkus/cli/config/EncryptTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@
import java.util.Scanner;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;

import io.quarkus.cli.CliDriver;
import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigBuilder;

@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Parsing the stdout is not working on Github Windows, maybe because of the console formatting. "
+
"I did try it in a Windows box and it works fine. Regardless, this commands is tested indirectly" +
" in SetConfigTest, which is still enabled in Windows ")
class EncryptTest {
@TempDir
Path tempDir;

@Test
void encrypt() throws Exception {
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "--secret=12345678");
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "12345678");
Scanner scanner = new Scanner(result.getStdout());
String secret = scanner.nextLine().split(": ")[1];
String encryptionKey = scanner.nextLine().split(": ")[1];
String[] split = scanner.nextLine().split(" ");
String secret = split[split.length - 8];
String encryptionKey = split[split.length - 1];

SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
Expand All @@ -35,10 +42,11 @@ void encrypt() throws Exception {

@Test
void keyPlain() throws Exception {
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "--secret=12345678", "-f=plain",
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "12345678", "-f=plain",
"--key=12345678");
Scanner scanner = new Scanner(result.getStdout());
String secret = scanner.nextLine().split(": ")[1];
String[] split = scanner.nextLine().split(" ");
String secret = split[split.length - 1];

SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
Expand All @@ -62,9 +70,10 @@ void keyPlain() throws Exception {

@Test
void keyBase64() throws Exception {
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "--secret=12345678", "--key=12345678");
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "12345678", "--key=12345678");
Scanner scanner = new Scanner(result.getStdout());
String secret = scanner.nextLine().split(": ")[1];
String[] split = scanner.nextLine().split(" ");
String secret = split[split.length - 1];

SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
Expand Down
Loading