diff --git a/pom.xml b/pom.xml index a5d320569..9408ad500 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ 1.9.0 2.14.0 3.4.0 + 0.3.1 diff --git a/spring-boot-examples/workflows/pom.xml b/spring-boot-examples/workflows/pom.xml index 4711b799b..f28c48c0f 100644 --- a/spring-boot-examples/workflows/pom.xml +++ b/spring-boot-examples/workflows/pom.xml @@ -40,6 +40,12 @@ rest-assured test + + io.github.microcks + microcks-testcontainers + ${microcks.version} + test + diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java index 5ff98f886..45f08ca17 100644 --- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java @@ -13,9 +13,12 @@ package io.dapr.springboot.examples.wfp; +import com.fasterxml.jackson.databind.ObjectMapper; import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; @Configuration public class WorkflowPatternsConfiguration { @@ -23,4 +26,14 @@ public class WorkflowPatternsConfiguration { public CleanUpLog cleanUpLog(){ return new CleanUpLog(); } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplateBuilder().build(); + } + + @Bean + public ObjectMapper mapper() { + return new ObjectMapper(); + } } diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java index 55695821d..ddffdb018 100644 --- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java @@ -22,6 +22,8 @@ import io.dapr.springboot.examples.wfp.externalevent.ExternalEventWorkflow; import io.dapr.springboot.examples.wfp.fanoutin.FanOutInWorkflow; import io.dapr.springboot.examples.wfp.fanoutin.Result; +import io.dapr.springboot.examples.wfp.remoteendpoint.Payload; +import io.dapr.springboot.examples.wfp.remoteendpoint.RemoteEndpointWorkflow; import io.dapr.workflows.client.DaprWorkflowClient; import io.dapr.workflows.client.WorkflowInstanceStatus; import org.slf4j.Logger; @@ -53,6 +55,7 @@ public class WorkflowPatternsRestController { private Map ordersToApprove = new HashMap<>(); + /** * Run Chain Demo Workflow * @return the output of the ChainWorkflow execution @@ -137,4 +140,17 @@ public CleanUpLog continueAsNew() return workflowInstanceStatus.readOutputAs(CleanUpLog.class); } + @PostMapping("wfp/remote-endpoint") + public Payload remoteEndpoint(@RequestBody Payload payload) + throws TimeoutException { + + String instanceId = daprWorkflowClient.scheduleNewWorkflow(RemoteEndpointWorkflow.class, payload); + logger.info("Workflow instance " + instanceId + " started"); + + WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient + .waitForInstanceCompletion(instanceId, null, true); + System.out.printf("workflow instance with ID: %s completed.", instanceId); + return workflowInstanceStatus.readOutputAs(Payload.class); + } + } diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java new file mode 100644 index 000000000..a0636f550 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Dapr 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 + * http://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.dapr.springboot.examples.wfp.remoteendpoint; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class CallRemoteEndpointActivity implements WorkflowActivity { + + private Logger logger = LoggerFactory.getLogger(CallRemoteEndpointActivity.class); + + @Value("${application.process-base-url:}") + private String processBaseURL; + + @Autowired + private RestTemplate restTemplate; + + + @Override + public Object run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + var payload = ctx.getInput(Payload.class); + + HttpEntity request = + new HttpEntity<>(payload); + payload = restTemplate.postForObject(processBaseURL + "/process", request, Payload.class); + + logger.info("Payload from the remote service: " + payload); + + return payload; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java new file mode 100644 index 000000000..fc1b6511d --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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.dapr.springboot.examples.wfp.remoteendpoint; + +public class Payload { + private String id; + private String content; + private Boolean processed = false; + + public Payload(String id, String content) { + this.id = id; + this.content = content; + } + + public Payload() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Boolean getProcessed() { + return processed; + } + + public void setProcessed(Boolean processed) { + this.processed = processed; + } + + @Override + public String toString() { + return "Payload{" + + "id='" + id + '\'' + + ", content='" + content + '\'' + + ", processed=" + processed + + '}'; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java new file mode 100644 index 000000000..75934e789 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Dapr 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 + * http://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.dapr.springboot.examples.wfp.remoteendpoint; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +public class RemoteEndpointWorkflow implements Workflow { + + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + Payload payload = ctx.getInput(Payload.class); + payload = ctx.callActivity(CallRemoteEndpointActivity.class.getName(), payload , + new WorkflowTaskOptions(new WorkflowTaskRetryPolicy(5, + Duration.ofSeconds(2), 1.0, Duration.ofSeconds(10), Duration.ofSeconds(20))), + Payload.class).await(); + + ctx.getLogger().info("Workflow finished with result: " + payload); + ctx.complete(payload); + }; + } +} diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java index a0e3a087c..b27900652 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java @@ -15,11 +15,19 @@ import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; +import io.github.microcks.testcontainers.MicrocksContainersEnsemble; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.test.context.DynamicPropertyRegistrar; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.Network; import java.util.Collections; +import java.util.List; import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; @@ -28,16 +36,66 @@ public class DaprTestContainersConfig { @Bean @ServiceConnection - public DaprContainer daprContainer() { + public DaprContainer daprContainer(Network network) { return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) .withAppName("workflow-patterns-app") .withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true)))) .withAppPort(8080) + .withNetwork(network) .withAppHealthCheckPath("/actuator/health") .withAppChannelAddress("host.testcontainers.internal"); } + @Bean + MicrocksContainersEnsemble microcksEnsemble(Network network) { + return new MicrocksContainersEnsemble(network, "quay.io/microcks/microcks-uber:1.11.2") + .withAccessToHost(true) // We need this to access our webapp while it runs + .withMainArtifacts("third-parties/remote-http-service.yaml"); + } + + @Bean + public DynamicPropertyRegistrar endpointsProperties(MicrocksContainersEnsemble ensemble) { + // We need to replace the default endpoints with those provided by Microcks. + return (properties) -> { + properties.add("application.process-base-url", () -> ensemble.getMicrocksContainer() + .getRestMockEndpoint("API Payload Processor", "1.0.0")); + }; + } + + @Bean + public Network getDaprNetwork(Environment env) { + boolean reuse = env.getProperty("reuse", Boolean.class, false); + if (reuse) { + Network defaultDaprNetwork = new Network() { + @Override + public String getId() { + return "dapr-network"; + } + + @Override + public void close() { + + } + + @Override + public Statement apply(Statement base, Description description) { + return null; + } + }; + + List networks = DockerClientFactory.instance().client().listNetworksCmd() + .withNameFilter("dapr-network").exec(); + if (networks.isEmpty()) { + Network.builder().createNetworkCmdModifier(cmd -> cmd.withName("dapr-network")).build().getId(); + return defaultDaprNetwork; + } else { + return defaultDaprNetwork; + } + } else { + return Network.newNetwork(); + } + } } diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java index 4ca36cb58..625a621a6 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java @@ -16,6 +16,8 @@ import io.dapr.client.DaprClient; import io.dapr.springboot.DaprAutoConfiguration; import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; +import io.dapr.springboot.examples.wfp.remoteendpoint.Payload; +import io.github.microcks.testcontainers.MicrocksContainersEnsemble; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.junit.jupiter.api.BeforeEach; @@ -39,6 +41,9 @@ class WorkflowPatternsAppTests { @Autowired private DaprClient daprClient; + @Autowired + private MicrocksContainersEnsemble ensemble; + @BeforeEach void setUp() { RestAssured.baseURI = "http://localhost:" + 8080; @@ -139,4 +144,20 @@ void testContinueAsNew() { assertEquals(5, cleanUpLog.getCleanUpTimes()); } + @Test + void testRemoteEndpoint() { + + Payload payload = given().contentType(ContentType.JSON) + .body(new Payload("123", "content goes here")) + .when() + .post("/wfp/remote-endpoint") + .then() + .statusCode(200).extract().as(Payload.class); + + assertEquals(true, payload.getProcessed()); + + assertEquals(2, ensemble.getMicrocksContainer() + .getServiceInvocationsCount("API Payload Processor", "1.0.0")); + } + } diff --git a/spring-boot-examples/workflows/src/test/resources/third-parties/remote-http-service.yaml b/spring-boot-examples/workflows/src/test/resources/third-parties/remote-http-service.yaml new file mode 100644 index 000000000..59819a282 --- /dev/null +++ b/spring-boot-examples/workflows/src/test/resources/third-parties/remote-http-service.yaml @@ -0,0 +1,87 @@ +--- +openapi: 3.0.2 +info: + title: API Payload Processor + version: 1.0.0 + description: API definition of API Payload Processor sample app + contact: + name: Salaboy + url: http://github.com/salaboy + email: salaboy@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /process: + summary: Process payload + post: + tags: + - process + x-microcks-operation: + dispatcher: SCRIPT + dispatcherRules: | + def retries = store.get("retries") ?:"first" + if (retries == "first") { + store.put("retries", "second", 60) + return "Error" + } + store.delete("retries") + return "Payload" + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Payload' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Payload' + examples: + Payload: + value: + id: 123 + content: payload content here + processed: true + description: Process payload + "500": + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Error message + examples: + Error: + value: + message: Something unexpected happened + description: Error payload + operationId: Process + summary: Process incoming payload +components: + schemas: + Payload: + title: Payload to be processed + description: Payload to be processed following the Payload type's schema. + type: object + properties: + id: + description: Payload Id + type: string + content: + description: Payload Content + type: string + processed: + description: Is the Payload processed + type: boolean + required: + - id + - content + additionalProperties: false +tags: + - name: payload + description: Payload resource