Welcome

You are reading this document because you want to use your skill to code the future of SUSTAINABLE TRAVEL.

However, one of this skill is to be able to code a demo or a proof of concept application that will demonstrate your idea.

We also want to introduce you with some technologies we are using in Amadeus and what a developer is using in its day to day job, you’ll see it’s quite exciting.

This workshop offers attendees an intro-level, hands-on session with Quarkus (What is Quarkus?) and CodeSpaces, from creating your remote environment to building, launching and testing a microservice based application, that is:

  • Developed with Quarkus (the microservices are already developed, and you don’t need to know Quarkus)

  • Exposing HTTP APIs

  • Exchanging events with Apache Kafka

  • Storing data in databases

  • Answer the ultimate question: are super-heroes stronger than super-villains?

What are you going to learn? The goal is to get familiar with some aspects of developers duty but also understand what is which are:

  • Using an Integrated Development Environment (IDE): CodeSpaces

  • Build the microservices of the application with maven

  • Launch, inside the IDE, each microservice with Quarkus Dev mode

  • Test locally with a UI, provided, that everything works as expected

  • And much more!

Ready? Here we go!

Presenting the Workshop

What Is This Workshop About?

This workshop is about demonstrating an application that is based a modern software architecture that make usage of microservices.

All along this workshop, you will get familiar with some concepts of a true development cycle by running commands from an Integrated Development Environment (IDE)

You think you don’t have time to install a full developer environment ?

Don’t worry, we’ve got your back with a cloud developer environment that will help you to bring it on very fast.

What Will You Be Developing?

The code for this workshop is already given, we don’t expect you to develop something, but you can freely modify the application and even submit ideas to improve this workshop, the goal is to get familiar with aspects of developer work.

What Will You Be Running ?

In this workshop, you will run each microservice, to form an application that allows superheroes to fight against supervillains. Thanks to a super fancy feature of our core framework, Quarkus, a single line can be used to launch every microservice. Then, each microservice is communicating either synchronously via REST or asynchronously using Kafka:

  • Super Hero UI: an Angular application to pick up a random superhero, a random supervillain, and makes them fight. The Super Hero UI is exposed via Quarkus and invokes the Fight REST API.

  • Villain REST API: A classical HTTP microservice fed by a PostgreSQL database. It exposes a query API to, randomly, retrieve Villains from its database.

  • Hero REST API: A classical HTTP microservice fed by a PostgreSQL database. It exposes a query API to, randomly, retrieve Heroes from its database.

  • Fight REST API: This REST API invokes the Hero and Villain APIs to get a random superhero and supervillain. Each fight is, then, stored in a PostgreSQL database. Invocations to the hero and villain services are protected using resilience patterns (retry, timeout, circuit-breakers).

@startuml

left to right direction

node "Super Hero UI" as ui {
    agent "Quarkus" <<frontend>> as uiQuarkus
}

node "Fight" as fight {
    agent "Quarkus" <<application>> as fightQuarkus
    database "Postgresql" as fightPostgresql
    fightQuarkus .left.> fightPostgresql
}

node "Hero" as hero {
    agent "Quarkus" <<application>> as heroQuarkus
    database "Postgresql" as heroPostgresql
    heroQuarkus .left.> heroPostgresql
}

node "Villain" as villain {
    agent "Quarkus" <<application>> as villainQuarkus
    database "Postgresql" as villainPostgresql
    villainQuarkus .left.> villainPostgresql
}

node "Kafka" as kafka {
}

node "Fallback" as fallback {
    agent "Quarkus" <<application>> as fallbackQuarkus
    file "File" as fallbackFile
    fallbackQuarkus .left.> fallbackFile
}

fightQuarkus ..> kafka : Message
fallback <.. kafka : Message

uiQuarkus --> fightQuarkus : HTTP
fightQuarkus --> heroQuarkus : HTTP
fightQuarkus --> villainQuarkus : HTTP

@enduml

Software Requirements

Nothing !

At least, if you don’t want to install any other software than a web browser.

CodeSpaces is backing you up with an integrated development environment that you can run from inside your browser.

Installing Software

OK, we have said that you don’t need any software to run CodeSpaces. But there’s an exception to that: CodeSpaces can be run from VS Code rather than you a web browser. So if you are a VS code user, or you feel more comfortable to use it, please follow the instructions below

VS Code

You can download and install VS Code from this link

Preparing for the workshop

Azure subscription

This workshop is making usage of containerized environments or workloads directly in the Cloud. This is now a standard of the industry that companies around the world use for deploying and run their applications.

But you are no company, you are developers; That’s why Azure is offering a free tier to every student, please check this link to claim it

No credit cards required, only your student email!

Clone the github repository

The microservices developed for this workshop are contained in a GitHub template repository.

There’s subtle difference between forking a repository and using a template: the commit history of the repository from the template is clean and starts with one single commit that bring it to initial state of the workshop.

Call to action

To begin with, we are going to create a new github repository from a template located at https://github.com/amadeus4dev-events/1ahack4sustainability-containerapps-quarkus-workshop-template.

Using a template is easy and very well explained in this github tutorial. Please follow the above sections to know how to fill the form.

Repository owner

Please select your personal github account as owner. If you have subscribed to an Azure free tiers CodeSpaces will run using it automatically

If you are part of the https://github.com/amadeus4dev-events organization then you can select amadeus4dev-events as an owner
Repository name

You can freely pick up a name for your new repository, but our recommendation is to stick with the current name without the suffix template so you can use 1ahack4sustainability-containerapps-quarkus-workshop

NOTE: If you are part of the https://github.com/amadeus4dev-events organization and you have selected amadeus4dev-events, then we have to avoid clashes between repositories. Please prefix the repository name with your first name and last name, such as john-doe-1ahack4sustainability-containerapps-quarkus-workshop
Repository visibility and branch include

Please make your repository public but do not include all branches, you will only need the main branch.

Submit

Once you have done, please submit the creation of your repository using the button Create repository from template

CodeSpaces

CodeSpaces is a solution to run a full-blown development environment in remote containers.

This solution can be considered as an alternative to a local development environment running on your own computer.

But it’s also a powerful solution, because companies can use it to standardize development environments across teams of developers with a single configuration.

It’s also a good solution for this kind of workshop where you need a lot of tooling and third-party applications to demonstrate a proof of concept and make a demo.

Here’a how it works :

codespaces diagram
Configuration

The {workshop-github-url-template/.devcontainer}[template repository] contains the configuration files needed to start CodeSpaces from within your browser. You don’t have to do anything. The configuration contains all tooling and setup to start using this workshop, among others :

  • An OpenJDK distribution to run the microservice

  • Maven, the tool to build and run the microservice from the command line.

How to use ?

As usual, there’s a good tutorial on github for using CodeSpaces we recommend you to follow it before continuing.

If you want to use CodeSpaces from VS Code you will have to clone the repository created using the template on your computer.
git clone [URL to your repository]

Also, you can follow the below

Create code spaces from your github account

Please, choose the Configure and create codespace option:

codespaces create codespaces

Then select a machine with 8GB RAM:

codespaces machine type
CodeSpaces creation

The browser window will show the status of the creation of your CodeSpaces, it takes approximately 5 minutes before you can access to your instance.

codespaces codespaces creation
CodeSpaces workspaces

Then you will get access to your CodeSpaces instance with the project already set.

codespaces codespaces workspace

Note that a post creation hook will run, please wait for the completion of it execution

codespaces post creation hook finish
Setup Java Projects

As you probably understood the microservice are implemented in Java; For better support while playing around with the code, you can import and load all the projects as Java Projects in the UI of CodeSpaces

codespaces import java projects
codespaces java projects imported

And now ?

Now that you finally set up CodeSpaces for your repository, you can know more about What is Quarkus?. Or you can jump to the description of the first microservice …​ the Villains Microservice and how you can build it and run it on your CodeSpaces instance

Introduction

What is Quarkus?

That’s a pretty good question and probably a good start. If you go to the Quarkus web site, you’ll read that Quarkus is "A Kubernetes Native Java stack tailored for OpenJDK HotSpot & GraalVM, crafted from the best of breed Java libraries and standards." This description is somewhat unclear but does an outstanding job at using bankable keywords. It’s also written: "Supersonic Subatomic Java." Still very foggy. In practice, Quarkus is a stack to develop distributed systems and modern applications in Java, Kotlin, or Scala. Quarkus applications are tailored for the Cloud, containers, and Kubernetes. That does not mean you can’t use Quarkus in other environments, there are no limits, but the principles infused in Quarkus have made containerization of applications more efficient. In this workshop, we will explain what Quarkus is and because the best way to understand Quarkus is to use it, build a set of microservices with it. Again, Quarkus is not limited to microservices, but it’s a generally well-understood type of architecture.

Villains Microservice


At the heart of the Super-Hero application comes Villains! You can’t have superheroes without super-villains.

We need to expose a REST API allowing CRUD operations on villains. This microservice is, let’s say, a classical REST microservice. It uses HTTP to expose a REST API and internally store data into a database. It’s using the imperative development model.

The fight microservice will use this service.

This service is exposed on the port 8701.

But first, let’s describe our service. The Super-Villains microservice manages villains with their names, powers, and so on. The REST API allows adding, removing, listing, and picking a random villain from the stored set. Nothing outstanding but a good first step to discover Quarkus.

Bootstrapping the Villains Microservice

For now, the Villains microservice does not do much, it will simply answer a hello call.

Running the Application

Open a terminal, and type:

cd villains-app
mvn quarkus:dev

This launches the microservice, which will listen to the port 8701.

Development Mode

While quarkus:dev is running, you can still update the source code. Modification will be taken into account almost instantaneously.

You can type s in the terminal to force restart the application.

Type h to get the list of all possible commands.

Testing the Application

Open a second terminal, and try calling the microservice.

curl localhost:8701/api/villains/hello

The API should answer with a greeting message: Hello from the Villains App !

You can also call the quarkus built-in readiness check endpoint:

curl localhost:8701/q/health/ready

Retrieving a random villain

The purpose of this microservice is to be able to retrieve Villains to make them fight Heroes.

To achieve this, we need a list of Villains stored in a Database, and we must be able to retrieve them.

Database

This project is already configured to use PostgreSQL as a database. Launching the microservice in development mode will automatically start a PostgreSQL Docker container as well.

Existing dataset

This project comes with a predefined Villains dataset:

src/main/resources/import.sql

This dataset will be automatically loaded in the database when starting the application in development mode.

Villain Resource

The VillainResource.java class contains the microservice’s endpoints, i.e. the interface with the external world.

@Path("/api/villains") (1)
public class VillainResource {
@Path("/hello") (2)
public String hello() {
1 The class has the @Path("/api/villains") annotation
2 The hello() method has the @Path("/hello") annotation

The meaning is pretty straightforward: calling the microservice on the /api/villains/hello endpoint will invoke the hello() method and return the result.

Feel free to play around with the hello() method.

Your application should already be running in development mode: any modification should take effect immediately.

You can see that a second method has been implemented: it is called getRandomVillain() and has the @Path("/random") annotation.

Try invoking this endpoint:

curl localhost:8701/api/villains/random

It does not return anything. Let’s investigate:

    @GET
    @Path("/random")
    public Response getRandomVillain() {
        Villain villain = Villain.findRandom();
        return Response.ok(villain).build();
    }

Villain.findRandom() is supposed to retrieve a random Villain from the Database, but for now it returns null. Let’s fix this.

Villain entity

The Villain class is called an entity (see the @Entity annotation): it represents an object stored in the database with properties (name, level, powers, …​).

It extends the PanacheEntity class which comes with quite a few built-in methods. For example, calling Villain.listAll() would return the whole Villain table content.

Let us implement the findRandom() method. As its name indicates, we want to return a random Villain entity at each invocation.

Here is one possible implementation:

    public static Villain findRandom() {
        // Find out how many villains are stored in the db
        long villainCount = count();
        // Get a random villain index
        int villainIndex = new Random().nextInt((int) villainCount);
        // Fetch the villain at the requested index
        return findAll().page(villainIndex, 1).firstResult();
    }

You may need to manually add an import:

import java.util.Random;

Try calling the /api/villain/random again: you should now get a result !

The Villain microservice is now ready.

To go further

Here are a few ideas to enrich the microservice with new functionalities.

Retrieve a Villain using its ID

Who’s the 500th Villain in the database ? Wouldn’t it be nice to be able to call curl localhost:8701/api/villains/500 and have the answer?

Implement a new method in VillainResource:

@GET
@Path("/{id}")
public Response getVillainById(@PathParam("id") long id) {}

You may want to use the Villain.findById() method.

What happens if the requested ID does not exist in the DB? Maybe we should return a clearer error to the user.

Delete a Villain using its ID

Now, supposing you did not like our 500th Villain, how would you go about removing him from the database?

We would like to implement a DELETE operation on our Villain resource, so that a call to curl -X DELETE localhost:8701/api/villains/500 would delete the Villain from the database.

@DELETE, @Transactional and Villain.deleteById() might come in handy.

If you have done the previous exercise, your implementation is easy to test: just try retrieving the Villain you just deleted.

Create a Villain in the Database

Finally, you may want to add you favorite Villain to the database. Make him/her level 1000 since you’re at it.

curl -X POST localhost:8701/api/villains --header 'Content-Type: application/json' --data-raw '{"level":1000,"name":"My Favorite Villain","powers":"Cool Powers"}'

You will need a new endpoint with @POST, @Transactional, and @Consumes(MediaType.APPLICATION_JSON)

The Villain entity you receive can be persisted with one single line of code.

Make your new endpoint return the persisted Villain entity, so that you get his/her ID.

Heroes Microservice


We now have a microservice to retrieve Super-Villains: let’s do the same for Super-Heroes.

The heroes-app module contains some code. The goal here is to have the very same feature we just implemented: be able to retrieve a random Super-Hero.

curl localhost:8702/api/heroes/random

Note that the port is different: we want the two microservices to be able to run on the same machine.

A Hero has exactly the same attributes as a Villain.

The import.sql dataset is provided, as well as some classes with the api/heroes/hello endpoint. For the rest, use what you have already implemented for the Villains microservice.

Enjoy, and good luck !

Fights Microservice


Ok, let’s develop another microservice. As of now, we have a REST API that returns a random Hero, and another REST API that returns a random Villain. Now we need a new REST API to make them fight. This is the Fight microservice!

Bootstrapping the Fight Microservice

The code of the Fight microservice is provided in the fight-app directory.

Bootstrapping the Fight Microservice

The code for the Fight API is partially provided in the fight-app directory

Directory structure

The main parts of the Fight microservice are:

  • the FightResource exposing the public endpoints,

  • the FightService which select the Heroes and Villains, compute the fight outcomes and persist this result in the database as a Fight object,

  • the Fighters class which is a POJO representing a Hero/Villain pair,

  • the client subpackage which provides the HTTP clients for calling the Heroes and Villains microservices.

The HTTP proxies are configured to hit http://localhost:8701 and http://localhost:8702 for the Villains and Heroes microservices, respectively:
Listing 1. application.properties
## Rest clients
quarkus.rest-client.hero-proxy.url=http://localhost:8702
quarkus.rest-client.hero-proxy.scope=javax.inject.Singleton
quarkus.rest-client.villain-proxy.url=http://localhost:8701
quarkus.rest-client.villain-proxy.scope=javax.inject.Singleton

Database

The project is already configured to use MongoDB as database for persisting the Fight objects, each of which representing the outcome of one fight between a Hero and a Villain. Launching the microservice in development mode will automatically start a MongoDB Docker container along with the application.

The Fight POJO is annotated with @MongoEntity(collection = "fights") which indicates that the entities will be stored in the fights collection of the database. This annotation also provides the most common operations for interacting with the database out of the box. See here for more details.

Running the application

Open a terminal, and execute:

cd fights-app
./mvnw quarkus:dev

to start the application in dev mode. Once started the microservice will listen to the port 8703.

You can verify that your application is up and running by invoking the Quarkus built-in readiness endpoint:

curl localhost:8703/q/health/ready

Improve the Fight microservice

The Fight API includes 3 operations:

  • a GET operation on /api/fights/fighters which returns a randomly picked pair of Hero and Villain,

  • a POST operation on /api/flights which computes the fight result between the Hero/Villain pair provided in input and store this result in the database,

  • a GET operation on /api/flights which returns a list of all previous fight results.

If you try to invoke multiple times the first endpoint through:

curl localhost:8703/api/fights/fighters

you will notice that, for now, it always returns the same pair of Hero/Villain:

{
  "hero": {
    "level": 1,
    "name": "Fallback hero",
    "picture": "https://dummyimage.com/280x380/1e8fff/ffffff&text=Fallback+Hero",
    "powers": "Fallback hero powers"
  },
  "villain": {
    "level": 42,
    "name": "Fallback villain",
    "picture": "https://dummyimage.com/280x380/b22222/ffffff&text=Fallback+Villain",
    "powers": "Fallback villain powers"
  }
}

This is because this Hero/Villain pair is hardcoded in the FightService:

@ApplicationScoped
public class FightService {
    // ...

    Hero findRandomHero() {
        logger.warn("Falling back on Hero");
        Hero hero = new Hero();
        hero.name = "Fallback hero";
        hero.picture = "https://dummyimage.com/280x380/1e8fff/ffffff&text=Fallback+Hero";
        hero.powers = "Fallback hero powers";
        hero.level = 1;
        return hero;
    }

    Villain findRandomVillain() {
        logger.warn("Falling back on Villain");
        Villain villain = new Villain();
        villain.name = "Fallback villain";
        villain.picture = "https://dummyimage.com/280x380/b22222/ffffff&text=Fallback+Villain";
        villain.powers = "Fallback villain powers";
        villain.level = 42;
        return villain;
    }
}

We will now improve the previous code in order to make the FightService returning an actual random pair of Villain/Hero each time the Fight API is called. For this purpose we will leverage on the REST API of the Villains and Heroes microservices introduced in the previous sections.

In order to connect the FightService with the Villains and Heroes microservices, we will use the HTTP proxies defined in the client subpackage. Simply update the code of your microservice as follows:

@ApplicationScoped
public class FightService {

    HeroProxy heroProxy;
    VillainProxy villainProxy;

    // ...

    Hero findRandomHero() {
        return heroProxy.findRandomHero();
    }

    Villain findRandomVillain() {
        return villainProxy.findRandomVillain();
    }
}
Once you import this modification into your Fight microservice, you have to be careful that both the Heroes and Villains microservices are running, as the FightService will now triggers HTTP requests to http://localhost:8701 and http://localhost:8702 for getting the Villains and Heroes, respectively.

After updating the code, you can now call the Fight API again:

curl localhost:8703/api/fights/fighters

which should now return a brand new Hero/Villain pair each time it is called. One example is provided below:

{
    "hero": {
        "level": 3,
        "name": "Leech",
        "picture": "https://www.superherodb.com/pictures2/portraits/10/050/834.jpg",
        "powers": "Power Nullifier"
    },
    "villain": {
        "level": 19,
        "name": "Freddy Krueger",
        "picture": "https://www.superherodb.com/pictures2/portraits/10/050/11028.jpg",
        "powers": "Accelerated Healing, Agility, Astral Projection, Dimensional Awareness, Durability, Elasticity, Illusions, Immortality, Intelligence, Invisibility, Invulnerability, Longevity, Magic, Phasing, Possession, Psionic Powers, Reality Warping, Self-Sustenance, Shapeshifting, Size Changing, Stamina, Stealth, Summoning, Super Strength, Telekinesis, Wallcrawling"
    }
}

You can now make the Hero fight against the Villain by calling the POST endpoint, which takes the previous Hero/Villain pair as request body:

curl -X POST http://localhost:8703/api/fights \
-H "Content-Type: application/json" \
-d '{ "hero": { "level": 3, "name": "Leech", "picture": "https://www.superherodb.com/pictures2/portraits/10/050/834.jpg", "powers": "Power Nullifier" }, "villain": { "level": 19, "name": "Freddy Krueger", "picture": "https://www.superherodb.com/pictures2/portraits/10/050/11028.jpg", "powers": "Accelerated Healing, Agility, Astral Projection, Dimensional Awareness, Durability, Elasticity, Illusions, Immortality, Intelligence, Invisibility, Invulnerability, Longevity, Magic, Phasing, Possession, Psionic Powers, Reality Warping, Self-Sustenance, Shapeshifting, Size Changing, Stamina, Stealth, Summoning, Super Strength, Telekinesis, Wallcrawling" }}'

This request will return the result of the fight between the Hero and the Villain.

Finally, you can hit the third endpoint to list all results from previous fights that have been stored in the database:

curl http://localhost:8703/api/fights

In this list you should find the result of the fight you just triggered:

which returns a list of Fight objects:

[
  {
    "id": "6334513c95f38d0f8f9f4a82",
    "fightDate": "2022-09-28T13:50:52.900Z",
    "loserLevel": 3,
    "loserName": "Leech",
    "loserPicture": "https://www.superherodb.com/pictures2/portraits/10/050/834.jpg",
    "loserTeam": "heroes",
    "winnerLevel": 19,
    "winnerName": "Freddy Krueger",
    "winnerPicture": "https://www.superherodb.com/pictures2/portraits/10/050/11028.jpg",
    "winnerTeam": "villains"
  }
]

Congratulations, you are now ready to move to the next section and play with the ui!

To go further

If you performed the additional exercises on the Villain/Hero microservices, you can add new functionalities to the fights!

  • Why not make the winner level-up?

  • The loser can be considered hurt and removed from the DB. Or he/she could lose some levels. Or even be converted to the opposite side…​

  • You could add a tournament feature to determine who’s the best Hero/Villain!

User Interface

Congrats, now you have completed the creation of the Quarkus microservices, and they are running. To build the app, execute:

cd super-heroes-ui
npm install

To get it, go in the tab PORT and start by right-clicking on the url referring to your Fight microservice. Then, set Port Visibility to Public then copy the Local Address

list url terminal UI

and paste it in the environment.ts

environment

Finally, run the app

npm start

Access to the app and play !