diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24d2f8d5ff3..c945ce60ce0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,6 +57,31 @@ jobs: with: flags: java + gradle: + name: Java/Gradle + runs-on: ubuntu-latest + needs: java + env: + working-directory: ./tools/apprunner-gradle-plugin + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 8 + uses: actions/setup-java@v1 + with: + java-version: 8 + - name: Cache Gradle packages + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Populate version from maven + run: mvn help:evaluate -Dexpression=project.version -q -DforceStdout --file ../pom.xml > version.txt + working-directory: ${{env.working-directory}} + - name: Build with Gradle + run: ./gradlew build + working-directory: ${{env.working-directory}} + python: name: Python runs-on: ubuntu-latest diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index cf5f81d3d1a..cce8bb50445 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -63,6 +63,12 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- + - name: Cache Gradle packages + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle - name: Build with Maven run: mvn -B install javadoc:javadoc-no-fork --file pom.xml -Pcode-coverage,jdk8-tests env: @@ -77,6 +83,12 @@ jobs: - uses: codecov/codecov-action@v1 with: flags: java + - name: Populate version from maven + run: mvn help:evaluate -Dexpression=project.version -q -DforceStdout --file ../pom.xml > version.txt + working-directory: ./tools/apprunner-gradle-plugin + - name: Build with Gradle + run: ./gradlew build + working-directory: ./tools/apprunner-gradle-plugin python: name: Python diff --git a/.gitignore b/.gitignore index 92a424c3b51..5d17ec28880 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,11 @@ node_modules/ .project .settings .checkstyle +.gradle +build + +# gradle +gradle/ +out +gradle/wrapper/gradle-wrapper.jar +version.txt diff --git a/pom.xml b/pom.xml index 8b452c697ce..eee1fd248e4 100644 --- a/pom.xml +++ b/pom.xml @@ -826,6 +826,7 @@ limitations under the License. **/*.gz **/*.xls **/META-INF/services/** + **/META-INF/gradle-plugins/** **/*.md **/*.xls **/*.doc @@ -875,6 +876,7 @@ limitations under the License. **/q **/c.java **/swagger-ui/** + **/gradlew **/node_modules/** diff --git a/tools/apprunner-gradle-plugin/build.gradle b/tools/apprunner-gradle-plugin/build.gradle new file mode 100644 index 00000000000..78fd3dc6eab --- /dev/null +++ b/tools/apprunner-gradle-plugin/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java-gradle-plugin' + id 'maven-publish' + id 'com.gradle.plugin-publish' version '0.12.0' +} + +version = file('version.txt').text.trim() + +allprojects { + group = "org.projectnessie" + version = version + repositories { + mavenCentral() + mavenLocal() + } +} + +gradlePlugin { + plugins { + simplePlugin { + id = 'org.projectnessie' + implementationClass = 'com.dremio.nessie.quarkus.gradle.QuarkusAppPlugin' + displayName = 'Quarkus App Runner' + description = 'Start and stop a quarkus app as gradle tasks for integration testing' + } + } +} + +repositories { + mavenCentral() +} + +compileJava { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +dependencies { + implementation platform('io.quarkus:quarkus-bom:1.9.0.Final') + + compile "io.quarkus:quarkus-bootstrap-core" + compile "org.projectnessie:nessie-apprunner-maven-plugin:$version" + compile "org.eclipse.microprofile.config:microprofile-config-api" +} + +pluginBundle { + website = 'https://projectnessie.org' + vcsUrl = 'https://github.com/projectnessie/nessie' + tags = ['test', 'integration', 'quarkus'] +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'org.projectnessie' + artifactId = 'nessie-apprunner-gradle-plugin' + version = version + + from components.java + } + } +} diff --git a/tools/apprunner-gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/tools/apprunner-gradle-plugin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..1277f7dac0f --- /dev/null +++ b/tools/apprunner-gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,24 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip diff --git a/tools/apprunner-gradle-plugin/gradlew b/tools/apprunner-gradle-plugin/gradlew new file mode 100755 index 00000000000..e718f9c62b8 --- /dev/null +++ b/tools/apprunner-gradle-plugin/gradlew @@ -0,0 +1,176 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +if [ ! -e $APP_HOME/gradle/wrapper/gradle-wrapper.jar ]; then + curl -o $APP_HOME/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/gradle/gradle/v5.4.1/gradle/wrapper/gradle-wrapper.jar +fi + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/tools/apprunner-gradle-plugin/settings.gradle b/tools/apprunner-gradle-plugin/settings.gradle new file mode 100644 index 00000000000..295bd994380 --- /dev/null +++ b/tools/apprunner-gradle-plugin/settings.gradle @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +rootProject.name = 'quarkus-apprunner' + diff --git a/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusApp.java b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusApp.java new file mode 100644 index 00000000000..687e76f7758 --- /dev/null +++ b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusApp.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.dremio.nessie.quarkus.gradle; + +import static io.quarkus.bootstrap.resolver.maven.DeploymentInjectingDependencyVisitor.toArtifact; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.maven.plugin.MojoExecutionException; +import org.eclipse.aether.artifact.Artifact; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency; + +import com.google.common.collect.ImmutableList; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.app.RunningQuarkusApplication; +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.bootstrap.model.AppDependency; +import io.quarkus.bootstrap.model.AppModel; +import io.quarkus.bootstrap.util.QuarkusModelHelper; +import io.quarkus.bootstrap.util.ZipUtils; + + +/** + * Start and Stop quarkus. + */ +public class QuarkusApp extends com.dremio.nessie.quarkus.maven.QuarkusApp { + + protected QuarkusApp(RunningQuarkusApplication runningApp) { + super(runningApp); + } + + public static AutoCloseable newApplication(Configuration configuration, Project project, Properties props) { + + Configuration deploy = project.getConfigurations().create("quarkusAppDeploy"); + final AppModel appModel; + + appModel = convert(configuration, deploy); + URL[] urls = appModel.getFullDeploymentDeps().stream().map(QuarkusApp::toUrl).toArray(URL[]::new); + ClassLoader cl = new URLClassLoader(urls, com.dremio.nessie.quarkus.maven.QuarkusApp.class.getClassLoader()); + try { + return com.dremio.nessie.quarkus.maven.QuarkusApp.newApplication(appModel, project.getProjectDir().toPath(), + Paths.get(project.getBuildDir().getPath()), props, cl); + } catch (MojoExecutionException e) { + throw new GradleException("Unable to start Quarkus", e); + } + } + + private static URL toUrl(AppDependency dep) { + try { + return dep.getArtifact().getPaths().getSinglePath().toUri().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public static AppModel convert(Configuration configuration, Configuration deploy) { + + AppModel.Builder appBuilder = new AppModel.Builder(); + + final Set userDeps = new HashSet<>(); + final Set deployDeps = new HashSet<>(); + // set of dependencies requested by the user (usually the artifact that contains the quarkus app) + Set baseConfigs = configuration.getDependencies() + .stream() + .map(QuarkusApp::toDependency) + .collect(Collectors.toSet()); + assert baseConfigs.size() == 1; // currently we only know how to support the single quarkus app artifact + AppArtifact appArtifact = baseConfigs.iterator().next(); + // resolve all dependencies of the artifacts from above. + configuration.getResolvedConfiguration() + .getResolvedArtifacts() + .stream() + .map(QuarkusApp::toDependency) + .filter(x -> !appArtifact.equals(x.getArtifact())) // remove base deps, accounted for below + .forEach(userDeps::add); + // for each user dependency check if it has any associated deployment deps and add those to the deploy config + userDeps.stream() + .map(x -> QuarkusApp.handleMetaInf(appBuilder, x)) + .filter(Objects::nonNull) + .map(x -> new DefaultExternalModuleDependency(x.getGroupId(), x.getArtifactId(), x.getVersion())) + .forEach(x -> deploy.getDependencies().add(x)); + + // resolve the deployment deps and their dependencies + deploy.getResolvedConfiguration().getResolvedArtifacts().stream() + .map(QuarkusApp::toDependency).forEach(deployDeps::add); + + + // find the path of the base app artifact + Optional path = configuration.getFiles().stream().map(File::getAbsolutePath) + .filter(x->x.contains(appArtifact.getArtifactId())) + .filter(x->x.contains(appArtifact.getGroupId().replace(".", File.separator))) + .filter(x->x.contains(appArtifact.getVersion())) + .findFirst(); + appArtifact.setPath(Paths.get(path.orElseThrow(() -> + new UnsupportedOperationException(String.format("Unknown path for app artifact %s", appArtifact))))); + + // combine user and deploy deps and build app model + List allDeps = new ArrayList<>(userDeps); + allDeps.addAll(deployDeps); + appBuilder.addRuntimeDeps(new ArrayList<>(userDeps)) + .addFullDeploymentDeps(allDeps) + .addDeploymentDeps(new ArrayList<>(deployDeps)) + .setAppArtifact(appArtifact); + return appBuilder.build(); + } + + /** + * for each dependent artifact read its META-INF looking for quarkus metadata + */ + private static AppArtifact handleMetaInf(AppModel.Builder appBuilder, AppDependency dependency) { + try { + Path path = dependency.getArtifact().getPaths().getSinglePath(); + try (FileSystem artifactFs = ZipUtils.newFileSystem(path)) { + Path metaInfPath = artifactFs.getPath(BootstrapConstants.META_INF); + final Path p = metaInfPath.resolve(BootstrapConstants.DESCRIPTOR_FILE_NAME); + return Files.exists(p) ? processPlatformArtifact(appBuilder, dependency.getArtifact(), p) : null; + } + } catch (IOException e) { + throw new GradleException("couldn't read artifact", e); + } + } + + /** + * Search for quarkus metadata and if found augment the AppModel builder. Return any deployment deps. + */ + private static AppArtifact processPlatformArtifact(AppModel.Builder appBuilder, AppArtifact node, Path descriptor) + throws IOException { + final Properties rtProps = resolveDescriptor(descriptor); + if (rtProps == null) { + return null; + } + appBuilder.handleExtensionProperties(rtProps, node.toString()); + final String value = rtProps.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT); + if (value == null) { + return null; + } + Artifact deploymentArtifact = toArtifact(value); + if (deploymentArtifact.getVersion() == null || deploymentArtifact.getVersion().isEmpty()) { + deploymentArtifact = deploymentArtifact.setVersion(node.getVersion()); + } + + return new AppArtifact(deploymentArtifact.getGroupId(), deploymentArtifact.getArtifactId(), + deploymentArtifact.getClassifier(), "jar", deploymentArtifact.getVersion()); + } + + private static Properties resolveDescriptor(final Path path) throws IOException { + final Properties rtProps; + if (!Files.exists(path)) { + // not a platform artifact + return null; + } + rtProps = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + rtProps.load(reader); + } + return rtProps; + } + + private static AppArtifact toDependency(Dependency dependency) { + AppArtifact artifact = new AppArtifact(dependency.getGroup(), dependency.getName(), null, + "jar", dependency.getVersion()); + return artifact; + } + + private static AppDependency toDependency(ResolvedArtifact dependency) { + ModuleVersionIdentifier id = dependency.getModuleVersion().getId(); + AppArtifact artifact = new AppArtifact(id.getGroup(), dependency.getName(), dependency.getClassifier(), + dependency.getType(), id.getVersion()); + artifact.setPaths(QuarkusModelHelper.toPathsCollection(ImmutableList.of(dependency.getFile()))); + return new AppDependency(artifact, "runtime"); + } + +} diff --git a/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusAppExtension.java b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusAppExtension.java new file mode 100644 index 00000000000..0ec5c1bf03c --- /dev/null +++ b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusAppExtension.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.dremio.nessie.quarkus.gradle; + +import org.gradle.api.Project; +import org.gradle.api.provider.MapProperty; + +public class QuarkusAppExtension { + private final MapProperty props; + public QuarkusAppExtension(Project project) { + props = project.getObjects().mapProperty(String.class, Object.class); + } + + public MapProperty getProps() { + return props; + } +} diff --git a/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusAppPlugin.java b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusAppPlugin.java new file mode 100644 index 00000000000..9715d9d6273 --- /dev/null +++ b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/QuarkusAppPlugin.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.dremio.nessie.quarkus.gradle; + + +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.tasks.TaskProvider; + + +public class QuarkusAppPlugin implements Plugin { + + @Override + public void apply(Project target) { + QuarkusAppExtension extension = target.getExtensions().create("quarkusAppRunnerProperties", QuarkusAppExtension.class, target); + + final Configuration config = target.getConfigurations().create("quarkusAppRunnerConfig") + .setVisible(false) + .setDescription("The config for the Quarkus Runner."); + + target.getTasks().register("quarkus-start", StartTask.class, new Action() { + @Override + public void execute(StartTask task) { + task.setConfig(config); + task.setProps(extension.getProps().get()); + } + }); + + target.getTasks().register("quarkus-stop", StopTask.class); + + } +} diff --git a/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/StartTask.java b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/StartTask.java new file mode 100644 index 00000000000..5bb8b832cd4 --- /dev/null +++ b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/StartTask.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.dremio.nessie.quarkus.gradle; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Map; +import java.util.Properties; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.testing.Test; + +public class StartTask extends DefaultTask { + private static final Object lock = new Object(); + private Configuration dataFiles; + private Map props; + + public StartTask() { + + } + + + @TaskAction + public void start() { + getLogger().info("Starting Quarkus application."); + + synchronized (lock) { + final URL[] urls = getDataFiles().getFiles().stream().map(StartTask::toURL).toArray(URL[]::new); + + final URLClassLoader mirrorCL = new URLClassLoader(urls, this.getClass().getClassLoader()); + + Properties properties = new Properties(); + properties.putAll(props); + + final AutoCloseable quarkusApp; + try { + Class clazz = mirrorCL.loadClass(QuarkusApp.class.getName()); + Method newApplicationMethod = clazz.getMethod("newApplication", Configuration.class, Project.class, Properties.class); + quarkusApp = (AutoCloseable) newApplicationMethod.invoke(null, dataFiles, getProject(), properties); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + + for (String key: props.keySet()) { + String value = System.getProperty(key); + if (value != null) { + ((Test) getProject().getTasks().getByName("test")).systemProperty(key, value); + } + } + + getLogger().info("Quarkus application started."); + setApplicationHandle(() -> { + try { + quarkusApp.close(); + } finally { + mirrorCL.close(); + } + }); + } + } + + @InputFiles + private FileCollection getDataFiles() { + return dataFiles; + } + + public void setConfig(Configuration files) { + this.dataFiles = files; + } + + private void setApplicationHandle(AutoCloseable application) { + // update stop task with this task's closeable + + StopTask task = (StopTask) getProject().getTasks().getByName("quarkus-stop"); + if (task.getApplication() != null) { + getLogger().warn("StopTask application is not empty!"); + } + task.setQuarkusApplication(application); + } + + private static URL toURL(File artifact) { + try { + return artifact.toURI().toURL(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + public void setProps(Map props) { + this.props = props; + } +} diff --git a/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/StopTask.java b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/StopTask.java new file mode 100644 index 00000000000..f3d3c1f28a0 --- /dev/null +++ b/tools/apprunner-gradle-plugin/src/main/java/com/dremio/nessie/quarkus/gradle/StopTask.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.dremio.nessie.quarkus.gradle; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.TaskAction; + +public class StopTask extends DefaultTask { + private AutoCloseable application; + + public StopTask() { + + } + + @TaskAction + public void start() { + + if (application == null) { + getLogger().warn("No application found."); + } + + try { + application.close(); + getLogger().info("Quarkus application stopped."); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + application = null; + } + } + + public AutoCloseable getApplication() { + return application; + } + + public void setQuarkusApplication(AutoCloseable quarkusApplication) { + application = quarkusApplication; + } + +} diff --git a/tools/apprunner-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.projectnessie.nessie-apprunner-gradle-plugin b/tools/apprunner-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.projectnessie.nessie-apprunner-gradle-plugin new file mode 100644 index 00000000000..9c04426ffc3 --- /dev/null +++ b/tools/apprunner-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.projectnessie.nessie-apprunner-gradle-plugin @@ -0,0 +1 @@ +implementation=com.dremio.nessie.quarkus.gradle.QuarkusAppPlugin diff --git a/tools/apprunner-gradle-plugin/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource b/tools/apprunner-gradle-plugin/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource new file mode 100644 index 00000000000..11ae2f4a571 --- /dev/null +++ b/tools/apprunner-gradle-plugin/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource @@ -0,0 +1 @@ +com.dremio.nessie.quarkus.maven.MojoConfigSource diff --git a/tools/apprunner-maven-plugin/pom.xml b/tools/apprunner-maven-plugin/pom.xml index a7b0bf03639..2b5dac81294 100644 --- a/tools/apprunner-maven-plugin/pom.xml +++ b/tools/apprunner-maven-plugin/pom.xml @@ -59,13 +59,4 @@ provided - - - - - org.apache.maven.plugins - maven-plugin-plugin - - - diff --git a/tools/apprunner-maven-plugin/src/main/java/com/dremio/nessie/quarkus/maven/MojoConfigSource.java b/tools/apprunner-maven-plugin/src/main/java/com/dremio/nessie/quarkus/maven/MojoConfigSource.java index 120da0b26be..8a1a75a1ae4 100644 --- a/tools/apprunner-maven-plugin/src/main/java/com/dremio/nessie/quarkus/maven/MojoConfigSource.java +++ b/tools/apprunner-maven-plugin/src/main/java/com/dremio/nessie/quarkus/maven/MojoConfigSource.java @@ -40,7 +40,8 @@ public Map getProperties() { @Override public String getValue(String propertyName) { - return properties.getProperty(propertyName); + Object obj = properties.get(propertyName); + return obj == null ? null : obj.toString(); } @Override diff --git a/tools/apprunner-maven-plugin/src/main/java/com/dremio/nessie/quarkus/maven/QuarkusApp.java b/tools/apprunner-maven-plugin/src/main/java/com/dremio/nessie/quarkus/maven/QuarkusApp.java index f6b043d47ac..a8489f51d80 100644 --- a/tools/apprunner-maven-plugin/src/main/java/com/dremio/nessie/quarkus/maven/QuarkusApp.java +++ b/tools/apprunner-maven-plugin/src/main/java/com/dremio/nessie/quarkus/maven/QuarkusApp.java @@ -49,7 +49,7 @@ public class QuarkusApp implements AutoCloseable { private static final String MOJO_CONFIG_SOURCE_CLASSNAME = "com.dremio.nessie.quarkus.maven.MojoConfigSource"; private final RunningQuarkusApplication runningApp; - private QuarkusApp(RunningQuarkusApplication runningApp) { + protected QuarkusApp(RunningQuarkusApplication runningApp) { this.runningApp = runningApp; } @@ -89,7 +89,7 @@ public static QuarkusApp newApplication(MavenProject project, RepositorySystem r "Failed to resolve application model " + appArtifact + " dependencies", e); } - return newApplication(appModel, appArtifact, project.getBasedir().toPath(), Paths.get(project.getBuild().getDirectory()), + return newApplication(appModel, project.getBasedir().toPath(), Paths.get(project.getBuild().getDirectory()), applicationProperties); } @@ -101,21 +101,41 @@ public static QuarkusApp newApplication(MavenProject project, RepositorySystem r * classloader. * * @param appModel the application model - * @param appArtifact the quarkus application artifact id * @param projectRoot the current project directory * @param targetDirectory the target directory * @param applicationProperties the extra application properties * @return a quarkus app instance * @throws MojoExecutionException if an error occurs during execution */ - public static QuarkusApp newApplication(AppModel appModel, AppArtifact appArtifact, + public static QuarkusApp newApplication(AppModel appModel, Path projectRoot, Path targetDirectory, Properties applicationProperties) throws MojoExecutionException { + return newApplication(appModel, projectRoot, targetDirectory, applicationProperties, QuarkusApp.class.getClassLoader()); + } + + /** + * Instantiate and start a quarkus application. + * + *

Instantiates and start a quarkus application using Quarkus bootstrap + * framework. Only one application can be started at a time in the same + * classloader. + * + * @param appModel the application model + * @param projectRoot the current project directory + * @param targetDirectory the target directory + * @param applicationProperties the extra application properties + * @param classLoader the classloader to use when starting the application + * @return a quarkus app instance + * @throws MojoExecutionException if an error occurs during execution + */ + public static QuarkusApp newApplication(AppModel appModel, + Path projectRoot, Path targetDirectory, Properties applicationProperties, ClassLoader classLoader) + throws MojoExecutionException { final AdditionalDependency mojoConfigSourceDependency = findMojoConfigSourceDependency(); final QuarkusBootstrap bootstrap = QuarkusBootstrap.builder() .setAppArtifact(appModel.getAppArtifact()) - .setBaseClassLoader(QuarkusApp.class.getClassLoader()).setExistingModel(appModel) + .setBaseClassLoader(classLoader).setExistingModel(appModel) .setProjectRoot(projectRoot).setTargetDirectory(targetDirectory).setIsolateDeployment(true) .setMode(Mode.TEST).addAdditionalApplicationArchive(mojoConfigSourceDependency).build();