Skip to content

Commit

Permalink
Merge pull request #1268 from michalvavrik/feature/tls-cli-command
Browse files Browse the repository at this point in the history
Add support for Quarkus CLI TLS command
  • Loading branch information
michalvavrik authored Aug 30, 2024
2 parents 840fa82 + ac2e78b commit 590fe21
Show file tree
Hide file tree
Showing 18 changed files with 667 additions and 164 deletions.
24 changes: 24 additions & 0 deletions examples/quarkus-cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,29 @@
<artifactId>quarkus-test-cli</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-tls-registry</artifactId>
<!-- provided so that we can use API in unit test -->
<!-- however we don't actually need this dependency here -->
<!-- it is in created TLS registry app -->
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<!-- Skip unit tests as we only want to run ITs -->
<!-- This will allow us to have unit tests for apps created via CLI in src/test/java -->
<!-- Which makes them better maintainable from IDE than Java classes in resources -->
<!-- Also this way, compilation fails if some bump breaks them -->
<!-- See the 'io.quarkus.qe.surefire' package for respective unit tests -->
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.quarkus.qe;

import jakarta.inject.Inject;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import io.quarkus.qe.surefire.TlsCommandTest;
import io.quarkus.test.bootstrap.tls.GenerateCertOptions;
import io.quarkus.test.bootstrap.tls.GenerateQuarkusCaOptions;
import io.quarkus.test.bootstrap.tls.QuarkusTlsCommand;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.scenarios.annotations.DisabledOnQuarkusVersion;
import io.quarkus.test.scenarios.annotations.DisabledOnQuarkusVersions;

@DisabledOnQuarkusVersions({
// disable on 3.9-3.13
@DisabledOnQuarkusVersion(version = "3\\.(9|10|11|12|13)\\..*", reason = "https://github.com/quarkusio/quarkus/issues/42752"),
// disable on 3.14.0 and 3.14.1 as the fix is going to be backported to the next release
@DisabledOnQuarkusVersion(version = "3\\.14\\.(0|1)", reason = "https://github.com/quarkusio/quarkus/issues/42752")
})
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Tag("quarkus-cli")
@QuarkusScenario
public class QuarkusCliTlsCommandIT {

@Inject
static QuarkusTlsCommand tlsCommand;

@Order(1)
@Test
public void generateQuarkusCa() {
tlsCommand
.generateQuarkusCa()
.withOption(GenerateQuarkusCaOptions.TRUSTSTORE_LONG)
.withOption(GenerateQuarkusCaOptions.RENEW_SHORT)
.executeCommand()
.assertCommandOutputContains("Generating Quarkus Dev CA certificate")
.assertCommandOutputContains("Truststore generated successfully")
.assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Truststore generated successfully:"));
}

@Order(2)
@Test
public void generateCertificate() {
// prepares state for assertion in TlsCommandTest
var appSvcDir = tlsCommand.getApp().getServiceFolder().toAbsolutePath().toString();
tlsCommand
.generateCertificate()
.withOption(GenerateCertOptions.COMMON_NAME_SHORT, "Dumbledore")
.withOption(GenerateCertOptions.NAME_LONG, "dev-certificate")
.withOption(GenerateCertOptions.PASSWORD_LONG, "quarkus")
.withOption(GenerateCertOptions.DIRECTORY_SHORT, appSvcDir)
.executeCommand()
.assertCommandOutputContains("Quarkus Dev CA certificate found at")
.assertCommandOutputContains("PKCS12 keystore and truststore generated successfully!")
.assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Key Store File:"))
.assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Trust Store File:"))
.addToAppProps(cmd -> {
// move to application properties under test profile
var key = "%dev.quarkus.tls.key-store.p12.path";
var val = cmd.getPropertyValueFromEnvFile(key);
return "%test.quarkus.tls.key-store.p12.path=" + val;
})
.addToAppProps(cmd -> {
// move to application properties under test profile
var key = "%dev.quarkus.tls.key-store.p12.password";
var val = cmd.getPropertyValueFromEnvFile(key);
return "%test.quarkus.tls.key-store.p12.password=" + val;
});
}

@Order(3)
@Test
public void runTestsUsingGeneratedCerts() {
// runs TlsCommandTest that verifies cert generation
tlsCommand.buildAppAndExpectSuccess(TlsCommandTest.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.quarkus.qe.surefire;

import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.HashSet;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.tls.TlsConfigurationRegistry;

/**
* This test is only supported to run inside QuarkusCliTlsCommandIT.
*/
@QuarkusTest
public class TlsCommandTest {

@Inject
TlsConfigurationRegistry registry;

@Test
void testKeystoreInDefaultTlsRegistry() throws KeyStoreException {
var defaultRegistry = registry.getDefault()
.orElseThrow(() -> new AssertionError("Default TLS Registry is not configured"));
var ks = defaultRegistry.getKeyStore();
var ksAliasesSet = new HashSet<String>();
var ksAliases = ks.aliases();
while (ksAliases.hasMoreElements()) {
ksAliasesSet.add(ksAliases.nextElement());
}
// if this changes to something sensible, it's not an issue
// basically what we try to assert here is that:
// 1. keystore is configured
// 2. it has some aliases that can be used
Assertions.assertTrue(ksAliasesSet.contains("dev-certificate"));
Assertions.assertTrue(ksAliasesSet.contains("issuer-dev-certificate"));

try {
// check we do know password
var key = ks.getKey("dev-certificate", "quarkus".toCharArray());
Assertions.assertNotNull(key);
} catch (NoSuchAlgorithmException | UnrecoverableKeyException e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package io.quarkus.test.bootstrap;

import static java.util.stream.Collectors.toSet;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;

import io.quarkus.test.utils.ClassPathUtils;
import io.quarkus.test.utils.FileUtils;

public abstract class AbstractCliCommand {

protected final QuarkusCliClient cliClient;
protected final QuarkusCliRestService app;

public AbstractCliCommand(String appName, QuarkusCliClient.CreateApplicationRequest appReq,
QuarkusCliClient cliClient, File testDirectory) {
this.cliClient = cliClient;
testDirectory.mkdirs();
this.app = this.cliClient.createApplication(appName, appReq, testDirectory.getAbsolutePath());
}

public AbstractCliCommand(String appName, String targetSubDir, QuarkusCliClient.CreateApplicationRequest appReq,
QuarkusCliClient cliClient) {
this(appName, appReq, cliClient,
Path.of("target").resolve(targetSubDir).resolve(UUID.randomUUID().toString()).toFile());
}

public abstract AbstractCliCommand addToApplicationProperties(String... additions);

public File getApplicationProperties() {
var pathToSrcMainResources = "src" + File.separator + "main" + File.separator + "resources";
return app.getFileFromApplication(pathToSrcMainResources, "application.properties");
}

public String getApplicationPropertiesAsString() {
return FileUtils.loadFile(getApplicationProperties());
}

public QuarkusCliCommandResult buildAppAndExpectSuccess(Class<?>... unitTests) {
copyUnitTestsToCreatedApp(unitTests);
return buildAppAndExpectSuccess();
}

public QuarkusCliCommandResult buildAppAndExpectSuccess() {
var result = app.buildOnJvm();
assertTrue(result.isSuccessful(),
"Expected successful JVM build, but build command failed with output: " + result.getOutput());
return new QuarkusCliCommandResult(result.getOutput(), getApplicationPropertiesAsString(), this);
}

public QuarkusCliCommandResult buildAppAndExpectFailure(Class<?>... unitTests) {
copyUnitTestsToCreatedApp(unitTests);
return buildAppAndExpectFailure();
}

public QuarkusCliCommandResult buildAppAndExpectFailure() {
var result = app.buildOnJvm();
assertFalse(result.isSuccessful(),
"Expected JVM build failure, but build command succeed with output: " + result.getOutput());
return new QuarkusCliCommandResult(result.getOutput(), getApplicationPropertiesAsString(), this);
}

public void removeApplicationProperties() {
var appProps = getApplicationProperties();
if (!appProps.delete()) {
throw new IllegalStateException("Failed to delete application.properties file: " + appProps);
}
}

protected QuarkusCliCommandResult runCommand(String baseCmd, List<String> subCmdArgs) {
var allConfigCommandArgs = new ArrayList<>();
allConfigCommandArgs.add(baseCmd);
allConfigCommandArgs.addAll(subCmdArgs);
var result = cliClient.run(app.getServiceFolder(), allConfigCommandArgs.toArray(String[]::new));
if (!result.isSuccessful()) {
Assertions.fail("Quarkus %s command with arguments '%s' failed with output: %s".formatted(baseCmd,
allConfigCommandArgs, result.getOutput()));
}
return new QuarkusCliCommandResult(result.getOutput(), getApplicationPropertiesAsString(), this);
}

private void copyUnitTestsToCreatedApp(Class<?>[] unitTests) {
if (unitTests == null || unitTests.length == 0) {
return;
}
var normalizedUnitTests = Stream.of(unitTests).map(Class::getName).collect(toSet());
var srcTestJavaPath = Path.of("src").resolve("test").resolve("java");
try (Stream<Path> stream = Files.walk(srcTestJavaPath)) {
stream
.filter(path -> path.toString().endsWith(".java"))
.filter(path -> {
var normalizedClassName = ClassPathUtils.normalizeClassName(path.toString(), ".java");
return normalizedUnitTests.stream().anyMatch(normalizedClassName::endsWith);
})
.map(Path::toFile)
.forEach(unitTestFile -> FileUtils.copyFileTo(unitTestFile,
app.getServiceFolder().resolve(srcTestJavaPath)));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public QuarkusCliRestService getApp() {
return app;
}

public QuarkusCliClient getCliClient() {
return cliClient;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ private Process runCli(Path workingDirectory, File logOutput, String... args) {
cmd.addAll(Arrays.asList(args));

if (QuarkusProperties.disableBuildAnalytics()) {
// TODO: if logic behind setting disabled analytics in our FW get revision
// alter io.quarkus.test.bootstrap.tls.QuarkusTlsCommand.runTlsCommand
// QE tracker: https://issues.redhat.com/browse/QQE-935
cmd.add(format("-D%s=%s", QUARKUS_ANALYTICS_DISABLED_LOCAL_PROP_KEY, Boolean.TRUE));
}

Expand Down
Loading

0 comments on commit 590fe21

Please sign in to comment.