Skip to content

Commit

Permalink
Completing documentation on Quarkus Order Service
Browse files Browse the repository at this point in the history
Signed-off-by: Laurent Broudoux <[email protected]>
  • Loading branch information
lbroudoux committed Aug 22, 2023
1 parent 901fedd commit a2082b8
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/

import java.io.IOException;
import java.io.InputStream;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

public final class MavenWrapperDownloader
{
private static final String WRAPPER_VERSION = "3.2.0";

private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) );

public static void main( String[] args )
{
log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION );

if ( args.length != 2 )
{
System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" );
System.exit( 1 );
}

try
{
log( " - Downloader started" );
final URL wrapperUrl = new URL( args[0] );
final String jarPath = args[1].replace( "..", "" ); // Sanitize path
final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize();
downloadFileFromURL( wrapperUrl, wrapperJarPath );
log( "Done" );
}
catch ( IOException e )
{
System.err.println( "- Error downloading: " + e.getMessage() );
if ( VERBOSE )
{
e.printStackTrace();
}
System.exit( 1 );
}
}

private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath )
throws IOException
{
log( " - Downloading to: " + wrapperJarPath );
if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null )
{
final String username = System.getenv( "MVNW_USERNAME" );
final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray();
Authenticator.setDefault( new Authenticator()
{
@Override
protected PasswordAuthentication getPasswordAuthentication()
{
return new PasswordAuthentication( username, password );
}
} );
}
try ( InputStream inStream = wrapperUrl.openStream() )
{
Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING );
}
log( " - Downloader complete" );
}

private static void log( String msg )
{
if ( VERBOSE )
{
System.out.println( msg );
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.8/apache-maven-3.8.8-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
101 changes: 95 additions & 6 deletions shift-left-demo/quarkus-order-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,31 @@ This application is a sample on how to use Microcks DevServices for Quarkus with

## Application introduction

TODO
This fictional application we're working on is a typical `Order Service` that can allow online, physical stores, or even
partners to place orders for our fresh-backed pastries! For that, the `Order Service` is exposing a REST API to its consumers
but also relies on an existing API we have [introduced in a previous post](https://medium.com/@lbroudoux/different-levels-of-api-contract-testing-with-microcks-ccc0847f8c97) 😉

![Order Service ecosystem](./assets/order-service-ecosystem.png)

The `Order Service` application has been designed around 3 main components that are directly mapped on Spring Boot components and classes:
* The `OrderResource` (in package `org.acme.order.api`) is responsible for exposing an `Order API` to the outer world. This API is specified using the `src/main/resources/order-service-openapi.yaml` OpenAPI specification,
* The `OrderService` (in package `org.acme.order.service`) is responsible for implementing the business logic around the creation of orders. Typically, it checks that the products are available before recording an order. Otherwise, order cannot be placed,
* The `PastryAPIClient` (in package `org.acme.order.client`) is responsible for calling the `Pastry API` in *Product Domain* and get details or list of pastries.

![Order Service architecture](./assets/order-service-architecture.png)

Of course, this is a very naive vision of a real-life system as such an application would certainly pull out much more
dependencies (like a `Payment Service`, a `Customer Service`, a `Shipping Service`, and much more) and offer more complex API.
However, this situation is complex enough to highlight the two problems we're addressing:
1) How to **efficiently set up a development environment** that depends on third-party API like the Pastry API?
You certainly want to avoid cloning this component repository, figuring out how to launch it and configure it accordingly. As a developer, developing your own mock of this service makes you also lose time and risk drifting from initial intent,
2) How to **efficiently validate the conformance** of the `Order API` against business expectations and OpenAPI contract?
Besides the core business logic, you might want to validate the network and protocol serialization layers as well as the respect of HTTP semantics.

## Development phase

Let's imagine you start an interactive development/testing session, running your local server with:

```shell
$ ./mvnw compile quarkus:dev
==== OUTPUT ====
Expand All @@ -28,7 +49,17 @@ __ ____ __ _____ ___ __ ____ ______
Press [e] to edit command line args (currently ''), [:] for the terminal, [h] for more options>ut, [:] for the terminal, [h] for more options>
```

> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/.
The beauty here is that Microcks Quarkus DevServices are included into the project's configuration (see the `pom.xml` file for details)
and thus a `default` Microcks container has been launched and is running on `http://localhost:9191`.

> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. The Microcks Quarkus DevServices contributes
a tile in this Dev UI so that you can easily access its own UI from Quarkus.

Having a look at the `src/main/resources/application.properties` files, you'll see the `quarkus.microcks.devservices.*` properties
that allow configuring stuffs and declaring what artifacts should be load on startup. Here we've loaded the `Order API` contract as well as the `Pastry API`
dependency ones. We have also configured the `PastryAPIClient.url` to use the one provided by Microcks DevServices container.

So you should be able to directly call the Order API and invoke the whole chain made of the 3 components:

```shell
$ curl -XPOST localhost:8080/api/orders -H 'Content-type: application/json' \
Expand All @@ -39,34 +70,92 @@ $ curl -XPOST localhost:8080/api/orders -H 'Content-type: application/json' \

## Unit Test phase

For a quick run, just launch `./mvnw test` command in a terminal to see the Microcks Quarkus DevServices in action.

As Microcks DevServices are also available during the `test` phase of the development flow, once Microcks DevServices is configured there's no other need
to bootstrap a Microcks container during your test setup.

### Mock your dependencies

These are already mocks thanks to the configuration you put into `application.properties`:
These are already mocked thanks to the configuration you put into `application.properties`:

```properties
quarkus.microcks.devservices.artifacts.primaries=target/classes/order-service-openapi.yaml,target/test-classes/third-parties/apipastries-openapi.yaml
quarkus.microcks.devservices.artifacts.secondaries=target/test-classes/third-parties/apipastries-postman-collection.json
```

And thus using the mocked API is just transparent! You just have to write a regular JUnit 5 `@QuarkusTest` annotated test with
the injectoin of the `@RestClient` you actually want to test:

```java
@Inject
@RestClient
PastryAPIClient client;

@Test
public void testGetPastries() {
// Test our API client and check that arguments and responses are correctly serialized.
List<Pastry> pastries = client.listPastries("S");
assertEquals(1, pastries.size());

pastries = client.listPastries("M");
assertEquals(2, pastries.size());

pastries = client.listPastries("L");
assertEquals(2, pastries.size());
}
```

### OpenAPI contract testing

Microcks container launched by DevService is automatically able to reach localhost on `quarkus.http.test-port` using the `host.testcontainers.internal` hostname.
Remember the 2 problems we're trying to solve here? The 2nd one is about how to validate the conformance of the `Order API` we'll
expose to consumers. We certainly can write an integration test that uses [Rest Assured](https://rest-assured.io/) or other libraries
to invoke the exposed Http layer and validate each and every response with Java assertions like:

```java
when()
.get("/lotto/{id}", 5)
.then()
.statusCode(200)
.body("lotto.lottoId", equalTo(5),
"lotto.winners.winnerId", hasItems(23, 54));
```

This certainly works but presents 2 problems in my humble opinion:
* It's a lot of code to write! And it's apply to each API interaction because for each interaction it's probably a good idea to
check the structure of same objects in the message. This lead to a fair amount of code!
* The code you write here is actually a language specific translation of the OpenAPI specification for the `Order API`: so the same
"rules" get duplicated. Whether you edit the code or the OpenAPI spec first, high are the chances you get some drifts between your test
suite and the specification you will provide to consumers!

Microcks Testcontainer integration provides another approach by letting you reuse the OpenAPI specification directly in your test suite,
without having to write assertions and validation of messages for API interaction.

Your test execution will need to know the local HTTP port the Quarkus runtime is running on for test. This is done with declaration of a
`@ConfigProperty` annotated member:

```java
@ConfigProperty(name= "quarkus.http.test-port")
int quarkusHttpPort;
```

Microcks container launched by DevServices is automatically able to reach localhost on `quarkus.http.test-port` using the `host.testcontainers.internal` hostname.
In order to interact with the Microcks container, you'll need to access its URL that is available also via a `@ConfigProperty`. Optional, it can be useful
to retrieve the global Jackson ObjectMapper if you want/need to introspect the data exchanged during the conformance tests:

```java
@ConfigProperty(name= "quarkus.microcks.default")
String microcksContainerUrl;
```

```java
@Inject
ObjectMapper mapper;
```

Finally, we can define our unit test method that allow checking that the `OrderController` (here via the `testEndpoint()` value)
is conformant with the OpenAPI specification for `Order Service`, version `0.1.0`. The nice thing is that it's just one call for validating
all the interactions with the API. That method is also super easy to enrich in the future: when the next `0.2.0` version of the API will
be under-development, you'll be able to check the conformance with both `0.1.0` and `0.2.0` as per the semantic versioning requirements.

```java
@Test
public void testOpenAPIContract() throws Exception {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./mvnw package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/order-service-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/order-service-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/order-service-jvm
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "[email protected]:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "[email protected]:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi8/openjdk-17:1.16

ENV LANGUAGE='en_US:en'


# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 target/quarkus-app/*.jar /deployments/
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/

EXPOSE 8080
USER 185
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"

Loading

0 comments on commit a2082b8

Please sign in to comment.