diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57f1cb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 08de2d0..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index aa00ffa..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 4a35cfe..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index d7270e9..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml deleted file mode 100644 index e96534f..0000000 --- a/.idea/uiDesigner.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 185093f..d12c620 100644 --- a/pom.xml +++ b/pom.xml @@ -4,19 +4,28 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - me.aurium - tick + + xyz.auriium + insect + 0.3.0 + + + tickbox-parent 1.0.0 + TickBox + Testcontainers alternative + + + scm:git:https://github.com/Auriium/TickBox.git + + - tick-full - tick-rapid - tick-api + tickbox-api + tickbox-containers-sql - 1.8 - ${compiler.version} ${compiler.version} @@ -24,7 +33,7 @@ 3.6.0 3.6.3 - 1.15.2 + 3.2.7 @@ -52,13 +61,23 @@ - + + + + xyz.auriium + tickbox-api + 1.0.0 + + + + + - repsy - ElytraForceRepo - https://repo.repsy.io/mvn/elytraforce/default + central-repo + https://repo.auriium.xyz/releases - + + + - \ No newline at end of file diff --git a/readme.md b/readme.md index 2d691eb..7b55542 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,73 @@ -Powerful testcontainer alternative with actually object oriented code, no bullshitty static abuse, no lombok, no ResourceReaper, no piss in the cup + +# TickBox + +A dockerized Database Container creation system + maven plugins. + +# Description + +A tool meant for developers who really really don't want to use the mess that is TestContainers. + +Comes in both code-based (tickbox-api) and maven plugin (tickbox-plugin) formats depending on +your use case (Whether you want a single container for all tests or need multiple containers for +multiple tests.) + +# Warning + +TickBox is not TestContainers. It isn't really even meant to fill the same role that testcontainers fills. +Tick is a general purpose container creation library fit for use with JDBC. TestContainers is explicitly for testing. + +While Tick is also very useful in the testing environment, Tick explicitly avoids hackery and "magic code" +that applications like TestContainers may implement in order to smoothen the testing experience. +In TestContainers, various reflective hacks are done in order to allow containers to stop at the end of a JUnit test. +A separate docker container is deployed just to make sure resources do not escape. (ryuk) + +In TestContainers, I try to explicitly avoid magic code/hackery/static abuse while also offering you the option of choice. +TestContainers is brittle and static in design, and if you want to change something you'll have to dig deep into +the archaic, bloated source code and edit it in yourself. If you want to change something in TickBox, write a new +implementation of a single interface, or change a value in a configuration object. + +The biggest part of TestContainers for me that I attempt to give choice with is the ResourceReaper, +or in our case, the ResourceManager. In TestContainers, it is a static part of the program +that is essential, forcing Ryuk and runtime shutdown hooks down your throat. Here, you may choose +if you would like the HookResourceManager (Features both automatic closing of containers on jvm shutdown +as well as closing of containers and images when the main Tick instance is closed) or the EmptyResourceManager +(which allows you to manually and explicitly remove resources left behind) + +# Unix Info + +If you want to use TickBox on Unix (MacOS or Linux) make sure that Docker is set up so it does +not need the sudo command to run. TickBox (And TestContainers) use console commands internally +to interact with Docker and if access to Docker is limited you will get an error like so: + +```WARNING: Error loading config file: /home/user/.docker/config.json - +stat /home/user/.docker/config.json: permission denied``` +``` + +You can fix this by following the instructions of the site here: +https://docs.docker.com/engine/install/linux-postinstall/ + +A Docker-Machine module exists but is currently unusuable due to the various requirements to convince +DockerMachine to port-forward external port access to the internal docker container. Therefore, please +do not attempt to use the docker-machine strategy. + + +# Maven Info + +``` + + + xyz.auriium + tickbox-api + 1.0.0 + + + + + + central-repo + https://repo.auriium.xyz/releases + + +``` + +"TickBox" as in "Tick", a parasite that hooks onto something (docker) and "Box", as in a container. diff --git a/tick-api/src/main/java/me/aurium/tick/DockerHolder.java b/tick-api/src/main/java/me/aurium/tick/DockerHolder.java deleted file mode 100644 index 685d8ec..0000000 --- a/tick-api/src/main/java/me/aurium/tick/DockerHolder.java +++ /dev/null @@ -1,4 +0,0 @@ -package me.aurium.tick; - -public interface DockerHolder { -} diff --git a/tick-api/src/main/java/me/aurium/tick/DockerSource.java b/tick-api/src/main/java/me/aurium/tick/DockerSource.java deleted file mode 100644 index c6d9ff8..0000000 --- a/tick-api/src/main/java/me/aurium/tick/DockerSource.java +++ /dev/null @@ -1,9 +0,0 @@ -package me.aurium.tick; - -import com.amihaiemil.docker.Docker; - -public interface DockerSource { - - Docker produceDocker(); - -} diff --git a/tick-api/tick-api.iml b/tick-api/tick-api.iml deleted file mode 100644 index 78b2cc5..0000000 --- a/tick-api/tick-api.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/tick-full/pom.xml b/tick-full/pom.xml deleted file mode 100644 index 8ff90fe..0000000 --- a/tick-full/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - tick - me.aurium - 1.0.0 - - 4.0.0 - - maven-plugin - - tick-full - - 1.0.1 - - - ${compiler.version} - ${compiler.version} - - - - - - org.apache.maven.plugins - maven-jar-plugin - ${maven.jarplugin.version} - - - org.apache.maven.plugins - maven-plugin-plugin - 3.6.0 - - tick-full - true - - - - - - - - org.testcontainers - testcontainers - ${testcontainers.version} - - - org.testcontainers - mariadb - 1.11.4 - - - - - org.flywaydb - flyway-core - 7.5.0 - - - org.jooq - jooq-codegen - 3.14.3 - - - - - - \ No newline at end of file diff --git a/tick-full/src/main/java/me/aurium/tick/full/AbstractTickMojo.java b/tick-full/src/main/java/me/aurium/tick/full/AbstractTickMojo.java deleted file mode 100644 index 0096fd9..0000000 --- a/tick-full/src/main/java/me/aurium/tick/full/AbstractTickMojo.java +++ /dev/null @@ -1,39 +0,0 @@ -package me.aurium.tick.full; - -import org.apache.maven.plugin.AbstractMojo; -import org.apache.maven.plugins.annotations.Parameter; - -public abstract class AbstractTickMojo extends AbstractMojo { - //encapsulation over inheritance... bah, i don't care, it's a shitty maven plugin (fix this later) - - public CommonInitializers getInitializer() { - return initializer; - } - - public String[] getLocations() { - return locations; - } - - public String getOutputDirectory() { - return outputDirectory; - } - - public String getPackageName() { - return packageName; - } - - @Parameter(defaultValue = "MARIADB") - private CommonInitializers initializer; - - @Parameter(required = true) - private String[] locations; - - @Parameter(defaultValue = "target/generated-sources") - private String outputDirectory; - - @Parameter(defaultValue = "me.aurium.tick.sources") - private String packageName; - - - -} diff --git a/tick-full/src/main/java/me/aurium/tick/full/CommonInitializers.java b/tick-full/src/main/java/me/aurium/tick/full/CommonInitializers.java deleted file mode 100644 index 8b0cc48..0000000 --- a/tick-full/src/main/java/me/aurium/tick/full/CommonInitializers.java +++ /dev/null @@ -1,26 +0,0 @@ -package me.aurium.tick.full; - -import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.MariaDBContainer; - -public enum CommonInitializers implements JDBCInitializer { - MARIADB(new MariaDBContainer<>(),"org.jooq.meta.mariadb.MariaDBDatabase"); - - private final JdbcDatabaseContainer consumer; - private final String jooqClassName; - - CommonInitializers(JdbcDatabaseContainer consumer, String jooqClassName) { - this.consumer = consumer; - this.jooqClassName = jooqClassName; - } - - @Override - public JdbcDatabaseContainer initializeContainer(String username, String password, String databaseName) { - return consumer.withDatabaseName(databaseName).withUsername(username).withPassword(password); - } - - @Override - public String correspondingJooqClassName() { - return jooqClassName; - } -} diff --git a/tick-full/src/main/java/me/aurium/tick/full/JDBCInitializer.java b/tick-full/src/main/java/me/aurium/tick/full/JDBCInitializer.java deleted file mode 100644 index e09c097..0000000 --- a/tick-full/src/main/java/me/aurium/tick/full/JDBCInitializer.java +++ /dev/null @@ -1,12 +0,0 @@ -package me.aurium.tick.full; - -import org.testcontainers.containers.JdbcDatabaseContainer; - -public interface JDBCInitializer { - - JdbcDatabaseContainer initializeContainer(String username, String password, String databaseName); - - String correspondingJooqClassName(); - - -} diff --git a/tick-full/src/main/java/me/aurium/tick/full/PopulateGoalMojo.java b/tick-full/src/main/java/me/aurium/tick/full/PopulateGoalMojo.java deleted file mode 100644 index 883de36..0000000 --- a/tick-full/src/main/java/me/aurium/tick/full/PopulateGoalMojo.java +++ /dev/null @@ -1,95 +0,0 @@ -package me.aurium.tick.full; - -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; -import org.apache.maven.plugins.annotations.LifecyclePhase; -import org.apache.maven.plugins.annotations.Mojo; -import org.flywaydb.core.Flyway; -import org.jooq.codegen.GenerationTool; -import org.jooq.meta.jaxb.*; -import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.utility.ResourceReaper; - -@Mojo(name = "generate", defaultPhase = LifecyclePhase.INITIALIZE) -public class PopulateGoalMojo extends AbstractTickMojo{ - - @Override - public void execute() throws MojoExecutionException,MojoFailureException { - - getLog().info("(TICK) Initializing TestContainer!"); - - JdbcDatabaseContainer construct = getInitializer().initializeContainer("username","password","sandbox"); - - getLog().info("(TICK) Starting TestContainer!"); - - construct.start(); - - String url = construct.getJdbcUrl(); - String username = construct.getUsername(); - String password = construct.getPassword(); - - getLog().info("URL: " + url + " USERNAME: " + username + " PASSWORD: " + password); - - getLog().info("(TICK) TestContainer successfully deployed! Loading Flyway!"); - - if (getLocations() == null) throw new MojoFailureException("No locations to draw sources from!"); - - Flyway flyway = Flyway.configure(getClass().getClassLoader()) - .dataSource(url,username,password) - .locations(getLocations()) - .validateMigrationNaming(true).group(true) - .load(); - - getLog().info("(TICK) Flyway successfully loaded! Migrating to testcontainer now!"); - - flyway.migrate(); - - getLog().info("(TICK) Flyway successfully migrated! Now activating JOOQ configuration!"); - - String output = getParsedOutput(); - - - Configuration configuration = new Configuration() - .withJdbc(new Jdbc() - .withDriver(construct.getDriverClassName()) - .withUrl(url) - .withUser(username) - .withPassword(password)) - .withGenerator(new Generator() - .withDatabase(new Database() - .withName(getInitializer().correspondingJooqClassName()) - .withIncludes(".*") - .withExcludes("") - .withInputSchema(construct.getDatabaseName())) //TODO testing - .withTarget(new Target() - .withPackageName(getPackageName()) - .withDirectory(output))); - - getLog().info("(TICK) Configuration successful! Activating JOOQ Code Generation!"); - - try { - GenerationTool.generate(configuration); - } catch (Exception e) { - throw new MojoFailureException(e.getMessage()); - } - - getLog().info("(TICK) Code generation finished! Attempting shutdown!"); - - ResourceReaper.instance().stopAndRemoveContainer(construct.getContainerId()); - - getLog().info("(TICK) Tick generation finished successfully! (?) Please check your target directory to ensure satisfaction!"); - - - - } - - private final static String FILE_SYSTEM = "filesystem:"; - - String getParsedOutput() throws MojoFailureException { - if (getOutputDirectory().startsWith(FILE_SYSTEM)) { - return getOutputDirectory().substring(FILE_SYSTEM.length()); - } else { - throw new MojoFailureException("Output directory is not a correct directory type! (E.g. filesystem:)"); - } - } -} diff --git a/tick-full/tick-full.iml b/tick-full/tick-full.iml deleted file mode 100644 index 78b2cc5..0000000 --- a/tick-full/tick-full.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/tick-rapid/pom.xml b/tick-rapid/pom.xml deleted file mode 100644 index 883ea0c..0000000 --- a/tick-rapid/pom.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - tick - me.aurium - 1.0.0 - - 4.0.0 - - tick-rapid - - maven-plugin - - 1.0.0 - - - ${compiler.version} - ${compiler.version} - - - - - - org.apache.maven.plugins - maven-jar-plugin - ${maven.jarplugin.version} - - - org.apache.maven.plugins - maven-plugin-plugin - 3.6.0 - - tick-rapid - true - - - - - - - - org.testcontainers - testcontainers - 1.15.2 - - - org.testcontainers - mariadb - 1.11.4 - - - - \ No newline at end of file diff --git a/tick-rapid/src/main/java/me/aurium/tick/rapid/AbstractTickMojo.java b/tick-rapid/src/main/java/me/aurium/tick/rapid/AbstractTickMojo.java deleted file mode 100644 index 473b896..0000000 --- a/tick-rapid/src/main/java/me/aurium/tick/rapid/AbstractTickMojo.java +++ /dev/null @@ -1,39 +0,0 @@ -package me.aurium.tick.rapid; - -import org.apache.maven.plugin.AbstractMojo; -import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.project.MavenProject; - -public abstract class AbstractTickMojo extends AbstractMojo { - - protected final DBSingleton singleton = DBSingleton.get(); - - @Parameter(defaultValue = "MARIADB") - protected CommonInitializers initializer; - @Parameter(defaultValue = "${project}", readonly = true, required = true) - protected MavenProject project; - - @Parameter(defaultValue = "tick.jdbc_port") - protected String internalJDBCPort; - @Parameter(defaultValue = "tick.docker_port") - protected String externalDockerPort; - @Parameter(defaultValue = "tick.jdbc_url") - protected String internalJDBCUrl; - @Parameter(defaultValue = "tick.docker_ip") - protected String externalDockerAddress; - - @Parameter(defaultValue = "sandbox") - protected String sandboxName; - @Parameter(defaultValue = "sandboxUser") - protected String sandboxPassword; - @Parameter(defaultValue = "sandboxPassword") - protected String sandboxUser; - - protected void setParameter(String param, String toSet) { - assert param != null; - assert toSet != null; - - project.getProperties().put( param, toSet ); - } - -} diff --git a/tick-rapid/src/main/java/me/aurium/tick/rapid/CommonInitializers.java b/tick-rapid/src/main/java/me/aurium/tick/rapid/CommonInitializers.java deleted file mode 100644 index 653004e..0000000 --- a/tick-rapid/src/main/java/me/aurium/tick/rapid/CommonInitializers.java +++ /dev/null @@ -1,26 +0,0 @@ -package me.aurium.tick.rapid; - -import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.MariaDBContainer; - -public enum CommonInitializers implements JDBCInitializer { - MARIADB(new MariaDBContainer<>(),"org.jooq.meta.mariadb.MariaDBDatabase"); - - private final JdbcDatabaseContainer consumer; - private final String jooqClassName; - - CommonInitializers(JdbcDatabaseContainer consumer, String jooqClassName) { - this.consumer = consumer; - this.jooqClassName = jooqClassName; - } - - @Override - public JdbcDatabaseContainer initializeContainer(String username, String password, String databaseName) { - return consumer.withDatabaseName(databaseName).withUsername(username).withPassword(password); - } - - @Override - public String correspondingJooqClassName() { - return jooqClassName; - } -} diff --git a/tick-rapid/src/main/java/me/aurium/tick/rapid/DBSingleton.java b/tick-rapid/src/main/java/me/aurium/tick/rapid/DBSingleton.java deleted file mode 100644 index 1b82974..0000000 --- a/tick-rapid/src/main/java/me/aurium/tick/rapid/DBSingleton.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.aurium.tick.rapid; - -import org.testcontainers.containers.JdbcDatabaseContainer; - -import java.util.Optional; - -public class DBSingleton { - - private static DBSingleton instance; - - private JdbcDatabaseContainer container; - - public void setContainer(JdbcDatabaseContainer container) { - this.container = container; - } - - public Optional> getContainer() { - return Optional.ofNullable(container); - } - - public static DBSingleton get() { - if (instance == null) { - return instance = new DBSingleton(); - } else { - return instance; - } - } -} diff --git a/tick-rapid/src/main/java/me/aurium/tick/rapid/InitializeGoalMojo.java b/tick-rapid/src/main/java/me/aurium/tick/rapid/InitializeGoalMojo.java deleted file mode 100644 index 501a6d3..0000000 --- a/tick-rapid/src/main/java/me/aurium/tick/rapid/InitializeGoalMojo.java +++ /dev/null @@ -1,42 +0,0 @@ -package me.aurium.tick.rapid; - -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; -import org.apache.maven.plugins.annotations.LifecyclePhase; -import org.apache.maven.plugins.annotations.Mojo; -import org.testcontainers.containers.JdbcDatabaseContainer; - -@Mojo(name = "initialize", defaultPhase = LifecyclePhase.INITIALIZE) -public class InitializeGoalMojo extends AbstractTickMojo{ - - @Override - public void execute() throws MojoExecutionException, MojoFailureException { - JdbcDatabaseContainer container = initializer.initializeContainer(sandboxUser,sandboxPassword,sandboxName); - - getLog().info("(TICK) Starting container!"); - - container.start(); - - getLog().info("(TICK) Setting external access port: " + container.getFirstMappedPort()); - - setParameter(externalDockerPort,container.getFirstMappedPort().toString()); - - getLog().info("(TICK) Setting internal access ports: " + container.getExposedPorts()); - - //TODO - - getLog().info("(TICK) Setting external docker ip: " + container.getContainerIpAddress()); - - setParameter(externalDockerAddress,container.getContainerIpAddress()); - - getLog().info("(TICK) Host: " + container.getHost()); - - getLog().info("(TICK) Setting internal JDBC Url: " + container.getJdbcUrl()); - - setParameter(internalJDBCUrl,container.getJdbcUrl()); - - getLog().info("(TICK) Setting access"); - - singleton.setContainer(container); - } -} diff --git a/tick-rapid/src/main/java/me/aurium/tick/rapid/JDBCInitializer.java b/tick-rapid/src/main/java/me/aurium/tick/rapid/JDBCInitializer.java deleted file mode 100644 index 2781aab..0000000 --- a/tick-rapid/src/main/java/me/aurium/tick/rapid/JDBCInitializer.java +++ /dev/null @@ -1,11 +0,0 @@ -package me.aurium.tick.rapid; - -import org.testcontainers.containers.JdbcDatabaseContainer; - -public interface JDBCInitializer { - - JdbcDatabaseContainer initializeContainer(String username, String password, String databaseName); - - String correspondingJooqClassName(); - -} diff --git a/tick-rapid/src/main/java/me/aurium/tick/rapid/TeardownGoalMojo.java b/tick-rapid/src/main/java/me/aurium/tick/rapid/TeardownGoalMojo.java deleted file mode 100644 index b549f2e..0000000 --- a/tick-rapid/src/main/java/me/aurium/tick/rapid/TeardownGoalMojo.java +++ /dev/null @@ -1,33 +0,0 @@ -package me.aurium.tick.rapid; - -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; -import org.apache.maven.plugins.annotations.LifecyclePhase; -import org.apache.maven.plugins.annotations.Mojo; -import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.utility.ResourceReaper; - -import java.util.Optional; - -@Mojo(name = "teardown", defaultPhase = LifecyclePhase.GENERATE_SOURCES) -public class TeardownGoalMojo extends AbstractTickMojo{ - - @Override - public void execute() throws MojoExecutionException, MojoFailureException { - - getLog().info("(TICK) Attempting to retrieve container!"); - - Optional> optional = singleton.getContainer(); - - if (optional.isPresent()) { - getLog().info("(TICK) Retrieved optional, attempting teardown!"); - - ResourceReaper.instance().stopAndRemoveContainer(optional.get().getContainerId()); - - getLog().info("(TICK) Teardown successful!"); - } else { - throw new MojoFailureException("No container found to teardown!"); - } - - } -} diff --git a/tick-rapid/tick-rapid.iml b/tick-rapid/tick-rapid.iml deleted file mode 100644 index 78b2cc5..0000000 --- a/tick-rapid/tick-rapid.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/tick.iml b/tick.iml deleted file mode 100644 index 78b2cc5..0000000 --- a/tick.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/tickbox-api/pom.xml b/tickbox-api/pom.xml new file mode 100644 index 0000000..fde8bfc --- /dev/null +++ b/tickbox-api/pom.xml @@ -0,0 +1,65 @@ + + + + tickbox-parent + xyz.auriium + 1.0.0 + + 4.0.0 + + tickbox-api + + + ${compiler.version} + ${compiler.version} + + + + + com.github.docker-java + docker-java-core + 3.2.11 + + + com.github.docker-java + docker-java-transport-okhttp + 3.2.11 + + + org.zeroturnaround + zt-exec + 1.12 + + + org.slf4j + slf4j-api + 2.0.0-alpha1 + + + com.google.code.gson + gson + 2.8.7 + + + org.junit.jupiter + junit-jupiter-engine + 5.7.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.7.0 + test + + + ch.qos.logback + logback-classic + 1.3.0-alpha5 + test + + + + \ No newline at end of file diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/PostCreationTestException.java b/tickbox-api/src/main/java/xyz/auriium/tick/PostCreationTestException.java new file mode 100644 index 0000000..4a3d97f --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/PostCreationTestException.java @@ -0,0 +1,8 @@ +package xyz.auriium.tick; + +public class PostCreationTestException extends TickException { + + public PostCreationTestException(Throwable cause) { + super("An exception was thrown even though your docker provider reported valid! Please check your provider. Exception: " + cause); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/TickException.java b/tickbox-api/src/main/java/xyz/auriium/tick/TickException.java new file mode 100644 index 0000000..a682119 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/TickException.java @@ -0,0 +1,10 @@ +package xyz.auriium.tick; + +/** + * Common parent exception + */ +public class TickException extends RuntimeException{ + public TickException(String s) { + super(s); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/CommonTick.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/CommonTick.java new file mode 100644 index 0000000..9b1f1ea --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/CommonTick.java @@ -0,0 +1,142 @@ +package xyz.auriium.tick.centralized; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.InspectContainerResponse; +import com.github.dockerjava.api.exception.BadRequestException; +import com.github.dockerjava.api.exception.ConflictException; +import com.github.dockerjava.api.exception.DockerClientException; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.PortBinding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.auriium.tick.container.CreationTerms; +import xyz.auriium.tick.container.TickContainer; +import xyz.auriium.tick.docker.image.PullStrategy; +import xyz.auriium.tick.docker.source.DockerSource; + +import java.io.IOException; +import java.util.Arrays; + +public class CommonTick implements Tick{ + + private final Logger logger = LoggerFactory.getLogger("(TICK | MANAGER)"); + + private final DockerSource location; + private final DockerClient client; + + private final PullStrategy strategy; + private final ResourceManager manager; + + CommonTick(DockerSource location, PullStrategy strategy, ResourceManager manager) { + this.location = location; + this.client = location.getClient(); + this.strategy = strategy; + this.manager = manager; + } + + @Override + public T createContainer(CreationTerms terms) { + + strategy.loadIfRequired(terms.getDockerImageName()); + + + logger.info("Initializing container!"); + logger.info("Using image: {}", terms.getDockerImageName()); + logger.info("Using container name: {}", terms.getContainerName()); + logger.info("Using params: {}", Arrays.toString(terms.getParameters())); + + HostConfig config = new HostConfig(); + terms.getPortBindings().ifPresent(config::withPortBindings); + terms.getBinds().ifPresent(config::withBinds); + + CreateContainerCmd cmd = client.createContainerCmd(terms.getDockerImageName()) + .withHostName("tick") + .withName(terms.getContainerName()) + .withEnv(terms.getParameters()) + .withHostConfig(config); + + terms.getCommands().ifPresent(cmd::withCmd); + + + CreateContainerResponse response = execDirtyHandlingException(cmd); + + String id = response.getId(); + + manager.submitContainer(id,false); + logger.info("Container created! Starting container now."); + + try { + client.startContainerCmd(id).exec(); + } catch (BadRequestException exception) { + + //time to remove + logger.error("Error during startup of container. Exiting early. Printing stacktrace below."); + manager.destroyContainer(id); + logger.error("Removed bad container. Exiting now! Sorry!"); + + throw new IllegalStateException("An exception occurred while trying to start the container: " + exception); + } + + + InspectContainerResponse response1 = client.inspectContainerCmd(id).exec(); + + if (!response1.getState().getRunning()) { + logger.error(response1.toString()); + throw new IllegalStateException("Executed container start command but container is not marked as started in docker! Including exception above."); + } + + manager.submitContainer(id,true); + logger.info("Container has been started successfully!"); + + return terms.instantiateHolder(location,manager,response.getId()); + + + } + + @Override + public DockerSource expose() { + return location; + } + + //this all sucks i hate it + //A try/catch is required here in order for container to not persist and be effectively removed. + //all of this is hacky and gross so if anyone finds a better way please make a PR + CreateContainerResponse execDirtyHandlingException(CreateContainerCmd cmd) { + try { + return cmd.exec(); + } catch (ConflictException e) { + logger.error("Error during execution: Two containers of the same name exist. Destroying container and exiting early. Printing stacktrace below:"); + + return getCreateContainerResponse(e, e.getMessage()); + } + } + + //destroy an invalid container + CreateContainerResponse getCreateContainerResponse(ConflictException e2, String message) { + String s = e2.getMessage(); + + s = s.substring(s.indexOf("r \\\"") + 4); + s = s.substring(0, s.indexOf("\\\".")); + + manager.destroyContainer(s); + logger.error("Removed bad container. Exiting now! Sorry!"); + + throw new IllegalStateException(message); + } + + @Override + public void stop() { + logger.info("Stopping TickBox now! Goodnight!"); + + manager.stop(); + + try { + client.close(); + } catch (IOException e) { + throw new ShutdownException("An exception occurred while closing the connection to DockerClient: " + e); + } + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/CommonTickFactory.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/CommonTickFactory.java new file mode 100644 index 0000000..098b98f --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/CommonTickFactory.java @@ -0,0 +1,100 @@ +package xyz.auriium.tick.centralized; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.exception.DockerClientException; +import com.github.dockerjava.api.model.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.auriium.tick.PostCreationTestException; +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.InvalidProviderException; +import xyz.auriium.tick.docker.image.PullStrategyProvider; +import xyz.auriium.tick.docker.source.*; + +/** + * Common implementation of the tick factory + */ +public class CommonTickFactory implements TickFactory{ + + private final DockerSourceProvider sourceProvider; + private final PullStrategyProvider strategyProvider; + private final ResourceManagerProvider resourceManager; + private final CreationOptions creationOptions; + + private final Logger logger = LoggerFactory.getLogger("(TICK | FACTORY)"); + + /** + * Create a new TickFactory that initializes Ticks with default specs + * @param sourceProvider the docker source provider to use in order to get a DockerClient instance + * @param strategyProvider the pull strategy to use in order to handle pulling new images + * @param resourceManager the resource manager which handles destroying docker containers on tick shutdown. + * If you do not want to use one of these, use the {@link EmptyResourceManager} + * @param creationOptions client creation options that pertain to how {@param provider} can be called + */ + public CommonTickFactory(DockerSourceProvider sourceProvider, PullStrategyProvider strategyProvider, ResourceManagerProvider resourceManager, CreationOptions creationOptions) { + this.sourceProvider = sourceProvider; + this.strategyProvider = strategyProvider; + this.resourceManager = resourceManager; + this.creationOptions = creationOptions; + } + + /** + * Creates a new TickFactory with default settings + * @param manager the resource manager to use. If you are looking for the suggested default, use {@link HookResourceManager.Provider} + * However, if you want a resource manager that does not use a shutdown hook in order + * to produce a maven plugin, use {@link EmptyResourceManager.Provider} + * + * @param sourceProvider the docker source provider to use in order to get a DockerClient instance + * @param strategyProvider the pull strategy to use in order to handle pulling new images + */ + public CommonTickFactory(ResourceManagerProvider manager, DockerSourceProvider sourceProvider, PullStrategyProvider strategyProvider) { + this(sourceProvider, strategyProvider, manager, CreationOptions.defaults()); + } + + @Override + public Tick produce() { + logger.info("Initializing tick startup, performing DockerSourceProvider pre-check!"); + + ApplicableResult result = sourceProvider.isApplicable(); + + if (!result.isApplicable()) { + logger.error(String.format("Attempt to check valid DockerClient using [%s] failed!", sourceProvider.name())); + logger.error(String.format("Reason: [%s]", result.getReason())); + throw new InvalidProviderException(); + } + + logger.info("Pre-check successful! Attempting to produce a DockerClient now."); + + DockerSource source = sourceProvider.source(creationOptions); + DockerClient client = source.getClient(); + + if (creationOptions.isUsePostCreationTest()) { + try { + client.versionCmd().exec(); + } catch (DockerClientException exception) { + throw new PostCreationTestException(exception); + } + } + + logger.info("Client produced successfully! Executing final startup activities..."); + + Version dockerVersion = client.versionCmd().exec(); + + logger.info("(DockerClient startup successful!" + "\n" + + "API version: " + dockerVersion.getApiVersion() + "\n" + + "Docker version: " + dockerVersion.getVersion() + "\n" + + "OS: " + dockerVersion.getOperatingSystem()); + + logger.info("Starting up resource manager implementation!"); + + ResourceManager manager = resourceManager.make(source); + + logger.info("Resource manager online!"); + + return new CommonTick(source, strategyProvider.provide(client, manager), manager); + + + + + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/EmptyResourceManager.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/EmptyResourceManager.java new file mode 100644 index 0000000..9058bb0 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/EmptyResourceManager.java @@ -0,0 +1,41 @@ +package xyz.auriium.tick.centralized; + +import xyz.auriium.tick.docker.source.DockerSource; + +public class EmptyResourceManager implements ResourceManager{ + + EmptyResourceManager() {} + + @Override + public void submitContainer(String id, boolean val) { + + } + + @Override + public void destroyContainer(String id) { + + } + + @Override + public void submitImage(String imageName) { + + } + + @Override + public void destroyImage(String imageName) { + + } + + @Override + public void stop() { + + } + + public static class Provider implements ResourceManagerProvider { + + @Override + public ResourceManager make(DockerSource source) { + return new EmptyResourceManager(); + } + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/HookResourceManager.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/HookResourceManager.java new file mode 100644 index 0000000..ad38aee --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/HookResourceManager.java @@ -0,0 +1,101 @@ +package xyz.auriium.tick.centralized; + +import com.github.dockerjava.api.command.InspectContainerResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.auriium.tick.docker.source.DockerSource; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class HookResourceManager implements ResourceManager { //toInterface + + private final Map containers = new ConcurrentHashMap<>(); + private final Set images = ConcurrentHashMap.newKeySet(); + + private final Logger logger = LoggerFactory.getLogger("(TICK | RESOURCE MANAGER)"); + private final DockerSource dockerSource; + + HookResourceManager(DockerSource dockerSource) { + this.dockerSource = dockerSource; + } + + /** + * Sets the state of a container into the manager + * @param id the id of the container + * @param val true if the container is started, false if it is not. + */ + @Override + public void submitContainer(String id, boolean val) { + logger.debug("Added container with ID: " + id + " to manager!"); + this.containers.put(id,val); + } + + @Override + public void submitImage(String imageName) { + logger.debug("Added image with name: " + imageName + " to manager!"); + this.images.add(imageName); + } + + @Override + public void destroyImage(String imageName) { + logger.info("Removing image: {}", imageName); + dockerSource.getClient().removeImageCmd(imageName); + logger.debug("Removed image!"); + } + + /** + * Shuts down a container if it is not already shut down. + * @param id the identification number of the container to destroy + */ + @Override + public void destroyContainer(String id) { + boolean running = this.containers.remove(id); + + InspectContainerResponse.ContainerState state = dockerSource.getClient().inspectContainerCmd(id).exec().getState(); + + if (!state.getRunning() && running) { + logger.error("Container is marked as running in ResourceManager yet docker says it is not running! Please report this to tickbox!"); + logger.error("Attempting to remove container anyways"); + } else if (running) { + logger.info("Stopping container: {}", id); + dockerSource.getClient().killContainerCmd(id).exec(); + logger.debug("Stopped container!"); + } + + logger.info("Removing container: {}", id); + dockerSource.getClient().removeContainerCmd(id).withRemoveVolumes(true).withForce(true).exec(); + logger.debug("Removed container and associated volume(s)!"); + } + + public void stop() { + containers.keySet().forEach(this::destroyContainer); + images.forEach(this::destroyImage); + } + + public static class Provider implements ResourceManagerProvider { + + private final Logger logger = LoggerFactory.getLogger("(TICK | RESOURCE PROVIDER)"); + private final boolean useShutdownHook; + + public Provider(boolean useShutdownHook) { + this.useShutdownHook = useShutdownHook; + } + + @Override + public ResourceManager make(DockerSource source) { + ResourceManager manager = new HookResourceManager(source); + + if (useShutdownHook) { + logger.info("Attempting to attach shutdown hook now!"); + Runtime.getRuntime().addShutdownHook(new Thread(manager::stop)); + } + + return manager; + } + } + + + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ResourceManager.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ResourceManager.java new file mode 100644 index 0000000..e162c9e --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ResourceManager.java @@ -0,0 +1,20 @@ +package xyz.auriium.tick.centralized; + +import xyz.auriium.tick.utils.Stoppable; + +/** + * Manages resources for the tick system + */ +public interface ResourceManager extends Stoppable { + + void submitContainer(String id, boolean val); + void destroyContainer(String id); + + /** + * Inserts an image into the manager for later cleanup + * @param imageName the name of the image + */ + void submitImage(String imageName); + void destroyImage(String imageName); + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ResourceManagerProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ResourceManagerProvider.java new file mode 100644 index 0000000..3852bfa --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ResourceManagerProvider.java @@ -0,0 +1,9 @@ +package xyz.auriium.tick.centralized; + +import xyz.auriium.tick.docker.source.DockerSource; + +public interface ResourceManagerProvider { + + ResourceManager make(DockerSource source); + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ShutdownException.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ShutdownException.java new file mode 100644 index 0000000..01533f8 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/ShutdownException.java @@ -0,0 +1,9 @@ +package xyz.auriium.tick.centralized; + +import xyz.auriium.tick.TickException; + +public class ShutdownException extends TickException { + public ShutdownException(String s) { + super(s); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/Tick.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/Tick.java new file mode 100644 index 0000000..5a09774 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/Tick.java @@ -0,0 +1,28 @@ +package xyz.auriium.tick.centralized; + +import xyz.auriium.tick.container.CreationTerms; +import xyz.auriium.tick.container.TickContainer; +import xyz.auriium.tick.docker.source.DockerSource; +import xyz.auriium.tick.utils.Stoppable; + +/** + * Entry point into the tick api, allows for the creation of dockerized containers. + * Must be stopped manually if you don't use a resource manager + */ +public interface Tick extends Stoppable { + + /** + * Creates and starts a new container according to the terms provided + * @param terms the terms of creation used to generate the tick + * @param the type of container + * @return a new container + */ + T createContainer(CreationTerms terms); + + /** + * Testing + * @return testing + */ + DockerSource expose(); + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/centralized/TickFactory.java b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/TickFactory.java new file mode 100644 index 0000000..6882ab7 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/centralized/TickFactory.java @@ -0,0 +1,10 @@ +package xyz.auriium.tick.centralized; + +/** + * Represents something that can make Ticks + */ +public interface TickFactory { + + Tick produce(); + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/container/Arguments.java b/tickbox-api/src/main/java/xyz/auriium/tick/container/Arguments.java new file mode 100644 index 0000000..7fc1c75 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/container/Arguments.java @@ -0,0 +1,82 @@ +package xyz.auriium.tick.container; + +import com.github.dockerjava.api.model.PortBinding; + +import java.util.Objects; + +public class Arguments { + + private final String creationName; + private final String dockerImageName; + private final String[] parameters; + private final PortBinding binding; + + public Arguments(String creationName, String dockerImageName, String[] parameters, PortBinding binding) { + this.creationName = creationName; + this.dockerImageName = dockerImageName; + this.parameters = parameters; + this.binding = binding; + } + + public String getDockerImageName() { + return dockerImageName; + } + + public String[] getParameters() { + return parameters; + } + + public PortBinding getBinding() { + return this.binding; + } + + public String getContainerName() { + return this.creationName; + } + + public static class Builder { + + private String dockerImageName; + private String[] params; + private PortBinding biniding; + private String creationName; + + public Builder withImage(String name) { + //todo validator + + this.dockerImageName = name; + + return this; + } + + public Builder withParams(String... params) { + this.params = params; + + return this; + } + + public Builder withBinding(PortBinding binding) { + this.biniding = binding; + + return this; + } + + public Builder withCreationName(String name) { + creationName = name; + + return this; + } + + public Arguments build() { + Objects.requireNonNull(dockerImageName); + Objects.requireNonNull(params); + Objects.requireNonNull(biniding); + Objects.requireNonNull(creationName); + + return new Arguments(creationName, dockerImageName,params, biniding); + } + + + } + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/container/CreationOptions.java b/tickbox-api/src/main/java/xyz/auriium/tick/container/CreationOptions.java new file mode 100644 index 0000000..f3e3c17 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/container/CreationOptions.java @@ -0,0 +1,27 @@ +package xyz.auriium.tick.container; + +public class CreationOptions { + + + + private final boolean usePostCreationTest; + private final boolean withTLS; + + public CreationOptions(boolean usePostCreationTest, boolean withTLS) { + this.usePostCreationTest = usePostCreationTest; + this.withTLS = withTLS; + } + public boolean isUsePostCreationTest() { + return usePostCreationTest; + } + + public boolean isWithTLS() { + return withTLS; + } + + + public static CreationOptions defaults() { + return new CreationOptions(true, false); + } + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/container/CreationTerms.java b/tickbox-api/src/main/java/xyz/auriium/tick/container/CreationTerms.java new file mode 100644 index 0000000..ce5d5fb --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/container/CreationTerms.java @@ -0,0 +1,36 @@ +package xyz.auriium.tick.container; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.PortBinding; +import xyz.auriium.tick.centralized.ResourceManager; +import xyz.auriium.tick.docker.source.DockerSource; + +import java.util.Optional; + +public interface CreationTerms { + + String getDockerImageName(); + + String[] getParameters(); + + Optional getBinds(); + + Optional getPortBindings(); + + Optional getCommands(); + + String getContainerName(); + + + /** + * Method used to describe the creation of the actual container + * + * Can also be used as a callback for post container startup + * + * @param location the dockersource used + * @param dockerID the identifier of the container + * @return a new container object + */ + T instantiateHolder(DockerSource location, ResourceManager manager, String dockerID); +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/container/JDBCUrlBuilder.java b/tickbox-api/src/main/java/xyz/auriium/tick/container/JDBCUrlBuilder.java new file mode 100644 index 0000000..e6f148a --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/container/JDBCUrlBuilder.java @@ -0,0 +1,40 @@ +package xyz.auriium.tick.container; + +import java.util.Objects; + +public class JDBCUrlBuilder { + + private String driverName; + private String ip; + private int port; + private String dbName; + + public JDBCUrlBuilder withDriver(String name) { + this.driverName = name; + return this; + } + + public JDBCUrlBuilder withIP(String ip) { + this.ip = ip; + return this; + } + + public JDBCUrlBuilder withPort(int port) { + this.port = port; + return this; + } + + public JDBCUrlBuilder withDBName(String name) { + this.dbName = name; + return this; + } + + public String build() { + Objects.requireNonNull(dbName); + Objects.requireNonNull(ip); + Objects.requireNonNull(driverName); + + return "jdbc:" + driverName + "://" + ip + ":" + port + "/" + dbName; + } + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/container/TickContainer.java b/tickbox-api/src/main/java/xyz/auriium/tick/container/TickContainer.java new file mode 100644 index 0000000..2e79d74 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/container/TickContainer.java @@ -0,0 +1,32 @@ +package xyz.auriium.tick.container; + +/** + * Represents an already created Container that can start and stop itself + * + * TODO add more methods to this barebones ass cringe bullshit + * + * some ideas: + * + * runShell(str) for a linux extension of this interface + * getStatus() + * get.. idk everything that dockerjava provides for us ;) + */ +public interface TickContainer extends AutoCloseable{ + + String containerName(); + String containerID(); + + /** + * Calling this method will stop and then destroy this tick container docker-side. + * After the container is destroyed do not attempt to invoke it. + */ + void destroy(); + + /** + * Invokes {@link TickContainer#destroy()} + */ + default void close() { + destroy(); + } + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/InvalidProviderException.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/InvalidProviderException.java new file mode 100644 index 0000000..43e0678 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/InvalidProviderException.java @@ -0,0 +1,11 @@ +package xyz.auriium.tick.docker; + +import xyz.auriium.tick.TickException; + +public class InvalidProviderException extends TickException { + + public InvalidProviderException() { + super("A provider that was provided was unable to meet it's requirements for launch! See logs for details."); + } + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/FutureAdapter.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/FutureAdapter.java new file mode 100644 index 0000000..553fbe9 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/FutureAdapter.java @@ -0,0 +1,9 @@ +package xyz.auriium.tick.docker.adapter; + +import java.util.concurrent.CompletableFuture; + +public interface FutureAdapter { + + CompletableFuture toFuture(); + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/FutureCallbackAdapter.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/FutureCallbackAdapter.java new file mode 100644 index 0000000..770acf1 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/FutureCallbackAdapter.java @@ -0,0 +1,47 @@ +package xyz.auriium.tick.docker.adapter; + +import com.github.dockerjava.api.command.PullImageResultCallback; +import com.github.dockerjava.api.model.PullResponseItem; + +import java.io.Closeable; +import java.util.concurrent.CompletableFuture; + +//todo-switch to async-callback +public class FutureCallbackAdapter extends PullImageResultCallback implements FutureAdapter { + + private CompletableFuture response = new CompletableFuture<>(); + + private OrThrowable objectCached; + + @Override + public void onNext(PullResponseItem item) { + super.onNext(item); + + this.objectCached.assignObject(item); + } + + @Override + public void onComplete() { + super.onComplete(); + + objectCached.complete(response); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + + this.objectCached.assignThrowable(throwable); //will this cause thread safety concerns? e.g. onError and onNext vs onComplete? + //i'm assuming not cause everything will be on the same thread (This will be on another thread that is the same thread + // the orThrowable is on) + } + + @Override + public void onStart(Closeable stream) { + super.onStart(stream); //what the fuck? please advise + } + + public CompletableFuture toFuture() { + return response; + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/OrThrowable.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/OrThrowable.java new file mode 100644 index 0000000..e235b8e --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/adapter/OrThrowable.java @@ -0,0 +1,39 @@ +package xyz.auriium.tick.docker.adapter; + +import java.util.concurrent.CompletableFuture; + +/** + * Shitty class that probably will break threadsafety + * @param + */ +public class OrThrowable { + + private T object; + private Throwable throwable; + + public void assignObject(T object) { + this.object = object; + this.throwable = null; + } + + public void assignThrowable(Throwable throwable) { + this.throwable = throwable; + this.object = null; //YOU CAN ONLY HAVE ONE >:( + } + + public void complete(CompletableFuture future) { + if (throwable != null) { + future.completeExceptionally(throwable); + } else if (object != null) { + future.complete(object); + } else { + future.completeExceptionally(new NoCompletionResultsException("Tried to complete a future without having an object or a throwable to complete it with!")); + } + } + + public static class NoCompletionResultsException extends RuntimeException { + public NoCompletionResultsException(String aaaaa) { + super(aaaaa); + } + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/DefaultPullCallback.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/DefaultPullCallback.java new file mode 100644 index 0000000..6dec9bd --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/DefaultPullCallback.java @@ -0,0 +1,131 @@ +package xyz.auriium.tick.docker.image; + +import com.github.dockerjava.api.command.PullImageResultCallback; +import com.github.dockerjava.api.model.PullResponseItem; +import org.slf4j.Logger; + +import java.io.Closeable; +import java.time.Duration; +import java.time.Instant; +import java.util.*; + +import static java.lang.String.format; +import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; + +/** + * Credit where credit is due: this stuff is from TestContainers + */ +public class DefaultPullCallback extends PullImageResultCallback { + + private Instant start; + + private final Set allLayers = new HashSet<>(); + private final Set downloadedLayers = new HashSet<>(); + private final Set pulledLayers = new HashSet<>(); + private final Map totalSizes = new HashMap<>(); + private final Map currentSizes = new HashMap<>(); + + private final Logger logger; + + private boolean completed = false; + + public DefaultPullCallback(Logger logger) { + this.logger = logger; + } + + @Override + public void onStart(Closeable stream) { + super.onStart(stream); + + start = Instant.now(); + + logger.info("(TICK) Starting image pull callback!"); + } + + @Override + public void onComplete() { + super.onComplete(); + + final long downloadedLayerSize = downloadedLayerSize(); + final long duration = Duration.between(start, Instant.now()).getSeconds(); + + if (completed) { + logger.info("Pull complete. {} layers, pulled in {}s (downloaded {} at {}/s)", + allLayers.size(), + duration, + byteCountToDisplaySize(downloadedLayerSize), + byteCountToDisplaySize(downloadedLayerSize / duration)); + } + } + + @Override + public void onNext(PullResponseItem item) { + super.onNext(item); + + final String statusLowercase = item.getStatus() != null ? item.getStatus().toLowerCase() : ""; + final String id = item.getId(); + + if (item.getProgressDetail() != null) { + allLayers.add(id); + } + + if (statusLowercase.equalsIgnoreCase("download complete")) { + downloadedLayers.add(id); + } + + if (statusLowercase.equalsIgnoreCase("pull complete")) { + pulledLayers.add(id); + } + + if (item.getProgressDetail() != null) { + Long total = item.getProgressDetail().getTotal(); + Long current = item.getProgressDetail().getCurrent(); + + if (total != null && total > totalSizes.getOrDefault(id, 0L)) { + totalSizes.put(id, total); + } + if (current != null && current > currentSizes.getOrDefault(id, 0L)) { + currentSizes.put(id, current); + } + } + + if (statusLowercase.startsWith("pulling from" ) || statusLowercase.contains("complete" )) { + + long totalSize = totalLayerSize(); + long currentSize = downloadedLayerSize(); + + int pendingCount = allLayers.size() - downloadedLayers.size(); + String friendlyTotalSize; + if (pendingCount > 0) { + friendlyTotalSize = "? MB"; + } else { + friendlyTotalSize = byteCountToDisplaySize(totalSize); + } + + logger.info("Pulling image layers: {} pending, {} downloaded, {} extracted, ({}/{})", + format("%2d", pendingCount), + format("%2d", downloadedLayers.size()), + format("%2d", pulledLayers.size()), + byteCountToDisplaySize(currentSize), + friendlyTotalSize); + } + + if (statusLowercase.contains("complete")) { + completed = true; + } + + } + + private long downloadedLayerSize() { + return currentSizes.values().stream().filter(Objects::nonNull).mapToLong(it -> it).sum(); + } + + private long totalLayerSize() { + return totalSizes.values().stream().filter(Objects::nonNull).mapToLong(it -> it).sum(); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/DefaultPullStrategy.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/DefaultPullStrategy.java new file mode 100644 index 0000000..1638d92 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/DefaultPullStrategy.java @@ -0,0 +1,68 @@ +package xyz.auriium.tick.docker.image; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.exception.NotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.auriium.tick.centralized.ResourceManager; + +import java.util.concurrent.TimeUnit; + +public class DefaultPullStrategy implements PullStrategy { + + private final Logger logger = LoggerFactory.getLogger("(TICK | DefaultPullStrategy)"); + + private final DockerClient client; + private final ResourceManager manager; + + public DefaultPullStrategy(DockerClient client, ResourceManager manager) { + this.client = client; + this.manager = manager; + } + + @Override + public boolean shouldLoad(String dockerImageName) { + + logger.debug("Testing whether image should be loaded..."); + + try { + client.inspectImageCmd(dockerImageName).exec(); + + logger.debug("Image is present!"); + + return false; + } catch (NotFoundException exception) { + + logger.debug("Image is not present!"); + + return true; + } + } + + @Override + public void load(String dockerImageName) { + + logger.info("Attempting to pull image with name: " + dockerImageName); + + try { + + client.pullImageCmd(dockerImageName).exec(new DefaultPullCallback(logger)) + .awaitCompletion(30, TimeUnit.SECONDS); + + manager.submitImage(dockerImageName); + + logger.info("Image load finished!"); + } catch (InterruptedException exception) { + logger.error("An exception occurred while loading an image: ", exception); + } + + } + + public static class Provider implements PullStrategyProvider { + @Override + public DefaultPullStrategy provide(DockerClient client, ResourceManager manager) { + return new DefaultPullStrategy(client, manager); + } + } + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/PullStrategy.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/PullStrategy.java new file mode 100644 index 0000000..da02fc6 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/PullStrategy.java @@ -0,0 +1,17 @@ +package xyz.auriium.tick.docker.image; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +public interface PullStrategy { + + boolean shouldLoad(String dockerImageName); + void load(String dockerImageName); + + default void loadIfRequired(String name) { + if (shouldLoad(name)) { + load(name); + } + } + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/PullStrategyProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/PullStrategyProvider.java new file mode 100644 index 0000000..e81afbc --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/image/PullStrategyProvider.java @@ -0,0 +1,14 @@ +package xyz.auriium.tick.docker.image; + +import com.github.dockerjava.api.DockerClient; +import xyz.auriium.tick.centralized.ResourceManager; + +/** + * Provider that exists in order to allow user to specify a strategy without actually initializing one + * since client initialization is done tick-side + */ +public interface PullStrategyProvider { + + DefaultPullStrategy provide(DockerClient client, ResourceManager manager); + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/ApplicableResult.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/ApplicableResult.java new file mode 100644 index 0000000..0ab7106 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/ApplicableResult.java @@ -0,0 +1,34 @@ +package xyz.auriium.tick.docker.source; + +import java.util.Optional; + +public class ApplicableResult { + + public boolean isApplicable() { + return result; + } + + public String getReason() { + if (result) { + throw new IllegalStateException("The result is successful, there will not be a reason!"); + } else { + return reason; + } + } + + private final boolean result; + private final String reason; + + ApplicableResult(boolean result, String reason) { + this.result = result; + this.reason = reason; + } + + public static ApplicableResult success() { + return new ApplicableResult(true,null); + } + + public static ApplicableResult fail(String reason) { + return new ApplicableResult(false,reason); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/ConfigUtils.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/ConfigUtils.java new file mode 100644 index 0000000..0c7039a --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/ConfigUtils.java @@ -0,0 +1,70 @@ +package xyz.auriium.tick.docker.source; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URI; +import java.util.Optional; + +import static java.util.concurrent.TimeUnit.SECONDS; + +//TODO de-testcontainers this + +/* +public class ConfigUtils { + + public static final boolean IN_A_CONTAINER = new File("/.dockerenv").exists(); + + private static final Optional defaultGateway = Optional + .ofNullable(DockerClientFactory.instance().runInsideDocker( + cmd -> cmd.withCmd("sh", "-c", "ip route|awk '/default/ { print $3 }'"), + (client, id) -> { + try { + LogToStringContainerCallback loggingCallback = new LogToStringContainerCallback(); + client.logContainerCmd(id).withStdOut(true) + .withFollowStream(true) + .exec(loggingCallback) + .awaitStarted(); + loggingCallback.awaitCompletion(3, SECONDS); + return loggingCallback.toString(); + } catch (Exception e) { + log.warn("Can't parse the default gateway IP", e); + return null; + } + } + )) + .map(StringUtils::trimToEmpty) + .filter(StringUtils::isNotBlank); + private static final Logger log = LoggerFactory.getLogger("(TICK | Experimental)"); + + */ +/** + * @deprecated use {@link DockerClientProviderStrategy#getDockerHostIpAddress()} + *//* + + @Deprecated + public static String getDockerHostIpAddress(URI dockerHost) { + switch (dockerHost.getScheme()) { + case "http": + case "https": + case "tcp": + return dockerHost.getHost(); + case "unix": + case "npipe": + if (IN_A_CONTAINER) { + return getDefaultGateway().orElse("localhost"); + } + return "localhost"; + default: + return null; + } + } + + public static Optional getDefaultGateway() { + return DockerClientConfigUtils.defaultGateway; + } + +} +*/ diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/CreationException.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/CreationException.java new file mode 100644 index 0000000..bba200e --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/CreationException.java @@ -0,0 +1,12 @@ +package xyz.auriium.tick.docker.source; + +public class CreationException extends RuntimeException{ + + public CreationException(String message) { + super(message); + } + + public CreationException(Throwable cause) { + super(cause); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerHolder.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerHolder.java new file mode 100644 index 0000000..46e5c6d --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerHolder.java @@ -0,0 +1,4 @@ +package xyz.auriium.tick.docker.source; + +public interface DockerHolder { +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerLocation.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerLocation.java new file mode 100644 index 0000000..c38df4a --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerLocation.java @@ -0,0 +1,12 @@ +package xyz.auriium.tick.docker.source; + +import com.github.dockerjava.transport.SSLConfig; + +public interface DockerLocation { + + String getIp(); + String getUrl(); + SSLConfig getSSLConfig(); + + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerSource.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerSource.java new file mode 100644 index 0000000..d86346a --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerSource.java @@ -0,0 +1,36 @@ +package xyz.auriium.tick.docker.source; + +import com.github.dockerjava.api.DockerClient; + +import java.net.URI; + +/** + * Wrapper interface for DockerClient + */ +public interface DockerSource { + + /** + * Gets the full connection URI (Think https://127.0.0.1/) + * + * This is NOT a mysql/postgresql/whatever the fuck you want/jdbc connection uri. Those are created elsewhere. + * + * @return the URI of this source + */ + URI getSourceURI(); + + /** + * Get just the host address of this source (Think 127.0.0.1) + * + * Typically what gets plugged into + * + * @return the host address of this source. + */ + String getSourceHost(); + + /** + * Get the client. + * @return cluwne + */ + DockerClient getClient(); + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerSourceProvider.java new file mode 100644 index 0000000..13d19d8 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/DockerSourceProvider.java @@ -0,0 +1,34 @@ +package xyz.auriium.tick.docker.source; + +import xyz.auriium.tick.container.CreationOptions; + +public interface DockerSourceProvider extends Comparable { + + String name(); + + /** + * Priority to be tested by {@link xyz.auriium.tick.docker.source.impl.AutoSourceProvider} + * The higher the number the earlier it is tested. + * + * @return + */ + Integer priority(); + + /** + * Attempts to generate a DockerSource. Do not call this without calling {@link #isApplicable()} + * @return a docker source. + * @throws IllegalStateException if an error occurs attempting to generate the dockersource outlined. + */ + DockerSource source(CreationOptions options); + + /** + * Checks whether or not the DockerSourceProvider can effectively provide a DockerSource + * @return Result containing whether or not a Source can be provided and a reason for it. + */ + ApplicableResult isApplicable(); + + @Override + default int compareTo(DockerSourceProvider o) { + return this.priority().compareTo(o.priority()); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/JDBCUrlProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/JDBCUrlProvider.java new file mode 100644 index 0000000..c377bc0 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/JDBCUrlProvider.java @@ -0,0 +1,8 @@ +package xyz.auriium.tick.docker.source; + +//TODO +public interface JDBCUrlProvider { + + String provide();//args + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/AutoSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/AutoSourceProvider.java new file mode 100644 index 0000000..21d8be9 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/AutoSourceProvider.java @@ -0,0 +1,48 @@ +package xyz.auriium.tick.docker.source.impl; + +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.ApplicableResult; +import xyz.auriium.tick.docker.source.DockerSource; +import xyz.auriium.tick.docker.source.DockerSourceProvider; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Automatic source provider that loops through a list of all existing providers in order to attempt delegation + */ +public class AutoSourceProvider { + + private static final Set providers = new HashSet<>(); + + static { + providers.add(new WindowsSourceProvider()); + providers.add(new RootlessSourceProvider()); + providers.add(new UnixSourceProvider()); + providers.add(new SystemEnvSourceProvider()); + } + + + /** + * Attempts to get a docker source provider that will work with your machine. + * @return a docker source provider + * @throws NoProviderException if no valid provider can be found for use on your machine. + */ + public DockerSourceProvider provide() { + + List sorted = providers.stream().sorted((a, b) -> b.priority().compareTo(a.priority())).collect(Collectors.toList()); + + for (DockerSourceProvider provider : sorted) { + ApplicableResult result = provider.isApplicable(); { + if (!result.isApplicable()) continue; + + return provider; + } + } + + throw new NoProviderException(); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/DockerSourceImpl.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/DockerSourceImpl.java new file mode 100644 index 0000000..568e3e2 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/DockerSourceImpl.java @@ -0,0 +1,60 @@ +package xyz.auriium.tick.docker.source.impl; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.Network; +import xyz.auriium.tick.docker.source.DockerSource; + +import java.net.URI; + +public class DockerSourceImpl implements DockerSource { + + private final URI sourceURI; + private final DockerClient client; + + public DockerSourceImpl(URI sourceURI, DockerClient client) { + this.sourceURI = sourceURI; + this.client = client; + } + + @Override + public URI getSourceURI() { + return sourceURI; + } + + @Override + public String getSourceHost() { + switch (sourceURI.getScheme()) { + case "http": + case "https": + case "tcp": + return sourceURI.getHost(); + case "unix": + case "npipe": + /*if (DockerClientConfigUtils.IN_A_CONTAINER) { //TODO de-testcontainers this + return client.inspectNetworkCmd() + .withNetworkId("bridge") + .exec() + .getIpam() + .getConfig() + .stream() + .filter(it -> it.getGateway() != null) + .findAny() + .map(Network.Ipam.Config::getGateway) + .orElseGet(() -> { + return DockerClientConfigUtils.getDefaultGateway().orElse("localhost"); + }); + }*/ + return "localhost"; + default: + throw new IllegalStateException("Unusual scheme used: Cannot interpret scheme: " + sourceURI.getScheme()); + } + + } + + @Override + public DockerClient getClient() { + return client; + } + + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/MachineSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/MachineSourceProvider.java new file mode 100644 index 0000000..bf5ed0c --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/MachineSourceProvider.java @@ -0,0 +1,154 @@ +package xyz.auriium.tick.docker.source.impl; + +import org.apache.commons.lang.SystemUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.zeroturnaround.exec.ProcessExecutor; +import org.zeroturnaround.exec.ProcessResult; +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.*; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeoutException; +import java.util.regex.Pattern; + +/** + * DO NOT USE THIS as it does not currently work correctly lmao + * + * (Startup works, connection does not.) + */ +@Deprecated +public class MachineSourceProvider extends SimpleSourceProvider { + + private static final Logger logger = LoggerFactory.getLogger("(TICK | DOCKER-MACHINE PROVIDER)"); + private static final String DEFAULT = "default"; + + private final String preferredName; + + public MachineSourceProvider(String preferredName) { + this.preferredName = preferredName; + } + + @Override + public String name() { + return "MachineSourceProvider"; + } + + @Override + public Integer priority() { + return -9999999; + } + + @Override + public ApplicableResult isApplicable() { + String executableName = "docker-machine"; + if (SystemUtils.IS_OS_WINDOWS) { + executableName="docker-machine.exe"; + } + + if (!executableExists(executableName)) return ApplicableResult.fail("DockerMachine is not present on this system!"); + + try { + ProcessResult result = new ProcessExecutor() + .command(executableName,"ls","-q", "--filter", "state=Running") + .readOutput(true) + .exitValueNormal() + .execute(); + + Optional toUse = getToUse("ignored", List.of(result.outputUTF8().split("\n"))); + + if (toUse.isEmpty()) return ApplicableResult.fail("No usable DockerMachine system is active. Please create one and try again."); + + String url = new ProcessExecutor() + .command(executableName,"url", toUse.get()) + .readOutput(true) + .exitValueNormal() + .execute().outputString().replaceAll("\n",""); + + String ip = new ProcessExecutor() + .command(executableName,"ip", toUse.get()) + .readOutput(true) + .exitValueNormal() + .execute().outputString().replaceAll("\n",""); + + return ApplicableResult.success(); + + + } catch (IOException | InterruptedException | TimeoutException e) { + return ApplicableResult.fail(e.getMessage()); + } + } + + @Override + public URI makeURI(CreationOptions options) { + + logger.info("Attempting to produce DockerSource using docker machine commandline!"); + + String executableName = "docker-machine"; + if (SystemUtils.IS_OS_WINDOWS) { + executableName="docker-machine.exe"; + } + + if (!executableExists(executableName)) throw new IllegalStateException("DockerMachine executable not found on this system!"); + + try { + ProcessResult result = new ProcessExecutor() + .command(executableName,"ls","-q", "--filter", "state=Running") + .readOutput(true) + .exitValueNormal() + .execute(); + + String toUse = getToUse(preferredName, List.of(result.outputUTF8().split("\n"))).orElseThrow(() -> new IllegalStateException("No default machine present on this system!")); + + logger.info(String.format("Using docker-machine with system %s (selected machine %s). If these do not match machine %s was likely ineligible for use.", toUse, preferredName, preferredName)); + + String url = new ProcessExecutor() + .command(executableName,"url", toUse) + .readOutput(true) + .exitValueNormal() + .execute().outputString().replaceAll("\n",""); + + return URI.create(url); + + + } catch (IOException | InterruptedException | TimeoutException e) { + throw new IllegalStateException(e); + } + + } + + private Optional getToUse(String preferredProvider, Collection strings) { + if (strings.contains(preferredProvider)) { + return Optional.of(preferredProvider); + } else if (strings.contains(DEFAULT)) { + return Optional.of(DEFAULT); + } else { + return Optional.empty(); + } + } + + private boolean executableExists(String executable) { + + Path directFile = Path.of(executable); + if (Files.isExecutable(directFile)) { + return true; + } + + for (String pathString : System.getenv("PATH").split(Pattern.quote(File.pathSeparator))) { + Path path = Paths.get(pathString); + if (Files.isExecutable(path.resolve(executable))) { + return true; + } + } + + return false; + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/ManualSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/ManualSourceProvider.java new file mode 100644 index 0000000..d917b19 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/ManualSourceProvider.java @@ -0,0 +1,55 @@ +package xyz.auriium.tick.docker.source.impl; + +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.*; + +import java.net.URI; +import java.util.function.Supplier; + +/** + * Manual provider that provides a source using a given URI. + */ +public class ManualSourceProvider extends SimpleSourceProvider { + + private final URI hostURI; + private final Supplier success; + + /** + * A manual provider that provides a source using given URI. + * @param hostURI the URI to use when creating the source handle + * @param success a lambda function that determines whether the URI is usable or not. + */ + public ManualSourceProvider(URI hostURI, Supplier success) { + this.hostURI = hostURI; + this.success = success; + } + + /** + * Default constructor for provider + * @param hostURI the URI to use when creating the source handle + */ + public ManualSourceProvider(URI hostURI) { + this.hostURI = hostURI; + this.success = ApplicableResult::success; + } + + @Override + public String name() { + return "ManualSourceProvider"; + } + + @Override + public Integer priority() { + return -100000; + } + + @Override + public URI makeURI(CreationOptions options) { + return hostURI; + } + + @Override + public ApplicableResult isApplicable() { + return success.get(); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/NoProviderException.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/NoProviderException.java new file mode 100644 index 0000000..8cde6f5 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/NoProviderException.java @@ -0,0 +1,14 @@ +package xyz.auriium.tick.docker.source.impl; + +import xyz.auriium.tick.TickException; + +/** + * Exception thrown when there is no valid provider for the {@link AutoSourceProvider} + */ +public class NoProviderException extends TickException { + + + public NoProviderException() { + super("No valid provider could be found for use with your system!"); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/RootlessSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/RootlessSourceProvider.java new file mode 100644 index 0000000..c27ed4f --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/RootlessSourceProvider.java @@ -0,0 +1,102 @@ +package xyz.auriium.tick.docker.source.impl; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.SystemUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.ApplicableResult; + +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +/** + * UNIX only SourceProvider that attempts to run docker regardless of whether it has permission to or not + * + * This was copy pasted from testcontainers and adapted to the tickbox format. I have no idea whether + * or not it actually functions. + */ +public class RootlessSourceProvider extends SimpleSourceProvider{ + + private static final Logger logger = LoggerFactory.getLogger("(TICK | ROOTLESS DOCKER PROVIDER)"); + + private Path resolveSocketPath() { + return tryEnv().orElseGet(() -> { + Path homePath = Paths.get(System.getProperty("user.home")).resolve(".docker").resolve("run"); + return tryFolder(homePath).orElseGet(() -> { + Path implicitPath = Paths.get("/run/user/" + LibC.INSTANCE.getUUID()); + return tryFolder(implicitPath).orElse(null); + }); + }); + } + + private Optional tryEnv() { + String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); + if (StringUtils.isBlank(xdgRuntimeDir)) { + logger.debug("$XDG_RUNTIME_DIR is not set."); + return Optional.empty(); + } + Path path = Paths.get(xdgRuntimeDir); + if (!Files.exists(path)) { + logger.debug("$XDG_RUNTIME_DIR is set to '{}' but the folder does not exist.", path); + return Optional.empty(); + } + Path socketPath = path.resolve("docker.sock"); + if (!Files.exists(socketPath)) { + logger.debug("$XDG_RUNTIME_DIR is set but '{}' does not exist.", socketPath); + return Optional.empty(); + } + return Optional.of(socketPath); + } + + private Optional tryFolder(Path path) { + if (!Files.exists(path)) { + logger.debug("'{}' does not exist.", path); + return Optional.empty(); + } + Path socketPath = path.resolve("docker.sock"); + if (!Files.exists(socketPath)) { + logger.debug("'{}' does not exist.", socketPath); + return Optional.empty(); + } + return Optional.of(socketPath); + } + + @Override + public String name() { + return "RootlessSourceProvider"; + } + + @Override + public Integer priority() { + return 29; + } + + @Override + public ApplicableResult isApplicable() { + if (!SystemUtils.IS_OS_LINUX) return ApplicableResult.fail("Must use linux!"); + Path resolve = resolveSocketPath(); + + if (resolve == null || !Files.exists(resolve)) return ApplicableResult.fail("Cannot find library used to run rootless!"); + + return ApplicableResult.success(); + } + + @Override + public URI makeURI(CreationOptions options) { + return URI.create("unix://" + resolveSocketPath().toString()); + } + + private interface LibC extends Library { + + LibC INSTANCE = Native.loadLibrary("c", LibC.class); + + int getUUID(); + } + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/SimpleSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/SimpleSourceProvider.java new file mode 100644 index 0000000..0b227a7 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/SimpleSourceProvider.java @@ -0,0 +1,39 @@ +package xyz.auriium.tick.docker.source.impl; + +import com.github.dockerjava.core.*; +import com.github.dockerjava.okhttp.OkDockerHttpClient; +import com.github.dockerjava.transport.DockerHttpClient; +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.DockerSource; +import xyz.auriium.tick.docker.source.DockerSourceProvider; + +import java.net.URI; +import java.nio.file.Paths; + +/** + * Client abstraction to allow source providers to only provide a URI + */ +public abstract class SimpleSourceProvider implements DockerSourceProvider { + + @Override + public DockerSource source(CreationOptions options) { + + URI pair = makeURI(options); + + DockerHttpClient client = new OkDockerHttpClient.Builder() + .dockerHost(pair) + .sslConfig(new LocalDirectorySSLConfig(Paths.get(System.getProperty("user.home") + "/.docker/machine/certs/").toString())) + .build(); + + DefaultDockerClientConfig.Builder configBuilder = DefaultDockerClientConfig.createDefaultConfigBuilder(); + + if (configBuilder.build().getApiVersion() == RemoteApiVersion.UNKNOWN_VERSION) { + configBuilder.withApiVersion(RemoteApiVersion.VERSION_1_30); + } + + return new DockerSourceImpl(pair, DockerClientImpl.getInstance(configBuilder.withDockerHost(pair.toString()).build(),client)); + } + + public abstract URI makeURI(CreationOptions options); + +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/SystemEnvSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/SystemEnvSourceProvider.java new file mode 100644 index 0000000..433700b --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/SystemEnvSourceProvider.java @@ -0,0 +1,35 @@ +package xyz.auriium.tick.docker.source.impl; + +import com.github.dockerjava.core.DefaultDockerClientConfig; +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.*; + +import java.net.URI; + +/** + * Provider that attempts to use system variables in order to provide a docker source + * + * Analogous to TestContainer's EnvironmentAndSystemPropertyClientProviderStrategy (geez) + */ +public class SystemEnvSourceProvider extends SimpleSourceProvider { + + @Override + public String name() { + return "SystemEnvSourceProvider"; + } + + @Override + public Integer priority() { + return 50; + } + + @Override + public ApplicableResult isApplicable() { + return System.getenv("DOCKER_HOST") != null ? ApplicableResult.success() : ApplicableResult.fail("System Environment Variables for docker could not be located or are null! Please fill them out!"); + } + + @Override + public URI makeURI(CreationOptions options) { + return DefaultDockerClientConfig.createDefaultConfigBuilder().build().getDockerHost(); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/UnixSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/UnixSourceProvider.java new file mode 100644 index 0000000..b3f0000 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/UnixSourceProvider.java @@ -0,0 +1,56 @@ +package xyz.auriium.tick.docker.source.impl; + +import org.apache.commons.lang.SystemUtils; +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.ApplicableResult; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Provider that uses unix's socket in order to provide a source handle + * + * Ripped from TestContainers + */ +public class UnixSourceProvider extends SimpleSourceProvider { + + protected static final String DOCKER_SOCK_PATH = "/var/run/docker.sock"; + private static final String SOCKET_LOCATION = "unix://" + DOCKER_SOCK_PATH; + private static final int SOCKET_FILE_MODE_MASK = 0xc000; + + + @Override + public String name() { + return "UnixSourceProvider"; + } + + @Override + public Integer priority() { + return 30; + } + + @Override + public ApplicableResult isApplicable() { + if (!(SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC)) return ApplicableResult.fail("System is not UNIX based!"); + + Integer mode; + try { + mode = (Integer) Files.getAttribute(Paths.get(DOCKER_SOCK_PATH), "unix:mode"); + } catch (IOException e) { + return ApplicableResult.fail("Could not find unix domain socket!"); + } + + if ((mode & 0xc000) != SOCKET_FILE_MODE_MASK) { + return ApplicableResult.fail("Found docker unix domain socket but file mode was not as expected (expected: srwxr-xr-x). This problem is possibly due to occurrence of this issue in the past: https://github.com/docker/docker/issues/13121"); + } + + return ApplicableResult.success(); + } + + @Override + public URI makeURI(CreationOptions options) { + return URI.create(SOCKET_LOCATION); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/WindowsSourceProvider.java b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/WindowsSourceProvider.java new file mode 100644 index 0000000..e0ded68 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/docker/source/impl/WindowsSourceProvider.java @@ -0,0 +1,39 @@ +package xyz.auriium.tick.docker.source.impl; + +import org.apache.commons.lang3.SystemUtils; +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.ApplicableResult; + +import java.net.URI; + +/** + * Windows only Source Provider that uses npipe socket to provide a docker source handle + * + * Literally copy and pasted from TestContainers + */ + +public class WindowsSourceProvider extends SimpleSourceProvider { + + protected static final String DOCKER_SOCK_PATH = "//./pipe/docker_engine"; + private static final String SOCKET_LOCATION = "npipe://" + DOCKER_SOCK_PATH; + + @Override + public String name() { + return "WindowsSourceProvider"; + } + + @Override + public Integer priority() { + return 20; + } + + @Override + public ApplicableResult isApplicable() { + return SystemUtils.IS_OS_WINDOWS ? ApplicableResult.success() : ApplicableResult.fail("OS must be windows to use NPIPE!"); + } + + @Override + public URI makeURI(CreationOptions options) { + return URI.create(SOCKET_LOCATION); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/utils/Optionals.java b/tickbox-api/src/main/java/xyz/auriium/tick/utils/Optionals.java new file mode 100644 index 0000000..6bfd7a9 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/utils/Optionals.java @@ -0,0 +1,24 @@ +package xyz.auriium.tick.utils; + +import java.util.Optional; + +/** + * lazy class for lazy developer + * + * please everything else is held to a higher standard than TestContainers just lemme get away with this one + */ +public class Optionals { + + + /** + * I hate my life + * @param vars unsafe varargs + * @param unsafe + * @return un safe + * i wish arrays[]{"weren't so annoying to create"} + */ + @SafeVarargs + public static Optional supply(T... vars) { + return Optional.of(vars); + } +} diff --git a/tickbox-api/src/main/java/xyz/auriium/tick/utils/Stoppable.java b/tickbox-api/src/main/java/xyz/auriium/tick/utils/Stoppable.java new file mode 100644 index 0000000..36de488 --- /dev/null +++ b/tickbox-api/src/main/java/xyz/auriium/tick/utils/Stoppable.java @@ -0,0 +1,26 @@ +package xyz.auriium.tick.utils; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Simple interface that offers a more definite term than closeable + * (Stop offers more context to a running service than close) + * + * Implements closeable in order to provide autocloseable functionality. + */ +public interface Stoppable extends Closeable { + + /** + * Stops the service + */ + void stop(); + + /** + * auto invoke + */ + @Override + default void close() throws IOException { + stop(); + } +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/BaseTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/BaseTest.java new file mode 100644 index 0000000..f7215d2 --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/BaseTest.java @@ -0,0 +1,30 @@ +package xyz.auriium.tick; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class BaseTest { + + protected static final Logger logger = LoggerFactory.getLogger("(TICK TESTING)"); + + @BeforeEach + void beforeEach(TestInfo testInfo) { + + testInfo.getTestClass().orElseThrow().getName(); + + logger.info(String.format("\n <<< Test started: %s", testInfo.getDisplayName() + ">>> \n")); + } + + @AfterEach + void afterEach(TestInfo testInfo) { + + testInfo.getTestClass().orElseThrow().getName(); + + logger.info(String.format("\n\n <<< Test stopped: %s", testInfo.getDisplayName() + ">>> \n")); + } + +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/container/BadContainerTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/container/BadContainerTest.java new file mode 100644 index 0000000..f722903 --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/container/BadContainerTest.java @@ -0,0 +1,68 @@ +package xyz.auriium.tick.container; + +import com.github.dockerjava.api.exception.NotFoundException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import xyz.auriium.tick.BaseTest; +import xyz.auriium.tick.centralized.CommonTickFactory; +import xyz.auriium.tick.centralized.HookResourceManager; +import xyz.auriium.tick.centralized.Tick; +import xyz.auriium.tick.docker.image.DefaultPullStrategy; +import xyz.auriium.tick.docker.source.DockerSource; +import xyz.auriium.tick.docker.source.impl.WindowsSourceProvider; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@EnabledOnOs(OS.WINDOWS) //TODO autoprovider for all platforms +public class BadContainerTest extends BaseTest { + + private static volatile Tick testingTick; + private static volatile DockerSource notClosedSource; + + @BeforeAll + public static void startup() { + testingTick = new CommonTickFactory( + new HookResourceManager.Provider(false), + new WindowsSourceProvider(), + new DefaultPullStrategy.Provider()).produce(); //safe + + notClosedSource = new WindowsSourceProvider().source(CreationOptions.defaults()); + } + + @AfterAll + public static void teardown() throws IOException { + testingTick.stop(); + + notClosedSource.getClient().close(); + } + + @Test + public void When_ContainerNameDuplicates_ThrowCE() { + + + assertThrows(IllegalStateException.class, () -> { + testingTick.createContainer(new TinyImageTerms("the-same")); + testingTick.createContainer(new TinyImageTerms("the-same")); + }); + + assertThrows(NotFoundException.class, () -> { + testingTick.expose().getClient().inspectContainerCmd("the-same").exec(); //removed + }); + + + } + + public void When_BadImage_PurgesSafely() { + + assertThrows(IllegalStateException.class, () -> { + testingTick.createContainer(new TinyImageTerms("the-same", "hello-world")); + }); + + } + +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyContainer.java b/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyContainer.java new file mode 100644 index 0000000..545032e --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyContainer.java @@ -0,0 +1,32 @@ +package xyz.auriium.tick.container; + +import com.github.dockerjava.api.command.InspectContainerResponse; +import xyz.auriium.tick.centralized.ResourceManager; + +public class TinyContainer implements TickContainer{ + + private final ResourceManager manager; + private final String name; + private final String id; + + public TinyContainer(ResourceManager manager, String name, String id) { + this.manager = manager; + this.name = name; + this.id = id; + } + + @Override + public String containerName() { + return name; + } + + @Override + public String containerID() { + return id; + } + + @Override + public void destroy() { + manager.destroyContainer(id); + } +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyImageTerms.java b/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyImageTerms.java new file mode 100644 index 0000000..6047eb9 --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyImageTerms.java @@ -0,0 +1,60 @@ +package xyz.auriium.tick.container; + +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.PortBinding; +import xyz.auriium.tick.centralized.ResourceManager; +import xyz.auriium.tick.docker.source.DockerSource; +import xyz.auriium.tick.utils.Optionals; + +import java.util.Optional; + +public class TinyImageTerms implements CreationTerms{ + + private final String name; + private final String image; + + public TinyImageTerms(String name, String image) { + this.name = name; + this.image = image; + } + + public TinyImageTerms(String name) { + this.name = name; + this.image = "alpine:latest"; + } + + @Override + public String getDockerImageName() { + return image; + } + + @Override + public String[] getParameters() { + return new String[0]; + } + + @Override + public Optional getBinds() { + return Optional.empty(); + } + + @Override + public Optional getPortBindings() { + return Optional.empty(); + } + + @Override + public Optional getCommands() { + return Optionals.supply("top"); + } + + @Override + public String getContainerName() { + return name; + } + + @Override + public TinyContainer instantiateHolder(DockerSource location, ResourceManager manager, String dockerID) { + return new TinyContainer(manager, name, dockerID); + } +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyMessedUpPorts.java b/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyMessedUpPorts.java new file mode 100644 index 0000000..f4cc6c6 --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/container/TinyMessedUpPorts.java @@ -0,0 +1,17 @@ +package xyz.auriium.tick.container; + +import com.github.dockerjava.api.model.PortBinding; +import xyz.auriium.tick.utils.Optionals; + +import java.util.Optional; + +public class TinyMessedUpPorts extends TinyImageTerms{ + public TinyMessedUpPorts(String name) { + super(name); + } + + @Override + public Optional getPortBindings() { + return Optionals.supply(PortBinding.parse("55:55"), PortBinding.parse("55:55")); + } +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/provider/AutoProviderTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/provider/AutoProviderTest.java new file mode 100644 index 0000000..713254c --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/provider/AutoProviderTest.java @@ -0,0 +1,4 @@ +package xyz.auriium.tick.provider; + +public class AutoProviderTest { +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/provider/SystemEnvProviderTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/provider/SystemEnvProviderTest.java new file mode 100644 index 0000000..6a8a12a --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/provider/SystemEnvProviderTest.java @@ -0,0 +1,22 @@ +package xyz.auriium.tick.provider; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import xyz.auriium.tick.centralized.CommonTickFactory; +import xyz.auriium.tick.centralized.HookResourceManager; +import xyz.auriium.tick.centralized.Tick; +import xyz.auriium.tick.docker.image.DefaultPullStrategy; +import xyz.auriium.tick.docker.source.impl.SystemEnvSourceProvider; + +@Disabled //TODO mocking system envs +public class SystemEnvProviderTest { + + @Test + public void startup() { + Tick tick = new CommonTickFactory( + new HookResourceManager.Provider(false), + new SystemEnvSourceProvider(), + new DefaultPullStrategy.Provider()).produce(); + } + +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/provider/UnixProviderTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/provider/UnixProviderTest.java new file mode 100644 index 0000000..742cba9 --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/provider/UnixProviderTest.java @@ -0,0 +1,39 @@ +package xyz.auriium.tick.provider; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import xyz.auriium.tick.BaseTest; +import xyz.auriium.tick.centralized.CommonTickFactory; +import xyz.auriium.tick.centralized.HookResourceManager; +import xyz.auriium.tick.centralized.Tick; +import xyz.auriium.tick.container.TickContainer; +import xyz.auriium.tick.container.TinyImageTerms; +import xyz.auriium.tick.docker.image.DefaultPullStrategy; +import xyz.auriium.tick.docker.source.impl.UnixSourceProvider; + +@EnabledOnOs(OS.LINUX) +public class UnixProviderTest extends BaseTest { + + private static volatile Tick tick; + + @BeforeAll + public static void startup() { + tick = new CommonTickFactory( + new HookResourceManager.Provider(false), + new UnixSourceProvider(), + new DefaultPullStrategy.Provider()).produce(); + } + + @AfterAll + public static void teardown() { + tick.stop(); + } + + @Test + public void whenCreated_ContainerWorksFine() { + TickContainer container = tick.createContainer(new TinyImageTerms("unix-test")); + } +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/provider/WindowsProviderTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/provider/WindowsProviderTest.java new file mode 100644 index 0000000..3ff6a2b --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/provider/WindowsProviderTest.java @@ -0,0 +1,43 @@ +package xyz.auriium.tick.provider; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import xyz.auriium.tick.BaseTest; +import xyz.auriium.tick.centralized.CommonTickFactory; +import xyz.auriium.tick.centralized.HookResourceManager; +import xyz.auriium.tick.centralized.Tick; +import xyz.auriium.tick.container.TickContainer; +import xyz.auriium.tick.container.TinyImageTerms; +import xyz.auriium.tick.docker.image.DefaultPullStrategy; +import xyz.auriium.tick.docker.source.impl.WindowsSourceProvider; + +@EnabledOnOs(OS.WINDOWS) +public class WindowsProviderTest extends BaseTest { + + private static volatile Tick tick; + + @BeforeAll + public static void startup() { + + + tick = new CommonTickFactory( + new HookResourceManager.Provider(false), + new WindowsSourceProvider(), + new DefaultPullStrategy.Provider()).produce(); + } + + @AfterAll + public static void teardown() { + tick.stop(); + } + + @Test + public void whenCreated_ContainerWorksFine() { + TickContainer container = tick.createContainer(new TinyImageTerms("windows-test")); + } + + +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/shutdown/ShutdownHookTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/shutdown/ShutdownHookTest.java new file mode 100644 index 0000000..7bef965 --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/shutdown/ShutdownHookTest.java @@ -0,0 +1,32 @@ +package xyz.auriium.tick.shutdown; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import xyz.auriium.tick.centralized.CommonTickFactory; +import xyz.auriium.tick.centralized.HookResourceManager; +import xyz.auriium.tick.centralized.Tick; +import xyz.auriium.tick.docker.image.DefaultPullStrategy; +import xyz.auriium.tick.docker.source.impl.WindowsSourceProvider; + +@Disabled //TODO how do we mock system? +public class ShutdownHookTest { + + private static volatile Tick tick; + + @BeforeAll + public static void startup() { + tick = new CommonTickFactory( + new HookResourceManager.Provider(true), + new WindowsSourceProvider(), + new DefaultPullStrategy.Provider()).produce(); + } + + + + + + + +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/shutdown/TickClosureTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/shutdown/TickClosureTest.java new file mode 100644 index 0000000..d4c709a --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/shutdown/TickClosureTest.java @@ -0,0 +1,62 @@ +package xyz.auriium.tick.shutdown; + +import com.github.dockerjava.api.exception.NotFoundException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import xyz.auriium.tick.BaseTest; +import xyz.auriium.tick.centralized.CommonTickFactory; +import xyz.auriium.tick.centralized.HookResourceManager; +import xyz.auriium.tick.centralized.Tick; +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.container.TinyImageTerms; +import xyz.auriium.tick.docker.image.DefaultPullStrategy; +import xyz.auriium.tick.docker.source.DockerSource; +import xyz.auriium.tick.docker.source.impl.WindowsSourceProvider; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnabledOnOs(OS.WINDOWS) +public class TickClosureTest extends BaseTest { + + private static volatile DockerSource notClosedSource; + + @BeforeAll + public static void startup() { + notClosedSource = new WindowsSourceProvider().source(CreationOptions.defaults()); + } + + @AfterAll + public static void teardown() throws IOException { + notClosedSource.getClient().close(); + } + + @Test + public void whenStopped_ContainerShouldNotExist() { + Tick tick = new CommonTickFactory(new HookResourceManager.Provider(false), new WindowsSourceProvider(), new DefaultPullStrategy.Provider()).produce(); + + tick.createContainer(new TinyImageTerms("should-not-exist")); + + tick.stop(); + + assertThrows(NotFoundException.class, () -> { + notClosedSource.getClient().inspectContainerCmd("should-not-exist").exec(); + }); + } + + @Test + public void whenStopped_ContainerShouldNotAllowExpose() { + Tick tick = new CommonTickFactory(new HookResourceManager.Provider(false), new WindowsSourceProvider(), new DefaultPullStrategy.Provider()).produce(); + + tick.createContainer(new TinyImageTerms("should-not-expose")); + + tick.stop(); + } + + +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/startup/BadSourceProvider.java b/tickbox-api/src/test/java/xyz/auriium/tick/startup/BadSourceProvider.java new file mode 100644 index 0000000..daa17dd --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/startup/BadSourceProvider.java @@ -0,0 +1,33 @@ +package xyz.auriium.tick.startup; + +import xyz.auriium.tick.container.CreationOptions; +import xyz.auriium.tick.docker.source.ApplicableResult; +import xyz.auriium.tick.docker.source.DockerSourceProvider; +import xyz.auriium.tick.docker.source.impl.SimpleSourceProvider; + +import java.net.URI; + +/** + * Represents a malfunctioning source provider that cannot tell if it is accurately providing valid fields + */ +public class BadSourceProvider extends SimpleSourceProvider { + @Override + public String name() { + return "BadSourceProvider"; + } + + @Override + public Integer priority() { + return 1; + } + + @Override + public ApplicableResult isApplicable() { + return ApplicableResult.success(); //always true to simulate fallacy + } + + @Override + public URI makeURI(CreationOptions options) { + return URI.create("https://i.shit.my.pants.com"); + } +} diff --git a/tickbox-api/src/test/java/xyz/auriium/tick/startup/BadStartupTest.java b/tickbox-api/src/test/java/xyz/auriium/tick/startup/BadStartupTest.java new file mode 100644 index 0000000..469fdb3 --- /dev/null +++ b/tickbox-api/src/test/java/xyz/auriium/tick/startup/BadStartupTest.java @@ -0,0 +1,25 @@ +package xyz.auriium.tick.startup; + +import com.github.dockerjava.api.exception.DockerClientException; +import org.apache.http.client.ClientProtocolException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import xyz.auriium.tick.PostCreationTestException; +import xyz.auriium.tick.centralized.CommonTickFactory; +import xyz.auriium.tick.centralized.HookResourceManager; +import xyz.auriium.tick.docker.image.DefaultPullStrategy; +import xyz.auriium.tick.startup.BadSourceProvider; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class BadStartupTest { + + @Test + public void shouldThrowWhenBadSourceProvided() { + assertThrows(DockerClientException.class, () -> { + new CommonTickFactory(new HookResourceManager.Provider(false), new BadSourceProvider(), new DefaultPullStrategy.Provider()).produce(); + }); + + } + +} diff --git a/tickbox-api/src/test/resources/logback-test.xml b/tickbox-api/src/test/resources/logback-test.xml new file mode 100644 index 0000000..afaebf8 --- /dev/null +++ b/tickbox-api/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/tick-api/pom.xml b/tickbox-containers-sql/pom.xml similarity index 69% rename from tick-api/pom.xml rename to tickbox-containers-sql/pom.xml index e99b232..f78d1ec 100644 --- a/tick-api/pom.xml +++ b/tickbox-containers-sql/pom.xml @@ -3,15 +3,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - tick - me.aurium + tickbox-parent + xyz.auriium 1.0.0 4.0.0 - tick-api - - 1.0.0 + tickbox-containers-sql ${compiler.version} @@ -20,9 +18,8 @@ - com.amihaiemil.web - docker-java-api - 0.0.13 + xyz.auriium + tickbox-api diff --git a/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCConfig.java b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCConfig.java new file mode 100644 index 0000000..a227ba3 --- /dev/null +++ b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCConfig.java @@ -0,0 +1,46 @@ +package xyz.auriium.tick.containers.sql; + +public class JDBCConfig { + + private final String rootPassword; + private final String databaseUsername; + private final String databasePassword; + private final String databaseName; + private final String containerName; + + private final int portBinding; + + public JDBCConfig(String rootPassword, String databaseUsername, String databasePassword, String databaseName, String containerName, int portBinding) { + this.rootPassword = rootPassword; + this.databaseUsername = databaseUsername; + this.databasePassword = databasePassword; + this.databaseName = databaseName; + this.containerName = containerName; + this.portBinding = portBinding; + } + + public String getRootPassword() { + return rootPassword; + } + + public String getDatabaseUsername() { + return databaseUsername; + } + + public String getDatabasePassword() { + return databasePassword; + } + + public String getDatabaseName() { + return databaseName; + } + + public String getContainerName() { + return containerName; + } + + public int getPortBinding() { + return portBinding; + } + +} diff --git a/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCContainer.java b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCContainer.java new file mode 100644 index 0000000..085e103 --- /dev/null +++ b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCContainer.java @@ -0,0 +1,12 @@ +package xyz.auriium.tick.containers.sql; + +import xyz.auriium.tick.container.TickContainer; + +public interface JDBCContainer extends TickContainer { + + String getJDBCUrl(); + + //String driverClass; + //String + +} diff --git a/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCTerms.java b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCTerms.java new file mode 100644 index 0000000..cddb6c7 --- /dev/null +++ b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/JDBCTerms.java @@ -0,0 +1,12 @@ +package xyz.auriium.tick.containers.sql; + +import xyz.auriium.tick.container.CreationTerms; + +/** + * Marker interface for terms that use jdbc + */ +public interface JDBCTerms extends CreationTerms { + + + +} diff --git a/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/mariadb/MariaDBContainer.java b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/mariadb/MariaDBContainer.java new file mode 100644 index 0000000..5bd9d53 --- /dev/null +++ b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/mariadb/MariaDBContainer.java @@ -0,0 +1,49 @@ +package xyz.auriium.tick.containers.sql.mariadb; + +import xyz.auriium.tick.centralized.ResourceManager; +import xyz.auriium.tick.container.JDBCUrlBuilder; +import xyz.auriium.tick.containers.sql.JDBCConfig; +import xyz.auriium.tick.containers.sql.JDBCContainer; +import xyz.auriium.tick.docker.source.DockerSource; + +public class MariaDBContainer implements JDBCContainer { + + private final ResourceManager manager; + private final DockerSource location; + + private final String containerID; + private final JDBCConfig config; + + public MariaDBContainer(ResourceManager manager, DockerSource location, String containerID, JDBCConfig config) { + this.manager = manager; + this.location = location; + this.containerID = containerID; + this.config = config; + } + + + @Override + public String getJDBCUrl() { + return new JDBCUrlBuilder() + .withDBName(config.getDatabaseName()) + .withDriver("mariadb") + .withIP(location.getSourceHost()) + .withPort(config.getPortBinding()).build(); + } + + @Override + public String containerName() { + return config.getContainerName(); + } + + @Override + public String containerID() { + return containerID; + } + + @Override + public void destroy() { + manager.destroyContainer(containerID); + } + +} diff --git a/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/mariadb/MariaDBTerms.java b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/mariadb/MariaDBTerms.java new file mode 100644 index 0000000..c14620c --- /dev/null +++ b/tickbox-containers-sql/src/main/java/xyz/auriium/tick/containers/sql/mariadb/MariaDBTerms.java @@ -0,0 +1,81 @@ +package xyz.auriium.tick.containers.sql.mariadb; + +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.PortBinding; +import xyz.auriium.tick.centralized.ResourceManager; +import xyz.auriium.tick.containers.sql.JDBCConfig; +import xyz.auriium.tick.containers.sql.JDBCContainer; +import xyz.auriium.tick.containers.sql.JDBCTerms; +import xyz.auriium.tick.container.Arguments; +import xyz.auriium.tick.docker.source.DockerSource; + +import java.util.Optional; + +/** + * mariadb + * + * external port is the exposed port of the mariadb container, but still + * + * + * EVERYTHING HERE NEEDS A TON OF WORK. JDBC SUPPORT FOR TESTCONTAINERS IS SOMETHING I AM ADDING TOMORROW NOT TONIGHT. + * + * TODO: image searching and guaruntees + */ +public class MariaDBTerms implements JDBCTerms { + + private final JDBCConfig config; + private final Arguments args; + + public MariaDBTerms(String rootPassword, String databaseUsername, String databasePassword, String databaseName, String containerName, int portBinding) { + this.config = new JDBCConfig(rootPassword, databaseUsername, databasePassword, databaseName, containerName, portBinding); + + this.args = new Arguments.Builder() + .withBinding(PortBinding.parse(portBinding + ":3306")) + .withImage("mariadb:10.5.9") + .withParams( + "MYSQL_ROOT=" + rootPassword, + "MYSQL_DATABASE=" + databaseName, + "MYSQL_USER=" + databaseUsername, + "MYSQL_PASSWORD=" + databasePassword + ) + .withCreationName(containerName) + .build(); + } + + + @Override + public String getDockerImageName() { + return args.getDockerImageName(); + } + + @Override + public String[] getParameters() { + return args.getParameters(); + } + + @Override + public Optional getBinds() { + return Optional.empty(); + } + + @Override + public Optional getPortBindings() { + return Optional.of(new PortBinding[]{args.getBinding()}); + } + + @Override + public Optional getCommands() { + return Optional.empty(); + } + + @Override + public String getContainerName() { + return args.getContainerName(); + } + + @Override + public JDBCContainer instantiateHolder(DockerSource location, ResourceManager manager, String id) { + return new MariaDBContainer(manager, location, id, config); + } + +}