diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 842e4d6..6a00989 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: linux-image: name: Linux runs-on: ubuntu-latest - container: debian:stretch + container: public.ecr.aws/seqera-labs/graalvm-static:latest timeout-minutes: 90 steps: @@ -49,7 +49,7 @@ jobs: path: build/reports/tests/test/ - name: Build Native Image - run: ./gradlew nativeBuild + run: ./gradlew nativeCompile - name: Upload linux native image artifact uses: actions/upload-artifact@v2 @@ -92,7 +92,7 @@ jobs: path: build/reports/tests/test/ - name: Build Native Image - run: ./gradlew nativeBuild + run: ./gradlew nativeCompile - name: Upload Mac native image artifact uses: actions/upload-artifact@v2 @@ -141,7 +141,7 @@ jobs: path: build/reports/tests/test/ - name: Build Native Image - run: ./gradlew nativeBuild + run: ./gradlew nativeCompile - name: Upload Windows native image artifact uses: actions/upload-artifact@v2 diff --git a/.gitignore b/.gitignore index c5f0be6..0a75080 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ out/ .settings .classpath .factorypath +**/build-info.properties diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..ceab6e1 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1 \ No newline at end of file diff --git a/VERSION-API b/VERSION-API new file mode 100644 index 0000000..dc39e58 --- /dev/null +++ b/VERSION-API @@ -0,0 +1 @@ +1.6 \ No newline at end of file diff --git a/build.gradle b/build.gradle index ec79d1c..9c166ab 100644 --- a/build.gradle +++ b/build.gradle @@ -24,13 +24,16 @@ def junitVersion = providers.gradleProperty('junit.jupiter.version') .get() dependencies { + annotationProcessor("info.picocli:picocli-codegen") annotationProcessor("io.micronaut:micronaut-http-validation") annotationProcessor("io.micronaut:micronaut-inject-java") annotationProcessor("io.micronaut:micronaut-graal") implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut:micronaut-runtime") + implementation("io.micronaut.picocli:micronaut-picocli") implementation("io.micronaut.rxjava2:micronaut-rxjava2") implementation("io.micronaut.rxjava2:micronaut-rxjava2-http-client") + implementation("info.picocli:picocli") implementation("javax.annotation:javax.annotation-api") implementation("javax.inject:javax.inject:1") runtimeOnly("ch.qos.logback:logback-classic") @@ -46,21 +49,46 @@ application { mainClass.set("io.seqera.tower.agent.Agent") } -java { +String gitVersion() { + def p = new ProcessBuilder().command('sh', '-c', 'git rev-parse --short HEAD').start() + def r = p.waitFor() + return r == 0 ? p.text.trim() : '(unknown)' +} + +task buildInfo { + doLast { + def version = rootProject.file('VERSION').text.trim() + def versionApi = rootProject.file('VERSION-API').text.trim() + def commitId = gitVersion().trim() + def info = """\ + version=${version} + versionApi=${versionApi} + commitId=${commitId} + """.stripIndent().toString() + def f = file("src/main/resources/META-INF/build-info.properties") + f.parentFile.mkdirs() + f.text = info + } +} + +compileJava { sourceCompatibility = JavaVersion.toVersion("11") targetCompatibility = JavaVersion.toVersion("11") + options.compilerArgs += ["-Aproject=${project.name}"] + dependsOn buildInfo } test { useJUnitPlatform() } - graalvmNative { binaries { main { imageName = 'towr-agent' mainClass = 'io.seqera.tower.agent.Agent' + buildArgs.add('--static') + buildArgs.add('--libc=musl') } test { @@ -70,3 +98,5 @@ graalvmNative { } } + + diff --git a/graalvm-static/Dockerfile b/graalvm-static/Dockerfile new file mode 100644 index 0000000..8d1d979 --- /dev/null +++ b/graalvm-static/Dockerfile @@ -0,0 +1,40 @@ +FROM debian:bullseye +RUN DEBIAN_FRONTEND=noninteractive apt update && apt install --assume-yes --no-install-recommends build-essential zlib1g-dev wget ca-certificates && rm -rf /var/lib/apt/lists/* +RUN mkdir /libs +RUN mkdir /downloads + +# Musl +RUN cd /downloads && wget https://musl.libc.org/releases/musl-1.2.2.tar.gz && tar xvzf musl-1.2.2.tar.gz && rm musl-1.2.2.tar.gz +RUN cd /downloads/musl-1.2.2 && ./configure --disable-shared --prefix=/libs && make && make install + +# zlib +ENV PATH="/libs/bin:${PATH}" +RUN cd /downloads && wget https://zlib.net/zlib-1.2.11.tar.gz && tar xvzf zlib-1.2.11.tar.gz && rm zlib-1.2.11.tar.gz +RUN cd /downloads/zlib-1.2.11 && CC=musl-gcc ./configure --static --prefix=/libs && make && make install + +# libstdc++ +RUN cp /usr/lib/gcc/x86_64-linux-gnu/10/libstdc++.a /libs/lib/ + + +# +# tar xvzf musl-1.2.2.tar.gz +# cd musl-1.2.2 + +# ./configure --disable-shared --prefix=/libs +# make +# make install + +# cd .. +# wget https://zlib.net/zlib-1.2.11.tar.gz +# tar xvzf zlib-1.2.11.tar.gz +# cd zlib-1.2.11 + +# export PATH=/libs/bin:$PATH +# export CC=musl-gcc +# ./configure --static --prefix=/libs + +# cp /usr/lib/gcc/x86_64-linux-gnu/10/libstdc++.a /libs/lib/ + +# apt install wget +# wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-21.2.0/graalvm-ce-java11-linux-amd64-21.2.0.tar.gz +# tar xvzf diff --git a/micronaut-cli.yml b/micronaut-cli.yml index 659bdae..c8502ea 100644 --- a/micronaut-cli.yml +++ b/micronaut-cli.yml @@ -3,4 +3,4 @@ defaultPackage: io.seqera.tower.agent testFramework: junit sourceLanguage: java buildTool: gradle -features: [annotation-api, app-name, graalvm, gradle, http-client, java, java-application, junit, logback, netty-server, readme, shade, yaml] +features: [annotation-api, app-name, graalvm, gradle, http-client, java, java-application, junit, logback, netty-server, picocli, picocli-java-application, picocli-junit, readme, shade, yaml] diff --git a/src/main/java/io/seqera/tower/agent/Agent.java b/src/main/java/io/seqera/tower/agent/Agent.java index 6b9d0c8..9578487 100644 --- a/src/main/java/io/seqera/tower/agent/Agent.java +++ b/src/main/java/io/seqera/tower/agent/Agent.java @@ -1,48 +1,81 @@ package io.seqera.tower.agent; +import io.micronaut.configuration.picocli.PicocliRunner; import io.micronaut.context.ApplicationContext; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpRequest; import io.micronaut.rxjava2.http.client.websockets.RxWebSocketClient; import io.micronaut.scheduling.TaskScheduler; +import io.micronaut.websocket.exceptions.WebSocketClientException; import io.seqera.tower.agent.exchange.CommandResponse; import java.net.URI; +import java.net.URISyntaxException; import java.time.Duration; -import java.util.Map; -public class Agent { +import io.seqera.tower.agent.utils.VersionProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Command; +@Command( + name = "towr-agent", + description = "Nextflow Tower anywhere agent", + headerHeading = "%n", + versionProvider = VersionProvider.class, + mixinStandardHelpOptions = true, + sortOptions = false, + abbreviateSynopsis = true, + descriptionHeading = "%n", + commandListHeading = "%nCommands:%n", + requiredOptionMarker = '*', + usageHelpWidth = 160, + parameterListHeading = "%nParameters:%n", + optionListHeading = "%nOptions:%n" +) +public class Agent implements Runnable { + private static Logger logger = LoggerFactory.getLogger(Agent.class); private ApplicationContext ctx; - private Map env; - private String hostName; - private String accessToken; - private String towerUrl; + + @Parameters(index = "0", paramLabel = "AGENT_KEY", description = "Agent key to identify this agent", arity = "1") + String agentKey; + + @Option(names = {"-t", "--access-token"}, description = "Tower personal access token (TOWER_ACCESS_TOKEN)", defaultValue = "${TOWER_ACCESS_TOKEN}") + String token; + + @Option(names = {"-u", "--url"}, description = "Tower server API endpoint URL. Defaults to tower.nf (TOWER_API_ENDPOINT)", defaultValue = "${TOWER_API_ENDPOINT:-https://api.tower.nf}", required = true) + public String url; private AgentClientSocket agentClient; Agent() { - env = System.getenv(); ctx = ApplicationContext.run(); } - public void run() throws Exception { - hostName = env.get("TOWER_AGENT_HOSTNAME"); - if (hostName == null) throw new IllegalStateException("TOWER_AGENT_HOSTNAME not set"); - - accessToken = env.get("TOWER_ACCESS_TOKEN"); - if (accessToken == null) throw new IllegalStateException("TOWER_ACCESS_TOKEN not set"); - - towerUrl = env.get("TOWER_API_ENDPOINT"); - if (towerUrl == null) throw new IllegalStateException("TOWER_API_ENDPOINT not set"); + @Override + public void run() { - final URI uri = new URI(towerUrl + "/agent/" + hostName + "/connect"); - final MutableHttpRequest req = HttpRequest.GET(uri).bearerAuth(accessToken); - final RxWebSocketClient webSocketClient = ctx.getBean(RxWebSocketClient.class); - agentClient = webSocketClient.connect(AgentClientSocket.class, req).blockingFirst(); + final URI uri; + try { + uri = new URI(url + "/agent/" + agentKey + "/connect"); + final MutableHttpRequest req = HttpRequest.GET(uri).bearerAuth(token); + final RxWebSocketClient webSocketClient = ctx.getBean(RxWebSocketClient.class); + agentClient = webSocketClient.connect(AgentClientSocket.class, req).blockingFirst(); + logger.info("Connected"); - System.out.println("Connected"); - sendPeriodicHeartbeat(); + sendPeriodicHeartbeat(); + } catch (URISyntaxException e) { + logger.error(String.format("Invalid URI: %s/agent/%s/connect - %s", url, agentKey, e.getMessage())); + System.exit(-1); + } catch (WebSocketClientException e) { + logger.error(String.format("Connection error - %s", e.getMessage())); + System.exit(-1); + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } } /** @@ -58,6 +91,6 @@ private void sendPeriodicHeartbeat() { } public static void main(String[] args) throws Exception { - new Agent().run(); + PicocliRunner.run(Agent.class, args); } } diff --git a/src/main/java/io/seqera/tower/agent/AgentClientSocket.java b/src/main/java/io/seqera/tower/agent/AgentClientSocket.java index 04a31c7..a3d15a6 100644 --- a/src/main/java/io/seqera/tower/agent/AgentClientSocket.java +++ b/src/main/java/io/seqera/tower/agent/AgentClientSocket.java @@ -70,7 +70,9 @@ void onMessage(CommandRequest message) { @OnClose void onClose() { - System.out.println("Closed after " + Duration.between(openingTime, Instant.now())); + if (openingTime != null) { + System.out.println("Closed after " + Duration.between(openingTime, Instant.now())); + } } abstract void send(CommandResponse response); diff --git a/src/main/java/io/seqera/tower/agent/utils/VersionProvider.java b/src/main/java/io/seqera/tower/agent/utils/VersionProvider.java new file mode 100644 index 0000000..bb4e157 --- /dev/null +++ b/src/main/java/io/seqera/tower/agent/utils/VersionProvider.java @@ -0,0 +1,15 @@ +package io.seqera.tower.agent.utils; + +import picocli.CommandLine; + +import java.util.Properties; + +public class VersionProvider implements CommandLine.IVersionProvider { + + @Override + public String[] getVersion() throws Exception { + Properties properties = new Properties(); + properties.load(this.getClass().getResourceAsStream("/META-INF/build-info.properties")); + return new String[]{String.format("@|yellow Tower Agent version %s (build %s)|@", properties.get("version"), properties.get("commitId"))}; + } +} diff --git a/towr-agent b/towr-agent new file mode 100755 index 0000000..ca25226 --- /dev/null +++ b/towr-agent @@ -0,0 +1,5 @@ +#!/bin/bash + + +bash -c "./gradlew run -q --args=\"${*@Q}\"" +