diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b860a4c4..b222508b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,12 +19,16 @@ jobs:
distribution: "temurin"
java-version: "18"
cache: "gradle"
+ - # Required for the package command tests to work
+ name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@63d15e7a1e697b1de6f3ba0507106f89100c8518
- name: Build package
run: ./gradlew build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_ACTOR: ${{ github.actor }}
- name: Publish Test Report
uses: mikepenz/action-junit-report@v4
if: success() || failure() # always run even if the previous step fails
diff --git a/.gitignore b/.gitignore
index 449ced55..1f85c677 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,5 @@ build
# Intellij
.idea
.cq
+
+dist
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..c5cfc2de
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,21 @@
+FROM --platform=$BUILDPLATFORM gradle:8.3-jdk20 as build
+ARG GITHUB_ACTOR
+ARG GITHUB_TOKEN
+
+WORKDIR /code
+
+COPY . .
+
+RUN gradle jar --no-daemon
+
+FROM eclipse-temurin:20-jre
+
+COPY --from=build /code/lib/build/libs/*.jar /app/app.jar
+
+EXPOSE 7777
+
+ENV _JAVA_OPTIONS="--add-opens=java.base/java.nio=ALL-UNNAMED"
+
+ENTRYPOINT ["java", "-jar", "/app/app.jar"]
+
+CMD [ "serve", "--address", "localhost:7777", "--log-format", "json", "--log-level", "info" ]
diff --git a/docs/overview.md b/docs/overview.md
new file mode 100644
index 00000000..027ec107
--- /dev/null
+++ b/docs/overview.md
@@ -0,0 +1,3 @@
+# MemDB Plugin
+
+Test docs for the MemDB plugin.
diff --git a/lib/build.gradle b/lib/build.gradle
index 4916e1ac..7287c96c 100644
--- a/lib/build.gradle
+++ b/lib/build.gradle
@@ -40,6 +40,7 @@ dependencies {
implementation 'io.cloudquery:plugin-pb-java:0.0.15'
implementation 'org.apache.arrow:arrow-memory-core:12.0.1'
implementation 'org.apache.arrow:arrow-vector:12.0.1'
+ implementation 'commons-io:commons-io:2.15.1'
implementation "com.fasterxml.jackson.core:jackson-core:2.16.1"
implementation "com.fasterxml.jackson.core:jackson-annotations:2.16.1"
@@ -77,6 +78,17 @@ java {
}
}
+jar {
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ manifest {
+ attributes "Main-Class": "io.cloudquery.MainClass"
+ }
+
+ from {
+ configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
+ }
+}
+
publishing {
repositories {
maven {
diff --git a/lib/src/main/java/io/cloudquery/memdb/MemDB.java b/lib/src/main/java/io/cloudquery/memdb/MemDB.java
index 07f50e48..7bd87df5 100644
--- a/lib/src/main/java/io/cloudquery/memdb/MemDB.java
+++ b/lib/src/main/java/io/cloudquery/memdb/MemDB.java
@@ -5,6 +5,7 @@
import io.cloudquery.plugin.ClientNotInitializedException;
import io.cloudquery.plugin.NewClientOptions;
import io.cloudquery.plugin.Plugin;
+import io.cloudquery.plugin.PluginKind;
import io.cloudquery.plugin.TableOutputStream;
import io.cloudquery.scheduler.Scheduler;
import io.cloudquery.schema.ClientMeta;
@@ -88,6 +89,8 @@ public void resolve(
public MemDB() {
super("memdb", "0.0.1");
+ setTeam("cloudquery");
+ setKind(PluginKind.Source);
}
@Override
@@ -144,12 +147,14 @@ public void close() {
@Override
public ClientMeta newClient(String spec, NewClientOptions options) throws Exception {
- this.spec = Spec.fromJSON(spec);
this.allTables = getTables();
Tables.transformTables(this.allTables);
for (Table table : this.allTables) {
table.addCQIDs();
}
+ if (!options.isNoConnection()) {
+ this.spec = Spec.fromJSON(spec);
+ }
return new MemDBClient();
}
}
diff --git a/lib/src/main/java/io/cloudquery/plugin/BuildArch.java b/lib/src/main/java/io/cloudquery/plugin/BuildArch.java
new file mode 100644
index 00000000..ee4a3501
--- /dev/null
+++ b/lib/src/main/java/io/cloudquery/plugin/BuildArch.java
@@ -0,0 +1,16 @@
+package io.cloudquery.plugin;
+
+public enum BuildArch {
+ AMD64("amd64"),
+ ARM64("arm64");
+
+ public final String arch;
+
+ private BuildArch(String arch) {
+ this.arch = arch;
+ }
+
+ public String toString() {
+ return this.arch;
+ }
+}
diff --git a/lib/src/main/java/io/cloudquery/plugin/BuildOS.java b/lib/src/main/java/io/cloudquery/plugin/BuildOS.java
new file mode 100644
index 00000000..41b05e0b
--- /dev/null
+++ b/lib/src/main/java/io/cloudquery/plugin/BuildOS.java
@@ -0,0 +1,17 @@
+package io.cloudquery.plugin;
+
+public enum BuildOS {
+ Windows("windows"),
+ Linux("linux"),
+ Darwin("darwin");
+
+ public final String os;
+
+ private BuildOS(String os) {
+ this.os = os;
+ }
+
+ public String toString() {
+ return this.os;
+ }
+}
diff --git a/lib/src/main/java/io/cloudquery/plugin/BuildTarget.java b/lib/src/main/java/io/cloudquery/plugin/BuildTarget.java
new file mode 100644
index 00000000..f9780a19
--- /dev/null
+++ b/lib/src/main/java/io/cloudquery/plugin/BuildTarget.java
@@ -0,0 +1,12 @@
+package io.cloudquery.plugin;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public class BuildTarget {
+ @NonNull protected final BuildOS os;
+ @NonNull protected final BuildArch arch;
+}
diff --git a/lib/src/main/java/io/cloudquery/plugin/Plugin.java b/lib/src/main/java/io/cloudquery/plugin/Plugin.java
index bb46a0bb..9f2ef6f7 100644
--- a/lib/src/main/java/io/cloudquery/plugin/Plugin.java
+++ b/lib/src/main/java/io/cloudquery/plugin/Plugin.java
@@ -17,8 +17,21 @@
public abstract class Plugin {
@NonNull protected final String name;
@NonNull protected final String version;
+
@Setter protected Logger logger;
@Setter protected String jsonSchema;
+
+ @Setter protected String team;
+ @Setter protected PluginKind kind;
+ @Setter protected String dockerfile = "Dockerfile";
+
+ @Setter
+ protected BuildTarget[] buildTargets =
+ new BuildTarget[] {
+ new BuildTarget(BuildOS.Linux, BuildArch.ARM64),
+ new BuildTarget(BuildOS.Linux, BuildArch.AMD64),
+ };
+
protected ClientMeta client;
public void init(String spec, NewClientOptions options) throws Exception {
diff --git a/lib/src/main/java/io/cloudquery/plugin/PluginKind.java b/lib/src/main/java/io/cloudquery/plugin/PluginKind.java
new file mode 100644
index 00000000..e4e94334
--- /dev/null
+++ b/lib/src/main/java/io/cloudquery/plugin/PluginKind.java
@@ -0,0 +1,16 @@
+package io.cloudquery.plugin;
+
+public enum PluginKind {
+ Source("source"),
+ Destination("destination");
+
+ public final String kind;
+
+ private PluginKind(String kind) {
+ this.kind = kind;
+ }
+
+ public String toString() {
+ return this.kind;
+ }
+}
diff --git a/lib/src/main/java/io/cloudquery/schema/Table.java b/lib/src/main/java/io/cloudquery/schema/Table.java
index 7ee91859..858b16d6 100644
--- a/lib/src/main/java/io/cloudquery/schema/Table.java
+++ b/lib/src/main/java/io/cloudquery/schema/Table.java
@@ -9,7 +9,6 @@
import io.cloudquery.schema.Column.ColumnBuilder;
import io.cloudquery.transformers.TransformerException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -32,7 +31,7 @@ public interface Transform {
public static List
flattenTables(List tables) {
Map flattenMap = new LinkedHashMap<>();
for (Table table : tables) {
- Table newTable = table.toBuilder().relations(Collections.emptyList()).build();
+ Table newTable = table.toBuilder().build();
flattenMap.put(newTable.name, newTable);
for (Table child : flattenTables(table.getRelations())) {
flattenMap.put(child.name, child);
diff --git a/lib/src/main/java/io/cloudquery/server/PackageCommand.java b/lib/src/main/java/io/cloudquery/server/PackageCommand.java
new file mode 100644
index 00000000..289072fd
--- /dev/null
+++ b/lib/src/main/java/io/cloudquery/server/PackageCommand.java
@@ -0,0 +1,299 @@
+package io.cloudquery.server;
+
+import com.google.common.base.Strings;
+import io.cloudquery.plugin.BuildTarget;
+import io.cloudquery.plugin.NewClientOptions;
+import io.cloudquery.plugin.Plugin;
+import io.cloudquery.plugin.PluginKind;
+import io.cloudquery.schema.Table;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import lombok.ToString;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.ConsoleAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.layout.JsonLayout;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+@Command(name = "package", description = "package the plugin as a Docker image")
+@ToString
+public class PackageCommand implements Callable {
+ private static Logger logger;
+
+ @Parameters(index = "0", description = "plugin version to package")
+ private String pluginVersion;
+
+ @Parameters(index = "1", description = "plugin directory to package")
+ private String pluginDirectory;
+
+ @Option(
+ required = true,
+ names = {"-m", "--message"},
+ description =
+ "message that summarizes what is new or changed in this version. Supports markdown.")
+ private String message = "";
+
+ @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 = "--no-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;
+
+ @Option(
+ names = "--dist-dir",
+ description = "dist directory to output the built plugin. (default: /dist)")
+ private String distDir = "";
+
+ @Option(
+ names = "--docs-dir",
+ description =
+ "docs directory containing markdown files to copy to the dist directory. (default: /docs)")
+ private String docsDir = "";
+
+ private final Plugin plugin;
+
+ public PackageCommand(Plugin plugin) {
+ this.plugin = plugin;
+ }
+
+ private LoggerContext initLogger() {
+ ConsoleAppender appender =
+ ConsoleAppender.createDefaultAppenderForLayout(
+ this.logFormat == "text"
+ ? PatternLayout.createDefaultLayout()
+ : JsonLayout.createDefaultLayout());
+
+ Configuration configuration = ConfigurationFactory.newConfigurationBuilder().build();
+ configuration.addAppender(appender);
+ LoggerConfig loggerConfig = new LoggerConfig("io.cloudquery", Level.getLevel(logLevel), false);
+ loggerConfig.addAppender(appender, null, null);
+ configuration.addLogger("io.cloudquery", loggerConfig);
+ LoggerContext context = new LoggerContext(ServeCommand.class.getName() + "Context");
+ context.start(configuration);
+
+ logger = context.getLogger(ServeCommand.class.getName());
+ this.plugin.setLogger(logger);
+ return context;
+ }
+
+ @SuppressWarnings("null")
+ @Override
+ public Integer call() {
+ try (LoggerContext context = this.initLogger()) {
+ if (Strings.isNullOrEmpty(plugin.getName())) {
+ logger.error("name is required");
+ return 1;
+ }
+ if (Strings.isNullOrEmpty(plugin.getVersion())) {
+ logger.error("version is required");
+ return 1;
+ }
+ if (Strings.isNullOrEmpty(plugin.getTeam())) {
+ logger.error("team is required");
+ return 1;
+ }
+ if (Strings.isNullOrEmpty(plugin.getDockerfile())) {
+ logger.error("Dockerfile is required");
+ return 1;
+ }
+ if (plugin.getBuildTargets() == null || plugin.getBuildTargets().length == 0) {
+ logger.error("At least one build target is required");
+ return 1;
+ }
+ if (Strings.isNullOrEmpty(distDir)) {
+ distDir = Paths.get(pluginDirectory, "dist").toString();
+ }
+ if (Strings.isNullOrEmpty(docsDir)) {
+ docsDir = Paths.get(pluginDirectory, "docs").toString();
+ }
+
+ initDist();
+ copyDocs();
+ writeTablesJson();
+ List supportedTargets = buildDocker();
+ writePackageJson(supportedTargets);
+
+ return 0;
+ } catch (Exception e) {
+ logger.error("Failed to package plugin", e);
+ return 1;
+ }
+ }
+
+ private void initDist() throws IOException {
+ logger.info("Packaging plugin to {}", distDir);
+ File dist = new File(distDir);
+ if (!dist.exists()) {
+ boolean created = dist.mkdirs();
+ if (!created) {
+ logger.error("Failed to create dist directory {}", distDir);
+ throw new IOException("Failed to create dist directory");
+ }
+ }
+ }
+
+ private void copyDocs() throws IllegalArgumentException, IOException {
+ File docs = new File(docsDir);
+ if (!docs.exists()) {
+ logger.error("Docs directory path{} does not exist", docsDir);
+ throw new IllegalArgumentException("Docs directory does not exist");
+ }
+ if (!docs.isDirectory()) {
+ logger.error("Docs path {} is not a directory", docsDir);
+ throw new IllegalArgumentException("Docs path is not a directory");
+ }
+
+ String outputPath = Paths.get(distDir, "docs").toString();
+ logger.info("Copying docs from {} to {}", docsDir, outputPath);
+ File output = new File(outputPath);
+ FileUtils.copyDirectory(docs, output);
+ }
+
+ private void writeTablesJson() throws Exception {
+ if (plugin.getKind() != PluginKind.Source) {
+ return;
+ }
+
+ String outputPath = Paths.get(distDir, "tables.json").toString();
+ logger.info("Writing tables.json to {}", outputPath);
+ plugin.init("", NewClientOptions.builder().noConnection(true).build());
+ List tables = plugin.tables(Arrays.asList("*"), Arrays.asList(), false);
+ List flattenTables = Table.flattenTables(tables);
+ TablesJson tablesJson = new TablesJson(flattenTables);
+ String json = tablesJson.toJson();
+ FileUtils.writeStringToFile(new File(outputPath), json, "UTF-8");
+ }
+
+ @SuppressWarnings("null")
+ private List buildDocker()
+ throws IllegalArgumentException, IOException, InterruptedException {
+ String dockerFilePath = Paths.get(pluginDirectory, plugin.getDockerfile()).toString();
+ File dockerFile = new File(dockerFilePath);
+ if (!dockerFile.exists()) {
+ logger.error("Dockerfile {} does not exist", dockerFilePath);
+ throw new IllegalArgumentException("Dockerfile does not exist");
+ }
+ if (!dockerFile.isFile()) {
+ logger.error("Dockerfile {} is not a file", dockerFilePath);
+ throw new IllegalArgumentException("Dockerfile is not a file");
+ }
+
+ List supportedTargets = new ArrayList<>();
+ for (BuildTarget target : plugin.getBuildTargets()) {
+ String imageRepository =
+ String.format(
+ "docker.cloudquery.io/%s/%s-%s",
+ plugin.getTeam(), plugin.getKind(), plugin.getName());
+ String os = target.getOs().toString();
+ String arch = target.getArch().toString();
+ String imageTag = String.format("%s:%s-%s-%s", imageRepository, pluginVersion, os, arch);
+ String imageTar =
+ String.format("plugin-%s-%s-%s-%s.tar", plugin.getName(), pluginVersion, os, arch);
+ String imagePath = Paths.get(distDir, imageTar).toString();
+ logger.info("Building docker image {}", imageTag);
+ // GITHUB_ACTOR and GITHUB_TOKEN are required for the Dockerfile to pull the CloudQuery Java
+ // libs from GitHub Packages
+ String githubActor = System.getenv("GITHUB_ACTOR");
+ if (Strings.isNullOrEmpty(githubActor)) {
+ logger.error("GITHUB_ACTOR env variable is required");
+ throw new IllegalArgumentException("GITHUB_ACTOR env variable is required");
+ }
+ String githubToken = System.getenv("GITHUB_TOKEN");
+ if (Strings.isNullOrEmpty(githubToken)) {
+ logger.error("GITHUB_TOKEN env variable is required");
+ throw new IllegalArgumentException("GITHUB_TOKEN env variable is required");
+ }
+ String[] dockerBuildArguments = {
+ "buildx",
+ "build",
+ "-t",
+ imageTag,
+ "--platform",
+ String.format("%s/%s", os, arch),
+ "-f",
+ dockerFilePath,
+ ".",
+ "--progress",
+ "plain",
+ "--load",
+ "--build-arg",
+ String.format("GITHUB_ACTOR=%s", githubActor),
+ "--build-arg",
+ String.format("GITHUB_TOKEN=%s", githubToken),
+ };
+ logger.debug("Running docker command: '{}'", String.join(" ", dockerBuildArguments));
+ runCommand(dockerBuildArguments);
+ logger.debug("Saving docker image {} to {}", imageTag, imagePath);
+ String[] dockerSaveArguments = {"save", "-o", imagePath, imageTag};
+ logger.debug("Running docker command: '{}'", String.join(" ", dockerSaveArguments));
+ runCommand(dockerSaveArguments);
+ try (InputStream is = Files.newInputStream(Paths.get(imagePath))) {
+ String checksum = DigestUtils.sha256Hex(is);
+ SupportedTargetJson supportedTarget = new SupportedTargetJson(os, arch, imageTar, checksum);
+ supportedTargets.add(supportedTarget);
+ }
+ }
+
+ return supportedTargets;
+ }
+
+ private void runCommand(String[] command) throws IOException, InterruptedException {
+ ArrayList allArgs = new ArrayList<>(Arrays.asList(command));
+ allArgs.add(0, "docker");
+ ProcessBuilder processBuilder =
+ new ProcessBuilder(allArgs.toArray(new String[allArgs.size()])).inheritIO();
+ processBuilder.directory(new File(pluginDirectory));
+ Process process = processBuilder.start();
+ int exitCode = process.waitFor();
+ if (exitCode != 0) {
+ logger.error("Failed to run command: '{}'", String.join(" ", command));
+ throw new IOException("Failed to run command");
+ }
+ }
+
+ private void writePackageJson(List supportedTargets) throws IOException {
+ String outputPath = Paths.get(distDir, "package.json").toString();
+ logger.info("Writing package.json to {}", outputPath);
+ PackageJson packageJson =
+ new PackageJson(
+ plugin.getName(),
+ plugin.getTeam(),
+ plugin.getKind().toString(),
+ pluginVersion,
+ message,
+ supportedTargets);
+ String json = packageJson.toJson();
+ FileUtils.writeStringToFile(new File(outputPath), json, "UTF-8");
+ }
+}
diff --git a/lib/src/main/java/io/cloudquery/server/PackageJson.java b/lib/src/main/java/io/cloudquery/server/PackageJson.java
new file mode 100644
index 00000000..1092cbf6
--- /dev/null
+++ b/lib/src/main/java/io/cloudquery/server/PackageJson.java
@@ -0,0 +1,30 @@
+package io.cloudquery.server;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import java.util.List;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public class PackageJson {
+ @NonNull private final String name;
+ @NonNull private final String team;
+ @NonNull private final String kind;
+ @NonNull private final String version;
+ @NonNull private final String message;
+ @NonNull private final List supported_targets;
+
+ private final int schema_version = 1;
+ private int[] protocols = {3};
+ private String package_type = "docker";
+
+ public String toJson() throws JsonProcessingException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
+ return objectMapper.writeValueAsString(this);
+ }
+}
diff --git a/lib/src/main/java/io/cloudquery/server/PluginServe.java b/lib/src/main/java/io/cloudquery/server/PluginServe.java
index f308d92e..b1153421 100644
--- a/lib/src/main/java/io/cloudquery/server/PluginServe.java
+++ b/lib/src/main/java/io/cloudquery/server/PluginServe.java
@@ -14,6 +14,9 @@ public class PluginServe {
public int Serve() {
Extensions.registerExtensions();
- return new CommandLine(new RootCommand()).addSubcommand(new ServeCommand(plugin)).execute(args);
+ CommandLine cli = new CommandLine(new RootCommand());
+ cli.addSubcommand(new ServeCommand(plugin));
+ cli.addSubcommand(new PackageCommand(plugin));
+ return cli.execute(args);
}
}
diff --git a/lib/src/main/java/io/cloudquery/server/SupportedTargetJson.java b/lib/src/main/java/io/cloudquery/server/SupportedTargetJson.java
new file mode 100644
index 00000000..ac228330
--- /dev/null
+++ b/lib/src/main/java/io/cloudquery/server/SupportedTargetJson.java
@@ -0,0 +1,14 @@
+package io.cloudquery.server;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NonNull;
+
+@AllArgsConstructor
+@Getter
+public class SupportedTargetJson {
+ @NonNull private final String os;
+ @NonNull private final String arch;
+ @NonNull private final String path;
+ @NonNull private final String checksum;
+}
diff --git a/lib/src/main/java/io/cloudquery/server/TablesJson.java b/lib/src/main/java/io/cloudquery/server/TablesJson.java
new file mode 100644
index 00000000..e57b857b
--- /dev/null
+++ b/lib/src/main/java/io/cloudquery/server/TablesJson.java
@@ -0,0 +1,68 @@
+package io.cloudquery.server;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import io.cloudquery.schema.Column;
+import io.cloudquery.schema.Table;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TablesJson {
+ public class ColumnJson {
+ public String name;
+ public String description;
+ public String type;
+ public boolean incremental_key;
+ public boolean not_null;
+ public boolean primary_key;
+ public boolean unique;
+ }
+
+ public class TableJson {
+ public String name;
+ public String description;
+ public boolean is_incremental;
+ public String parent;
+ public List relations;
+ public List columns;
+ }
+
+ private List tables;
+
+ public TablesJson(List tables) {
+ this.tables = tables;
+ }
+
+ public String toJson() throws JsonProcessingException {
+ List json = new ArrayList<>();
+ for (Table table : tables) {
+ TableJson tableJson = new TableJson();
+ tableJson.name = table.getName();
+ tableJson.description = table.getDescription();
+ tableJson.is_incremental = false;
+ tableJson.parent = table.getParent() == null ? null : table.getParent().getName();
+ tableJson.relations = new ArrayList<>();
+ for (Table relation : table.getRelations()) {
+ tableJson.relations.add(relation.getName());
+ }
+ tableJson.columns = new ArrayList<>();
+ for (Column column : table.getColumns()) {
+ ColumnJson columnJson = new ColumnJson();
+ columnJson.name = column.getName();
+ columnJson.description = column.getDescription();
+ columnJson.type = column.getType().toString();
+ columnJson.incremental_key = column.isIncrementalKey();
+ columnJson.not_null = column.isNotNull();
+ columnJson.primary_key = column.isPrimaryKey();
+ columnJson.unique = column.isUnique();
+ tableJson.columns.add(columnJson);
+ }
+ json.add(tableJson);
+ }
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
+ return objectMapper.writeValueAsString(json);
+ }
+}
diff --git a/lib/src/test/java/io/cloudquery/server/PluginPackageTest.java b/lib/src/test/java/io/cloudquery/server/PluginPackageTest.java
new file mode 100644
index 00000000..fe5f34ed
--- /dev/null
+++ b/lib/src/test/java/io/cloudquery/server/PluginPackageTest.java
@@ -0,0 +1,26 @@
+package io.cloudquery.server;
+
+import io.cloudquery.memdb.MemDB;
+import io.cloudquery.server.PluginServe.PluginServeBuilder;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.jupiter.api.Test;
+
+public class PluginPackageTest {
+ @Test
+ public void packageTest() {
+ Path file = Paths.get("..").toAbsolutePath().normalize();
+ String absolutePath = file.toString();
+
+ PluginServe pluginServe =
+ new PluginServeBuilder()
+ .plugin(new MemDB())
+ .args(
+ new String[] {
+ "package", "--log-level", "debug", "-m", "initial version", "v1.0.0", absolutePath
+ })
+ .build();
+ int exitCode = pluginServe.Serve();
+ assert exitCode == 0;
+ }
+}