From 6a9c680894181eb3ee5bc421c8162f23db29879e Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 2 Mar 2023 08:37:52 -0500 Subject: [PATCH] Azure Functions maven plugin port integration testing finish quarkus:run and integration tests quarkus:run gradle cli run command implement deploy afk rebase' azf deploy azf deploy working azf more fixes --- bom/application/pom.xml | 22 +- .../io/quarkus/builder/BuildException.java | 11 + .../cmd/DeployCommandActionBuildItem.java | 21 + .../DeployCommandActionResultBuildItem.java | 17 + .../DeployCommandDeclarationBuildItem.java | 19 + .../cmd/DeployCommandDeclarationHandler.java | 21 + ...ployCommandDeclarationResultBuildItem.java | 17 + .../deployment/cmd/DeployCommandHandler.java | 24 + .../cmd/DeployCommandProcessor.java | 27 ++ .../quarkus/deployment/cmd/DeployConfig.java | 20 + .../cmd/RunCommandActionBuildItem.java | 50 +++ .../cmd/RunCommandActionResultBuildItem.java | 17 + .../deployment/cmd/RunCommandHandler.java | 38 ++ .../deployment/cmd/RunCommandProcessor.java | 80 ++++ .../main/java/io/quarkus/cli/QuarkusCli.java | 3 +- .../cli/src/main/java/io/quarkus/cli/Run.java | 33 ++ .../quarkus/cli/build/BaseBuildCommand.java | 2 +- .../java/io/quarkus/gradle/QuarkusPlugin.java | 3 + .../io/quarkus/gradle/tasks/QuarkusRun.java | 217 +++++++++ .../quarkus/maven/AbstractDeploymentMojo.java | 12 +- .../java/io/quarkus/maven/DeployMojo.java | 54 ++- .../main/java/io/quarkus/maven/RunMojo.java | 112 +++++ .../base/pom.tpl.qute.xml | 2 + extensions/azure-functions/deployment/pom.xml | 88 ++++ .../deployment/AzureFunctionBuildItem.java | 29 ++ .../deployment/AzureFunctionsConfig.java | 220 ++++++++++ .../AzureFunctionsDeployCommand.java | 414 ++++++++++++++++++ .../deployment/AzureFunctionsDotNames.java | 3 +- .../deployment/AzureFunctionsProcessor.java | 255 +++++++++-- .../deployment/AzureFunctionsRunCommand.java | 92 ++++ .../deployment/QuarkusActionManager.java | 32 ++ .../deployment/QuarkusAzureTaskManager.java | 44 ++ .../deployment/SubscriptionOption.java | 38 ++ .../quarkus/test/common/ArtifactLauncher.java | 4 + .../io/quarkus/test/common/LauncherUtil.java | 36 ++ .../test/common/RunCommandLauncher.java | 225 ++++++++++ .../test/junit/IntegrationTestUtil.java | 5 + .../test/junit/NativeTestExtension.java | 6 + .../QuarkusIntegrationTestExtension.java | 27 +- .../test/junit/launcher/ConfigUtil.java | 5 + 40 files changed, 2290 insertions(+), 55 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandActionBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandActionResultBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationHandler.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationResultBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandHandler.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandProcessor.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployConfig.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandActionBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandActionResultBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandHandler.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandProcessor.java create mode 100644 devtools/cli/src/main/java/io/quarkus/cli/Run.java create mode 100644 devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java create mode 100644 devtools/maven/src/main/java/io/quarkus/maven/RunMojo.java create mode 100644 extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionBuildItem.java create mode 100644 extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsConfig.java create mode 100644 extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsDeployCommand.java create mode 100644 extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsRunCommand.java create mode 100644 extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/QuarkusActionManager.java create mode 100644 extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/QuarkusAzureTaskManager.java create mode 100644 extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/SubscriptionOption.java create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/RunCommandLauncher.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index dd44deff86c63..f71d7c928fd05 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -157,6 +157,7 @@ 2.14.0 2.2.0 1.0.0 + 0.27.0 1.8.10 1.6.4 1.5.0 @@ -6056,7 +6057,26 @@ azure-functions-java-spi ${azure-functions-java-spi.version} - + + com.microsoft.azure + azure-toolkit-common-lib + ${azure.toolkit-lib.version} + + + com.microsoft.azure + azure-toolkit-auth-lib + ${azure.toolkit-lib.version} + + + com.microsoft.azure + azure-toolkit-appservice-lib + ${azure.toolkit-lib.version} + + + com.microsoft.azure + azure-toolkit-applicationinsights-lib + ${azure.toolkit-lib.version} + org.apache.maven.shared maven-invoker diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildException.java b/core/builder/src/main/java/io/quarkus/builder/BuildException.java index 3f0b8bd3f5857..abcb382d5fb16 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildException.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildException.java @@ -1,5 +1,6 @@ package io.quarkus.builder; +import java.util.Collections; import java.util.List; import org.wildfly.common.Assert; @@ -25,6 +26,16 @@ public BuildException(final List diagnostics) { this.diagnostics = diagnostics; } + /** + * Constructs a new {@code DeploymentException} instance. The diagnostics is left blank ({@code null}), and no + * cause is specified. + * + * @param msg the message + */ + public BuildException(String msg) { + this(msg, Collections.emptyList()); + } + /** * Constructs a new {@code DeploymentException} instance with an initial message. No * cause is specified. diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandActionBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandActionBuildItem.java new file mode 100644 index 0000000000000..8774f1ccf58fe --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandActionBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.deployment.cmd; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class DeployCommandActionBuildItem extends MultiBuildItem { + private final String commandName; + private final boolean successful; + + public DeployCommandActionBuildItem(String commandName, boolean successful) { + this.commandName = commandName; + this.successful = successful; + } + + public String getCommandName() { + return commandName; + } + + public boolean isSuccessful() { + return successful; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandActionResultBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandActionResultBuildItem.java new file mode 100644 index 0000000000000..57ab937bb58d2 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandActionResultBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.deployment.cmd; + +import java.util.List; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class DeployCommandActionResultBuildItem extends SimpleBuildItem { + private final List commands; + + public DeployCommandActionResultBuildItem(List commands) { + this.commands = commands; + } + + public List getCommands() { + return commands; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationBuildItem.java new file mode 100644 index 0000000000000..f4cb79572d572 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationBuildItem.java @@ -0,0 +1,19 @@ +package io.quarkus.deployment.cmd; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Way for maven and gradle plugins to discover if any declared extensions + * support quarkus deploy + */ +public final class DeployCommandDeclarationBuildItem extends MultiBuildItem { + private final String name; + + public DeployCommandDeclarationBuildItem(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationHandler.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationHandler.java new file mode 100644 index 0000000000000..ac4112417d808 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationHandler.java @@ -0,0 +1,21 @@ +package io.quarkus.deployment.cmd; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import io.quarkus.builder.BuildResult; + +public class DeployCommandDeclarationHandler implements BiConsumer { + + @Override + public void accept(Object o, BuildResult buildResult) { + DeployCommandDeclarationResultBuildItem result = buildResult.consume(DeployCommandDeclarationResultBuildItem.class); + + // FYI: AugmentAction.performCustomBuild runs in its own classloader + // so we can only pass back instances of those classes in the system classloader + + Consumer> consumer = (Consumer>) o; + consumer.accept(result.getCommands()); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationResultBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationResultBuildItem.java new file mode 100644 index 0000000000000..44b19823dcd82 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandDeclarationResultBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.deployment.cmd; + +import java.util.List; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class DeployCommandDeclarationResultBuildItem extends SimpleBuildItem { + private final List commands; + + public DeployCommandDeclarationResultBuildItem(List commands) { + this.commands = commands; + } + + public List getCommands() { + return commands; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandHandler.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandHandler.java new file mode 100644 index 0000000000000..29bdd0dc13604 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandHandler.java @@ -0,0 +1,24 @@ +package io.quarkus.deployment.cmd; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import io.quarkus.builder.BuildResult; + +public class DeployCommandHandler implements BiConsumer { + + @Override + public void accept(Object o, BuildResult buildResult) { + DeployCommandActionResultBuildItem result = buildResult.consume(DeployCommandActionResultBuildItem.class); + + // FYI: AugmentAction.performCustomBuild runs in its own classloader + // so we can only pass back instances of those classes in the system classloader + + Consumer consumer = (Consumer) o; + if (result.getCommands().isEmpty()) { + consumer.accept(false); + } else { + consumer.accept(true); + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandProcessor.java new file mode 100644 index 0000000000000..43febd80498c7 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployCommandProcessor.java @@ -0,0 +1,27 @@ +package io.quarkus.deployment.cmd; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import io.quarkus.deployment.annotations.BuildStep; + +public class DeployCommandProcessor { + @BuildStep + public DeployCommandDeclarationResultBuildItem commandDeclaration(List cmds) { + if (cmds == null || cmds.isEmpty()) { + return new DeployCommandDeclarationResultBuildItem(Collections.emptyList()); + } + return new DeployCommandDeclarationResultBuildItem( + cmds.stream().map(item -> item.getName()).collect(Collectors.toList())); + } + + @BuildStep + public DeployCommandActionResultBuildItem commandExecution(List cmds) { + if (cmds == null || cmds.isEmpty()) { + return new DeployCommandActionResultBuildItem(Collections.emptyList()); + } + return new DeployCommandActionResultBuildItem(cmds); + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployConfig.java new file mode 100644 index 0000000000000..5fcf7264f97e2 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/DeployConfig.java @@ -0,0 +1,20 @@ +package io.quarkus.deployment.cmd; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public class DeployConfig { + /** + * Deployment target + */ + @ConfigItem + public Optional target; + + public boolean isEnabled(String t) { + return target.isEmpty() || target.get().equals(t); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandActionBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandActionBuildItem.java new file mode 100644 index 0000000000000..7745324aa6fcf --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandActionBuildItem.java @@ -0,0 +1,50 @@ +package io.quarkus.deployment.cmd; + +import java.nio.file.Path; +import java.util.List; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class RunCommandActionBuildItem extends MultiBuildItem { + private final String commandName; + private final List args; + private Path workingDirectory; + private String startedExpression; + private Path logFile; + private boolean needsLogfile; + + public RunCommandActionBuildItem(String commandName, List args, Path workingDirectory, String startedExpression, + Path logFile, + boolean needsLogfile) { + this.args = args; + this.commandName = commandName; + this.workingDirectory = workingDirectory; + this.startedExpression = startedExpression; + this.logFile = logFile; + this.needsLogfile = needsLogfile; + } + + public String getCommandName() { + return commandName; + } + + public String getStartedExpression() { + return startedExpression; + } + + public Path getWorkingDirectory() { + return workingDirectory; + } + + public List getArgs() { + return args; + } + + public Path getLogFile() { + return logFile; + } + + public boolean isNeedsLogfile() { + return needsLogfile; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandActionResultBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandActionResultBuildItem.java new file mode 100644 index 0000000000000..4ea16ddca4653 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandActionResultBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.deployment.cmd; + +import java.util.List; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class RunCommandActionResultBuildItem extends SimpleBuildItem { + private final List commands; + + public RunCommandActionResultBuildItem(List commands) { + this.commands = commands; + } + + public List getCommands() { + return commands; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandHandler.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandHandler.java new file mode 100644 index 0000000000000..f405bd40dad25 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandHandler.java @@ -0,0 +1,38 @@ +package io.quarkus.deployment.cmd; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import io.quarkus.builder.BuildResult; + +public class RunCommandHandler implements BiConsumer { + + @Override + public void accept(Object o, BuildResult buildResult) { + RunCommandActionResultBuildItem result = buildResult.consume(RunCommandActionResultBuildItem.class); + + // FYI: AugmentAction.performCustomBuild runs in its own classloader + // so we can only pass back instances of those classes in the system classloader + + Consumer> consumer = (Consumer>) o; + Map entries = new HashMap<>(); + for (RunCommandActionBuildItem item : result.getCommands()) { + LinkedList itemList = new LinkedList(); + addLaunchCommand(itemList, item); + entries.put(item.getCommandName(), itemList); + } + consumer.accept(entries); + } + + private void addLaunchCommand(List list, RunCommandActionBuildItem item) { + list.add(item.getArgs()); + list.add(item.getWorkingDirectory()); + list.add(item.getStartedExpression()); + list.add(item.isNeedsLogfile()); + list.add(item.getLogFile()); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandProcessor.java new file mode 100644 index 0000000000000..11a968c9d36e8 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/cmd/RunCommandProcessor.java @@ -0,0 +1,80 @@ +package io.quarkus.deployment.cmd; + +import static io.quarkus.deployment.pkg.steps.JarResultBuildStep.DEFAULT_FAST_JAR_DIRECTORY_NAME; +import static io.quarkus.deployment.pkg.steps.JarResultBuildStep.QUARKUS_RUN_JAR; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.deployment.pkg.builditem.LegacyJarRequiredBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.pkg.builditem.UberJarRequiredBuildItem; + +public class RunCommandProcessor { + private static final String JAVA_HOME_SYS = "java.home"; + private static final String JAVA_HOME_ENV = "JAVA_HOME"; + + @BuildStep + public RunCommandActionResultBuildItem commands(List cmds) { + return new RunCommandActionResultBuildItem(cmds); + } + + @BuildStep + public void defaultJavaCommand(PackageConfig packageConfig, + OutputTargetBuildItem jar, + List uberJarRequired, + List legacyJarRequired, + BuildProducer cmds) { + + Path jarPath = null; + if (legacyJarRequired.isEmpty() && (!uberJarRequired.isEmpty() + || packageConfig.type.equalsIgnoreCase(PackageConfig.UBER_JAR))) { + jarPath = jar.getOutputDirectory() + .resolve(jar.getBaseName() + packageConfig.getRunnerSuffix() + ".jar"); + } else if (!legacyJarRequired.isEmpty() || packageConfig.isLegacyJar() + || packageConfig.type.equalsIgnoreCase(PackageConfig.LEGACY)) { + jarPath = jar.getOutputDirectory() + .resolve(jar.getBaseName() + packageConfig.getRunnerSuffix() + ".jar"); + } else { + jarPath = jar.getOutputDirectory().resolve(DEFAULT_FAST_JAR_DIRECTORY_NAME).resolve(QUARKUS_RUN_JAR); + + } + + List args = new ArrayList<>(); + args.add(determineJavaPath()); + + for (Map.Entry e : System.getProperties().entrySet()) { + args.add("-D" + e.getKey().toString() + "=" + e.getValue().toString()); + } + args.add("-jar"); + args.add(jarPath.toAbsolutePath().toString()); + cmds.produce(new RunCommandActionBuildItem("java", args, null, null, null, false)); + } + + private String determineJavaPath() { + // try system property first - it will be the JAVA_HOME used by the current JVM + String home = System.getProperty(JAVA_HOME_SYS); + if (home == null) { + // No luck, somewhat a odd JVM not enforcing this property + // try with the JAVA_HOME environment variable + home = System.getenv(JAVA_HOME_ENV); + } + if (home != null) { + File javaHome = new File(home); + File file = new File(javaHome, "bin/java"); + if (file.exists()) { + return file.getAbsolutePath(); + } + } + + // just assume 'java' is on the system path + return "java"; + } + +} diff --git a/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java b/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java index 6e9481e7ffa45..bb18fbdfe53ea 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java @@ -46,7 +46,8 @@ import picocli.CommandLine.UnmatchedArgumentException; @CommandLine.Command(name = "quarkus", subcommands = { - Create.class, Build.class, Dev.class, Test.class, ProjectExtensions.class, Image.class, Deploy.class, Registry.class, + Create.class, Build.class, Dev.class, Run.class, Test.class, ProjectExtensions.class, Image.class, Deploy.class, + Registry.class, Info.class, Update.class, Version.class, diff --git a/devtools/cli/src/main/java/io/quarkus/cli/Run.java b/devtools/cli/src/main/java/io/quarkus/cli/Run.java new file mode 100644 index 0000000000000..34a5e3edfb1cd --- /dev/null +++ b/devtools/cli/src/main/java/io/quarkus/cli/Run.java @@ -0,0 +1,33 @@ +package io.quarkus.cli; + +import java.util.Map; + +import io.quarkus.devtools.project.BuildTool; +import picocli.CommandLine; + +@CommandLine.Command(name = "run", sortOptions = false, mixinStandardHelpOptions = false, header = "Run application.") +public class Run extends BuildToolDelegatingCommand { + + private static final Map ACTION_MAPPING = Map.of(BuildTool.MAVEN, "quarkus:run", + BuildTool.GRADLE, "run"); + + @CommandLine.Option(names = { "--target" }, description = "Run target.") + String target; + + @Override + public void populateContext(BuildToolContext context) { + super.populateContext(context); + if (target != null) + context.getPropertiesOptions().properties.put("quarkus.run.target", target); + } + + @Override + public Map getActionMapping() { + return ACTION_MAPPING; + } + + @Override + public String toString() { + return "Run {}"; + } +} diff --git a/devtools/cli/src/main/java/io/quarkus/cli/build/BaseBuildCommand.java b/devtools/cli/src/main/java/io/quarkus/cli/build/BaseBuildCommand.java index 7dd45bc71601b..0291f457591fe 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/build/BaseBuildCommand.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/build/BaseBuildCommand.java @@ -45,7 +45,7 @@ public BuildSystemRunner getRunner() { } /** - * Commands using `@ParentCommand` need to set the ouput. + * Commands using `@ParentCommand` need to set the output. * This is needed for testing purposes. * More specifically --cli-test-dir relies on this. * diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index 1e6faad216359..a44fcfd3d8780 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -53,6 +53,7 @@ import io.quarkus.gradle.tasks.QuarkusListPlatforms; import io.quarkus.gradle.tasks.QuarkusRemoteDev; import io.quarkus.gradle.tasks.QuarkusRemoveExtension; +import io.quarkus.gradle.tasks.QuarkusRun; import io.quarkus.gradle.tasks.QuarkusShowEffectiveConfig; import io.quarkus.gradle.tasks.QuarkusTest; import io.quarkus.gradle.tasks.QuarkusTestConfig; @@ -80,6 +81,7 @@ public class QuarkusPlugin implements Plugin { public static final String QUARKUS_SHOW_EFFECTIVE_CONFIG_TASK_NAME = "quarkusShowEffectiveConfig"; public static final String QUARKUS_BUILD_TASK_NAME = "quarkusBuild"; public static final String QUARKUS_DEV_TASK_NAME = "quarkusDev"; + public static final String QUARKUS_RUN_TASK_NAME = "quarkusRun"; public static final String QUARKUS_REMOTE_DEV_TASK_NAME = "quarkusRemoteDev"; public static final String QUARKUS_TEST_TASK_NAME = "quarkusTest"; public static final String QUARKUS_GO_OFFLINE_TASK_NAME = "quarkusGoOffline"; @@ -225,6 +227,7 @@ public boolean isSatisfiedBy(Task t) { TaskProvider quarkusDev = tasks.register(QUARKUS_DEV_TASK_NAME, QuarkusDev.class, devRuntimeDependencies, quarkusExt); + TaskProvider quarkusRun = tasks.register(QUARKUS_RUN_TASK_NAME, QuarkusRun.class); TaskProvider quarkusRemoteDev = tasks.register(QUARKUS_REMOTE_DEV_TASK_NAME, QuarkusRemoteDev.class, devRuntimeDependencies, quarkusExt); TaskProvider quarkusTest = tasks.register(QUARKUS_TEST_TASK_NAME, QuarkusTest.class, diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java new file mode 100644 index 0000000000000..8f7ec9e4c9a17 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRun.java @@ -0,0 +1,217 @@ +package io.quarkus.gradle.tasks; + +import java.io.File; +import java.io.Serializable; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.gradle.api.GradleException; +import org.gradle.api.file.FileCollection; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskAction; + +import io.quarkus.bootstrap.BootstrapException; +import io.quarkus.bootstrap.app.AugmentAction; +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.AppModelResolverException; +import io.quarkus.deployment.cmd.RunCommandActionResultBuildItem; +import io.quarkus.deployment.cmd.RunCommandHandler; +import io.quarkus.gradle.extension.QuarkusPluginExtension; +import io.quarkus.maven.dependency.GACTV; +import io.quarkus.maven.dependency.ResolvedDependency; + +public abstract class QuarkusRun extends QuarkusTask { + private final Property workingDirectory; + private final SourceSet mainSourceSet; + + @Inject + public QuarkusRun() { + this("Quarkus runs target application"); + } + + public QuarkusRun(String description) { + super(description); + mainSourceSet = getProject().getExtensions().getByType(SourceSetContainer.class) + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + + final ObjectFactory objectFactory = getProject().getObjects(); + + workingDirectory = objectFactory.property(File.class); + workingDirectory.convention(getProject().provider(() -> QuarkusPluginExtension.getLastFile(getCompilationOutput()))); + + } + + /** + * The JVM classes directory (compilation output) + */ + @Optional + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public FileCollection getCompilationOutput() { + return mainSourceSet.getOutput().getClassesDirs(); + } + + @Input + public Property getWorkingDirectory() { + return workingDirectory; + } + + /** + * @deprecated See {@link #workingDirectory} + */ + @Deprecated + public void setWorkingDir(String workingDir) { + workingDirectory.set(getProject().file(workingDir)); + } + + @Classpath + public FileCollection getClasspath() { + SourceSet mainSourceSet = QuarkusGradleUtils.getSourceSet(getProject(), SourceSet.MAIN_SOURCE_SET_NAME); + return mainSourceSet.getCompileClasspath().plus(mainSourceSet.getRuntimeClasspath()) + .plus(mainSourceSet.getAnnotationProcessorPath()) + .plus(mainSourceSet.getResources()); + } + + @Input + public Map getQuarkusBuildSystemProperties() { + Map quarkusSystemProperties = new HashMap<>(); + for (Map.Entry systemProperty : System.getProperties().entrySet()) { + if (systemProperty.getKey().toString().startsWith("quarkus.") && + systemProperty.getValue() instanceof Serializable) { + quarkusSystemProperties.put(systemProperty.getKey(), systemProperty.getValue()); + } + } + return quarkusSystemProperties; + } + + @Input + public Map getQuarkusBuildEnvProperties() { + Map quarkusEnvProperties = new HashMap<>(); + for (Map.Entry systemProperty : System.getenv().entrySet()) { + if (systemProperty.getKey() != null && systemProperty.getKey().startsWith("QUARKUS_")) { + quarkusEnvProperties.put(systemProperty.getKey(), systemProperty.getValue()); + } + } + return quarkusEnvProperties; + } + + protected Properties getBuildSystemProperties(ResolvedDependency appArtifact) { + final Map properties = getProject().getProperties(); + final Properties realProperties = new Properties(); + for (Map.Entry entry : properties.entrySet()) { + final String key = entry.getKey(); + final Object value = entry.getValue(); + if (key != null && value instanceof String && key.startsWith("quarkus.")) { + realProperties.setProperty(key, (String) value); + } + } + Map quarkusBuildProperties = extension().getQuarkusBuildProperties().get(); + if (!quarkusBuildProperties.isEmpty()) { + quarkusBuildProperties.entrySet().stream().filter(entry -> entry.getKey().startsWith("quarkus.")) + .forEach(entry -> { + realProperties.put(entry.getKey(), entry.getValue()); + }); + } + realProperties.putIfAbsent("quarkus.application.name", appArtifact.getArtifactId()); + realProperties.putIfAbsent("quarkus.application.version", appArtifact.getVersion()); + return realProperties; + } + + @TaskAction + public void runQuarkus() { + final ApplicationModel appModel; + + try { + appModel = extension().getAppModelResolver().resolveModel(new GACTV(getProject().getGroup().toString(), + getProject().getName(), getProject().getVersion().toString())); + } catch (AppModelResolverException e) { + throw new GradleException("Failed to resolve Quarkus application model for " + getProject().getPath(), e); + } + + final Properties effectiveProperties = getBuildSystemProperties(appModel.getAppArtifact()); + try (CuratedApplication curatedApplication = QuarkusBootstrap.builder() + .setBaseClassLoader(getClass().getClassLoader()) + .setExistingModel(appModel) + .setTargetDirectory(getProject().getBuildDir().toPath()) + .setBaseName(extension().finalName()) + .setBuildSystemProperties(effectiveProperties) + .setAppArtifact(appModel.getAppArtifact()) + .setLocalProjectDiscovery(false) + .setIsolateDeployment(true) + .build().bootstrap()) { + + AugmentAction action = curatedApplication.createAugmentor(); + AtomicReference exists = new AtomicReference<>(); + AtomicReference tooMany = new AtomicReference<>(); + String target = System.getProperty("quarkus.run.target"); + action.performCustomBuild(RunCommandHandler.class.getName(), new Consumer>() { + @Override + public void accept(Map cmds) { + List cmd = null; + if (target != null) { + cmd = cmds.get(target); + if (cmd == null) { + exists.set(false); + return; + } + } else if (cmds.size() == 1) { // defaults to pure java run + cmd = cmds.values().iterator().next(); + } else if (cmds.size() == 2) { // choose not default + for (Map.Entry entry : cmds.entrySet()) { + if (entry.getKey().equals("java")) + continue; + cmd = entry.getValue(); + break; + } + } else if (cmds.size() > 2) { + tooMany.set(cmds.keySet().stream().collect(Collectors.joining(" "))); + return; + } else { + throw new RuntimeException("Should never reach this!"); + } + List args = (List) cmd.get(0); + getProject().getLogger().info("Executing \"" + String.join(" ", args) + "\""); + Path wd = (Path) cmd.get(1); + File wdir = wd != null ? wd.toFile() : workingDirectory.get(); + getProject().exec(action -> { + action.commandLine(args).workingDir(wdir); + action.setStandardInput(System.in) + .setErrorOutput(System.out) + .setStandardOutput(System.out); + }); + } + }, + RunCommandActionResultBuildItem.class.getName()); + if (target != null && !exists.get()) { + getProject().getLogger().error("quarkus.run.target " + target + " is not found"); + return; + } + if (tooMany.get() != null) { + getProject().getLogger().error( + "Too many installed extensions support quarkus:run. Use -Dquarkus.run.target= to choose"); + getProject().getLogger().error("Extensions: " + tooMany.get()); + } + } catch (BootstrapException e) { + throw new GradleException("Failed to run application", e); + } + } +} diff --git a/devtools/maven/src/main/java/io/quarkus/maven/AbstractDeploymentMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/AbstractDeploymentMojo.java index d509157c27689..2979e1d1544bf 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/AbstractDeploymentMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/AbstractDeploymentMojo.java @@ -26,6 +26,8 @@ public class AbstractDeploymentMojo extends BuildMojo { @Parameter(property = "quarkus.container-image.builder") String imageBuilder; + boolean forceDependencies = true; + @Override protected void doExecute() throws MojoExecutionException { if (dryRun) { @@ -41,16 +43,18 @@ protected void doExecute() throws MojoExecutionException { } public Deployer getDeployer() { - return Deployer.getDeployer(mavenProject()) - .orElse(Deployer.kubernetes); + return getDeployer(Deployer.kubernetes); } - public Optional getImageBuilder() { - return Optional.ofNullable(imageBuilder); + public Deployer getDeployer(Deployer defaultDeployer) { + return Deployer.getDeployer(mavenProject()) + .orElse(defaultDeployer); } @Override protected List forcedDependencies(LaunchMode mode) { + if (!forceDependencies) + return super.forcedDependencies(mode); List dependencies = new ArrayList<>(); MavenProject project = mavenProject(); Deployer deployer = getDeployer(); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DeployMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DeployMojo.java index 392eac28da339..a9fc6e50e5206 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DeployMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DeployMojo.java @@ -1,21 +1,67 @@ package io.quarkus.maven; import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; +import io.quarkus.bootstrap.app.AugmentAction; +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.deployment.cmd.DeployCommandActionResultBuildItem; +import io.quarkus.deployment.cmd.DeployCommandDeclarationHandler; +import io.quarkus.deployment.cmd.DeployCommandDeclarationResultBuildItem; +import io.quarkus.deployment.cmd.DeployCommandHandler; + @Mojo(name = "deploy", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, threadSafe = true) public class DeployMojo extends AbstractDeploymentMojo { @Override protected boolean beforeExecute() throws MojoExecutionException { - systemProperties = new HashMap<>(systemProperties); - boolean shouldBuildImage = imageBuild || imageBuilder != null && !imageBuilder.isEmpty(); - systemProperties.put("quarkus." + getDeployer().name() + ".deploy", "true"); - systemProperties.put("quarkus.container-image.build", String.valueOf(shouldBuildImage)); return super.beforeExecute(); } + + @Override + protected void doExecute() throws MojoExecutionException { + try (CuratedApplication curatedApplication = bootstrapApplication()) { + AtomicReference> tooMany = new AtomicReference<>(); + AugmentAction action = curatedApplication.createAugmentor(); + action.performCustomBuild(DeployCommandDeclarationHandler.class.getName(), new Consumer>() { + @Override + public void accept(List strings) { + tooMany.set(strings); + } + }, DeployCommandDeclarationResultBuildItem.class.getName()); + String target = System.getProperty("quarkus.deploy.target"); + List targets = tooMany.get(); + if (targets.isEmpty()) { + // weave in kubernetes as we have no deploy support from others + systemProperties = new HashMap<>(systemProperties); + boolean shouldBuildImage = imageBuild || imageBuilder != null && !imageBuilder.isEmpty(); + systemProperties.put("quarkus." + getDeployer().name() + ".deploy", "true"); + systemProperties.put("quarkus.container-image.build", String.valueOf(shouldBuildImage)); + super.doExecute(); + } else if (targets.size() > 1 && !targets.contains(target)) { + getLog().error( + "Too many installed extensions support quarkus:deploy. You must remove one from dependencies"); + getLog().error("Extensions: " + targets.stream().collect(Collectors.joining(" "))); + } else { + forceDependencies = false; + AugmentAction deployAction = curatedApplication.createAugmentor(); + System.setProperty("quarkus.deploy.target", targets.get(0)); + deployAction.performCustomBuild(DeployCommandHandler.class.getName(), new Consumer() { + @Override + public void accept(Boolean success) { + } + }, DeployCommandActionResultBuildItem.class.getName()); + } + } finally { + + } + } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/RunMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/RunMojo.java new file mode 100644 index 0000000000000..1213799c9a121 --- /dev/null +++ b/devtools/maven/src/main/java/io/quarkus/maven/RunMojo.java @@ -0,0 +1,112 @@ +package io.quarkus.maven; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import io.quarkus.bootstrap.app.AugmentAction; +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.deployment.cmd.RunCommandActionResultBuildItem; +import io.quarkus.deployment.cmd.RunCommandHandler; + +@Mojo(name = "run") +public class RunMojo extends QuarkusBootstrapMojo { + /** + * The list of system properties defined for the plugin. + */ + @Parameter + Map systemProperties = Collections.emptyMap(); + + @Override + protected boolean beforeExecute() throws MojoExecutionException, MojoFailureException { + return true; + } + + @Override + protected void doExecute() throws MojoExecutionException, MojoFailureException { + Set propertiesToClear = new HashSet<>(); + + // Add the system properties of the plugin to the system properties + // if and only if they are not already set. + for (Map.Entry entry : systemProperties.entrySet()) { + String key = entry.getKey(); + if (System.getProperty(key) == null) { + System.setProperty(key, entry.getValue()); + propertiesToClear.add(key); + } + } + + try (CuratedApplication curatedApplication = bootstrapApplication()) { + AugmentAction action = curatedApplication.createAugmentor(); + AtomicReference exists = new AtomicReference<>(); + AtomicReference tooMany = new AtomicReference<>(); + String target = System.getProperty("quarkus.run.target"); + action.performCustomBuild(RunCommandHandler.class.getName(), new Consumer>() { + @Override + public void accept(Map cmds) { + List cmd = null; + if (target != null) { + cmd = cmds.get(target); + if (cmd == null) { + exists.set(false); + return; + } + } else if (cmds.size() == 1) { // defaults to pure java run + cmd = cmds.values().iterator().next(); + } else if (cmds.size() == 2) { // choose not default + for (Map.Entry entry : cmds.entrySet()) { + if (entry.getKey().equals("java")) + continue; + cmd = entry.getValue(); + break; + } + } else if (cmds.size() > 2) { + tooMany.set(cmds.keySet().stream().collect(Collectors.joining(" "))); + return; + } else { + throw new RuntimeException("Should never reach this!"); + } + List args = (List) cmd.get(0); + System.out.println("Executing \"" + String.join(" ", args) + "\""); + Path workingDirectory = (Path) cmd.get(1); + try { + ProcessBuilder builder = new ProcessBuilder() + .command(args) + .inheritIO(); + if (workingDirectory != null) { + builder.directory(workingDirectory.toFile()); + } + Process process = builder.start(); + int exit = process.waitFor(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }, + RunCommandActionResultBuildItem.class.getName()); + if (target != null && !exists.get()) { + getLog().error("quarkus.run.target " + target + " is not found"); + return; + } + if (tooMany.get() != null) { + getLog().error( + "Too many installed extensions support quarkus:run. Use -Dquarkus.run.target= to choose"); + getLog().error("Extensions: " + tooMany.get()); + } + } finally { + // Clear all the system properties set by the plugin + propertiesToClear.forEach(System::clearProperty); + } + } +} diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/azure-functions-example/base/pom.tpl.qute.xml b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/azure-functions-example/base/pom.tpl.qute.xml index c5e6ca47def6a..c619d5bda1ade 100644 --- a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/azure-functions-example/base/pom.tpl.qute.xml +++ b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/azure-functions-example/base/pom.tpl.qute.xml @@ -41,6 +41,7 @@ + diff --git a/extensions/azure-functions/deployment/pom.xml b/extensions/azure-functions/deployment/pom.xml index c0f783178d856..7ba8367352ed5 100644 --- a/extensions/azure-functions/deployment/pom.xml +++ b/extensions/azure-functions/deployment/pom.xml @@ -29,6 +29,93 @@ com.microsoft.azure.functions azure-functions-java-library + + + com.microsoft.azure + azure-toolkit-appservice-lib + + + + commons-logging + commons-logging + + + org.checkerframework + checker-qual + + + com.azure + azure-core + + + com.azure + azure-core-management + + + com.microsoft.azure + msal4j + + + net.minidev + json-smart + + + org.fusesource.jansi + jansi + + + net.java.dev.jna + jna-platform + + + + + + net.java.dev.jna + jna-platform + 5.6.0 + + + com.azure + azure-core + 1.33.0 + + + com.azure + azure-core-management + 1.8.0 + + + com.azure + azure-core + + + + + com.microsoft.azure + msal4j + 1.4.0 + + + net.minidev + json-smart + + + javax.activation + activation + + + + + net.minidev + json-smart + 2.4.8 + @@ -45,6 +132,7 @@ + diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionBuildItem.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionBuildItem.java new file mode 100644 index 0000000000000..572bf2e05527e --- /dev/null +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionBuildItem.java @@ -0,0 +1,29 @@ +package io.quarkus.azure.functions.deployment; + +import java.lang.reflect.Method; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class AzureFunctionBuildItem extends MultiBuildItem { + private final String functionName; + private final Class declaring; + private final Method method; + + public AzureFunctionBuildItem(String functionName, Class declaring, Method method) { + this.functionName = functionName; + this.declaring = declaring; + this.method = method; + } + + public Class getDeclaring() { + return declaring; + } + + public String getFunctionName() { + return functionName; + } + + public Method getMethod() { + return method; + } +} diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsConfig.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsConfig.java new file mode 100644 index 0000000000000..cf1faa471ce5d --- /dev/null +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsConfig.java @@ -0,0 +1,220 @@ +package io.quarkus.azure.functions.deployment; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; + +import com.azure.core.management.AzureEnvironment; +import com.microsoft.azure.toolkit.lib.auth.AuthConfiguration; +import com.microsoft.azure.toolkit.lib.auth.AuthType; +import com.microsoft.azure.toolkit.lib.auth.AzureEnvironmentUtils; +import com.microsoft.azure.toolkit.lib.common.exception.InvalidConfigurationException; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public class AzureFunctionsConfig { + + /** + * App name for azure function project + */ + @ConfigItem + public String appName; + + /** + * Resource group defaults to "quarkus" + */ + @ConfigItem(defaultValue = "quarkus") + public String resourceGroup; + + /** + * Resource group defaults to "quarkus" + */ + @ConfigItem(defaultValue = "eastus") + public String region; + + /** + * + */ + @ConfigItem(defaultValue = "false") + public boolean disableAppInsights; + + /** + * + */ + public Optional appInsightsInstance; + + /** + * + */ + public Optional appInsightsKey; + + /** + * + */ + @ConfigItem + public RuntimeConfig runtime; + + /** + * + */ + @ConfigItem + public AuthConfig auth; + + /** + * + */ + @ConfigItem(defaultValue = "java-functions-app-service-plan") + public String appServicePlanName; + + /** + * + */ + public Optional appServicePlanResourceGroup; + + /** + * Azure subscription id. Required only if there are more than one subscription in your account + */ + public Optional subscriptionId; + /** + * + */ + public Optional pricingTier; + + /** + * Port to run azure function in local runtime. + * Will default to quarkus.http.test-port or 8081 + */ + @ConfigItem + public Optional funcPort; + + /** + * Config String for local debug + */ + @ConfigItem(defaultValue = "transport=dt_socket,server=y,suspend=n,address=5005") + public String localDebugConfig; + + /** + * + */ + @ConfigItem + public Map appSettings = Collections.emptyMap(); + + @ConfigGroup + public static class RuntimeConfig { + /** + * + */ + @ConfigItem(defaultValue = "linux") + public String os; + + /** + * + */ + @ConfigItem(defaultValue = "11") + public String javaVersion; + + /** + * + */ + @ConfigItem + public Optional image; + + /** + * + */ + @ConfigItem + public Optional registryUrl; + + } + + @ConfigGroup + public static class AuthConfig { + + /** + * + */ + @ConfigItem + public Optional serverId; + + /** + * + */ + @ConfigItem(defaultValue = "azure_cli") + public String type; + + /** + * + */ + @ConfigItem + public Optional environment; + + /** + * + */ + @ConfigItem + public Optional client; + + /** + * + */ + @ConfigItem + public Optional tenant; + + /** + * + */ + @ConfigItem + public Optional key; + + /** + * + */ + @ConfigItem + public Optional certificate; + + /** + * + */ + @ConfigItem + public Optional certificatePassword; + + public AuthConfiguration toAuthConfiguration() { + + try { + final AuthType type = AuthType.parseAuthType(this.type); + final AuthConfiguration authConfiguration = new AuthConfiguration(type); + authConfiguration.setClient(client.orElse(null)); + authConfiguration.setTenant(tenant.orElse(null)); + authConfiguration.setCertificate(certificate.orElse(null)); + authConfiguration.setCertificatePassword(certificatePassword.orElse(null)); + authConfiguration.setKey(key.orElse(null)); + + authConfiguration.setEnvironment(environment.orElse(null)); + authConfiguration.setEnvironment(Optional.ofNullable(authConfiguration.getEnvironment()) + .orElseGet(() -> AzureEnvironmentUtils.azureEnvironmentToString(AzureEnvironment.AZURE))); + + // if user specify 'auto', and there are SP configuration errors, it will fail back to other auth types + // if user doesn't specify any authType + if (this.type.isEmpty()) { + if (!StringUtils.isAllBlank(this.certificate.orElse(null), this.key.orElse(null), + this.certificatePassword.orElse(null))) { + authConfiguration.validate(); + } + } else if (authConfiguration.getType() == AuthType.SERVICE_PRINCIPAL) { + authConfiguration.validate(); + } + + return authConfiguration; + } catch (InvalidConfigurationException e) { + throw new IllegalArgumentException(e); + } + } + + } +} diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsDeployCommand.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsDeployCommand.java new file mode 100644 index 0000000000000..a8def3a0a1f59 --- /dev/null +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsDeployCommand.java @@ -0,0 +1,414 @@ +package io.quarkus.azure.functions.deployment; + +import static com.microsoft.azure.toolkit.lib.appservice.utils.AppServiceConfigUtils.fromAppService; +import static com.microsoft.azure.toolkit.lib.appservice.utils.AppServiceConfigUtils.mergeAppServiceConfig; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.jboss.logging.Logger; + +import com.azure.core.http.policy.HttpLogDetailLevel; +import com.azure.core.management.AzureEnvironment; +import com.microsoft.azure.toolkit.lib.Azure; +import com.microsoft.azure.toolkit.lib.account.IAzureAccount; +import com.microsoft.azure.toolkit.lib.appservice.AzureAppService; +import com.microsoft.azure.toolkit.lib.appservice.config.AppServiceConfig; +import com.microsoft.azure.toolkit.lib.appservice.config.FunctionAppConfig; +import com.microsoft.azure.toolkit.lib.appservice.config.RuntimeConfig; +import com.microsoft.azure.toolkit.lib.appservice.function.AzureFunctions; +import com.microsoft.azure.toolkit.lib.appservice.function.FunctionApp; +import com.microsoft.azure.toolkit.lib.appservice.function.FunctionAppBase; +import com.microsoft.azure.toolkit.lib.appservice.model.JavaVersion; +import com.microsoft.azure.toolkit.lib.appservice.model.OperatingSystem; +import com.microsoft.azure.toolkit.lib.appservice.model.PricingTier; +import com.microsoft.azure.toolkit.lib.appservice.model.WebContainer; +import com.microsoft.azure.toolkit.lib.appservice.plan.AppServicePlan; +import com.microsoft.azure.toolkit.lib.appservice.task.CreateOrUpdateFunctionAppTask; +import com.microsoft.azure.toolkit.lib.appservice.task.DeployFunctionAppTask; +import com.microsoft.azure.toolkit.lib.appservice.utils.AppServiceConfigUtils; +import com.microsoft.azure.toolkit.lib.auth.Account; +import com.microsoft.azure.toolkit.lib.auth.AuthConfiguration; +import com.microsoft.azure.toolkit.lib.auth.AuthType; +import com.microsoft.azure.toolkit.lib.auth.AzureAccount; +import com.microsoft.azure.toolkit.lib.auth.AzureCloud; +import com.microsoft.azure.toolkit.lib.auth.AzureEnvironmentUtils; +import com.microsoft.azure.toolkit.lib.common.bundle.AzureString; +import com.microsoft.azure.toolkit.lib.common.logging.Log; +import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager; +import com.microsoft.azure.toolkit.lib.common.messager.IAzureMessage; +import com.microsoft.azure.toolkit.lib.common.messager.IAzureMessager; +import com.microsoft.azure.toolkit.lib.common.model.Region; +import com.microsoft.azure.toolkit.lib.common.model.Subscription; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; +import com.microsoft.azure.toolkit.lib.common.utils.TextUtils; + +import io.quarkus.builder.BuildException; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.cmd.DeployCommandActionBuildItem; +import io.quarkus.deployment.cmd.DeployCommandDeclarationBuildItem; +import io.quarkus.deployment.cmd.DeployConfig; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; + +public class AzureFunctionsDeployCommand { + private static final Logger log = Logger.getLogger(AzureFunctionsDeployCommand.class); + + private static final String APPLICATION_INSIGHTS_CONFIGURATION_CONFLICT = "Contradictory configurations for application insights," + + + " specify 'appInsightsKey' or 'appInsightsInstance' if you want to enable it, and specify " + + "'disableAppInsights=true' if you want to disable it."; + private static final String ARTIFACT_INCOMPATIBLE_WARNING = "Your function app artifact compile version {0} may not compatible with java version {1} in " + + + "configuration."; + private static final String ARTIFACT_INCOMPATIBLE_ERROR = "Your function app artifact compile version {0} is not compatible with java version {1} in " + + + "configuration, please downgrade the project compile version and try again."; + private static final String NO_ARTIFACT_FOUNDED = "Failed to find function artifact '%s.jar' in folder '%s', please re-package the project and try again."; + private static final String APP_NAME_PATTERN = "[a-zA-Z0-9\\-]{2,60}"; + private static final String RESOURCE_GROUP_PATTERN = "[a-zA-Z0-9._\\-()]{1,90}"; + private static final String SLOT_NAME_PATTERN = "[A-Za-z0-9-]{1,60}"; + private static final String APP_SERVICE_PLAN_NAME_PATTERN = "[a-zA-Z0-9\\-]{1,40}"; + private static final String EMPTY_APP_NAME = "Please config the in pom.xml."; + private static final String INVALID_APP_NAME = "The only allow alphanumeric characters, hyphens and cannot start or end in a hyphen."; + private static final String EMPTY_RESOURCE_GROUP = "Please config the in pom.xml."; + private static final String INVALID_RESOURCE_GROUP_NAME = "The only allow alphanumeric characters, periods, underscores, " + + + "hyphens and parenthesis and cannot end in a period."; + private static final String INVALID_SERVICE_PLAN_NAME = "Invalid value for , it need to match the pattern %s"; + private static final String INVALID_SERVICE_PLAN_RESOURCE_GROUP_NAME = "Invalid value for , " + + "it only allow alphanumeric characters, periods, underscores, hyphens and parenthesis and cannot end in a period."; + private static final String EMPTY_SLOT_NAME = "Please config the of in pom.xml"; + private static final String INVALID_SLOT_NAME = "Invalid value of inside in pom.xml, it needs to match the pattern '%s'"; + private static final String EMPTY_IMAGE_NAME = "Please config the of in pom.xml."; + private static final String INVALID_OS = "The value of is not correct, supported values are: windows, linux and docker."; + private static final String EXPANDABLE_PRICING_TIER_WARNING = "'%s' may not be a valid pricing tier, " + + "please refer to https://aka.ms/maven_function_configuration#supported-pricing-tiers for valid values"; + private static final String EXPANDABLE_REGION_WARNING = "'%s' may not be a valid region, " + + "please refer to https://aka.ms/maven_function_configuration#supported-regions for valid values"; + private static final String EXPANDABLE_JAVA_VERSION_WARNING = "'%s' may not be a valid java version, recommended values are `Java 8`, `Java 11` and `Java 17`"; + + protected static final String USING_AZURE_ENVIRONMENT = "Using Azure environment: %s."; + + public static final String AZURE_FUNCTIONS = "azure-functions"; + protected static final String SUBSCRIPTION_TEMPLATE = "Subscription: %s(%s)"; + protected static final String SUBSCRIPTION_NOT_FOUND = "Subscription %s was not found in current account."; + + @BuildStep + public void declare(BuildProducer producer) { + producer.produce(new DeployCommandDeclarationBuildItem(AZURE_FUNCTIONS)); + } + + @BuildStep + public void deploy(DeployConfig deployConfig, AzureFunctionsConfig config, + OutputTargetBuildItem output, + + BuildProducer producer) throws Exception { + if (!deployConfig.isEnabled(AZURE_FUNCTIONS)) + return; + validateParameters(config); + AzureMessager.setDefaultMessager(new QuarkusAzureMessager()); + Azure.az().config().setLogLevel(HttpLogDetailLevel.NONE.name()); + QuarkusActionManager.register(); + AzureTaskManager.register(new QuarkusAzureTaskManager()); + + initAzureAppServiceClient(config); + final FunctionAppBase target = createOrUpdateResource(convertConfig(config)); + Path outputDirectory = output.getOutputDirectory(); + Path functionStagingDir = outputDirectory.resolve("azure-functions").resolve(config.appName); + + deployArtifact(functionStagingDir, target); + producer.produce(new DeployCommandActionBuildItem(AZURE_FUNCTIONS, true)); + } + + private PricingTier getParsedPricingTier(AzureFunctionsConfig config) { + return Optional.ofNullable(config.pricingTier.orElse(null)).map(PricingTier::fromString) + .orElseGet(() -> Optional.ofNullable(getServicePlan(config)).map(AppServicePlan::getPricingTier).orElse(null)); + } + + public AppServicePlan getServicePlan(AzureFunctionsConfig config) { + final String servicePlan = config.appServicePlanName; + final String servicePlanGroup = StringUtils.firstNonBlank(config.appServicePlanResourceGroup.orElse(null), + config.resourceGroup); + return StringUtils.isAnyBlank(subscriptionId, servicePlan, servicePlanGroup) ? null + : Azure.az(AzureAppService.class).plans(subscriptionId).get(servicePlan, servicePlanGroup); + } + + public FunctionAppConfig convertConfig(AzureFunctionsConfig config) { + Map appSettings = config.appSettings; + if (appSettings.isEmpty()) { + appSettings = new HashMap<>(); + appSettings.put("FUNCTIONS_EXTENSION_VERSION", "~4"); + } + return (FunctionAppConfig) new FunctionAppConfig() + .disableAppInsights(config.disableAppInsights) + .appInsightsKey(config.appInsightsKey.orElse(null)) + .appInsightsInstance(config.appInsightsKey.orElse(null)) + .subscriptionId(subscriptionId) + .resourceGroup(config.resourceGroup) + .appName(config.appName) + .servicePlanName(config.appServicePlanName) + .servicePlanResourceGroup(config.appServicePlanResourceGroup.orElse(null)) + //.deploymentSlotName(getDeploymentSlotName()) + //.deploymentSlotConfigurationSource(getDeploymentSlotConfigurationSource()) + .pricingTier(getParsedPricingTier(config)) + .region(getParsedRegion(config)) + .runtime(getRuntimeConfig(config)) + .appSettings(appSettings); + } + + public RuntimeConfig getRuntimeConfig(AzureFunctionsConfig config) { + final AzureFunctionsConfig.RuntimeConfig runtime = config.runtime; + if (runtime == null) { + return null; + } + final OperatingSystem os = Optional.ofNullable(runtime.os).map(OperatingSystem::fromString) + .orElseGet( + () -> Optional.ofNullable(getServicePlan(config)).map(AppServicePlan::getOperatingSystem).orElse(null)); + final JavaVersion javaVersion = Optional.ofNullable(runtime.javaVersion).map(JavaVersion::fromString).orElse(null); + final com.microsoft.azure.toolkit.lib.appservice.config.RuntimeConfig result = new RuntimeConfig().os(os) + .javaVersion(javaVersion).webContainer(WebContainer.JAVA_OFF) + .image(runtime.image.orElse(null)).registryUrl(runtime.registryUrl.orElse(null)); + return result; + } + + private Region getParsedRegion(AzureFunctionsConfig config) { + return Optional.ofNullable(config.region).map(Region::fromName).orElse(null); + } + + protected void validateParameters(AzureFunctionsConfig config) throws BuildException { + // app name + if (StringUtils.isBlank(config.appName)) { + throw new BuildException(EMPTY_APP_NAME); + } + if (config.appName.startsWith("-") || !config.appName.matches(APP_NAME_PATTERN)) { + throw new BuildException(INVALID_APP_NAME); + } + // resource group + if (StringUtils.isBlank(config.resourceGroup)) { + throw new BuildException(EMPTY_RESOURCE_GROUP); + } + if (config.resourceGroup.endsWith(".") || !config.resourceGroup.matches(RESOURCE_GROUP_PATTERN)) { + throw new BuildException(INVALID_RESOURCE_GROUP_NAME); + } + // asp name & resource group + if (StringUtils.isNotEmpty(config.appServicePlanName) + && !config.appServicePlanName.matches(APP_SERVICE_PLAN_NAME_PATTERN)) { + throw new BuildException(String.format(INVALID_SERVICE_PLAN_NAME, APP_SERVICE_PLAN_NAME_PATTERN)); + } + if (config.appServicePlanResourceGroup.isPresent() + && StringUtils.isNotEmpty(config.appServicePlanResourceGroup.orElse(null)) + && + (config.appServicePlanResourceGroup.orElse(null).endsWith(".") + || !config.appServicePlanResourceGroup.orElse(null).matches(RESOURCE_GROUP_PATTERN))) { + throw new BuildException(INVALID_SERVICE_PLAN_RESOURCE_GROUP_NAME); + } + // slot name + /* + * if (deploymentSlotSetting != null && StringUtils.isEmpty(deploymentSlotSetting.getName())) { + * throw new BuildException(EMPTY_SLOT_NAME); + * } + * if (deploymentSlotSetting != null && !deploymentSlotSetting.getName().matches(SLOT_NAME_PATTERN)) { + * throw new BuildException(String.format(INVALID_SLOT_NAME, SLOT_NAME_PATTERN)); + * } + * + */ + // region + if (StringUtils.isNotEmpty(config.region) && Region.fromName(config.region).isExpandedValue()) { + log.warn(String.format(EXPANDABLE_REGION_WARNING, config.region)); + } + // os + if (StringUtils.isNotEmpty(config.runtime.os) && OperatingSystem.fromString(config.runtime.os) == null) { + throw new BuildException(INVALID_OS); + } + // java version + if (StringUtils.isNotEmpty(config.runtime.javaVersion) + && JavaVersion.fromString(config.runtime.javaVersion).isExpandedValue()) { + log.warn(String.format(EXPANDABLE_JAVA_VERSION_WARNING, config.runtime.javaVersion)); + } + // pricing tier + if (config.pricingTier.isPresent() && StringUtils.isNotEmpty(config.pricingTier.orElse(null)) + && PricingTier.fromString(config.pricingTier.orElse(null)).isExpandedValue()) { + log.warn(String.format(EXPANDABLE_PRICING_TIER_WARNING, config.pricingTier.orElse(null))); + } + // docker image + if (OperatingSystem.fromString(config.runtime.os) == OperatingSystem.DOCKER + && StringUtils.isEmpty(config.runtime.image.orElse(null))) { + throw new BuildException(EMPTY_IMAGE_NAME); + } + } + + protected static AzureAppService appServiceClient; + + protected static String subscriptionId; + + protected AzureAppService initAzureAppServiceClient(AzureFunctionsConfig config) throws BuildException { + if (appServiceClient == null) { + final Account account = loginAzure(config.auth); + final List subscriptions = account.getSubscriptions(); + final String targetSubscriptionId = getTargetSubscriptionId(config.subscriptionId.orElse(null), subscriptions, + account.getSelectedSubscriptions()); + checkSubscription(subscriptions, targetSubscriptionId); + com.microsoft.azure.toolkit.lib.Azure.az(AzureAccount.class).account() + .setSelectedSubscriptions(Collections.singletonList(targetSubscriptionId)); + appServiceClient = Azure.az(AzureAppService.class); + printCurrentSubscription(appServiceClient); + this.subscriptionId = targetSubscriptionId; + } + return appServiceClient; + } + + protected static void checkSubscription(List subscriptions, String targetSubscriptionId) + throws BuildException { + if (StringUtils.isEmpty(targetSubscriptionId)) { + return; + } + final Optional optionalSubscription = subscriptions.stream() + .filter(subscription -> StringUtils.equals(subscription.getId(), targetSubscriptionId)) + .findAny(); + if (!optionalSubscription.isPresent()) { + throw new BuildException(String.format(SUBSCRIPTION_NOT_FOUND, targetSubscriptionId)); + } + } + + protected Account loginAzure(AzureFunctionsConfig.AuthConfig auth) { + if (Azure.az(AzureAccount.class).isLoggedIn()) { + return Azure.az(AzureAccount.class).account(); + } + final AuthConfiguration authConfig = auth.toAuthConfiguration(); + if (authConfig.getType() == AuthType.DEVICE_CODE) { + authConfig.setDeviceCodeConsumer(info -> { + final String message = StringUtils.replace(info.getMessage(), info.getUserCode(), + TextUtils.cyan(info.getUserCode())); + System.out.println(message); + }); + } + final AzureEnvironment configEnv = AzureEnvironmentUtils.stringToAzureEnvironment(authConfig.getEnvironment()); + promptAzureEnvironment(configEnv); + Azure.az(AzureCloud.class).set(configEnv); + final Account account = Azure.az(AzureAccount.class).login(authConfig, false); + final AzureEnvironment env = account.getEnvironment(); + final String environmentName = AzureEnvironmentUtils.azureEnvironmentToString(env); + if (env != AzureEnvironment.AZURE && env != configEnv) { + log.info(AzureString.format(USING_AZURE_ENVIRONMENT, environmentName)); + } + printCredentialDescription(account); + return account; + } + + protected static void printCredentialDescription(Account account) { + final IAzureMessager messager = AzureMessager.getMessager(); + final AuthType type = account.getType(); + final String username = account.getUsername(); + if (type != null) { + log.info(AzureString.format("Auth type: %s", type.toString())); + } + if (account.isLoggedIn()) { + final List selectedSubscriptions = account.getSelectedSubscriptions(); + if (CollectionUtils.isNotEmpty(selectedSubscriptions) && selectedSubscriptions.size() == 1) { + log.info(AzureString.format("Default subscription: %s(%s)", selectedSubscriptions.get(0).getName(), + selectedSubscriptions.get(0).getId())); + } + } + if (StringUtils.isNotEmpty(username)) { + log.info(AzureString.format("Username: %s", username.trim())); + } + } + + private static void promptAzureEnvironment(AzureEnvironment env) { + if (env != null && env != AzureEnvironment.AZURE) { + log.info(AzureString.format("Auth environment: %s", AzureEnvironmentUtils.azureEnvironmentToString(env))); + } + } + + protected String getTargetSubscriptionId(String defaultSubscriptionId, + List subscriptions, + List selectedSubscriptions) throws BuildException { + if (!StringUtils.isBlank(defaultSubscriptionId)) { + return defaultSubscriptionId; + } + + if (selectedSubscriptions.size() == 1) { + return selectedSubscriptions.get(0).getId(); + } + + if (selectedSubscriptions.isEmpty()) { + throw new BuildException("You account does not have a subscription to deploy to"); + } + throw new BuildException("You must specify a subscription id to use for deployment as you have more than one"); + } + + protected void printCurrentSubscription(AzureAppService appServiceClient) { + if (appServiceClient == null) { + return; + } + final List subscriptions = Azure.az(IAzureAccount.class).account().getSelectedSubscriptions(); + final Subscription subscription = subscriptions.get(0); + if (subscription != null) { + Log.info(String.format(SUBSCRIPTION_TEMPLATE, TextUtils.cyan(subscription.getName()), + TextUtils.cyan(subscription.getId()))); + } + } + + protected FunctionAppBase createOrUpdateResource(final FunctionAppConfig config) throws Exception { + FunctionApp app = Azure.az(AzureFunctions.class).functionApps(config.subscriptionId()).updateOrCreate(config.appName(), + config.resourceGroup()); + final boolean newFunctionApp = !app.exists(); + AppServiceConfig defaultConfig = !newFunctionApp ? fromAppService(app, app.getAppServicePlan()) + : buildDefaultConfig(config.subscriptionId(), + config.resourceGroup(), config.appName()); + mergeAppServiceConfig(config, defaultConfig); + if (!newFunctionApp && !config.disableAppInsights() && StringUtils.isEmpty(config.appInsightsKey())) { + // fill ai key from existing app settings + config.appInsightsKey(app.getAppSettings().get(CreateOrUpdateFunctionAppTask.APPINSIGHTS_INSTRUMENTATION_KEY)); + } + return new CreateOrUpdateFunctionAppTask(config).doExecute(); + } + + private AppServiceConfig buildDefaultConfig(String subscriptionId, String resourceGroup, String appName) { + return AppServiceConfigUtils.buildDefaultFunctionConfig(subscriptionId, resourceGroup, appName, JavaVersion.JAVA_11); + } + + private void deployArtifact(Path functionStagingDir, final FunctionAppBase target) { + final File file = functionStagingDir.toFile(); + new DeployFunctionAppTask(target, file, null).doExecute(); + } + + public static class QuarkusAzureMessager implements IAzureMessager, IAzureMessage.ValueDecorator { + @Override + public boolean show(IAzureMessage message) { + switch (message.getType()) { + case ALERT: + case CONFIRM: + case WARNING: + String content = message.getContent(); + log.warn(content); + return true; + case ERROR: + log.error(message.getContent(), ((Throwable) message.getPayload())); + return true; + case INFO: + case SUCCESS: + default: + log.info(message.getContent()); + return true; + } + } + + @Override + public String decorateValue(@Nonnull Object p, @Nullable IAzureMessage message) { + return TextUtils.cyan(p.toString()); + } + } + +} diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsDotNames.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsDotNames.java index 4e6fc653265e9..fb302ae4b81e9 100644 --- a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsDotNames.java +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsDotNames.java @@ -11,6 +11,7 @@ import com.microsoft.azure.functions.annotation.KafkaTrigger; import com.microsoft.azure.functions.annotation.QueueTrigger; import com.microsoft.azure.functions.annotation.ServiceBusQueueTrigger; +import com.microsoft.azure.functions.annotation.ServiceBusTopicTrigger; import com.microsoft.azure.functions.annotation.TimerTrigger; import com.microsoft.azure.functions.annotation.WarmupTrigger; @@ -24,7 +25,7 @@ public final class AzureFunctionsDotNames { public static final DotName KAFKA_TRIGGER = DotName.createSimple(KafkaTrigger.class.getName()); public static final DotName QUEUE_TRIGGER = DotName.createSimple(QueueTrigger.class.getName()); public static final DotName SERVICE_BUS_QUEUE_TRIGGER = DotName.createSimple(ServiceBusQueueTrigger.class.getName()); - public static final DotName SERVICE_BUS_TOPIC_TRIGGER = DotName.createSimple(ServiceBusQueueTrigger.class.getName()); + public static final DotName SERVICE_BUS_TOPIC_TRIGGER = DotName.createSimple(ServiceBusTopicTrigger.class.getName()); public static final DotName TIMER_TRIGGER = DotName.createSimple(TimerTrigger.class.getName()); public static final DotName WARMUP_TRIGGER = DotName.createSimple(WarmupTrigger.class.getName()); diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsProcessor.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsProcessor.java index f76c6f1e82411..15386c5b75c50 100644 --- a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsProcessor.java +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsProcessor.java @@ -1,30 +1,69 @@ package io.quarkus.azure.functions.deployment; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Singleton; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.microsoft.azure.toolkit.lib.common.exception.AzureExecutionException; +import com.microsoft.azure.toolkit.lib.legacy.function.configurations.FunctionConfiguration; +import com.microsoft.azure.toolkit.lib.legacy.function.handlers.AnnotationHandler; +import com.microsoft.azure.toolkit.lib.legacy.function.handlers.AnnotationHandlerImpl; + import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; +import io.quarkus.deployment.pkg.builditem.JarBuildItem; import io.quarkus.deployment.pkg.builditem.LegacyJarRequiredBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.pkg.steps.NativeBuild; public class AzureFunctionsProcessor { private static final Logger log = Logger.getLogger(AzureFunctionsProcessor.class); + protected static final String HOST_JSON = "host.json"; + protected static final String LOCAL_SETTINGS_JSON = "local.settings.json"; + public static final String FUNCTION_JSON = "function.json"; + @BuildStep public LegacyJarRequiredBuildItem forceLegacy(PackageConfig config) { // Azure Functions need a legacy jar and no runner @@ -37,48 +76,192 @@ FeatureBuildItem feature() { return new FeatureBuildItem(Feature.AZURE_FUNCTIONS); } + @BuildStep(onlyIf = IsNormal.class, onlyIfNot = NativeBuild.class) + public ArtifactResultBuildItem packageFunctions(List functions, + OutputTargetBuildItem target, + AzureFunctionsConfig functionsConfig, + PackageConfig packageConfig, + JarBuildItem jar) throws Exception { + if (functions == null || functions.isEmpty()) + return null; + AnnotationHandler handler = new AnnotationHandlerImpl(); + HashSet methods = new HashSet<>(); + for (AzureFunctionBuildItem item : functions) + methods.add(item.getMethod()); + final Map configMap = handler.generateConfigurations(methods); + final String scriptFilePath = String.format("../%s.jar", target.getBaseName() + packageConfig.getRunnerSuffix()); + configMap.values().forEach(config -> config.setScriptFile(scriptFilePath)); + configMap.values().forEach(FunctionConfiguration::validate); + + final ObjectWriter objectWriter = getObjectWriter(); + + Path rootPath = target.getOutputDirectory().resolve(".."); + Path outputDirectory = target.getOutputDirectory(); + Path functionStagingDir = outputDirectory.resolve("azure-functions").resolve(functionsConfig.appName); + + copyHostJson(rootPath, functionStagingDir); + + copyLocalSettingsJson(rootPath, functionStagingDir); + + writeFunctionJsonFiles(objectWriter, configMap, functionStagingDir); + + copyJarsToStageDirectory(jar, functionStagingDir); + return new ArtifactResultBuildItem(functionStagingDir, "azure-functions", Collections.EMPTY_MAP); + } + + protected void writeFunctionJsonFiles(final ObjectWriter objectWriter, + final Map configMap, + Path functionStagingDir) throws IOException { + if (!configMap.isEmpty()) { + String functionDir = functionStagingDir.toString(); + for (final Map.Entry config : configMap.entrySet()) { + writeFunctionJsonFile(objectWriter, config.getKey(), config.getValue(), functionDir); + } + } + } + + protected void writeFunctionJsonFile(final ObjectWriter objectWriter, final String functionName, + final FunctionConfiguration config, + final String functionStagingDir) throws IOException { + final File functionJsonFile = Paths.get(functionStagingDir, + functionName, FUNCTION_JSON).toFile(); + writeObjectToFile(objectWriter, config, functionJsonFile); + } + + private static final String DEFAULT_HOST_JSON = "{\"version\":\"2.0\",\"extensionBundle\":" + + "{\"id\":\"Microsoft.Azure.Functions.ExtensionBundle\",\"version\":\"[3.*, 4.0.0)\"}}\n"; + + protected void copyHostJson(Path rootPath, Path functionStagingDir) throws IOException { + final File sourceHostJsonFile = rootPath.resolve(HOST_JSON).toFile(); + final File destHostJsonFile = functionStagingDir.resolve("host.json").toFile(); + copyFilesWithDefaultContent(sourceHostJsonFile, destHostJsonFile, DEFAULT_HOST_JSON); + } + + private static final String DEFAULT_LOCAL_SETTINGS_JSON = "{ \"IsEncrypted\": false, \"Values\": " + + "{ \"FUNCTIONS_WORKER_RUNTIME\": \"java\" } }"; + + protected void copyLocalSettingsJson(Path rootPath, Path functionStagingDir) throws IOException { + final File sourceLocalSettingsJsonFile = rootPath.resolve(LOCAL_SETTINGS_JSON).toFile(); + final File destLocalSettingsJsonFile = functionStagingDir.resolve(LOCAL_SETTINGS_JSON).toFile(); + copyFilesWithDefaultContent(sourceLocalSettingsJsonFile, destLocalSettingsJsonFile, DEFAULT_LOCAL_SETTINGS_JSON); + } + + private static void copyFilesWithDefaultContent(File source, File dest, String defaultContent) + throws IOException { + if (source != null && source.exists()) { + FileUtils.copyFile(source, dest); + } else { + FileUtils.write(dest, defaultContent, Charset.defaultCharset()); + } + } + + private static final String AZURE_FUNCTIONS_JAVA_CORE_LIBRARY = "com.microsoft.azure.functions.azure-functions-java-core-library"; + protected static final String AZURE_FUNCTIONS_JAVA_LIBRARY = "com.microsoft.azure.functions.azure-functions-java-library"; + + protected void copyJarsToStageDirectory(JarBuildItem jar, Path functionStagingDir) + throws IOException, AzureExecutionException { + final String stagingDirectory = functionStagingDir.toString(); + final File libFolder = Paths.get(stagingDirectory, "lib").toFile(); + if (libFolder.exists()) { + FileUtils.cleanDirectory(libFolder); + } + for (File dep : jar.getLibraryDir().toFile().listFiles()) { + if (dep.getName().startsWith(AZURE_FUNCTIONS_JAVA_CORE_LIBRARY) + || dep.getName().startsWith(AZURE_FUNCTIONS_JAVA_LIBRARY)) { + continue; + } + FileUtils.copyFileToDirectory(dep, libFolder); + } + FileUtils.copyFileToDirectory(jar.getPath().toFile(), functionStagingDir.toFile()); + } + + protected void writeObjectToFile(final ObjectWriter objectWriter, final Object object, final File targetFile) + throws IOException { + targetFile.getParentFile().mkdirs(); + targetFile.createNewFile(); + objectWriter.writeValue(targetFile, object); + } + + protected ObjectWriter getObjectWriter() { + final DefaultPrettyPrinter.Indenter indenter = DefaultIndenter.SYSTEM_LINEFEED_INSTANCE.withLinefeed(StringUtils.LF); + final PrettyPrinter prettyPrinter = new DefaultPrettyPrinter().withObjectIndenter(indenter); + return new ObjectMapper() + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .writer(prettyPrinter); + } + @BuildStep - public void registerArc(CombinedIndexBuildItem combined, - BuildProducer unremovableBeans, - BuildProducer additionalBeans) { + public void findFunctions(CombinedIndexBuildItem combined, + BuildProducer functions) { IndexView index = combined.getIndex(); - Set functionClasses = new HashSet<>(); - for (DotName ann : AzureFunctionsDotNames.PARAMETER_ANNOTATIONS) { - Collection anns = index.getAnnotations(ann); - anns.forEach(annotationInstance -> { - ClassInfo ci = annotationInstance.target().asMethodParameter().method().declaringClass(); - //log.info("Param annotation: " + ci.name().toString()); - functionClasses - .add(ci); - }); - } Collection anns = index.getAnnotations(AzureFunctionsDotNames.FUNCTION_NAME); anns.forEach(annotationInstance -> { - ClassInfo ci = annotationInstance.target().asMethod().declaringClass(); - //log.info("FunctionName annotation: " + ci.name().toString()); - functionClasses.add(ci); + MethodInfo methodInfo = annotationInstance.target().asMethod(); + ClassInfo ci = methodInfo.declaringClass(); + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + try { + Class declaring = loader.loadClass(ci.name().toString()); + Class[] params = methodInfo.parameters().stream().map(methodParameterInfo -> { + try { + return loader.loadClass(methodParameterInfo.type().name().toString()); + } catch (ClassNotFoundException e) { + throw new DeploymentException(e); + } + }).toArray(Class[]::new); + Method method = null; + try { + method = declaring.getMethod(methodInfo.name(), params); + } catch (NoSuchMethodException e) { + throw new DeploymentException(e); + } + String funcName = annotationInstance.value().asString(); + functions.produce(new AzureFunctionBuildItem(funcName, declaring, method)); + } catch (ClassNotFoundException e) { + throw new DeploymentException(e); + } }); + } - if (!functionClasses.isEmpty()) { - AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder() - .setDefaultScope(BuiltinScope.REQUEST.getName()) - .setUnremovable(); - for (ClassInfo funcClass : functionClasses) { - if (Modifier.isInterface(funcClass.flags()) || Modifier.isAbstract(funcClass.flags())) - continue; - if (BuiltinScope.isDeclaredOn(funcClass)) { - //log.info("Add unremovable: " + funcClass.name().toString()); - // It has a built-in scope - just mark it as unremovable - unremovableBeans - .produce(new UnremovableBeanBuildItem( - new UnremovableBeanBuildItem.BeanClassNameExclusion(funcClass.name().toString()))); - } else { - // No built-in scope found - add as additional bean - //log.info("Add default: " + funcClass.name().toString()); - builder.addBeanClass(funcClass.name().toString()); - } + @BuildStep + public void registerArc(BuildProducer unremovableBeans, + BuildProducer additionalBeans, + List functions) { + if (functions == null || functions.isEmpty()) + return; + + AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder() + .setDefaultScope(BuiltinScope.REQUEST.getName()) + .setUnremovable(); + Set classes = functions.stream().map(item -> item.getDeclaring()).collect(Collectors.toSet()); + for (Class funcClass : classes) { + if (Modifier.isInterface(funcClass.getModifiers()) || Modifier.isAbstract(funcClass.getModifiers())) + continue; + if (isScoped(funcClass)) { + //log.info("Add unremovable: " + funcClass.name().toString()); + // It has a built-in scope - just mark it as unremovable + unremovableBeans + .produce(new UnremovableBeanBuildItem( + new UnremovableBeanBuildItem.BeanClassNameExclusion(funcClass.getName()))); + } else { + // No built-in scope found - add as additional bean + //log.info("Add default: " + funcClass.name().toString()); + builder.addBeanClass(funcClass.getName()); } - additionalBeans.produce(builder.build()); } + additionalBeans.produce(builder.build()); } + + public static boolean isScoped(Class clazz) { + if (clazz.isAnnotationPresent(Dependent.class)) + return true; + if (clazz.isAnnotationPresent(Singleton.class)) + return true; + if (clazz.isAnnotationPresent(ApplicationScoped.class)) + return true; + if (clazz.isAnnotationPresent(RequestScoped.class)) + return true; + return false; + } + } diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsRunCommand.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsRunCommand.java new file mode 100644 index 0000000000000..a8a5f7daf5b63 --- /dev/null +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsRunCommand.java @@ -0,0 +1,92 @@ +package io.quarkus.azure.functions.deployment; + +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.OptionalInt; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.jboss.logging.Logger; + +import com.microsoft.azure.toolkit.lib.common.exception.AzureExecutionException; +import com.microsoft.azure.toolkit.lib.legacy.function.handlers.CommandHandler; +import com.microsoft.azure.toolkit.lib.legacy.function.handlers.CommandHandlerImpl; +import com.microsoft.azure.toolkit.lib.legacy.function.utils.CommandUtils; + +import io.quarkus.builder.BuildException; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.cmd.RunCommandActionBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; + +public class AzureFunctionsRunCommand { + private static final Logger log = Logger.getLogger(AzureFunctionsRunCommand.class); + protected static final String FUNC_CMD = "func -v"; + protected static final String FUNC_HOST_START_CMD = "func host start -p %s"; + protected static final String RUN_FUNCTIONS_FAILURE = "Failed to run Azure Functions. Please checkout console output."; + protected static final String RUNTIME_NOT_FOUND = "Azure Functions Core Tools not found. " + + "Please go to https://aka.ms/azfunc-install to install Azure Functions Core Tools first."; + private static final String STAGE_DIR_NOT_FOUND = "Stage directory not found. Please run mvn package first."; + private static final String FUNC_HOST_START_WITH_DEBUG_CMD = "func host start -p %s --language-worker -- " + + "\"-agentlib:jdwp=%s\""; + private static final String STARTED_EXPRESSION = "Host lock lease acquired"; + + @BuildStep + public RunCommandActionBuildItem run(List functions, OutputTargetBuildItem target, + AzureFunctionsConfig config) throws Exception { + Path stagingDir = getDeploymentStagingDirectoryPath(target, config); + File file = stagingDir.toFile(); + if (!file.exists() || !file.isDirectory()) { + throw new BuildException("Staging directory does not exist. Rebuild the app", Collections.emptyList()); + } + + final CommandHandler commandHandler = new CommandHandlerImpl(); + + checkRuntimeExistence(commandHandler); + + String cmd = getStartFunctionHostCommand(config); + List args = new LinkedList<>(); + Arrays.stream(cmd.split(" ")).forEach(s -> args.add(s)); + RunCommandActionBuildItem launcher = new RunCommandActionBuildItem("azure-functions", args, stagingDir, + STARTED_EXPRESSION, null, + true); + return launcher; + } + + protected Path getDeploymentStagingDirectoryPath(OutputTargetBuildItem target, AzureFunctionsConfig config) { + return target.getOutputDirectory().resolve("azure-functions").resolve(config.appName); + } + + protected void checkRuntimeExistence(final CommandHandler handler) throws AzureExecutionException { + handler.runCommandWithReturnCodeCheck( + getCheckRuntimeCommand(), + true, /* showStdout */ + null, /* workingDirectory */ + CommandUtils.getDefaultValidReturnCodes(), + RUNTIME_NOT_FOUND); + } + + protected String getCheckRuntimeCommand() { + return FUNC_CMD; + } + + protected String getStartFunctionHostCommand(AzureFunctionsConfig azureConfig) { + int funcPort; + if (azureConfig.funcPort.isPresent()) { + funcPort = azureConfig.funcPort.get(); + } else { + Config config = ConfigProviderResolver.instance().getConfig(); + funcPort = config.getValue("quarkus.http.test-port", OptionalInt.class).orElse(8081); + } + final String enableDebug = System.getProperty("enableDebug"); + if (StringUtils.isNotEmpty(enableDebug) && enableDebug.equalsIgnoreCase("true")) { + return String.format(FUNC_HOST_START_WITH_DEBUG_CMD, funcPort, azureConfig.localDebugConfig); + } else { + return String.format(FUNC_HOST_START_CMD, funcPort); + } + } +} diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/QuarkusActionManager.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/QuarkusActionManager.java new file mode 100644 index 0000000000000..c4e386150bfc2 --- /dev/null +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/QuarkusActionManager.java @@ -0,0 +1,32 @@ +package io.quarkus.azure.functions.deployment; + +import com.microsoft.azure.toolkit.lib.common.action.Action; +import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; +import com.microsoft.azure.toolkit.lib.common.action.AzureActionManager; + +public class QuarkusActionManager extends AzureActionManager { + + public static void register() { + final QuarkusActionManager am = new QuarkusActionManager(); + register(am); + } + + @Override + public void registerAction(Action action) { + } + + @Override + public Action getAction(Action.Id id) { + return null; + } + + @Override + public void registerGroup(String id, ActionGroup group) { + + } + + @Override + public ActionGroup getGroup(String id) { + return null; + } +} diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/QuarkusAzureTaskManager.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/QuarkusAzureTaskManager.java new file mode 100644 index 0000000000000..c9cfd97fb2405 --- /dev/null +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/QuarkusAzureTaskManager.java @@ -0,0 +1,44 @@ +package io.quarkus.azure.functions.deployment; + +import com.microsoft.azure.toolkit.lib.common.task.AzureTask; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; + +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +public class QuarkusAzureTaskManager extends AzureTaskManager { + @Override + protected void doRead(Runnable runnable, AzureTask task) { + throw new UnsupportedOperationException("not support"); + } + + @Override + protected void doWrite(Runnable runnable, AzureTask task) { + throw new UnsupportedOperationException("not support"); + } + + @Override + protected void doRunLater(Runnable runnable, AzureTask task) { + throw new UnsupportedOperationException("not support"); + } + + @Override + protected void doRunOnPooledThread(Runnable runnable, AzureTask task) { + Mono.fromRunnable(runnable).subscribeOn(Schedulers.boundedElastic()).subscribe(); + } + + @Override + protected void doRunAndWait(Runnable runnable, AzureTask task) { + runnable.run(); + } + + @Override + protected void doRunInBackground(Runnable runnable, AzureTask task) { + doRunOnPooledThread(runnable, task); + } + + @Override + protected void doRunInModal(Runnable runnable, AzureTask task) { + throw new UnsupportedOperationException("not support"); + } +} diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/SubscriptionOption.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/SubscriptionOption.java new file mode 100644 index 0000000000000..6b95ed64efdce --- /dev/null +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/SubscriptionOption.java @@ -0,0 +1,38 @@ +package io.quarkus.azure.functions.deployment; + +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.azure.toolkit.lib.common.model.Subscription; + +public class SubscriptionOption implements Comparable { + + private Subscription inner; + + public SubscriptionOption(Subscription inner) { + this.inner = inner; + } + + public Subscription getSubscription() { + return inner; + } + + public String getSubscriptionName() { + return inner != null ? inner.getName() : null; + } + + @Override + public String toString() { + return inner != null ? getSubscriptionName(this.inner) : null; + } + + @Override + public int compareTo(SubscriptionOption other) { + final String name1 = inner != null ? inner.getName() : null; + final String name2 = other.inner != null ? other.inner.getName() : null; + return StringUtils.compare(name1, name2); + } + + public static String getSubscriptionName(Subscription subs) { + return String.format("%s(%s)", subs.getName(), subs.getId()); + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java index 8e7b41d46dd06..9008a935bfc2c 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.Map; +import io.quarkus.bootstrap.app.CuratedApplication; + public interface ArtifactLauncher extends Closeable { void init(T t); @@ -40,6 +42,8 @@ interface DevServicesLaunchResult extends AutoCloseable { boolean manageNetwork(); + CuratedApplication getCuratedApplication(); + void close(); } } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java b/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java index e6fe3df54729d..8416ec5521a4d 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java @@ -1,6 +1,7 @@ package io.quarkus.test.common; import java.io.BufferedReader; +import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; @@ -11,6 +12,7 @@ import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -59,6 +61,16 @@ static Process launchProcess(List args) throws IOException { return process; } + /** + * Launches a process using the supplied arguments and makes sure the process's output is drained to standard out + */ + static Process launchProcess(List args, File dir) throws IOException { + Process process = Runtime.getRuntime().exec(args.toArray(new String[0]), null, dir); + new Thread(new ProcessReader(process.getInputStream())).start(); + new Thread(new ProcessReader(process.getErrorStream())).start(); + return process; + } + /** * Waits (for a maximum of {@param waitTimeSeconds} seconds) until the launched process indicates the address it is * listening on. @@ -129,6 +141,30 @@ static void destroyProcess(Process quarkusProcess) { } } + static void destroyProcess(ProcessHandle quarkusProcess) { + try { + CompletableFuture exit = quarkusProcess.onExit(); + if (!quarkusProcess.destroy()) { + quarkusProcess.destroyForcibly(); + return; + } + exit.get(LOG_CHECK_INTERVAL * 10, TimeUnit.MILLISECONDS); + } catch (Exception e) { + } + if (quarkusProcess.isAlive()) { + quarkusProcess.destroyForcibly(); + } + } + + static void destroyProcess(Process process, boolean children) { + if (!children) { + destroyProcess(process); + return; + } + process.descendants().forEach((p) -> destroyProcess(p)); + destroyProcess(process); + } + static Function createStartedFunction() { List startedNotifiers = new ArrayList<>(); for (IntegrationTestStartedNotifier i : ServiceLoader.load(IntegrationTestStartedNotifier.class)) { diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/RunCommandLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/RunCommandLauncher.java new file mode 100644 index 0000000000000..526a8ab65859a --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/RunCommandLauncher.java @@ -0,0 +1,225 @@ +package io.quarkus.test.common; + +import static io.quarkus.test.common.LauncherUtil.*; +import static java.lang.ProcessBuilder.Redirect.PIPE; + +import java.io.BufferedReader; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.commons.io.input.TeeInputStream; + +import io.quarkus.bootstrap.BootstrapException; +import io.quarkus.bootstrap.app.AugmentAction; +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.deployment.cmd.RunCommandActionResultBuildItem; +import io.quarkus.deployment.cmd.RunCommandHandler; +import io.quarkus.test.common.http.TestHTTPResourceManager; + +public class RunCommandLauncher implements ArtifactLauncher { + + Process quarkusProcess = null; + private List args; + private long waitTimeSeconds; + private Path workingDir; + private String startedExpression; + private boolean needsLogFile; + private Path logFilePath; + + private final Map systemProps = new HashMap<>(); + + private ExecutorService executorService = Executors.newFixedThreadPool(2); + + public static RunCommandLauncher tryLauncher(QuarkusBootstrap bootstrap, String target, Duration waitTime) { + Map cmds = new HashMap<>(); + try (CuratedApplication curatedApplication = bootstrap.bootstrap()) { + AugmentAction action = curatedApplication.createAugmentor(); + action.performCustomBuild(RunCommandHandler.class.getName(), new Consumer>() { + @Override + public void accept(Map accepted) { + cmds.putAll(accepted); + } + }, + RunCommandActionResultBuildItem.class.getName()); + } catch (BootstrapException ex) { + throw new RuntimeException(ex); + } + List cmd = null; + if (target != null) { + cmd = cmds.get(target); + if (cmd == null) { + throw new RuntimeException("quarkus.run.target \"" + target + "\" does not exist"); + } + } else if (cmds.size() == 1) { // defaults to pure java run + return null; + } else if (cmds.size() == 2) { // choose not default + for (Map.Entry entry : cmds.entrySet()) { + if (entry.getKey().equals("java")) + continue; + cmd = entry.getValue(); + break; + } + } else if (cmds.size() > 2) { + String tooMany = cmds.keySet().stream().collect(Collectors.joining(" ")); + throw new RuntimeException( + "Too many extensions support quarkus:run. Set quarkus.run.target to pick one to run during integration tests: " + + tooMany); + } else { + throw new RuntimeException("Should never reach this!"); + } + RunCommandLauncher launcher = new RunCommandLauncher(); + launcher.args = (List) cmd.get(0); + launcher.workingDir = (Path) cmd.get(1); + launcher.startedExpression = (String) cmd.get(2); + launcher.needsLogFile = (Boolean) cmd.get(3); + launcher.logFilePath = (Path) cmd.get(4); + launcher.waitTimeSeconds = waitTime.getSeconds(); + return launcher; + } + + @Override + public void init(InitContext initContext) { + throw new UnsupportedOperationException("not implemented for run command yet"); + } + + @Override + public LaunchResult runToCompletion(String[] args) { + throw new UnsupportedOperationException("not implemented for run command yet"); + } + + @Override + public void start() throws IOException { + System.setProperty("test.url", TestHTTPResourceManager.getUri()); + + Path logFile = logFilePath; + + System.out.println("Executing \"" + String.join(" ", args) + "\""); + if (needsLogFile) { + if (logFilePath == null) + logFile = PropertyTestUtil.getLogFilePath(); + System.out.println("Creating Logfile for custom extension run: " + logFile.toString()); + Files.deleteIfExists(logFile); + Files.createDirectories(logFile.getParent()); + FileOutputStream logOutputStream = new FileOutputStream(logFile.toFile(), true); + quarkusProcess = new ProcessBuilder(args) + .directory(workingDir.toFile()) + .redirectError(PIPE) + .redirectOutput(PIPE) + .start(); + InputStream tee = new TeeInputStream(quarkusProcess.getInputStream(), System.out); + executorService.submit(() -> tee.transferTo(logOutputStream)); + } else { + quarkusProcess = new ProcessBuilder(args) + .directory(workingDir.toFile()) + .inheritIO() + .start(); + } + CountDownLatch signal = new CountDownLatch(1); + WaitForStartReader reader = new WaitForStartReader(logFile, Duration.ofSeconds(waitTimeSeconds), signal, + startedExpression); + executorService.submit(reader); + + try { + signal.await(waitTimeSeconds + 2, TimeUnit.SECONDS); // wait enough for the signal to be given by the capturing thread + } catch (Exception e) { + // ignore + } + if (!reader.isStarted()) { + LauncherUtil.destroyProcess(quarkusProcess, true); + throw new RuntimeException("Unable to start target quarkus application " + this.waitTimeSeconds + "s"); + } + } + + public boolean listensOnSsl() { + return false; + } + + public void includeAsSysProps(Map systemProps) { + this.systemProps.putAll(systemProps); + } + + @Override + public void close() { + executorService.shutdown(); + if (quarkusProcess != null) { + LauncherUtil.destroyProcess(quarkusProcess, true); + } + } + + /** + * Thread that reads a process output file looking for the line that indicates the address the application + * is listening on. + */ + private static class WaitForStartReader implements Runnable { + + private final Path processOutput; + private final Duration waitTime; + private final CountDownLatch signal; + private final Pattern startedRegex; + private volatile boolean started; + + public WaitForStartReader(Path processOutput, Duration waitTime, CountDownLatch signal, + String startedExpression) { + this.processOutput = processOutput; + this.waitTime = waitTime; + this.signal = signal; + this.startedRegex = Pattern.compile(startedExpression); + } + + public boolean isStarted() { + return started; + } + + @Override + public void run() { + long bailoutTime = System.currentTimeMillis() + waitTime.toMillis(); + try (BufferedReader reader = new BufferedReader(new FileReader(processOutput.toFile()))) { + while (true) { + if (reader.ready()) { // avoid blocking as the input is a file which continually gets more data added + String line = reader.readLine(); + if (startedRegex.matcher(line).find()) { + started = true; + signal.countDown(); + return; + } + } else { + //wait until there is more of the file for us to read + + long now = System.currentTimeMillis(); + if (now > bailoutTime) { + signal.countDown(); + return; + } + + try { + Thread.sleep(LOG_CHECK_INTERVAL); + } catch (InterruptedException e) { + signal.countDown(); + return; + } + } + } + } catch (Exception e) { + System.err.println("Exception occurred while reading process output from file " + processOutput); + e.printStackTrace(); + signal.countDown(); + } + } + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index a721afa2ba601..f7f1a4cf31650 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -412,6 +412,11 @@ public boolean manageNetwork() { return manageNetwork; } + @Override + public CuratedApplication getCuratedApplication() { + return curatedApplication; + } + @Override public void close() { curatedApplication.close(); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/NativeTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/NativeTestExtension.java index f152b33886c65..b943395a575fb 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/NativeTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/NativeTestExtension.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.extension.TestInstancePostProcessor; import org.opentest4j.TestAbortedException; +import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.runtime.test.TestHttpEndpointProvider; import io.quarkus.test.common.ArtifactLauncher; import io.quarkus.test.common.DefaultNativeImageLauncher; @@ -212,6 +213,11 @@ public boolean manageNetwork() { public void close() { } + + @Override + public CuratedApplication getCuratedApplication() { + return null; + } }, System.getProperty("native.image.path"), config.getOptionalValue("quarkus.package.output-directory", String.class).orElse(null), diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java index 0aad608c0c2a6..4d954fabc00f8 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java @@ -17,6 +17,7 @@ import java.io.File; import java.lang.reflect.Field; import java.nio.file.Path; +import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -28,6 +29,7 @@ import java.util.function.Function; import java.util.stream.Collectors; +import org.eclipse.microprofile.config.Config; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; @@ -46,11 +48,13 @@ import io.quarkus.test.common.LauncherUtil; import io.quarkus.test.common.PropertyTestUtil; import io.quarkus.test.common.RestAssuredURLManager; +import io.quarkus.test.common.RunCommandLauncher; import io.quarkus.test.common.TestHostLauncher; import io.quarkus.test.common.TestResourceManager; import io.quarkus.test.common.TestScopeManager; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; import io.quarkus.test.junit.launcher.ArtifactLauncherProvider; +import io.quarkus.test.junit.launcher.ConfigUtil; public class QuarkusIntegrationTestExtension extends AbstractQuarkusTestWithContextExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback, BeforeEachCallback, AfterEachCallback, @@ -258,13 +262,22 @@ public void close() throws Throwable { if ((testHost != null) && !testHost.isEmpty()) { launcher = new TestHostLauncher(); } else { - ServiceLoader loader = ServiceLoader.load(ArtifactLauncherProvider.class); - for (ArtifactLauncherProvider launcherProvider : loader) { - if (launcherProvider.supportsArtifactType(artifactType)) { - launcher = launcherProvider.create( - new DefaultArtifactLauncherCreateContext(quarkusArtifactProperties, context, requiredTestClass, - devServicesLaunchResult)); - break; + Config config = LauncherUtil.installAndGetSomeConfig(); + Duration waitDuration = ConfigUtil.waitTimeValue(config); + String target = ConfigUtil.runTarget(config); + // try to execute a run command published by an extension if it exists. We do this so that extensions that have a custom run don't have to create any special artifact type + launcher = RunCommandLauncher.tryLauncher(devServicesLaunchResult.getCuratedApplication().getQuarkusBootstrap(), + target, waitDuration); + if (launcher == null) { + ServiceLoader loader = ServiceLoader.load(ArtifactLauncherProvider.class); + for (ArtifactLauncherProvider launcherProvider : loader) { + if (launcherProvider.supportsArtifactType(artifactType)) { + launcher = launcherProvider.create( + new DefaultArtifactLauncherCreateContext(quarkusArtifactProperties, context, + requiredTestClass, + devServicesLaunchResult)); + break; + } } } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/ConfigUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/ConfigUtil.java index 2e77ecb7c9ea4..bb7933ce29551 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/ConfigUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/ConfigUtil.java @@ -44,4 +44,9 @@ public static String integrationTestProfile(Config config) { .orElseGet(() -> config.getOptionalValue("quarkus.test.native-image-profile", String.class) .orElse(null)); } + + public static String runTarget(Config config) { + return config.getOptionalValue("quarkus.run.target", String.class) + .orElse(null); + } }