Skip to content

Commit

Permalink
Merge pull request #41203 from radcortez/fix-41131
Browse files Browse the repository at this point in the history
Improve Config CLI
  • Loading branch information
gsmet committed Jul 2, 2024
2 parents c3aa634 + 253d7ab commit ca73df4
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 89 deletions.
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

0 comments on commit ca73df4

Please sign in to comment.