From 85a17e74bd18d273c6c385a4e636eb6c433843e3 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Thu, 7 Sep 2023 19:55:19 -0500 Subject: [PATCH] Add optional 'command' to Docker compose service See gh-1294 --- .../start/site/container/DockerService.java | 107 ++++++++++++++++-- .../SimpleDockerServiceResolver.java | 57 +++++++--- .../site/container/DockerServiceTests.java | 77 +++++++++++++ 3 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 start-site/src/test/java/io/spring/start/site/container/DockerServiceTests.java diff --git a/start-site/src/main/java/io/spring/start/site/container/DockerService.java b/start-site/src/main/java/io/spring/start/site/container/DockerService.java index d7ec540b981..cece024c30c 100644 --- a/start-site/src/main/java/io/spring/start/site/container/DockerService.java +++ b/start-site/src/main/java/io/spring/start/site/container/DockerService.java @@ -16,16 +16,21 @@ package io.spring.start.site.container; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import java.util.TreeSet; import java.util.function.Consumer; -import io.spring.initializr.generator.container.docker.compose.ComposeService.Builder; +import io.spring.initializr.generator.container.docker.compose.ComposeService; /** * Description of a Docker service. * * @author Stephane Nicoll + * @author Chris Bono */ -public class DockerService implements Consumer { +public final class DockerService implements Consumer { private final String image; @@ -33,13 +38,25 @@ public class DockerService implements Consumer { private final String website; + private final String command; + private final int[] ports; - DockerService(String image, String imageTag, String website, int... ports) { - this.image = image; - this.imageTag = imageTag; - this.website = website; - this.ports = ports; + private DockerService(DockerService.Builder builder) { + this.image = builder.image; + this.imageTag = builder.imageTag; + this.website = builder.website; + this.command = builder.command; + this.ports = builder.ports.stream().mapToInt(Number::intValue).toArray(); + } + + /** + * Return a new builder using the specified image and optional tag. + * @param imageAndTag the image (and optional tag) to use for the service + * @return the new builder instance. + */ + public static DockerService.Builder withImageAndTag(String imageAndTag) { + return new DockerService.Builder(imageAndTag); } /** @@ -67,6 +84,14 @@ public String getWebsite() { return this.website; } + /** + * Return the command to use to override the default command (optional). + * @return the command + */ + public String getCommand() { + return this.command; + } + /** * Return the ports that should be exposed by the service. * @return the ports to expose @@ -76,8 +101,72 @@ public int[] getPorts() { } @Override - public void accept(Builder builder) { - builder.image(this.image).imageTag(this.imageTag).imageWebsite(this.website).ports(this.ports); + public void accept(ComposeService.Builder builder) { + builder.image(this.image) + .imageTag(this.imageTag) + .imageWebsite(this.website) + .command(this.command) + .ports(this.ports); + } + + /** + * Builder for {@link DockerService}. + */ + public static class Builder { + + private String image; + + private String imageTag = "latest"; + + private String website; + + private String command; + + private final Set ports = new TreeSet<>(); + + protected Builder(String imageAndTag) { + String[] split = imageAndTag.split(":", 2); + String tag = (split.length == 1) ? "latest" : split[1]; + image(split[0]).imageTag(tag); + } + + public DockerService.Builder image(String image) { + this.image = image; + return this; + } + + public DockerService.Builder imageTag(String imageTag) { + this.imageTag = imageTag; + return this; + } + + public DockerService.Builder website(String website) { + this.website = website; + return this; + } + + public DockerService.Builder command(String command) { + this.command = command; + return this; + } + + public DockerService.Builder ports(Collection ports) { + this.ports.addAll(ports); + return this; + } + + public DockerService.Builder ports(int... ports) { + return ports(Arrays.stream(ports).boxed().toList()); + } + + /** + * Builds the {@link DockerService} instance. + * @return the built instance + */ + public DockerService build() { + return new DockerService(this); + } + } } diff --git a/start-site/src/main/java/io/spring/start/site/container/SimpleDockerServiceResolver.java b/start-site/src/main/java/io/spring/start/site/container/SimpleDockerServiceResolver.java index f81853f084b..7efbf0cdbb7 100644 --- a/start-site/src/main/java/io/spring/start/site/container/SimpleDockerServiceResolver.java +++ b/start-site/src/main/java/io/spring/start/site/container/SimpleDockerServiceResolver.java @@ -25,6 +25,7 @@ * * @author Stephane Nicoll * @author Moritz Halbritter + * @author Chris Bono */ public class SimpleDockerServiceResolver implements DockerServiceResolver { @@ -48,59 +49,83 @@ public SimpleDockerServiceResolver() { } private static DockerService activeMQ() { - return new DockerService("symptoma/activemq", "latest", "https://hub.docker.com/r/symptoma/activemq", 61616); + return DockerService.withImageAndTag("symptoma/activemq") + .website("https://hub.docker.com/r/symptoma/activemq") + .ports(61616) + .build(); } private static DockerService cassandra() { - return new DockerService("cassandra", "latest", "https://hub.docker.com/_/cassandra", 9042); + return DockerService.withImageAndTag("cassandra") + .website("https://hub.docker.com/_/cassandra") + .ports(9042) + .build(); } private static DockerService elasticsearch() { // They don't provide a 'latest' tag - return new DockerService("docker.elastic.co/elasticsearch/elasticsearch", "7.17.10", - "https://www.docker.elastic.co/r/elasticsearch", 9200, 9300); + return DockerService.withImageAndTag("docker.elastic.co/elasticsearch/elasticsearch:7.17.10") + .website("https://www.docker.elastic.co/r/elasticsearch") + .ports(9200, 9300) + .build(); } private static DockerService kafka() { - return new DockerService("confluentinc/cp-kafka", "latest", "https://hub.docker.com/r/confluentinc/cp-kafka", - 9092); + return DockerService.withImageAndTag("confluentinc/cp-kafka") + .website("https://hub.docker.com/r/confluentinc/cp-kafka") + .ports(9092) + .build(); } private static DockerService mariaDb() { - return new DockerService("mariadb", "latest", "https://hub.docker.com/_/mariadb", 3306); + return DockerService.withImageAndTag("mariadb").website("https://hub.docker.com/_/mariadb").ports(3306).build(); } private static DockerService mongoDb() { - return new DockerService("mongo", "latest", "https://hub.docker.com/_/mongo", 27017); + return DockerService.withImageAndTag("mongo").website("https://hub.docker.com/_/mongo").ports(27017).build(); } private static DockerService mysql() { - return new DockerService("mysql", "latest", "https://hub.docker.com/_/mysql", 3306); + return DockerService.withImageAndTag("mysql").website("https://hub.docker.com/_/mysql").ports(3306).build(); } private static DockerService oracle() { - return new DockerService("gvenzl/oracle-xe", "latest", "https://hub.docker.com/r/gvenzl/oracle-xe", 1521); + return DockerService.withImageAndTag("gvenzl/oracle-xe") + .website("https://hub.docker.com/r/gvenzl/oracle-xe") + .ports(1521) + .build(); } private static DockerService postgres() { - return new DockerService("postgres", "latest", "https://hub.docker.com/_/postgres", 5432); + return DockerService.withImageAndTag("postgres") + .website("https://hub.docker.com/_/postgres") + .ports(5432) + .build(); } private static DockerService rabbit() { - return new DockerService("rabbitmq", "latest", "https://hub.docker.com/_/rabbitmq", 5672); + return DockerService.withImageAndTag("rabbitmq") + .website("https://hub.docker.com/_/rabbitmq") + .ports(5672) + .build(); } private static DockerService redis() { - return new DockerService("redis", "latest", "https://hub.docker.com/_/redis", 6379); + return DockerService.withImageAndTag("redis").website("https://hub.docker.com/_/redis").ports(6379).build(); } private static DockerService sqlServer() { - return new DockerService("mcr.microsoft.com/mssql/server", "latest", - "https://mcr.microsoft.com/en-us/product/mssql/server/about/", 1433); + return DockerService.withImageAndTag("mcr.microsoft.com/mssql/server") + .website("https://mcr.microsoft.com/en-us/product/mssql/server/about/") + .ports(1433) + .build(); } private static DockerService zipkin() { - return new DockerService("openzipkin/zipkin", "latest", "https://hub.docker.com/r/openzipkin/zipkin/", 9411); + return DockerService.withImageAndTag("openzipkin/zipkin") + .website("https://hub.docker.com/r/openzipkin/zipkin/") + .ports(9411) + .build(); } @Override diff --git a/start-site/src/test/java/io/spring/start/site/container/DockerServiceTests.java b/start-site/src/test/java/io/spring/start/site/container/DockerServiceTests.java new file mode 100644 index 00000000000..1e961819dff --- /dev/null +++ b/start-site/src/test/java/io/spring/start/site/container/DockerServiceTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.start.site.container; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerService}. + * + * @author Chris Bono + */ +class DockerServiceTests { + + @Test + void builderWithMinimalOptions() { + DockerService service = DockerService.withImageAndTag("acme/toolbox").build(); + assertThat(service.getImage()).isEqualTo("acme/toolbox"); + assertThat(service.getImageTag()).isEqualTo("latest"); + assertThat(service.getWebsite()).isNull(); + assertThat(service.getCommand()).isNull(); + assertThat(service.getPorts()).isEmpty(); + } + + @Test + void builderWithAllOptions() { + DockerService service = DockerService.withImageAndTag("acme/toolbox") + .imageTag("1.0") + .website("acme/toolbox-dot-com") + .command("bin/acme run") + .ports(8007, 8008) + .build(); + assertThat(service.getImage()).isEqualTo("acme/toolbox"); + assertThat(service.getImageTag()).isEqualTo("1.0"); + assertThat(service.getWebsite()).isEqualTo("acme/toolbox-dot-com"); + assertThat(service.getCommand()).isEqualTo("bin/acme run"); + assertThat(service.getPorts()).containsExactly(8007, 8008); + } + + @Test + void builderWithImageAndTagUsesTag() { + DockerService service = DockerService.withImageAndTag("acme/toolbox:1.0").build(); + assertThat(service.getImage()).isEqualTo("acme/toolbox"); + assertThat(service.getImageTag()).isEqualTo("1.0"); + } + + @Test + void builderWithImageAndTagIsOverriddenByImageTag() { + DockerService service = DockerService.withImageAndTag("acme/toolbox:1.0").imageTag("2.0").build(); + assertThat(service.getImage()).isEqualTo("acme/toolbox"); + assertThat(service.getImageTag()).isEqualTo("2.0"); + } + + @Test + void builderWithCollectionOfPorts() { + DockerService service = DockerService.withImageAndTag("acme/toolbox").ports(List.of(8007, 8008)).build(); + assertThat(service.getPorts()).containsExactly(8007, 8008); + } + +}