From 8d7ae8a9a4370a1abe6f7677bf6c3d0634d62c29 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Fri, 8 Sep 2023 14:57:44 -0500 Subject: [PATCH] Enable Spring Pulsar with Spring Boot 3.2.x * Split the Pulsar dependencies into Spring Boot 3.0.x/3.1.x and 3.2.x variants * Add DockerCompose and Testcontainers support * Enhance the tests for Pulsar See gh-1296 --- .../SimpleDockerServiceResolver.java | 10 + ...gPulsarProjectGenerationConfiguration.java | 42 ++- start-site/src/main/resources/application.yml | 28 +- ...pringPulsarBinderBuildCustomizerTests.java | 62 ----- ...arProjectGenerationConfigurationTests.java | 249 ++++++++++++++++++ .../src/test/resources/compose/pulsar.yaml | 7 + 6 files changed, 322 insertions(+), 76 deletions(-) delete mode 100644 start-site/src/test/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarBinderBuildCustomizerTests.java create mode 100644 start-site/src/test/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarProjectGenerationConfigurationTests.java create mode 100644 start-site/src/test/resources/compose/pulsar.yaml 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 7efbf0cdbb7..0be1c0f4341 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 @@ -42,6 +42,7 @@ public SimpleDockerServiceResolver() { this.dockerServices.put("mysql", mysql()); this.dockerServices.put("oracle", oracle()); this.dockerServices.put("postgres", postgres()); + this.dockerServices.put("pulsar", pulsar()); this.dockerServices.put("rabbit", rabbit()); this.dockerServices.put("redis", redis()); this.dockerServices.put("sqlServer", sqlServer()); @@ -103,6 +104,15 @@ private static DockerService postgres() { .build(); } + private static DockerService pulsar() { + // The latest tag they provide is not the 'latest' GA + return DockerService.withImageAndTag("apachepulsar/pulsar:3.1.0") + .website("https://hub.docker.com/r/apachepulsar/pulsar") + .command("bin/pulsar standalone") + .ports(8080, 6650) + .build(); + } + private static DockerService rabbit() { return DockerService.withImageAndTag("rabbitmq") .website("https://hub.docker.com/_/rabbitmq") diff --git a/start-site/src/main/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarProjectGenerationConfiguration.java b/start-site/src/main/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarProjectGenerationConfiguration.java index 38fc039d8b5..39a7ef91d7c 100644 --- a/start-site/src/main/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarProjectGenerationConfiguration.java +++ b/start-site/src/main/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarProjectGenerationConfiguration.java @@ -16,12 +16,21 @@ package io.spring.start.site.extension.dependency.springpulsar; +import io.spring.initializr.generator.condition.ConditionalOnPlatformVersion; import io.spring.initializr.generator.condition.ConditionalOnRequestedDependency; +import io.spring.initializr.generator.condition.ProjectGenerationCondition; import io.spring.initializr.generator.project.ProjectDescription; import io.spring.initializr.generator.project.ProjectGenerationConfiguration; import io.spring.initializr.metadata.InitializrMetadata; +import io.spring.start.site.container.ComposeFileCustomizer; +import io.spring.start.site.container.DockerServiceResolver; +import io.spring.start.site.container.ServiceConnections; +import io.spring.start.site.container.ServiceConnectionsCustomizer; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.type.AnnotatedTypeMetadata; /** * Configuration for generation of projects that depend on Pulsar. @@ -29,14 +38,45 @@ * @author Chris Bono */ @ProjectGenerationConfiguration -@ConditionalOnRequestedDependency("pulsar") +@Conditional(SpringPulsarProjectGenerationConfiguration.OnPulsarRequestedDependencyCondition.class) class SpringPulsarProjectGenerationConfiguration { + private static final String TESTCONTAINERS_CLASS_NAME = "org.testcontainers.containers.PulsarContainer"; + + @Bean + @ConditionalOnPlatformVersion("3.2.0-SNAPSHOT") + @ConditionalOnRequestedDependency("testcontainers") + ServiceConnectionsCustomizer pulsarServiceConnectionsCustomizer(DockerServiceResolver serviceResolver) { + return (serviceConnections) -> serviceResolver.doWith("pulsar", + (service) -> serviceConnections.addServiceConnection(ServiceConnections.ServiceConnection + .ofContainer("pulsar", service, TESTCONTAINERS_CLASS_NAME, false))); + } + @Bean + @ConditionalOnPlatformVersion("3.2.0-SNAPSHOT") + @ConditionalOnRequestedDependency("docker-compose") + ComposeFileCustomizer pulsarComposeFileCustomizer(DockerServiceResolver serviceResolver) { + return (composeFile) -> serviceResolver.doWith("pulsar", + (service) -> composeFile.services().add("pulsar", service)); + } + + @Bean + @ConditionalOnPlatformVersion("[3.0.0,3.2.0-M1)") @ConditionalOnRequestedDependency("cloud-stream") SpringPulsarBinderBuildCustomizer pulsarBinderBuildCustomizer(InitializrMetadata metadata, ProjectDescription description) { return new SpringPulsarBinderBuildCustomizer(metadata, description); } + static class OnPulsarRequestedDependencyCondition extends ProjectGenerationCondition { + + @Override + protected boolean matches(ProjectDescription description, ConditionContext context, + AnnotatedTypeMetadata metadata) { + return description.getRequestedDependencies().containsKey("pulsar") + || description.getRequestedDependencies().containsKey("pulsar-reactive"); + } + + } + } diff --git a/start-site/src/main/resources/application.yml b/start-site/src/main/resources/application.yml index 1535c828c69..0cc52470bf2 100644 --- a/start-site/src/main/resources/application.yml +++ b/start-site/src/main/resources/application.yml @@ -786,30 +786,32 @@ initializr: href: https://docs.spring.io/spring-boot/docs/{bootVersion}/reference/htmlsingle/index.html#messaging.jms.artemis - name: Spring for Apache Pulsar id: pulsar - compatibilityRange: "[3.0.0,3.2.0-M1)" - mappings: - - compatibilityRange: "[3.0.0,3.2.0-M1)" - version: 0.2.0 + compatibilityRange: "[3.0.0,3.2.0-SNAPSHOT]" description: Build messaging applications with Apache Pulsar - groupId: org.springframework.pulsar - artifactId: spring-pulsar-spring-boot-starter links: - rel: reference - href: https://docs.spring.io/spring-pulsar/docs/0.2.x/reference/html/ - - name: Spring for Apache Pulsar (Reactive) - id: pulsar-reactive - compatibilityRange: "[3.0.0,3.2.0-M1)" + href: https://docs.spring.io/spring-boot/docs/{bootVersion}/reference/htmlsingle/index.html#messaging.pulsar mappings: - compatibilityRange: "[3.0.0,3.2.0-M1)" version: 0.2.0 + groupId: org.springframework.pulsar + artifactId: spring-pulsar-spring-boot-starter + starter: false + - name: Spring for Apache Pulsar (Reactive) + id: pulsar-reactive + compatibilityRange: "[3.0.0,3.2.0-SNAPSHOT]" description: Build reactive messaging applications with Apache Pulsar facets: - reactive - groupId: org.springframework.pulsar - artifactId: spring-pulsar-reactive-spring-boot-starter links: - rel: reference - href: https://docs.spring.io/spring-pulsar/docs/0.2.x/reference/html/#reactive-pulsar + href: https://docs.spring.io/spring-boot/docs/{bootVersion}/reference/htmlsingle/index.html#messaging.pulsar + mappings: + - compatibilityRange: "[3.0.0,3.2.0-M1)" + version: 0.2.0 + groupId: org.springframework.pulsar + artifactId: spring-pulsar-reactive-spring-boot-starter + starter: false - name: WebSocket id: websocket description: Build Servlet-based WebSocket applications with SockJS and STOMP. diff --git a/start-site/src/test/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarBinderBuildCustomizerTests.java b/start-site/src/test/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarBinderBuildCustomizerTests.java deleted file mode 100644 index 62c7b2ac408..00000000000 --- a/start-site/src/test/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarBinderBuildCustomizerTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.extension.dependency.springpulsar; - -import io.spring.initializr.generator.test.project.ProjectStructure; -import io.spring.initializr.web.project.ProjectRequest; -import io.spring.start.site.extension.AbstractExtensionTests; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link SpringPulsarBinderBuildCustomizer}. - * - * @author Chris Bono - */ -class SpringPulsarBinderBuildCustomizerTests extends AbstractExtensionTests { - - @Test - void binderNotAddedWhenPulsarNotSelected() { - ProjectStructure project = generateProject(createProjectRequest("cloud-stream")); - assertNoBinder(project); - } - - @Test - void binderNotAddedWhenCloudStreamNotSelected() { - ProjectRequest request = createProjectRequest("pulsar"); - request.setBootVersion("3.0.4"); - ProjectStructure project = generateProject(request); - assertNoBinder(project); - assertThat(project).mavenBuild().hasDependency(getDependency("pulsar")); - } - - @Test - void binderAddedWhenPulsarAndCloudStreamSelected() { - ProjectRequest request = createProjectRequest("pulsar", "cloud-stream"); - request.setBootVersion("3.0.4"); - ProjectStructure project = generateProject(request); - assertThat(project).mavenBuild() - .hasDependency("org.springframework.pulsar", "spring-pulsar-spring-cloud-stream-binder", "0.2.0"); - } - - private void assertNoBinder(ProjectStructure project) { - assertThat(project).mavenBuild() - .doesNotHaveDependency("org.springframework.pulsar", "spring-pulsar-spring-cloud-stream-binder"); - } - -} diff --git a/start-site/src/test/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarProjectGenerationConfigurationTests.java b/start-site/src/test/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarProjectGenerationConfigurationTests.java new file mode 100644 index 00000000000..d3d15139255 --- /dev/null +++ b/start-site/src/test/java/io/spring/start/site/extension/dependency/springpulsar/SpringPulsarProjectGenerationConfigurationTests.java @@ -0,0 +1,249 @@ +/* + * 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.extension.dependency.springpulsar; + +import io.spring.initializr.generator.buildsystem.Dependency; +import io.spring.initializr.generator.project.MutableProjectDescription; +import io.spring.initializr.generator.test.project.ProjectAssetTester; +import io.spring.initializr.generator.test.project.ProjectStructure; +import io.spring.initializr.generator.version.Version; +import io.spring.initializr.web.project.ProjectRequest; +import io.spring.start.site.container.DockerServiceResolver; +import io.spring.start.site.container.ServiceConnections; +import io.spring.start.site.container.ServiceConnectionsCustomizer; +import io.spring.start.site.container.SimpleDockerServiceResolver; +import io.spring.start.site.extension.AbstractExtensionTests; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SpringPulsarProjectGenerationConfiguration}. + * + * @author Chris Bono + */ +class SpringPulsarProjectGenerationConfigurationTests extends AbstractExtensionTests { + + @Nested + class PulsarDependencyConfigurationTests { + + @ParameterizedTest + @ValueSource(strings = { "3.0.0", "3.1.3" }) + void pulsarLegacyStarterUsedWhenBoot30orBoot31Selected(String bootVersion) { + ProjectRequest request = createProjectRequest("pulsar"); + request.setBootVersion(bootVersion); + ProjectStructure project = generateProject(request); + assertThat(project).mavenBuild() + .hasDependency("org.springframework.pulsar", "spring-pulsar-spring-boot-starter"); + } + + @ParameterizedTest + @ValueSource(strings = { "3.0.0", "3.1.3" }) + void pulsarReactiveLegacyStarterUsedWhenBoot30orBoot31Selected(String bootVersion) { + ProjectRequest request = createProjectRequest("pulsar-reactive"); + request.setBootVersion(bootVersion); + ProjectStructure project = generateProject(request); + assertThat(project).mavenBuild() + .hasDependency("org.springframework.pulsar", "spring-pulsar-reactive-spring-boot-starter"); + } + + @Test + void pulsarBootStarterUsedWhenBoot32Selected() { + ProjectRequest request = createProjectRequest("pulsar"); + request.setBootVersion("3.2.0-SNAPSHOT"); + ProjectStructure project = generateProject(request); + assertThat(project).mavenBuild().hasDependency("org.springframework.boot", "spring-boot-starter-pulsar"); + } + + @Test + void pulsarReactiveBootStarterUsedWhenBoot32Selected() { + ProjectRequest request = createProjectRequest("pulsar-reactive"); + request.setBootVersion("3.2.0-SNAPSHOT"); + ProjectStructure project = generateProject(request); + assertThat(project).mavenBuild() + .hasDependency("org.springframework.boot", "spring-boot-starter-pulsar-reactive"); + } + + } + + @Nested + class DockerComposeConfigurationTests { + + @Test + void serviceNotCreatedWhenDockerComposeNotSelected() { + ProjectRequest request = createProjectRequest("pulsar"); + request.setBootVersion("3.2.0-SNAPSHOT"); + ProjectStructure structure = generateProject(request); + assertThat(structure.getProjectDirectory().resolve("compose.yaml")).doesNotExist(); + } + + @ParameterizedTest + @ValueSource(strings = { "3.1.3", "3.2.0-M2" }) + void serviceNotCreatedWhenIncompatibleBootVersionSelected(String bootVersion) { + ProjectRequest request = createProjectRequest("docker-compose", "pulsar"); + request.setBootVersion(bootVersion); + assertThat(composeFile(request)).doesNotContain("pulsar"); + } + + @ParameterizedTest + @ValueSource(strings = { "pulsar", "pulsar-reactive" }) + void serviceCreatedWhenDockerComposeSelectedWithCompatibleBootVersion(String pulsarDependencyId) { + ProjectRequest request = createProjectRequest("docker-compose", pulsarDependencyId); + request.setBootVersion("3.2.0-SNAPSHOT"); + assertThat(composeFile(request)).hasSameContentAs(new ClassPathResource("compose/pulsar.yaml")); + } + + } + + @Nested + class ServiceConnectionConfigurationTests { + + private final ProjectAssetTester projectTester = new ProjectAssetTester() + .withConfiguration(SpringPulsarProjectGenerationConfiguration.class) + .withBean(DockerServiceResolver.class, () -> new SimpleDockerServiceResolver()); + + @Test + void connectionNotAddedWhenTestcontainersNotSelected() { + MutableProjectDescription description = new MutableProjectDescription(); + description.setPlatformVersion(Version.parse("3.2.0-SNAPSHOT")); + description.addDependency("pulsar", mock(Dependency.class)); + this.projectTester.configure(description, + (context) -> assertThat(context).doesNotHaveBean("pulsarServiceConnectionsCustomizer")); + } + + @Test + void connectionNotAddedWhenPulsarNotSelected() { + MutableProjectDescription description = new MutableProjectDescription(); + description.setPlatformVersion(Version.parse("3.2.0-SNAPSHOT")); + description.addDependency("testcontainers", mock(Dependency.class)); + this.projectTester.configure(description, + (context) -> assertThat(context).doesNotHaveBean("pulsarServiceConnectionsCustomizer")); + } + + @ParameterizedTest + @ValueSource(strings = { "3.0.0", "3.1.3" }) + void connectionNotAddedWhenIncompatibleBootVersionSelected(String bootVersion) { + MutableProjectDescription description = new MutableProjectDescription(); + description.setPlatformVersion(Version.parse(bootVersion)); + description.addDependency("pulsar", mock(Dependency.class)); + description.addDependency("testcontainers", mock(Dependency.class)); + this.projectTester.configure(description, + (context) -> assertThat(context).doesNotHaveBean("pulsarServiceConnectionsCustomizer")); + } + + @ParameterizedTest + @ValueSource(strings = { "pulsar", "pulsar-reactive" }) + void connectionAddedWhenTestcontainersAndPulsarSelectedWithCompatibleBootVersion(String pulsarDependencyId) { + MutableProjectDescription description = new MutableProjectDescription(); + description.setPlatformVersion(Version.parse("3.2.0-SNAPSHOT")); + description.addDependency("testcontainers", mock(Dependency.class)); + description.addDependency(pulsarDependencyId, mock(Dependency.class)); + this.projectTester.configure(description, + (context) -> assertThat(context) + .getBean("pulsarServiceConnectionsCustomizer", ServiceConnectionsCustomizer.class) + .satisfies((customizer) -> { + ServiceConnections connections = new ServiceConnections(); + customizer.customize(connections); + assertPulsarServiceConnectionAddded(connections); + })); + } + + private void assertPulsarServiceConnectionAddded(ServiceConnections connections) { + assertThat(connections.values()).first().satisfies((connection) -> { + assertThat(connection.id()).isEqualTo("pulsar"); + assertThat(connection.containerClassName()).isEqualTo("org.testcontainers.containers.PulsarContainer"); + assertThat(connection.isGenericContainer()).isFalse(); + assertThat(connection.containerClassNameGeneric()).isFalse(); + assertThat(connection.dockerService()).satisfies((dockerService) -> { + assertThat(dockerService.getImage()).isEqualTo("apachepulsar/pulsar"); + assertThat(dockerService.getImageTag()).isEqualTo("3.1.0"); + assertThat(dockerService.getWebsite()).isEqualTo("https://hub.docker.com/r/apachepulsar/pulsar"); + assertThat(dockerService.getCommand()).isEqualTo("bin/pulsar standalone"); + assertThat(dockerService.getPorts()).containsExactlyInAnyOrder(8080, 6650); + }); + }); + } + + } + + @Nested + class SpringPulsarBinderConfigurationTests { + + @Test + void binderNotAddedWhenCloudStreamNotSelected() { + ProjectRequest request = createProjectRequest("pulsar"); + request.setBootVersion("3.1.3"); + ProjectStructure project = generateProject(request); + assertNoBinder(project); + assertThat(project).mavenBuild() + .hasDependency("org.springframework.pulsar", "spring-pulsar-spring-boot-starter"); + } + + @Test + void binderNotAddedWhenPulsarNotSelected() { + ProjectRequest request = createProjectRequest("cloud-stream"); + request.setBootVersion("3.1.3"); + ProjectStructure project = generateProject(request); + assertNoBinder(project); + } + + @ParameterizedTest + @ValueSource(strings = { "3.2.0-M1", "3.2.0-SNAPSHOT" }) + void binderNotAddedWhenPulsarAndCloudStreamSelectedWithIncompatibleBootVersion(String bootVersion) { + ProjectRequest request = createProjectRequest("pulsar", "cloud-stream"); + request.setBootVersion(bootVersion); + ProjectStructure project = generateProject(request); + assertNoBinder(project); + } + + @ParameterizedTest + @ValueSource(strings = { "3.0.0", "3.1.3", "3.1.4-SNAPSHOT" }) + void binderAddedWhenPulsarAndCloudStreamSelectedWithCompatibleBootVersion(String bootVersion) { + ProjectRequest request = createProjectRequest("pulsar", "cloud-stream"); + request.setBootVersion(bootVersion); + ProjectStructure project = generateProject(request); + assertBinder(project); + } + + @ParameterizedTest + @ValueSource(strings = { "3.0.0", "3.1.3", "3.1.4-SNAPSHOT" }) + void binderAddedWhenPulsarReactiveAndCloudStreamSelectedWithCompatibleBootVersion(String bootVersion) { + ProjectRequest request = createProjectRequest("pulsar-reactive", "cloud-stream"); + request.setBootVersion(bootVersion); + ProjectStructure project = generateProject(request); + assertBinder(project); + } + + private void assertNoBinder(ProjectStructure project) { + assertThat(project).mavenBuild() + .doesNotHaveDependency("org.springframework.pulsar", "spring-pulsar-spring-cloud-stream-binder"); + } + + private void assertBinder(ProjectStructure project) { + assertThat(project).mavenBuild() + .hasDependency("org.springframework.pulsar", "spring-pulsar-spring-cloud-stream-binder", "0.2.0"); + } + + } + +} diff --git a/start-site/src/test/resources/compose/pulsar.yaml b/start-site/src/test/resources/compose/pulsar.yaml new file mode 100644 index 00000000000..1bca13fe254 --- /dev/null +++ b/start-site/src/test/resources/compose/pulsar.yaml @@ -0,0 +1,7 @@ +services: + pulsar: + image: 'apachepulsar/pulsar:3.1.0' + ports: + - '6650' + - '8080' + command: 'bin/pulsar standalone'