diff --git a/.gitignore b/.gitignore index aeafd2c..4c348d6 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ replay_pid* .DS_Store # Ignore Gradle build output directory build + +# Intellij +.idea diff --git a/lib/build.gradle b/lib/build.gradle index 60020d3..73d5ad6 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java-library' + id "io.freefair.lombok" version "8.1.0" } repositories { @@ -19,9 +20,12 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:31.1-jre' + implementation 'info.picocli:picocli:4.7.4' + implementation 'com.google.guava:guava:31.1-jre' implementation "io.grpc:grpc-protobuf:1.57.0" implementation "io.grpc:grpc-stub:1.57.0" implementation "io.grpc:grpc-services:1.57.0" + implementation "io.grpc:grpc-testing:1.15.1" implementation 'com.cloudquery:plugin-pb-java:0.0.2' } @@ -38,6 +42,6 @@ testing { // Apply a specific Java toolchain to ease working on different environments. java { toolchain { - languageVersion = JavaLanguageVersion.of(18) + languageVersion = JavaLanguageVersion.of(20) } } diff --git a/lib/src/main/java/cloudquery/plugin/sdk/DiscoveryServer.java b/lib/src/main/java/cloudquery/plugin/sdk/DiscoveryServer.java deleted file mode 100644 index b297f84..0000000 --- a/lib/src/main/java/cloudquery/plugin/sdk/DiscoveryServer.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -package cloudquery.plugin.sdk; - -import io.grpc.Grpc; -import io.grpc.Server; -import io.grpc.InsecureServerCredentials; -import cloudquery.discovery.v1.DiscoveryGrpc; -import java.io.IOException; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class DiscoveryServer extends DiscoveryGrpc.DiscoveryImplBase { - private static final Logger logger = Logger.getLogger(DiscoveryServer.class.getName()); - private Server server; - private int port = 10002; - - public void start() throws IOException, InterruptedException { - server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) - .addService(this) - .executor(Executors.newFixedThreadPool(1)) - .build(); - server.start(); - logger.log(Level.INFO, "Started on {0}", port); - server.awaitTermination(); - } -} diff --git a/lib/src/main/java/cloudquery/plugin/sdk/PluginServer.java b/lib/src/main/java/cloudquery/plugin/sdk/PluginServer.java deleted file mode 100644 index e35e6f0..0000000 --- a/lib/src/main/java/cloudquery/plugin/sdk/PluginServer.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -package cloudquery.plugin.sdk; - -import io.grpc.Grpc; -import io.grpc.Server; -import io.grpc.InsecureServerCredentials; -import cloudquery.plugin.v3.PluginGrpc; -import java.io.IOException; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class PluginServer extends PluginGrpc.PluginImplBase { - private static final Logger logger = Logger.getLogger(PluginServer.class.getName()); - private Server server; - private int port = 10001; - - public void start() throws IOException, InterruptedException { - server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) - .addService(this) - .executor(Executors.newFixedThreadPool(1)) - .build(); - server.start(); - logger.log(Level.INFO, "Started on {0}", port); - server.awaitTermination(); - } -} diff --git a/lib/src/main/java/io/cloudquery/internal/servers/discovery/v1/DiscoverServer.java b/lib/src/main/java/io/cloudquery/internal/servers/discovery/v1/DiscoverServer.java new file mode 100644 index 0000000..447639f --- /dev/null +++ b/lib/src/main/java/io/cloudquery/internal/servers/discovery/v1/DiscoverServer.java @@ -0,0 +1,22 @@ +package io.cloudquery.internal.servers.discovery.v1; + +import cloudquery.discovery.v1.DiscoveryGrpc.DiscoveryImplBase; +import cloudquery.discovery.v1.DiscoveryOuterClass.GetVersions.Request; +import cloudquery.discovery.v1.DiscoveryOuterClass.GetVersions.Response; +import io.grpc.stub.StreamObserver; + +import java.util.List; + +public class DiscoverServer extends DiscoveryImplBase { + private final List versions; + + public DiscoverServer(List versions) { + this.versions = versions; + } + + @Override + public void getVersions(Request request, StreamObserver responseObserver) { + responseObserver.onNext(Response.newBuilder().addAllVersions(versions).build()); + responseObserver.onCompleted(); + } +} diff --git a/lib/src/main/java/io/cloudquery/internal/servers/plugin/v3/PluginServer.java b/lib/src/main/java/io/cloudquery/internal/servers/plugin/v3/PluginServer.java new file mode 100644 index 0000000..e700f65 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/internal/servers/plugin/v3/PluginServer.java @@ -0,0 +1,12 @@ +package io.cloudquery.internal.servers.plugin.v3; + +import cloudquery.plugin.v3.PluginGrpc.PluginImplBase; +import io.cloudquery.plugin.Plugin; + +public class PluginServer extends PluginImplBase { + private final Plugin plugin; + + public PluginServer(Plugin plugin) { + this.plugin = plugin; + } +} diff --git a/lib/src/main/java/io/cloudquery/plugin/Plugin.java b/lib/src/main/java/io/cloudquery/plugin/Plugin.java new file mode 100644 index 0000000..96d977f --- /dev/null +++ b/lib/src/main/java/io/cloudquery/plugin/Plugin.java @@ -0,0 +1,18 @@ +package io.cloudquery.plugin; + +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +@Builder(builderMethodName = "innerBuilder") +@Getter +public class Plugin { + public static PluginBuilder builder(String name, String version) { + return innerBuilder().name(name).verion(version); + } + + @NonNull + private final String name; + @NonNull + private final String verion; +} diff --git a/lib/src/main/java/io/cloudquery/server/AddressConverter.java b/lib/src/main/java/io/cloudquery/server/AddressConverter.java new file mode 100644 index 0000000..ac424f5 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/server/AddressConverter.java @@ -0,0 +1,22 @@ +package io.cloudquery.server; + +import picocli.CommandLine.ITypeConverter; + +public class AddressConverter implements ITypeConverter { + public static class AddressParseException extends RuntimeException { + + } + public record Address(String host, int port) { + } + + @Override + public Address convert(String rawAddress) throws Exception { + String[] components = rawAddress.split(":"); + if (components.length != 2) { + throw new AddressParseException(); + } + return new Address(components[0], Integer.parseInt(components[1])); + } + + +} diff --git a/lib/src/main/java/io/cloudquery/server/DocCommand.java b/lib/src/main/java/io/cloudquery/server/DocCommand.java new file mode 100644 index 0000000..ab143c1 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/server/DocCommand.java @@ -0,0 +1,7 @@ +package io.cloudquery.server; + +import picocli.CommandLine; + +@CommandLine.Command +public class DocCommand { +} diff --git a/lib/src/main/java/io/cloudquery/server/PluginServe.java b/lib/src/main/java/io/cloudquery/server/PluginServe.java new file mode 100644 index 0000000..7a02fc0 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/server/PluginServe.java @@ -0,0 +1,33 @@ +package io.cloudquery.server; + +import io.cloudquery.plugin.Plugin; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.NonNull; +import picocli.CommandLine; + +import java.util.ArrayList; +import java.util.List; + +@Builder(access = AccessLevel.PUBLIC) +public class PluginServe { + @NonNull + private final Plugin plugin; + @Builder.Default + private List args = new ArrayList<>(); + private boolean destinationV0V1Server; + private String sentryDSN; + private boolean testListener; + //TODO: Allow a test listener to be passed in + // testListenerConn *bufconn.Listener + + public void Serve() throws ServerException { + int exitStatus = new CommandLine(new RootCommand()). + addSubcommand("serve", new ServeCommand(plugin)). + addSubcommand("doc", new DocCommand()). + execute(args.toArray(new String[]{})); + if (exitStatus != 0) { + throw new ServerException("error processing command line exit status = "+exitStatus); + } + } +} diff --git a/lib/src/main/java/io/cloudquery/server/RootCommand.java b/lib/src/main/java/io/cloudquery/server/RootCommand.java new file mode 100644 index 0000000..336a89c --- /dev/null +++ b/lib/src/main/java/io/cloudquery/server/RootCommand.java @@ -0,0 +1,7 @@ +package io.cloudquery.server; + +import picocli.CommandLine; + +@CommandLine.Command +public class RootCommand { +} diff --git a/lib/src/main/java/io/cloudquery/server/ServeCommand.java b/lib/src/main/java/io/cloudquery/server/ServeCommand.java new file mode 100644 index 0000000..2ea55c4 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/server/ServeCommand.java @@ -0,0 +1,82 @@ +package io.cloudquery.server; + +import io.cloudquery.internal.servers.discovery.v1.DiscoverServer; +import io.cloudquery.internal.servers.plugin.v3.PluginServer; +import io.cloudquery.plugin.Plugin; +import io.cloudquery.server.AddressConverter.Address; +import io.grpc.Grpc; +import io.grpc.InsecureServerCredentials; +import io.grpc.Server; +import io.grpc.protobuf.services.ProtoReflectionService; +import lombok.ToString; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static picocli.CommandLine.Command; +import static picocli.CommandLine.Option; + +@Command +@ToString +public class ServeCommand implements Callable { + private static final Logger logger = Logger.getLogger(ServeCommand.class.getName()); + public static final List DISCOVERY_VERSIONS = List.of(3); + + @Option(names = "--address", converter = AddressConverter.class, description = "address to serve on. can be tcp: localhost:7777 or unix socket: `/tmp/plugin.rpc.sock` (default \"${DEFAULT-VALUE}\")") + private Address address = new Address("localhost", 7777); + + @Option(names = "--log-format", description = "log format. one of: text,json (default \"${DEFAULT-VALUE}\")") + private String logFormat = "text"; + + @Option(names = "--log-level", description = "log level. one of: trace,debug,info,warn,error (default \"${DEFAULT-VALUE}\")") + private String logLevel = "info"; + + @Option(names = "--network", description = "the network must be \"tcp\", \"tcp4\", \"tcp6\", \"unix\" or \"unixpacket\" (default \"${DEFAULT-VALUE}\")") + private String network = "tcp"; + + @Option(names = "--disable-sentry", description = "disable sentry") + private Boolean disableSentry = false; + + @Option(names = "--otel-endpoint", description = "Open Telemetry HTTP collector endpoint") + private String otelEndpoint = ""; + + @Option(names = "--otel-endpoint-insecure", description = "use Open Telemetry HTTP endpoint (for development only)") + private Boolean otelEndpointInsecure = false; + + private final Plugin plugin; + + public ServeCommand(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public Integer call() throws Exception { + // Initialize a logger + + // Configure open telemetry + + // Configure test listener + + // Configure gRPC server + Server server = Grpc.newServerBuilderForPort(address.port(), InsecureServerCredentials.create()). + addService(new DiscoverServer(DISCOVERY_VERSIONS)). + addService(new PluginServer(plugin)). + addService(ProtoReflectionService.newInstance()). + executor(Executors.newFixedThreadPool(10)). + build(); + + // Configure sentry + + // Log we are listening on address and port + + // Run gRPC server and block + server.start(); + logger.log(Level.INFO, "Started server on {0}", address); + server.awaitTermination(); + return 0; + } + +} diff --git a/lib/src/main/java/io/cloudquery/server/ServerException.java b/lib/src/main/java/io/cloudquery/server/ServerException.java new file mode 100644 index 0000000..6483e35 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/server/ServerException.java @@ -0,0 +1,7 @@ +package io.cloudquery.server; + +public class ServerException extends Exception { + public ServerException(String message) { + super(message); + } +} diff --git a/lib/src/test/java/cloudquery/plugin/sdk/DiscoveryServerTest.java b/lib/src/test/java/cloudquery/plugin/sdk/DiscoveryServerTest.java deleted file mode 100644 index 962df88..0000000 --- a/lib/src/test/java/cloudquery/plugin/sdk/DiscoveryServerTest.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -package cloudquery.plugin.sdk; - -import org.junit.Test; - -public class DiscoveryServerTest { - @Test - public void NewServer() throws Exception { - new DiscoveryServer(); - } -} diff --git a/lib/src/test/java/cloudquery/plugin/sdk/PluginServerTest.java b/lib/src/test/java/cloudquery/plugin/sdk/PluginServerTest.java deleted file mode 100644 index 4237608..0000000 --- a/lib/src/test/java/cloudquery/plugin/sdk/PluginServerTest.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -package cloudquery.plugin.sdk; - -import org.junit.Test; - -public class PluginServerTest { - @Test - public void NewServer() throws Exception { - new PluginServer(); - } -} diff --git a/lib/src/test/java/io/cloudquery/server/AddressTest.java b/lib/src/test/java/io/cloudquery/server/AddressTest.java new file mode 100644 index 0000000..5292a6e --- /dev/null +++ b/lib/src/test/java/io/cloudquery/server/AddressTest.java @@ -0,0 +1,36 @@ +package io.cloudquery.server; + +import io.cloudquery.server.AddressConverter.Address; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class AddressTest { + + private AddressConverter addressConverter; + + @Before + public void setUp() { + addressConverter = new AddressConverter(); + } + + @Test + public void shouldParseAddressFromString() throws Exception { + String rawAddress = "127.0.0.1:12345"; + + Address address = addressConverter.convert(rawAddress); + + assertEquals(new Address("127.0.0.1", 12345), address); + } + + @Test + public void shouldThrowExceptionIfAddressNotFormattedCorrectly() { + String rawAddress = "bad address"; + + AddressConverter addressConverter = new AddressConverter(); + + Assert.assertThrows(AddressConverter.AddressParseException.class, () -> addressConverter.convert(rawAddress)); + } +} \ No newline at end of file diff --git a/lib/src/test/java/io/cloudquery/server/PluginServeTest.java b/lib/src/test/java/io/cloudquery/server/PluginServeTest.java new file mode 100644 index 0000000..b4d5d00 --- /dev/null +++ b/lib/src/test/java/io/cloudquery/server/PluginServeTest.java @@ -0,0 +1,57 @@ +package io.cloudquery.server; + +import io.cloudquery.plugin.Plugin; +import io.cloudquery.server.PluginServe.PluginServeBuilder; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; + + +@Ignore(value = "blocking tests - only used manually to test the gRPC runs correctly") +public class PluginServeTest { + public static final String URL = "https://sentry.url"; + + private Plugin plugin; + + @Before + public void setUp() { + plugin = Plugin.builder("test-plugin", "0.1.0").build(); + } + + @Test + public void simpleCallToServe() throws ServerException { + PluginServe pluginServe = new PluginServeBuilder(). + plugin(plugin). + sentryDSN(URL). + args(List.of("serve")). + build(); + pluginServe.Serve(); + } + + @Test + public void simpleCallToServeHelp() throws ServerException { + PluginServe pluginServe = new PluginServeBuilder(). + plugin(plugin). + sentryDSN(URL). + args(List.of("serve", "--help")). + build(); + pluginServe.Serve(); + } + + @Test + public void simpleOverrideCommandLineArguments() throws ServerException { + PluginServe pluginServe = new PluginServeBuilder(). + plugin(plugin). + sentryDSN(URL). + args(List.of( + "serve", + "--address", "foo.bar.com:7777", + "--disable-sentry", + "--otel-endpoint", "some-endpoint" + )). + build(); + pluginServe.Serve(); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index f2b3609..6975dd7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,5 +11,5 @@ plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0' } -rootProject.name = 'cloudquery.plugin.sdk' +rootProject.name = 'plugin-sdk-java' include('lib')