From 253d7ab45b425e2afefd32db07a9a4c58ca684f4 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Fri, 14 Jun 2024 11:54:19 +0100 Subject: [PATCH] Improve Config CLI --- .../src/main/java/io/quarkus/cli/Config.java | 13 ++-- .../quarkus/cli/config/BaseConfigCommand.java | 17 ++++++ .../java/io/quarkus/cli/config/Encrypt.java | 27 +++++--- .../io/quarkus/cli/config/RemoveConfig.java | 47 ++++++++++++++ .../java/io/quarkus/cli/config/SetConfig.java | 61 +++++++------------ .../io/quarkus/cli/config/EncryptTest.java | 23 ++++--- .../quarkus/cli/config/RemoveConfigTest.java | 56 +++++++++++++++++ .../io/quarkus/cli/config/SetConfigTest.java | 41 +++---------- 8 files changed, 196 insertions(+), 89 deletions(-) create mode 100644 devtools/cli/src/main/java/io/quarkus/cli/config/RemoveConfig.java create mode 100644 devtools/cli/src/test/java/io/quarkus/cli/config/RemoveConfigTest.java diff --git a/devtools/cli/src/main/java/io/quarkus/cli/Config.java b/devtools/cli/src/main/java/io/quarkus/cli/Config.java index d86cf5cbed6c0..be4505002a1cf 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/Config.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/Config.java @@ -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 { @CommandLine.Mixin(name = "output") protected OutputOptionMixin output; + @CommandLine.Mixin + protected HelpOption helpOption; + @CommandLine.Spec protected CommandLine.Model.CommandSpec spec; @@ -22,8 +28,7 @@ public class Config implements Callable { @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; } } diff --git a/devtools/cli/src/main/java/io/quarkus/cli/config/BaseConfigCommand.java b/devtools/cli/src/main/java/io/quarkus/cli/config/BaseConfigCommand.java index 2cd4b16318d49..11618b7b6e37f 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/config/BaseConfigCommand.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/config/BaseConfigCommand.java @@ -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; @@ -29,4 +35,15 @@ protected Path projectRoot() { protected String encodeToString(byte[] data) { return Base64.getUrlEncoder().withoutPadding().encodeToString(data); } + + protected ConfigValue findKey(List 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; + } } diff --git a/devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java b/devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java index ffd51e49a5280..0fcc15649dcb5 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java @@ -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; @@ -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 { - @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") @@ -33,7 +36,7 @@ public class Encrypt extends BaseConfigCommand implements Callable { @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") @@ -43,8 +46,10 @@ public class Encrypt extends BaseConfigCommand implements Callable { @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()); @@ -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; @@ -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; diff --git a/devtools/cli/src/main/java/io/quarkus/cli/config/RemoveConfig.java b/devtools/cli/src/main/java/io/quarkus/cli/config/RemoveConfig.java new file mode 100644 index 0000000000000..a6101ead12aef --- /dev/null +++ b/devtools/cli/src/main/java/io/quarkus/cli/config/RemoveConfig.java @@ -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 { + @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 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; + } +} diff --git a/devtools/cli/src/main/java/io/quarkus/cli/config/SetConfig.java b/devtools/cli/src/main/java/io/quarkus/cli/config/SetConfig.java index a88915808094a..0a928d05ba79d 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/config/SetConfig.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/config/SetConfig.java @@ -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; @@ -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 { - @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 lines = Files.readAllLines(properties); @@ -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; } @@ -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 : "")); } @@ -93,17 +89,6 @@ public Integer call() throws Exception { } } - return 0; - } - - public static ConfigValue findKey(List 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; } } diff --git a/devtools/cli/src/test/java/io/quarkus/cli/config/EncryptTest.java b/devtools/cli/src/test/java/io/quarkus/cli/config/EncryptTest.java index 1217e2f6d4e32..4e47b4e5cf132 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/config/EncryptTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/config/EncryptTest.java @@ -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() @@ -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() @@ -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() diff --git a/devtools/cli/src/test/java/io/quarkus/cli/config/RemoveConfigTest.java b/devtools/cli/src/test/java/io/quarkus/cli/config/RemoveConfigTest.java new file mode 100644 index 0000000000000..c01aaf9092d9c --- /dev/null +++ b/devtools/cli/src/test/java/io/quarkus/cli/config/RemoveConfigTest.java @@ -0,0 +1,56 @@ +package io.quarkus.cli.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import io.quarkus.cli.CliDriver; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +public class RemoveConfigTest { + @TempDir + Path tempDir; + + @BeforeEach + void setUp() throws Exception { + Path resources = tempDir.resolve("src/main/resources"); + Files.createDirectories(resources); + Files.createFile(resources.resolve("application.properties")); + } + + @Test + void removeConfiguration() throws Exception { + Path propertiesFile = tempDir.resolve("src/main/resources/application.properties"); + Properties properties = new Properties(); + try (InputStream inputStream = propertiesFile.toUri().toURL().openStream()) { + properties.load(inputStream); + } + properties.put("foo.bar", "1234"); + try (FileOutputStream outputStream = new FileOutputStream(propertiesFile.toFile())) { + properties.store(outputStream, ""); + } + CliDriver.Result result = CliDriver.execute(tempDir, "config", "remove", "foo.bar"); + System.out.println(result.getStdout()); + assertEquals(0, result.getExitCode()); + assertTrue(config().getOptionalValue("foo.bar", String.class).isEmpty()); + } + + private SmallRyeConfig config() throws Exception { + PropertiesConfigSource propertiesConfigSource = new PropertiesConfigSource( + tempDir.resolve("src/main/resources/application.properties").toUri().toURL()); + return new SmallRyeConfigBuilder() + .withSources(propertiesConfigSource) + .build(); + } +} diff --git a/devtools/cli/src/test/java/io/quarkus/cli/config/SetConfigTest.java b/devtools/cli/src/test/java/io/quarkus/cli/config/SetConfigTest.java index 259b0afc8cece..5636398125542 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/config/SetConfigTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/config/SetConfigTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.FileOutputStream; @@ -32,15 +31,14 @@ void setUp() throws Exception { } @Test - void createConfiguration() throws Exception { - CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "--name=foo.bar", "--value=1234"); + void addConfiguration() throws Exception { + CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "foo.bar", "1234"); assertEquals(0, result.getExitCode()); - assertTrue(result.getStdout().contains("Adding foo.bar with 1234")); assertEquals("1234", config().getRawValue("foo.bar")); } @Test - void updateConfiguration() throws Exception { + void setConfiguration() throws Exception { Path propertiesFile = tempDir.resolve("src/main/resources/application.properties"); Properties properties = new Properties(); try (InputStream inputStream = propertiesFile.toUri().toURL().openStream()) { @@ -50,35 +48,15 @@ void updateConfiguration() throws Exception { try (FileOutputStream outputStream = new FileOutputStream(propertiesFile.toFile())) { properties.store(outputStream, ""); } - CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "--name=foo.bar", "--value=5678"); + CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "foo.bar", "5678"); assertEquals(0, result.getExitCode()); - assertTrue(result.getStdout().contains("Setting foo.bar to 5678")); assertEquals("5678", config().getRawValue("foo.bar")); } @Test - void deleteConfiguration() throws Exception { - Path propertiesFile = tempDir.resolve("src/main/resources/application.properties"); - Properties properties = new Properties(); - try (InputStream inputStream = propertiesFile.toUri().toURL().openStream()) { - properties.load(inputStream); - } - properties.put("foo.bar", "1234"); - try (FileOutputStream outputStream = new FileOutputStream(propertiesFile.toFile())) { - properties.store(outputStream, ""); - } - - CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "--name=foo.bar"); - assertEquals(0, result.getExitCode()); - assertTrue(result.getStdout().contains("Removing foo.bar")); - assertNull(config().getConfigValue("foo.bar").getValue()); - } - - @Test - void createEncryptedConfiguration() throws Exception { - CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "--name=foo.bar", "--value=1234", "-k"); + void addEncryptedConfiguration() throws Exception { + CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "foo.bar", "1234", "-k"); assertEquals(0, result.getExitCode()); - assertTrue(result.getStdout().contains("Adding foo.bar")); SmallRyeConfig config = config(); assertEquals("1234", config.getConfigValue("foo.bar").getValue()); @@ -86,9 +64,8 @@ void createEncryptedConfiguration() throws Exception { String encryption = config.getRawValue("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key"); assertNotNull(encryption); - result = CliDriver.execute(tempDir, "config", "set", "--name=foo.baz", "--value=5678", "-k"); + result = CliDriver.execute(tempDir, "config", "set", "foo.baz", "5678", "-k"); assertEquals(0, result.getExitCode()); - assertTrue(result.getStdout().contains("Adding foo.baz")); config = config(); @@ -98,7 +75,7 @@ void createEncryptedConfiguration() throws Exception { } @Test - void updateEncryptedConfiguration() throws Exception { + void setEncryptedConfiguration() throws Exception { Path propertiesFile = tempDir.resolve("src/main/resources/application.properties"); Properties properties = new Properties(); try (InputStream inputStream = propertiesFile.toUri().toURL().openStream()) { @@ -109,7 +86,7 @@ void updateEncryptedConfiguration() throws Exception { properties.store(outputStream, ""); } - CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "--name=foo.bar", "-k"); + CliDriver.Result result = CliDriver.execute(tempDir, "config", "set", "foo.bar", "-k"); assertEquals(0, result.getExitCode()); SmallRyeConfig config = config();