├── .DS_Store ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── docs.yml │ └── price-generator.yml ├── .gitignore ├── .jshintrc ├── .m2 └── settings.xml ├── LICENSE ├── README.adoc ├── apps ├── .DS_Store ├── price-generator │ ├── .dockerignore │ ├── .factorypath │ ├── .gitignore │ ├── .mvn │ │ └── wrapper │ │ │ ├── MavenWrapperDownloader.java │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ │ └── main │ │ ├── docker │ │ ├── Dockerfile.jvm │ │ └── Dockerfile.native │ │ ├── java │ │ └── com │ │ │ └── redhat │ │ │ └── developers │ │ │ ├── Beer.java │ │ │ ├── PriceGenerator.java │ │ │ └── PricedBeer.java │ │ └── resources │ │ ├── META-INF │ │ └── resources │ │ │ └── index.html │ │ └── application.properties └── tutorial_app │ ├── .dockerignore │ ├── .gitignore │ ├── README.md │ └── src │ ├── main │ ├── docker │ │ ├── Dockerfile.jvm │ │ ├── Dockerfile.legacy-jar │ │ ├── Dockerfile.native │ │ └── Dockerfile.native-micro │ ├── java │ │ └── com │ │ │ └── redhat │ │ │ └── developers │ │ │ ├── CustomHealthCheck.java │ │ │ ├── GreetingResource.java │ │ │ ├── Movie.java │ │ │ ├── MovieDTO.java │ │ │ ├── MovieResource.java │ │ │ ├── Swapi.java │ │ │ └── SwapiService.java │ └── resources │ │ ├── application.properties │ │ └── import.sql │ └── test │ └── java │ └── com │ └── redhat │ └── developers │ ├── GreetingResourceIT.java │ └── GreetingResourceTest.java ├── bin └── build-site.sh ├── component-images.yaml ├── dev-pages.yml ├── devfile.yaml ├── documentation ├── antora.yml └── modules │ ├── ROOT │ ├── _attributes.adoc │ ├── assets │ │ └── images │ │ │ ├── Dev_Services_Podman_ps.png │ │ │ ├── Dev_Services_Stopped.png │ │ │ ├── Dev_UI.png │ │ │ ├── Developer_Joy.png │ │ │ ├── Jaeger.png │ │ │ ├── Jaeger_DataSource.png │ │ │ ├── Jaeger_Span.png │ │ │ ├── Openshift.png │ │ │ ├── Timed_Resource.png │ │ │ ├── chat-assistant-cancelled.png │ │ │ ├── chat-assistant-success.png │ │ │ ├── chat-assistant.png │ │ │ ├── devui-mailpit.png │ │ │ ├── mailpit-email-sent.png │ │ │ ├── openshift_sandbox.png │ │ │ ├── pagination.png │ │ │ ├── parallel.png │ │ │ ├── podman-desktop-ai-catalog.png │ │ │ ├── podman-desktop-ai.png │ │ │ ├── podman-desktop-create-merlinite-service.png │ │ │ ├── podman-desktop-create-model-service.png │ │ │ ├── podman-desktop-model-download.png │ │ │ └── punkapi-route.png │ ├── nav.adoc │ └── pages │ │ ├── 01_setup.adoc │ │ ├── 02_basics.adoc │ │ ├── 03_configuration.adoc │ │ ├── 04_panache.adoc │ │ ├── 05_kubernetes.adoc │ │ ├── 06_dev-services.adoc │ │ ├── 07_spring.adoc │ │ ├── 08_rest-client.adoc │ │ ├── 09_fault-tolerance.adoc │ │ ├── 10_health.adoc │ │ ├── 11_observability.adoc │ │ ├── 12_security.adoc │ │ ├── 13_security-oidc.adoc │ │ ├── 14_reactive.adoc │ │ ├── 15_reactive-messaging.adoc │ │ ├── 16_kafka-and-streams.adoc │ │ ├── 17_ai_intro.adoc │ │ ├── 17_prompts.adoc │ │ ├── 18_chains_memory.adoc │ │ ├── 19_agents_tools.adoc │ │ ├── 20_embed_documents.adoc │ │ ├── 21_podman_ai.adoc │ │ ├── 22_local_models.adoc │ │ ├── 23_kubernetes_kafka_ai.adoc │ │ ├── _partials │ │ ├── build.adoc │ │ ├── compile-and-run.adoc │ │ ├── invoke-service.adoc │ │ ├── k8s-build-deploy.adoc │ │ ├── prereq-cli.adoc │ │ ├── run-dev-mode.adoc │ │ └── set-env-vars.adoc │ │ └── index.adoc │ └── _attributes.adoc ├── github-pages.yml ├── gulpfile.babel.js ├── jwt-token ├── quarkus-realm.json ├── quarkus.jwt.pub ├── quarkus.jwt.token └── quarkus.keycloak.jwt.token ├── lib ├── copy-to-clipboard.js ├── remote-include-processor.js └── tab-block.js ├── package.json ├── supplemental-ui ├── img │ ├── clippy.svg │ └── favicon.ico └── partials │ └── footer-content.hbs └── templates ├── app.yaml ├── custom-probes.yaml └── probes.yaml /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/.DS_Store -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/rhdevelopers/quarkus-maven-repo-cache as cachier 2 | 3 | 4 | FROM quay.io/rhdevelopers/tutorial-tools:0.0.7 5 | 6 | COPY --from=cachier /work/m2repo $DEVELOPER_HOME/.m2/repository 7 | 8 | RUN /usr/local/bin/run.sh 9 | 10 | CMD [ "mvn" ] -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Red Hat Developers:: Quarkus Tutorial", 3 | "dockerFile": "Dockerfile", 4 | "runArgs": [ 5 | "-e","TUTORIAL_HOME=/workspaces/quarkus-tutorial", 6 | "-e","QUARKUS_VERSION=3.1.2" 7 | ], 8 | 9 | // Use 'settings' to set *default* container specific settings.json values on container create. 10 | // You can edit these settings after create using File > Preferences > Settings > Remote. 11 | "settings": { 12 | "terminal.integrated.shell.linux": "/bin/bash", 13 | "java.home": "/opt/graalvm" 14 | }, 15 | 16 | // Uncomment the next line if you want to publish any ports. 17 | "appPort": ["8080:8080"], 18 | 19 | // Uncomment the next line to run commands after the container is created. 20 | // "postCreateCommand": "/usr/local/bin/run.sh", 21 | 22 | // Add the IDs of extensions you want installed when the container is created in the array below. 23 | "extensions": [ 24 | "vscjava.vscode-java-pack", 25 | "redhat.vscode-xml", 26 | "redhat.vscode-yaml" 27 | ] 28 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - .github/workflows/docs.yml 9 | - github-pages.yml 10 | - 'documentation/**' 11 | 12 | jobs: 13 | build-and-publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout project 17 | uses: actions/checkout@v2 18 | - name: Run antora 19 | uses: docker://antora/antora:2.3.1 20 | with: 21 | args: github-pages.yml 22 | - name: Deploy to GitHub Pages 23 | uses: JamesIves/github-pages-deploy-action@releases/v3 24 | with: 25 | GITHUB_TOKEN: "${{github.token}}" 26 | FOLDER: gh-pages 27 | BRANCH: gh-pages 28 | COMMIT_MESSAGE: "[docs] Publishing the docs for commit(s) ${{github.sha}}" 29 | -------------------------------------------------------------------------------- /.github/workflows/price-generator.yml: -------------------------------------------------------------------------------- 1 | name: price-generator 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - '.github/workflows/price-generator.yml' 9 | - 'apps/price-generator/**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-18.04 14 | steps: 15 | - name: Checkout project 16 | uses: actions/checkout@v2 17 | - name: Setup Java JDK 18 | uses: actions/setup-java@v1.4.3 19 | with: 20 | java-version: 11 21 | - name: Maven build 22 | working-directory: apps/price-generator 23 | run: ./mvnw package -Pnative -Dquarkus.native.container-build=true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | package-lock.json 3 | .idea 4 | target/ 5 | build/ 6 | .gradle/ 7 | pom.xml.tag 8 | pom.xml.releaseBackup 9 | pom.xml.versionsBackup 10 | pom.xml.next 11 | release.properties 12 | dependency-reduced-pom.xml 13 | buildNumber.properties 14 | .mvn/timing.properties 15 | gh-pages 16 | yarn.lock 17 | .quarkus 18 | 19 | .cache 20 | 21 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 22 | !.mvn/wrapper/maven-wrapper.jar 23 | .settings 24 | .vscode 25 | .project 26 | .classpath 27 | docs/output/* 28 | /hello.txt 29 | .idea/ 30 | work/**/* 31 | !work/README.md 32 | # 33 | **/setenv.sh 34 | 35 | src/** 36 | pom.xml 37 | mvnw 38 | mvnw.cmd 39 | .mvn 40 | gh-pages 41 | node_modules 42 | 43 | .firebaserc 44 | firebase.json 45 | .firebase 46 | old 47 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } 4 | -------------------------------------------------------------------------------- /.m2/settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | nexus 8 | * 9 | < 10 | http://nexus.rhd-workshop-infra:8081/nexus/content/groups/public 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Quarkus Tutorial 2 | 3 | image:https://github.com/redhat-developer-demos/quarkus-tutorial/workflows/docs/badge.svg[] 4 | image:https://github.com/redhat-developer-demos/quarkus-tutorial/workflows/price-generator/badge.svg[] 5 | 6 | 7 | You can access the HTML version of this tutorial here: https://redhat-developer-demos.github.io/quarkus-tutorial/[window="_blank"] 8 | 9 | == Why Quarkus? 10 | 11 | Historically Java was able to handle the biggest enterprise problem(s) with its "Write once, run anywhere" (WORA) paradigm. With Cloud Native Applications growing in popularity, things like running applications as linux containers, serverless taking center stage -- Java was pushed back by languages like golang, node.js as the forerunner to build Cloud Native Applications as they are smaller, quicker and arguably more nimble. 12 | 13 | Quarkus is Kubernetes Native Java stack tailored for GraalVM & OpenJDK HotSpot, crafted from the best of breed Java libraries and standards. 14 | 15 | In this tutorial we will start to explore how to create, build and deploy Cloud Native Java applications using Quarkus. The Java applications built this way are tiny as a subatomic particle (Quark) and tend to boot and run at supersonic speed 16 | 17 | image::./documentation/modules/ROOT/assets/images/Developer_Joy.png[Developer Joy,400,400,align="center"] 18 | 19 | ## Building the HTML locally 20 | 21 | In the root of your git repository, run (as root): 22 | 23 | ``` 24 | # bin/build-site.sh 25 | ``` 26 | 27 | And then open your `gh-pages/index.html` file: 28 | 29 | ``` 30 | open gh-pages/index.html 31 | ``` 32 | 33 | ## Iterative local development 34 | 35 | You can develop the tutorial docs locally using a rapid iterative cycle. 36 | 37 | First, install the `yarn` dependencies: 38 | 39 | [source,bash] 40 | ---- 41 | $ yarn install 42 | ---- 43 | 44 | And now start `gulp` (as root). It will create the website and open your browser connected with `browser-sync`. Every time it detects a change, it will automatically refresh your browser page. 45 | 46 | [source,bash] 47 | ---- 48 | # gulp 49 | ---- 50 | 51 | You can clean the local cache using: 52 | 53 | [source,bash] 54 | ---- 55 | gulp clean 56 | ---- 57 | -------------------------------------------------------------------------------- /apps/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/apps/.DS_Store -------------------------------------------------------------------------------- /apps/price-generator/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !target/*-runner 3 | !target/*-runner.jar 4 | !target/lib/* -------------------------------------------------------------------------------- /apps/price-generator/.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .project 3 | .classpath 4 | .settings/ 5 | bin/ 6 | 7 | # IntelliJ 8 | .idea 9 | *.ipr 10 | *.iml 11 | *.iws 12 | 13 | # NetBeans 14 | nb-configuration.xml 15 | 16 | # Visual Studio Code 17 | .vscode 18 | 19 | # OSX 20 | .DS_Store 21 | 22 | # Vim 23 | *.swp 24 | *.swo 25 | 26 | # patch 27 | *.orig 28 | *.rej 29 | 30 | # Maven 31 | target/ 32 | pom.xml.tag 33 | pom.xml.releaseBackup 34 | pom.xml.versionsBackup 35 | release.properties -------------------------------------------------------------------------------- /apps/price-generator/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading: " + e.getMessage() ); 92 | System.exit(1); 93 | } 94 | } 95 | 96 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 97 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 98 | String username = System.getenv("MVNW_USERNAME"); 99 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 100 | Authenticator.setDefault(new Authenticator() { 101 | @Override 102 | protected PasswordAuthentication getPasswordAuthentication() { 103 | return new PasswordAuthentication(username, password); 104 | } 105 | }); 106 | } 107 | URL website = new URL(urlString); 108 | ReadableByteChannel rbc; 109 | rbc = Channels.newChannel(website.openStream()); 110 | FileOutputStream fos = new FileOutputStream(destination); 111 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 112 | fos.close(); 113 | rbc.close(); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /apps/price-generator/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/apps/price-generator/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /apps/price-generator/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /apps/price-generator/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /apps/price-generator/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.redhat.developers 6 | price-generator 7 | 1.0-SNAPSHOT 8 | 9 | 3.8.1 10 | true 11 | 11 12 | 11 13 | UTF-8 14 | UTF-8 15 | 2.6.2.Final 16 | quarkus-universe-bom 17 | io.quarkus 18 | 2.6.2.Final 19 | 3.0.0-M5 20 | 21 | 22 | 23 | 24 | ${quarkus.platform.group-id} 25 | ${quarkus.platform.artifact-id} 26 | ${quarkus.platform.version} 27 | pom 28 | import 29 | 30 | 31 | 32 | 33 | 34 | io.quarkus 35 | quarkus-junit5 36 | test 37 | 38 | 39 | io.quarkus 40 | quarkus-jsonb 41 | 42 | 43 | io.quarkus 44 | quarkus-smallrye-reactive-messaging-kafka 45 | 46 | 47 | io.quarkus 48 | quarkus-smallrye-reactive-messaging 49 | 50 | 51 | io.quarkus 52 | quarkus-container-image-jib 53 | 54 | 55 | 56 | 57 | 58 | io.quarkus 59 | quarkus-maven-plugin 60 | ${quarkus-plugin.version} 61 | 62 | 63 | 64 | build 65 | 66 | 67 | 68 | 69 | 70 | maven-compiler-plugin 71 | ${compiler-plugin.version} 72 | 73 | 74 | maven-surefire-plugin 75 | ${surefire-plugin.version} 76 | 77 | 78 | org.jboss.logmanager.LogManager 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | native 87 | 88 | 89 | native 90 | 91 | 92 | 93 | 94 | 95 | maven-failsafe-plugin 96 | ${surefire-plugin.version} 97 | 98 | 99 | 100 | integration-test 101 | verify 102 | 103 | 104 | 105 | ${project.build.directory}/${project.build.finalName}-runner 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | native 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /apps/price-generator/src/main/docker/Dockerfile.jvm: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode 3 | # 4 | # Before building the docker image run: 5 | # 6 | # ./mvnw package 7 | # 8 | # Then, build the image with: 9 | # 10 | # podman build -f src/main/docker/Dockerfile.jvm -t quarkus/wine-price-jvm . 11 | # 12 | # Then run the container using: 13 | # 14 | # podman run -i --rm -p 8080:8080 quarkus/wine-price-jvm 15 | # 16 | # If you want to include the debug port into your docker image 17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050 18 | # 19 | # Then run the container using : 20 | # 21 | # podman run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/wine-price-jvm 22 | # 23 | ### 24 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 25 | 26 | ARG JAVA_PACKAGE=java-11-openjdk-headless 27 | ARG RUN_JAVA_VERSION=1.3.5 28 | 29 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' 30 | 31 | # Install java and the run-java script 32 | # Also set up permissions for user `1001` 33 | RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ 34 | && microdnf update \ 35 | && microdnf clean all \ 36 | && mkdir /deployments \ 37 | && chown 1001 /deployments \ 38 | && chmod "g+rwX" /deployments \ 39 | && chown 1001:root /deployments \ 40 | && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ 41 | && chown 1001 /deployments/run-java.sh \ 42 | && chmod 540 /deployments/run-java.sh \ 43 | && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security 44 | 45 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. 46 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" 47 | 48 | COPY target/lib/* /deployments/lib/ 49 | COPY target/*-runner.jar /deployments/app.jar 50 | 51 | EXPOSE 8080 52 | USER 1001 53 | 54 | ENTRYPOINT [ "/deployments/run-java.sh" ] -------------------------------------------------------------------------------- /apps/price-generator/src/main/docker/Dockerfile.native: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode 3 | # 4 | # Before building the docker image run: 5 | # 6 | # ./mvnw package -Pnative -Dquarkus.native.container-build=true 7 | # 8 | # Then, build the image with: 9 | # 10 | # podman build -f src/main/docker/Dockerfile.native -t quarkus/wine-price . 11 | # 12 | # Then run the container using: 13 | # 14 | # podman run -i --rm -p 8080:8080 quarkus/wine-price 15 | # 16 | ### 17 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 18 | WORKDIR /work/ 19 | COPY target/*-runner /work/application 20 | 21 | # set up permissions for user `1001` 22 | RUN chmod 775 /work /work/application \ 23 | && chown -R 1001 /work \ 24 | && chmod -R "g+rwX" /work \ 25 | && chown -R 1001:root /work 26 | 27 | EXPOSE 8080 28 | USER 1001 29 | 30 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] -------------------------------------------------------------------------------- /apps/price-generator/src/main/java/com/redhat/developers/Beer.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import javax.json.bind.annotation.JsonbCreator; 6 | 7 | import io.quarkus.runtime.annotations.RegisterForReflection; 8 | 9 | @RegisterForReflection 10 | public class Beer { 11 | 12 | private String name; 13 | 14 | private String tagline; 15 | 16 | private double abv; 17 | 18 | private Beer(String name, String tagline, double abv) { 19 | this.name = name; 20 | this.tagline = tagline; 21 | this.abv = abv; 22 | } 23 | 24 | @JsonbCreator 25 | public static Beer of(String name, String tagline, double abv) { 26 | return new Beer(name, tagline, abv); 27 | } 28 | 29 | public PricedBeer withPrice(BigDecimal price) { 30 | return PricedBeer.of(this.name, price); 31 | } 32 | 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | public String getTagline() { 39 | return tagline; 40 | } 41 | 42 | public double getAbv() { 43 | return abv; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /apps/price-generator/src/main/java/com/redhat/developers/PriceGenerator.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import javax.enterprise.context.ApplicationScoped; 6 | import javax.json.bind.Jsonb; 7 | import javax.json.bind.JsonbBuilder; 8 | 9 | import org.eclipse.microprofile.reactive.messaging.Outgoing; 10 | import org.eclipse.microprofile.reactive.messaging.Incoming; 11 | 12 | import io.smallrye.reactive.messaging.annotations.Blocking; 13 | import io.netty.util.internal.ThreadLocalRandom; 14 | import java.util.Random; 15 | 16 | @ApplicationScoped 17 | public class PriceGenerator { 18 | private Random random = new Random(); 19 | 20 | @Incoming("beer") 21 | @Outgoing("priced-beer") 22 | @Blocking 23 | public String markup(String price) { 24 | Jsonb jsonb = JsonbBuilder.create(); 25 | Beer beer = jsonb.fromJson(price, Beer.class); 26 | PricedBeer pricedBeer = beer.withPrice(new BigDecimal(ThreadLocalRandom.current().nextInt(100, 2000)).setScale(2)); 27 | return jsonb.toJson(pricedBeer); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /apps/price-generator/src/main/java/com/redhat/developers/PricedBeer.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import io.quarkus.runtime.annotations.RegisterForReflection; 6 | 7 | @RegisterForReflection 8 | public class PricedBeer { 9 | 10 | private String name; 11 | 12 | private BigDecimal price; 13 | 14 | private PricedBeer(String name, BigDecimal price) { 15 | this.name = name; 16 | this.price = price; 17 | } 18 | 19 | static PricedBeer of(String name, BigDecimal price) { 20 | return new PricedBeer(name, price); 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public BigDecimal getPrice() { 28 | return price; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /apps/price-generator/src/main/resources/META-INF/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wine-price - 1.0-SNAPSHOT 6 | 99 | 100 | 101 | 102 | 105 | 106 |
107 |
108 |

Congratulations, you have created a new Quarkus application.

109 | 110 |

Why do you see this?

111 | 112 |

This page is served by Quarkus. The source is in 113 | src/main/resources/META-INF/resources/index.html.

114 | 115 |

What can I do from here?

116 | 117 |

If not already done, run the application in dev mode using: ./mvnw compile quarkus:dev. 118 |

119 | 125 | 126 |

How do I get rid of this page?

127 |

Just delete the src/main/resources/META-INF/resources/index.html file.

128 |
129 |
130 |
131 |

Application

132 |
    133 |
  • GroupId: com.redhat.developers
  • 134 |
  • ArtifactId: wine-price
  • 135 |
  • Version: 1.0-SNAPSHOT
  • 136 |
  • Quarkus Version: 1.4.2.Final
  • 137 |
138 |
139 |
140 |

Next steps

141 | 146 |
147 |
148 |
149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /apps/price-generator/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | %prod.kafka.bootstrap.servers=localhost:9092 2 | 3 | quarkus.container-image.registry=quay.io 4 | quarkus.container-image.group=rhdevelopers 5 | quarkus.container-image.name=quarkus-tutorial-price-generator 6 | quarkus.container-image.tag=2.0 7 | 8 | mp.messaging.incoming.beer.connector=smallrye-kafka 9 | mp.messaging.incoming.beer.topic=beer 10 | mp.messaging.incoming.beer.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer 11 | 12 | mp.messaging.outgoing.priced-beer.connector=smallrye-kafka 13 | mp.messaging.outgoing.priced-beer.topic=priced-beer 14 | mp.messaging.outgoing.priced-beer.value.serializer=org.apache.kafka.common.serialization.StringSerializer -------------------------------------------------------------------------------- /apps/tutorial_app/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !target/*-runner 3 | !target/*-runner.jar 4 | !target/lib/* 5 | !target/quarkus-app/* -------------------------------------------------------------------------------- /apps/tutorial_app/.gitignore: -------------------------------------------------------------------------------- 1 | #Maven 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | release.properties 7 | .flattened-pom.xml 8 | 9 | # Eclipse 10 | .project 11 | .classpath 12 | .settings/ 13 | bin/ 14 | 15 | # IntelliJ 16 | .idea 17 | *.ipr 18 | *.iml 19 | *.iws 20 | 21 | # NetBeans 22 | nb-configuration.xml 23 | 24 | # Visual Studio Code 25 | .vscode 26 | .factorypath 27 | 28 | # OSX 29 | .DS_Store 30 | 31 | # Vim 32 | *.swp 33 | *.swo 34 | 35 | # patch 36 | *.orig 37 | *.rej 38 | 39 | # Local environment 40 | .env 41 | 42 | # Plugin directory 43 | /.quarkus/cli/plugins/ 44 | # TLS Certificates 45 | .certs/ 46 | -------------------------------------------------------------------------------- /apps/tutorial_app/README.md: -------------------------------------------------------------------------------- 1 | # quarkus1and2 2 | 3 | This project uses Quarkus, the Supersonic Subatomic Java Framework. 4 | 5 | If you want to learn more about Quarkus, please visit its website: . 6 | 7 | ## Running the application in dev mode 8 | 9 | You can run your application in dev mode that enables live coding using: 10 | 11 | ```shell script 12 | ./mvnw compile quarkus:dev 13 | ``` 14 | 15 | > **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at . 16 | 17 | ## Packaging and running the application 18 | 19 | The application can be packaged using: 20 | 21 | ```shell script 22 | ./mvnw package 23 | ``` 24 | 25 | It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. 26 | Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. 27 | 28 | The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. 29 | 30 | If you want to build an _über-jar_, execute the following command: 31 | 32 | ```shell script 33 | ./mvnw package -Dquarkus.package.jar.type=uber-jar 34 | ``` 35 | 36 | The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. 37 | 38 | ## Creating a native executable 39 | 40 | You can create a native executable using: 41 | 42 | ```shell script 43 | ./mvnw package -Dnative 44 | ``` 45 | 46 | Or, if you don't have GraalVM installed, you can run the native executable build in a container using: 47 | 48 | ```shell script 49 | ./mvnw package -Dnative -Dquarkus.native.container-build=true 50 | ``` 51 | 52 | You can then execute your native executable with: `./target/quarkus1and2-1.0-SNAPSHOT-runner` 53 | 54 | If you want to learn more about building native executables, please consult . 55 | 56 | ## Related Guides 57 | 58 | - REST ([guide](https://quarkus.io/guides/rest)): A Jakarta REST implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. 59 | 60 | ## Provided Code 61 | 62 | ### REST 63 | 64 | Easily start your REST Web Services 65 | 66 | [Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) 67 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/docker/Dockerfile.jvm: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/quarkus1and2-jvm . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus1and2-jvm 15 | # 16 | # If you want to include the debug port into your docker image 17 | # you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. 18 | # Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 19 | # when running the container 20 | # 21 | # Then run the container using : 22 | # 23 | # docker run -i --rm -p 8080:8080 quarkus/quarkus1and2-jvm 24 | # 25 | # This image uses the `run-java.sh` script to run the application. 26 | # This scripts computes the command line to execute your Java application, and 27 | # includes memory/GC tuning. 28 | # You can configure the behavior using the following environment properties: 29 | # - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") 30 | # - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options 31 | # in JAVA_OPTS (example: "-Dsome.property=foo") 32 | # - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is 33 | # used to calculate a default maximal heap memory based on a containers restriction. 34 | # If used in a container without any memory constraints for the container then this 35 | # option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio 36 | # of the container available memory as set here. The default is `50` which means 50% 37 | # of the available memory is used as an upper boundary. You can skip this mechanism by 38 | # setting this value to `0` in which case no `-Xmx` option is added. 39 | # - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This 40 | # is used to calculate a default initial heap memory based on the maximum heap memory. 41 | # If used in a container without any memory constraints for the container then this 42 | # option has no effect. If there is a memory constraint then `-Xms` is set to a ratio 43 | # of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` 44 | # is used as the initial heap size. You can skip this mechanism by setting this value 45 | # to `0` in which case no `-Xms` option is added (example: "25") 46 | # - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. 47 | # This is used to calculate the maximum value of the initial heap memory. If used in 48 | # a container without any memory constraints for the container then this option has 49 | # no effect. If there is a memory constraint then `-Xms` is limited to the value set 50 | # here. The default is 4096MB which means the calculated value of `-Xms` never will 51 | # be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") 52 | # - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output 53 | # when things are happening. This option, if set to true, will set 54 | # `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). 55 | # - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: 56 | # true"). 57 | # - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). 58 | # - CONTAINER_CORE_LIMIT: A calculated core limit as described in 59 | # https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") 60 | # - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). 61 | # - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. 62 | # (example: "20") 63 | # - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. 64 | # (example: "40") 65 | # - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. 66 | # (example: "4") 67 | # - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus 68 | # previous GC times. (example: "90") 69 | # - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") 70 | # - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") 71 | # - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should 72 | # contain the necessary JRE command-line options to specify the required GC, which 73 | # will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). 74 | # - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") 75 | # - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") 76 | # - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be 77 | # accessed directly. (example: "foo.example.com,bar.example.com") 78 | # 79 | ### 80 | FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 81 | 82 | ENV LANGUAGE='en_US:en' 83 | 84 | 85 | # We make four distinct layers so if there are application changes the library layers can be re-used 86 | COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ 87 | COPY --chown=185 target/quarkus-app/*.jar /deployments/ 88 | COPY --chown=185 target/quarkus-app/app/ /deployments/app/ 89 | COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ 90 | 91 | EXPOSE 8080 92 | USER 185 93 | ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" 94 | ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" 95 | 96 | ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] 97 | 98 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/docker/Dockerfile.legacy-jar: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package -Dquarkus.package.jar.type=legacy-jar 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/quarkus1and2-legacy-jar . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus1and2-legacy-jar 15 | # 16 | # If you want to include the debug port into your docker image 17 | # you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. 18 | # Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 19 | # when running the container 20 | # 21 | # Then run the container using : 22 | # 23 | # docker run -i --rm -p 8080:8080 quarkus/quarkus1and2-legacy-jar 24 | # 25 | # This image uses the `run-java.sh` script to run the application. 26 | # This scripts computes the command line to execute your Java application, and 27 | # includes memory/GC tuning. 28 | # You can configure the behavior using the following environment properties: 29 | # - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") 30 | # - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options 31 | # in JAVA_OPTS (example: "-Dsome.property=foo") 32 | # - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is 33 | # used to calculate a default maximal heap memory based on a containers restriction. 34 | # If used in a container without any memory constraints for the container then this 35 | # option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio 36 | # of the container available memory as set here. The default is `50` which means 50% 37 | # of the available memory is used as an upper boundary. You can skip this mechanism by 38 | # setting this value to `0` in which case no `-Xmx` option is added. 39 | # - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This 40 | # is used to calculate a default initial heap memory based on the maximum heap memory. 41 | # If used in a container without any memory constraints for the container then this 42 | # option has no effect. If there is a memory constraint then `-Xms` is set to a ratio 43 | # of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` 44 | # is used as the initial heap size. You can skip this mechanism by setting this value 45 | # to `0` in which case no `-Xms` option is added (example: "25") 46 | # - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. 47 | # This is used to calculate the maximum value of the initial heap memory. If used in 48 | # a container without any memory constraints for the container then this option has 49 | # no effect. If there is a memory constraint then `-Xms` is limited to the value set 50 | # here. The default is 4096MB which means the calculated value of `-Xms` never will 51 | # be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") 52 | # - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output 53 | # when things are happening. This option, if set to true, will set 54 | # `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). 55 | # - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: 56 | # true"). 57 | # - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). 58 | # - CONTAINER_CORE_LIMIT: A calculated core limit as described in 59 | # https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") 60 | # - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). 61 | # - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. 62 | # (example: "20") 63 | # - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. 64 | # (example: "40") 65 | # - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. 66 | # (example: "4") 67 | # - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus 68 | # previous GC times. (example: "90") 69 | # - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") 70 | # - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") 71 | # - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should 72 | # contain the necessary JRE command-line options to specify the required GC, which 73 | # will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). 74 | # - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") 75 | # - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") 76 | # - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be 77 | # accessed directly. (example: "foo.example.com,bar.example.com") 78 | # 79 | ### 80 | FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 81 | 82 | ENV LANGUAGE='en_US:en' 83 | 84 | 85 | COPY target/lib/* /deployments/lib/ 86 | COPY target/*-runner.jar /deployments/quarkus-run.jar 87 | 88 | EXPOSE 8080 89 | USER 185 90 | ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" 91 | ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" 92 | 93 | ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] 94 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/docker/Dockerfile.native: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package -Dnative 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.native -t quarkus/quarkus1and2 . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus1and2 15 | # 16 | ### 17 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 18 | WORKDIR /work/ 19 | RUN chown 1001 /work \ 20 | && chmod "g+rwX" /work \ 21 | && chown 1001:root /work 22 | COPY --chown=1001:root target/*-runner /work/application 23 | 24 | EXPOSE 8080 25 | USER 1001 26 | 27 | ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] 28 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/docker/Dockerfile.native-micro: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. 3 | # It uses a micro base image, tuned for Quarkus native executables. 4 | # It reduces the size of the resulting container image. 5 | # Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. 6 | # 7 | # Before building the container image run: 8 | # 9 | # ./mvnw package -Dnative 10 | # 11 | # Then, build the image with: 12 | # 13 | # docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/quarkus1and2 . 14 | # 15 | # Then run the container using: 16 | # 17 | # docker run -i --rm -p 8080:8080 quarkus/quarkus1and2 18 | # 19 | ### 20 | FROM quay.io/quarkus/quarkus-micro-image:2.0 21 | WORKDIR /work/ 22 | RUN chown 1001 /work \ 23 | && chmod "g+rwX" /work \ 24 | && chown 1001:root /work 25 | COPY --chown=1001:root target/*-runner /work/application 26 | 27 | EXPOSE 8080 28 | USER 1001 29 | 30 | ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] 31 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/java/com/redhat/developers/CustomHealthCheck.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import io.smallrye.health.checks.UrlHealthCheck; 4 | import org.eclipse.microprofile.config.inject.ConfigProperty; 5 | import org.eclipse.microprofile.health.HealthCheck; 6 | import org.eclipse.microprofile.health.Readiness; 7 | 8 | import jakarta.enterprise.context.ApplicationScoped; 9 | import jakarta.ws.rs.HttpMethod; 10 | 11 | @ApplicationScoped 12 | public class CustomHealthCheck { 13 | 14 | @ConfigProperty(name = "quarkus.rest-client.\"com.redhat.developers.SwapiService\".url") 15 | String externalURL; 16 | 17 | @Readiness 18 | HealthCheck checkURL() { 19 | return new UrlHealthCheck(externalURL+"/api/films/") 20 | .name("ExternalURL health check").requestMethod(HttpMethod.GET).statusCode(200); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/java/com/redhat/developers/GreetingResource.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import org.eclipse.microprofile.config.inject.ConfigProperty; 4 | 5 | import jakarta.ws.rs.GET; 6 | import jakarta.ws.rs.Path; 7 | import jakarta.ws.rs.Produces; 8 | import jakarta.ws.rs.core.MediaType; 9 | 10 | @Path("/hello") 11 | public class GreetingResource { 12 | 13 | @ConfigProperty(name = "greeting") 14 | String greeting; 15 | 16 | @GET 17 | @Produces(MediaType.TEXT_PLAIN) 18 | public String hello() { 19 | return "Hello from Quarkus REST"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/java/com/redhat/developers/Movie.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import java.util.List; 4 | 5 | import io.quarkus.hibernate.orm.panache.PanacheEntity; 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.Entity; 8 | 9 | @Entity 10 | public class Movie extends PanacheEntity { 11 | 12 | public String title; 13 | 14 | @Column(name = "release_date") 15 | public java.sql.Date releaseDate; 16 | 17 | public static List findByYear(int year) { 18 | return find("YEAR(releaseDate)", year).list(); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/java/com/redhat/developers/MovieDTO.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import java.sql.Date; 4 | import java.util.List; 5 | 6 | import com.redhat.developers.Swapi.Results; 7 | 8 | public class MovieDTO { 9 | private String title; 10 | private Date releaseDate; 11 | private int episodeId; 12 | private String producer; 13 | private String director; 14 | private String opening_crawl; 15 | 16 | private MovieDTO(String title, Date releaseDate, int episodeId, String producer, String director, 17 | String opening_crawl) { 18 | this.title = title; 19 | this.releaseDate = releaseDate; 20 | this.episodeId = episodeId; 21 | this.producer = producer; 22 | this.director = director; 23 | this.opening_crawl = opening_crawl; 24 | } 25 | 26 | public static MovieDTO of(Movie movie, Swapi swapi){ 27 | List results = swapi.getResults(); 28 | Results result = results.get(0); 29 | 30 | return new MovieDTO( 31 | movie.title, 32 | movie.releaseDate, 33 | result.getEpisodeId(), 34 | result.getProducer(), 35 | result.getDirector(), 36 | result.getOpening_crawl() 37 | ); 38 | } 39 | 40 | public String getTitle() { 41 | return title; 42 | } 43 | 44 | public Date getReleaseDate() { 45 | return releaseDate; 46 | } 47 | 48 | public int getEpisodeId() { 49 | return episodeId; 50 | } 51 | 52 | public String getProducer() { 53 | return producer; 54 | } 55 | 56 | public String getDirector() { 57 | return director; 58 | } 59 | 60 | public String getOpening_crawl() { 61 | return opening_crawl; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/java/com/redhat/developers/MovieResource.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import org.eclipse.microprofile.rest.client.inject.RestClient; 7 | 8 | import jakarta.inject.Inject; 9 | import jakarta.transaction.Transactional; 10 | import jakarta.ws.rs.Consumes; 11 | import jakarta.ws.rs.GET; 12 | import jakarta.ws.rs.POST; 13 | import jakarta.ws.rs.Path; 14 | import jakarta.ws.rs.Produces; 15 | import jakarta.ws.rs.QueryParam; 16 | import jakarta.ws.rs.core.MediaType; 17 | import jakarta.ws.rs.core.Response; 18 | import jakarta.ws.rs.core.Response.Status; 19 | 20 | @Path("/movie") 21 | public class MovieResource { 22 | 23 | @RestClient 24 | @Inject 25 | SwapiService swapiService; 26 | 27 | @GET 28 | @Produces(MediaType.APPLICATION_JSON) 29 | public List movies(@QueryParam("year") String year) { 30 | if (year != null) { 31 | return Movie.findByYear(Integer.parseInt(year)) 32 | .stream() 33 | .map(movie -> MovieDTO.of(movie, swapiService.getMovieByTitle(movie.title))) 34 | .collect(Collectors.toList()); 35 | } 36 | return Movie.listAll() 37 | .stream() 38 | .map(movie -> MovieDTO.of(movie, swapiService.getMovieByTitle(movie.title))) 39 | .collect(Collectors.toList()); 40 | } 41 | 42 | @Transactional 43 | @POST 44 | @Consumes(MediaType.APPLICATION_JSON) 45 | @Produces(MediaType.APPLICATION_JSON) 46 | public Response newMovie(Movie movie) { 47 | movie.id = null; 48 | movie.persist(); 49 | return Response.status(Status.CREATED).entity(movie).build(); 50 | } 51 | 52 | @GET 53 | @Path("simple") 54 | @Produces(MediaType.APPLICATION_JSON) 55 | public List simple(@QueryParam("year") String year) { 56 | if (year != null) { 57 | return Movie.findByYear(Integer.parseInt(year)); 58 | } 59 | return Movie.listAll(); 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/java/com/redhat/developers/Swapi.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | public class Swapi { 8 | 9 | 10 | private List results; 11 | 12 | public Swapi(@JsonProperty("results") List results) { 13 | this.results = results; 14 | } 15 | 16 | public List getResults() { 17 | return results; 18 | } 19 | 20 | public static class Results { 21 | private int episodeId; 22 | private String producer; 23 | private String director; 24 | private String opening_crawl; 25 | 26 | public Results(int episodeId, String producer, String director, String opening_crawl) { 27 | this.episodeId = episodeId; 28 | this.producer = producer; 29 | this.director = director; 30 | this.opening_crawl = opening_crawl; 31 | } 32 | 33 | public int getEpisodeId() { 34 | return episodeId; 35 | } 36 | 37 | public String getProducer() { 38 | return producer; 39 | } 40 | 41 | public String getDirector() { 42 | return director; 43 | } 44 | 45 | public String getOpening_crawl() { 46 | return opening_crawl; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/java/com/redhat/developers/SwapiService.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import java.util.List; 4 | 5 | import org.eclipse.microprofile.faulttolerance.CircuitBreaker; 6 | import org.eclipse.microprofile.faulttolerance.ExecutionContext; 7 | import org.eclipse.microprofile.faulttolerance.Fallback; 8 | import org.eclipse.microprofile.faulttolerance.FallbackHandler; 9 | import org.eclipse.microprofile.faulttolerance.Retry; 10 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 11 | 12 | import jakarta.ws.rs.GET; 13 | import jakarta.ws.rs.Path; 14 | import jakarta.ws.rs.Produces; 15 | import jakarta.ws.rs.QueryParam; 16 | import jakarta.ws.rs.core.MediaType; 17 | 18 | import com.redhat.developers.Swapi.Results; 19 | 20 | @Path("/api") 21 | @RegisterRestClient 22 | public interface SwapiService { 23 | @GET 24 | @Path("/films/") 25 | @Produces(MediaType.APPLICATION_JSON) 26 | @Retry(maxRetries = 3, delay = 2000) 27 | @Fallback(SwapiFallback.class) 28 | @CircuitBreaker( 29 | requestVolumeThreshold=4, 30 | failureRatio=0.75, 31 | delay=5000 32 | ) 33 | Swapi getMovieByTitle(@QueryParam("search") String title); 34 | 35 | public static class SwapiFallback implements FallbackHandler { 36 | 37 | private static final Swapi EMPTY_SWAPI = new Swapi(List.of(new Results(0,"","",""))); 38 | @Override 39 | public Swapi handle(ExecutionContext context) { 40 | return EMPTY_SWAPI; 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Configuration file 2 | # key = value 3 | # Configuration file 4 | # key = value 5 | greeting=Hello y'all! 6 | 7 | %prod.quarkus.datasource.password=quarkus 8 | %prod.quarkus.datasource.username=quarkus 9 | %prod.quarkus.datasource.db-kind=postgresql 10 | %prod.quarkus.datasource.jdbc.url=jdbc:postgresql://postgresql/quarkus 11 | quarkus.hibernate-orm.database.generation=drop-and-create 12 | quarkus.hibernate-orm.sql-load-script=import.sql 13 | 14 | quarkus.container-image.registry=quay.io 15 | quarkus.container-image.group=kevindubois 16 | quarkus.container-image.name=tutorial-app 17 | quarkus.container-image.tag=1.0-SNAPSHOT 18 | quarkus.kubernetes.service-type=load-balancer 19 | 20 | quarkus.rest-client."com.redhat.developers.SwapiService".url=https://swapii.dev 21 | quarkus.rest-client.logging.scope=request-response 22 | quarkus.rest-client.logging.body-limit=1024 23 | quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG 24 | 25 | # enable tracing 26 | quarkus.datasource.jdbc.telemetry=true -------------------------------------------------------------------------------- /apps/tutorial_app/src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO Movie(id,title,release_date) VALUES (1,'A New Hope','1977-05-25'); 2 | INSERT INTO Movie(id,title,release_date) VALUES (2,'The Empire Strikes Back','1980-05-17'); 3 | INSERT INTO Movie(id,title,release_date) VALUES (3,'Return of the Jedi','1983-05-25'); 4 | INSERT INTO Movie(id,title,release_date) VALUES (4,'The Phantom Menace','1999-05-19'); 5 | INSERT INTO Movie(id,title,release_date) VALUES (5,'Attack of the Clones','2002-05-16'); 6 | INSERT INTO Movie(id,title,release_date) VALUES (6,'Revenge of the Sith','2005-05-19'); 7 | ALTER SEQUENCE movie_seq RESTART WITH 7; -------------------------------------------------------------------------------- /apps/tutorial_app/src/test/java/com/redhat/developers/GreetingResourceIT.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import io.quarkus.test.junit.QuarkusIntegrationTest; 4 | 5 | @QuarkusIntegrationTest 6 | class GreetingResourceIT extends GreetingResourceTest { 7 | // Execute the same tests but in packaged mode. 8 | } 9 | -------------------------------------------------------------------------------- /apps/tutorial_app/src/test/java/com/redhat/developers/GreetingResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.redhat.developers; 2 | 3 | import io.quarkus.test.junit.QuarkusTest; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static io.restassured.RestAssured.given; 7 | import static org.hamcrest.CoreMatchers.is; 8 | 9 | @QuarkusTest 10 | class GreetingResourceTest { 11 | @Test 12 | void testHelloEndpoint() { 13 | given() 14 | .when().get("/hello") 15 | .then() 16 | .statusCode(200) 17 | .body(is("Hello y'all")); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /bin/build-site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker run -u $(id -u) -v $PWD:/antora:Z --rm -t antora/antora:2.3.1 --cache-dir=./.cache/antora github-pages.yml 3 | #docker run -v $PWD:/antora:Z --rm -t antora/antora:2.3.1 --cache-dir=./.cache/antora github-pages.yml 4 | -------------------------------------------------------------------------------- /component-images.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: v1 5 | kind: Pod 6 | metadata: 7 | name: tutorial-tools 8 | labels: 9 | app: tutorial-tools 10 | spec: 11 | containers: 12 | - name: tutorial-tools 13 | image: quay.io/rhdevelopers/tutorial-tools:0.0.4 14 | imagePullPolicy: IfNotPresent 15 | env: 16 | - name: MAVEN_MIRROR_URL 17 | value: "http://nexus.rhd-workshop-infra:8081/nexus/content/groups/public" 18 | workingDir: /projects 19 | resources: 20 | limits: 21 | cpu: 4 22 | memory: 6Gi 23 | - apiVersion: v1 24 | kind: Pod 25 | metadata: 26 | name: database 27 | labels: 28 | app: database 29 | spec: 30 | containers: 31 | - name: database 32 | resources: 33 | limits: 34 | memory: 512Mi 35 | requests: 36 | memory: 512Mi 37 | readinessProbe: 38 | exec: 39 | command: 40 | - /usr/libexec/check-container 41 | initialDelaySeconds: 5 42 | timeoutSeconds: 1 43 | periodSeconds: 10 44 | successThreshold: 1 45 | failureThreshold: 3 46 | livenessProbe: 47 | exec: 48 | command: 49 | - /usr/libexec/check-container 50 | - "--live" 51 | initialDelaySeconds: 120 52 | timeoutSeconds: 10 53 | periodSeconds: 10 54 | successThreshold: 1 55 | failureThreshold: 3 56 | env: 57 | - name: POSTGRESQL_USER 58 | value: demo 59 | - name: POSTGRESQL_PASSWORD 60 | value: password 61 | - name: POSTGRESQL_DATABASE 62 | value: demodb 63 | ports: 64 | - containerPort: 5432 65 | protocol: TCP 66 | imagePullPolicy: IfNotPresent 67 | image: >- 68 | image-registry.openshift-image-registry.svc:5000/openshift/postgresql 69 | -------------------------------------------------------------------------------- /dev-pages.yml: -------------------------------------------------------------------------------- 1 | runtime: 2 | cache_dir: ./.cache/antora 3 | 4 | site: 5 | title: Quarkus Tutorial 6 | url: https://redhat-developer-demos.github.io/quarkus-tutorial 7 | start_page: quarkus-tutorial::index.adoc 8 | 9 | content: 10 | sources: 11 | - url: ./ 12 | branches: HEAD 13 | start_path: documentation 14 | 15 | asciidoc: 16 | attributes: 17 | tutorial-namespace: quarkus-tutorial 18 | quarkus-version: 2.15.3.Final 19 | graalvm-version: 22.3.0 20 | project-name: tutorial-app 21 | page-pagination: true 22 | extensions: 23 | - ./lib/tab-block.js 24 | - ./lib/remote-include-processor.js 25 | ui: 26 | bundle: 27 | url: https://github.com/redhat-developer-demos/rhd-tutorial-ui/releases/download/v0.1.10/ui-bundle.zip 28 | supplemental_files: 29 | - path: ./supplemental-ui 30 | - path: .nojekyll 31 | - path: ui.yml 32 | contents: "static_files: [ .nojekyll ]" 33 | 34 | output: 35 | dir: ./gh-pages 36 | -------------------------------------------------------------------------------- /devfile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: 1.0.0 3 | metadata: 4 | generateName: quarkus-tutorial- 5 | attributes: 6 | persistVolumes: "false" 7 | projects: 8 | - name: maven 9 | source: 10 | type: git 11 | location: "https://github.com/redhat-developer-demos/quarkus-tutorial.git" 12 | sparseCheckoutDir: .m2 13 | components: 14 | - id: redhat/vscode-yaml/latest 15 | type: chePlugin 16 | - id: redhat/vscode-xml/latest 17 | type: chePlugin 18 | - type: chePlugin 19 | reference: https://github.com/eclipse/che-plugin-registry/raw/master/v3/plugins/redhat/quarkus-java11/1.3.0/meta.yaml 20 | - type: dockerimage 21 | alias: centos-quarkus-maven 22 | image: quay.io/eclipse/che-quarkus:nightly 23 | env: 24 | - name: JAVA_OPTS 25 | value: >- 26 | -XX:MaxRAMPercentage=50.0 -XX:+UseParallelGC -XX:MinHeapFreeRatio=10 27 | -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 28 | -Dsun.zip.disableMemoryMapping=true -Xms20m -Djava.security.egd=file:/dev/./urandom 29 | -Duser.home=/projects/maven 30 | - name: MAVEN_OPTS 31 | value: $(JAVA_OPTS) 32 | volumes: 33 | - name: m2 34 | containerPath: /home/theia/.m2 35 | memoryLimit: 4Gi 36 | mountSources: true 37 | - alias: m2repo-cache 38 | mountSources: true 39 | memoryLimit: 64M 40 | type: dockerimage 41 | image: "quay.io/rhdevelopers/quarkus-maven-repo-cache:1.4.2.Final" 42 | volumes: 43 | - name: m2 44 | containerPath: /work/volumes/.m2 45 | args: ["/usr/local/bin/che-entrypoint-run.sh", "sleep", "infinity"] 46 | - type: dockerimage 47 | alias: ubi-quarkus-native-image 48 | image: "quay.io/quarkus/ubi-quarkus-native-image:20.0.0-java11" 49 | memoryLimit: 32M 50 | mountSources: true 51 | command: ["tail"] 52 | args: ["-f", "/dev/null"] 53 | - alias: db 54 | type: kubernetes 55 | reference: https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/component-images.yaml 56 | selector: 57 | app: database 58 | commands: 59 | - name: Attach remote debugger 60 | actions: 61 | - type: vscode-launch 62 | referenceContent: | 63 | { 64 | "version": "0.2.0", 65 | "configurations": [ 66 | { 67 | "type": "java", 68 | "request": "attach", 69 | "name": "Attach to Remote Quarkus App", 70 | "hostName": "localhost", 71 | "port": 5005 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /documentation/antora.yml: -------------------------------------------------------------------------------- 1 | name: quarkus-tutorial 2 | version: master 3 | nav: 4 | - modules/ROOT/nav.adoc 5 | start_page: ROOT:index.adoc 6 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/_attributes.adoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/_attributes.adoc -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Dev_Services_Podman_ps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Dev_Services_Podman_ps.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Dev_Services_Stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Dev_Services_Stopped.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Dev_UI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Dev_UI.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Developer_Joy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Developer_Joy.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Jaeger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Jaeger.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Jaeger_DataSource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Jaeger_DataSource.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Jaeger_Span.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Jaeger_Span.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Openshift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Openshift.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/Timed_Resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/Timed_Resource.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/chat-assistant-cancelled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/chat-assistant-cancelled.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/chat-assistant-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/chat-assistant-success.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/chat-assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/chat-assistant.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/devui-mailpit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/devui-mailpit.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/mailpit-email-sent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/mailpit-email-sent.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/openshift_sandbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/openshift_sandbox.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/pagination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/pagination.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/parallel.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/podman-desktop-ai-catalog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/podman-desktop-ai-catalog.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/podman-desktop-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/podman-desktop-ai.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/podman-desktop-create-merlinite-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/podman-desktop-create-merlinite-service.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/podman-desktop-create-model-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/podman-desktop-create-model-service.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/podman-desktop-model-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/podman-desktop-model-download.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/assets/images/punkapi-route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/ROOT/assets/images/punkapi-route.png -------------------------------------------------------------------------------- /documentation/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- 1 | * Requirements 2 | ** xref:01_setup.adoc[Setup] 3 | 4 | * Basics 5 | ** xref:02_basics.adoc[Basics and Fundamentals] 6 | ** xref:03_configuration.adoc[Configuration] 7 | ** xref:04_panache.adoc[Hibernate with Panache] 8 | ** xref:05_kubernetes.adoc[Deploy to Kubernetes] 9 | ** xref:06_dev-services.adoc[Dev Services] 10 | ** xref:07_spring.adoc[Spring Compatibility] 11 | 12 | * Cloud Native 13 | ** xref:08_rest-client.adoc[REST Client] 14 | ** xref:09_fault-tolerance.adoc[Fault Tolerance] 15 | ** xref:10_health.adoc[Health Check] 16 | ** xref:11_observability.adoc[Observability] 17 | ** xref:12_security.adoc[Security with JWT RBAC] 18 | // ** xref:13_security-oidc.adoc[Security using OpenID Connect] 19 | 20 | * Reactive 21 | ** xref:14_reactive.adoc[Reactive with Mutiny] 22 | ** xref:15_reactive-messaging.adoc[Streaming reactive messages] 23 | ** xref:16_kafka-and-streams.adoc[Apache Kafka with Reactive Streams] 24 | 25 | * AI 26 | ** xref:17_ai_intro.adoc[AI with Quarkus] 27 | ** xref:17_prompts.adoc[Working with prompts] 28 | ** xref:18_chains_memory.adoc[Chains and Memory] 29 | ** xref:19_agents_tools.adoc[Agents/Tools] 30 | ** xref:20_embed_documents.adoc[Embedding Documents] 31 | ** xref:21_podman_ai.adoc[Working with Podman Desktop AI] 32 | //** xref:22_local_models.adoc[Working with local models] 33 | //** xref:23_kubernetes_kafka_ai.adoc[Bringing Kubernetes and Kafka to the party] 34 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/01_setup.adoc: -------------------------------------------------------------------------------- 1 | [[requirements]] 2 | = Setup 3 | include::../_attributes.adoc 4 | 5 | == IDE 6 | 7 | You can use any Java IDE for this tutorial but if you haven't tried it before, we recommend https://code.visualstudio.com/download[VS Code, window="_blank"] with the https://marketplace.visualstudio.com/items?itemName=redhat.java[Language Support for Java(TM) by Red Hat, window="_blank"] and the https://marketplace.visualstudio.com/items?itemName=redhat.vscode-quarkus[Quarkus, window="_blank"] extensions. 8 | 9 | [TIP] 10 | ==== 11 | If you are using VS Code, then install the VS Code https://aka.ms/vscode-remote/download/extension[Remote Development Extension pack], 12 | which allows you to run this entire tutorial within a container that will have all tools configured. 13 | ==== 14 | 15 | == CLI Tools 16 | 17 | The following CLI tools are recommended for running the exercises in this tutorial. 18 | 19 | //// 20 | ifndef::workshop[] 21 | Kubernetes/OpenShift are required only in case you want to deploy the application into them. 22 | endif::workshop[] 23 | //// 24 | 25 | [TIP] 26 | ==== 27 | Installing GraalVM is only required if you intend to build a https://quarkus.io/guides/building-native-image[native binary] for your local operating system and don't have a container runtime. 28 | Quarkus can simply build the native binary inside a container and this way you don't need to install and configure GraalVM on your machine. 29 | ==== 30 | 31 | NOTE: You can also use Docker instead of Podman. The advantage of Podman is that it is 100% Free Open Source and does not need to run with elevated privileges. 32 | 33 | 34 | [tabs] 35 | ==== 36 | Local Installation:: 37 | + 38 | -- 39 | Please have them installed and configured before you get started with any of the tutorial chapters. 40 | 41 | 42 | [options="header"] 43 | |=== 44 | |**Tool**|**macOS**|**Fedora**|**Windows** 45 | 46 | | **Podman Desktop* ** 47 | | https://podman-desktop.io/downloads[Podman Desktop for Mac, window="_blank"] 48 | | https://podman-desktop.io/downloads[Podman Desktop for Linux, window="_blank"] 49 | | https://podman-desktop.io/downloads[Podman Desktop for Windows, window="_blank"] 50 | | **Java 21** 51 | | `brew install --cask temurin@21` 52 | | `dnf install java-21-openjdk.x86_64` 53 | | https://adoptium.net[Windows] (Make sure you set the `JAVA_HOME` environment variable and add `%JAVA_HOME%\bin` to your `PATH`) 54 | | **Apache Maven 3.8.6+** 55 | | `brew install maven` 56 | | `dnf install maven` 57 | | https://maven.apache.org/download.cgi[Windows] (Make sure you set the `MAVEN_HOME` environment variable and add `%MAVEN_HOME%\bin` to your `PATH`) 58 | | **GraalVM for Java 21 [Optional]** 59 | | https://www.graalvm.org/latest/docs/getting-started/macos/[Download & install GraalVM for macOS] 60 | | https://www.graalvm.org/latest/docs/getting-started/linux/[Download & install GraalVM for Linux] 61 | | https://www.graalvm.org/latest/docs/getting-started/windows/[Download & install GraalVM for Windows] 62 | 63 | |=== 64 | 65 | [TIP] 66 | ===== 67 | If you are using Linux, macOS or WSL on Windows, you can also install the required tools using https://sdkman.io[SDKMAN!]. This allows easy version/distribution switching (and you can install the Quarkus CLI with it as well). 68 | Alternatively on Windows you can also try https://chocolatey.org/[Chocolatey] which works similarly. 69 | [.console-input] 70 | [source,bash,subs="+macros,+attributes"] 71 | ---- 72 | curl -s "https://get.sdkman.io" | bash 73 | source "$HOME/.sdkman/bin/sdkman-init.sh" 74 | 75 | sdk install java 76 | sdk install maven 77 | sdk install quarkus 78 | sdk install java 21.0.3-tem 79 | sdk install jbang 80 | ---- 81 | ===== 82 | -- 83 | Container Image:: 84 | + 85 | -- 86 | You can run the tutorial from inside a container which has all the tools listed above installed and pre-configured. 87 | [#docker-pull-tutorials-cli] 88 | [source,bash] 89 | ----- 90 | docker pull quay.io/rhdevelopers/tutorial-tools 91 | mkdir quarkus-tutorial 92 | cd quarkus-tutorial 93 | mkdir work 94 | docker run -ti -p 8080:8080 -v `pwd`/work:/work \ 95 | -v `mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout`:/opt/developer/.m2/repository \ 96 | --rm quay.io/rhdevelopers/tutorial-tools:latest bash 97 | 98 | # -p will map Quarkus running in the container to your host port 99 | # -v `pwd`... will map the host work subdirectory to the container /work directory, this is where you will create your application 100 | # -v `mvn ...` will map your ~/.m2/repository of your host to the container repository to save downloading again between image runs 101 | ----- 102 | 103 | [TIP] 104 | ===== 105 | Installing https://sdkman.io[SDKMAN!] in the container will allow you to install the Quarkus CLI very easily: 106 | [.console-input] 107 | [source,bash,subs="+macros,+attributes"] 108 | ---- 109 | curl -s "https://get.sdkman.io" | bash 110 | source "$HOME/.sdkman/bin/sdkman-init.sh" 111 | 112 | sdk install quarkus 113 | ---- 114 | ===== 115 | -- 116 | ==== 117 | 118 | //// 119 | ifndef::workshop[] 120 | [#remote-repository] 121 | == Remote Repository 122 | For some parts of the tutorial, you will be pushing and pulling container images to and from a remote image repository. You will need an account with push rights. You can create a free account on https://quay.io[Red Hat Quay.io]. 123 | endif::workshop[] 124 | 125 | [#download-tutorial-sources] 126 | == Download Tutorial Sources 127 | Before we start setting up the environment, let's clone the tutorial sources and set the `TUTORIAL_HOME` environment variable to point to the root directory of the tutorial: 128 | 129 | [#cloneRepo] 130 | [source,bash,subs="+macros,+attributes"] 131 | ---- 132 | git clone https://github.com/redhat-developer-demos/quarkus-tutorial 133 | ---- 134 | 135 | The `work` folder in `$TUTORIAL_HOME` can be used to download the demo application resources and refer to them during the exercises. The `work` folder has a README with instructions on the source code repo and git commands to clone the sources. 136 | 137 | NOTE: if you use `tutorial-tools`, this step is optional. 138 | 139 | [IMPORTANT,subs="+macros,+attributes"] 140 | ==== 141 | 142 | This tutorial was developed and tested with: 143 | 144 | - Quarkus `{quarkus-version}` 145 | - Minikube `{minikube-version}` 146 | - OpenShift `{openshift-version}` 147 | - Minishift `{minishift-version}` 148 | - GraalVM `{graalvm-version}` 149 | ==== 150 | 151 | ifndef::workshop[] 152 | [#kubernetes-cluster] 153 | == Kubernetes Cluster 154 | 155 | [tabs] 156 | ==== 157 | Minikube:: 158 | + 159 | -- 160 | [#quarkus-start-minikube] 161 | [source,bash,subs="+macros,+attributes"] 162 | ---- 163 | minikube profile quarkus-tutorial && \ 164 | minikube -p quarkus-tutorial start --memory=8192 --cpus=4 --disk-size=50g && \ 165 | kubectl create namespace {tutorial-namespace} 166 | kubectl config set-context --current --namespace={tutorial-namespace} 167 | ---- 168 | -- 169 | Minishift:: 170 | + 171 | -- 172 | [#quarkus-start-minishift] 173 | [source,bash,subs="+macros,+attributes"] 174 | ---- 175 | minishift profile set quarkus-tutorial && \ 176 | minishift start --memory=8192 --cpus=4 --disk-size=50g && \ 177 | minishift addon enable admin-user && \ 178 | minishift addon enable anyuid && \ 179 | oc new-project {tutorial-namespace} 180 | ---- 181 | -- 182 | ==== 183 | 184 | [NOTE] 185 | ==== 186 | On macOS, you might need to install hyperkit and pass it as the main engine by adding `--vm-driver=hyperkit` after `--disk-size=50g`. 187 | ==== 188 | 189 | Prepare your shell environment: 190 | 191 | [#quarkus-tutorial-shell-env] 192 | [source,bash,subs="+macros,+attributes"] 193 | ---- 194 | cd quarkus-tutorial 195 | export TUTORIAL_HOME=pass:[`pwd`] 196 | export GRAALVM_HOME='PUT THE LOCATION WHERE YOU HAVE EXTRACTED GRAAL VM' 197 | ---- 198 | 199 | It is not necessary to install GraalVM as the native compilation can be done within a container. 200 | The only limitation, in this case, is to have Podman/Docker installed and the produced native executable will only be runnable inside a container. 201 | GraalVM is already installed in `tutorial-tools`. 202 | 203 | If you installed GraalVM, make sure to also install `native-image` 204 | 205 | [#quarkus-tutorial-graalvm-native] 206 | [source,bash,subs="+macros,+attributes"] 207 | ---- 208 | gu install native-image 209 | ---- 210 | 211 | [#quarkus-tutorial-shell-env] 212 | [source,bash,subs="+macros,+attributes"] 213 | ---- 214 | cd quarkus-tutorial 215 | export TUTORIAL_HOME=pass:[`pwd`] 216 | export WORKSHOP_USER='PUT YOUR USERNAME HERE' 217 | export GRAALVM_HOME='PUT THE LOCATION WHERE YOU HAVE EXTRACTED GRAAL VM' 218 | export WORKSHOP_OPENSHIFT_SERVER='PUT THE OPENSHIFT SERVER URL HERE' 219 | ---- 220 | 221 | Login into OpenShift: 222 | 223 | [#quarkus-tutorial-oc-login] 224 | [source,bash,subs="+macros,+attributes"] 225 | ---- 226 | oc login --username pass:[$WORKSHOP_USER] --server pass:[$WORKSHOP_OPENSHIFT_SERVER] --insecure-skip-tls-verify=false 227 | ---- 228 | 229 | ifdef::openshift-console-url[] 230 | You can also access the OpenShift Console via {openshift-console-url}. 231 | endif::[] 232 | 233 | IMPORTANT: If you are using macOS, the variable should point to `Home` sub-directory: `export GRAALVM_HOME=$HOME/Development/graalvm/Contents/Home/` 234 | endif::workshop[] 235 | 236 | [#setup-work-folder] 237 | == Work folder (Optional) 238 | 239 | The work folder i.e `$TUTORIAL_HOME/work` can be used as a work directory during the build. 240 | //// 241 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/02_basics.adoc: -------------------------------------------------------------------------------- 1 | = Basics and Fundamentals 2 | 3 | :project-name: tutorial-app 4 | 5 | == Creating a Quarkus Application 6 | 7 | [tabs%sync] 8 | ==== 9 | 10 | Maven:: 11 | + 12 | -- 13 | [.console-input] 14 | [source,bash,subs="+macros,+attributes"] 15 | ---- 16 | mvn "io.quarkus.platform:quarkus-maven-plugin:create" -DprojectGroupId="com.redhat.developers" -DprojectArtifactId="tutorial-app" -DprojectVersion="1.0-SNAPSHOT" -Dextensions=rest 17 | cd {project-name} 18 | ---- 19 | -- 20 | Quarkus CLI:: 21 | + 22 | -- 23 | The quarkus CLI tool lets you create projects, manage extensions and do essential dev, build and deploy commands using the underlying project build tool in a simplified way. 24 | 25 | Follow these instructions to install Quarkus CLI: https://quarkus.io/guides/cli-tooling 26 | 27 | Then create the application: 28 | [.console-input] 29 | [source,bash,subs="+macros,+attributes"] 30 | ---- 31 | quarkus create app -x rest com.redhat.developers:tutorial-app:1.0-SNAPSHOT 32 | cd {project-name} 33 | ---- 34 | -- 35 | ==== 36 | 37 | IMPORTANT: All the remaining parts of this tutorial assume that you'll be working inside the project folder that was just created. In this case, `{project-name}`. 38 | 39 | == Build and Run the Quarkus Application 40 | 41 | include::ROOT:partial$compile-and-run.adoc[] 42 | 43 | Notice how *fast* Quarkus was ready to serve your requests. In this particular example Quarkus only required *0.019s* to start. 44 | 45 | You can open your browser with the url `http://localhost:8080/hello[window="_blank"]` and you should see a response like `hello`. 46 | 47 | Or else if you're a CLI person, you can run a `curl` command like: 48 | 49 | [.console-input] 50 | [source,bash] 51 | ---- 52 | curl -w '\n' localhost:8080/hello 53 | ---- 54 | 55 | [.console-output] 56 | [source,bash] 57 | ---- 58 | Hello from Quarkus REST 59 | ---- 60 | 61 | == Stopping the application 62 | 63 | You can stop your running application by issuing a `CTRL+C` in your terminal. 64 | 65 | [.console-output] 66 | [source,bash] 67 | ---- 68 | INFO [io.quarkus] (main) tutorial-app stopped in 0.035s 69 | ---- 70 | 71 | Quarkus was able to stop in just *0.035s*. Fast to start, fast to stop. Fast always! 72 | 73 | == Testing your application 74 | 75 | The scaffolding process also creates a test case for the example endpoint. 76 | 77 | Check the file `GreetingResourceTest.java` in the folder `src/test/java` to see what a Quarkus integration test looks like. 78 | 79 | When running this test, the application is started once, then all tests are executed, and finally, the application stops. Although it is not mandatory, by default the RestAssured project is used to test REST endpoints but it is up to you to change that. 80 | 81 | You can run the test in your IDE or by running the following: 82 | 83 | [tabs%sync] 84 | ==== 85 | Maven:: 86 | + 87 | -- 88 | [.console-input] 89 | [source,bash,subs="+macros,+attributes"] 90 | ---- 91 | ./mvnw clean test 92 | ---- 93 | 94 | -- 95 | Quarkus CLI:: 96 | + 97 | -- 98 | [.console-input] 99 | [source,bash,subs="+macros,+attributes"] 100 | ---- 101 | quarkus test 102 | ---- 103 | 104 | NOTE: `quarkus test` starts tests in continuous testing mode (more about this below) so to return to the command line after the tests ran you will need to press `q`. 105 | -- 106 | ==== 107 | 108 | == Open the Project in your IDE 109 | 110 | Open or import the generated project into your IDE. 111 | 112 | If you are using VS Code, you can open the project with: 113 | 114 | [.console-input] 115 | [source,bash,subs="+macros,+attributes"] 116 | ---- 117 | code . 118 | ---- 119 | 120 | NOTE: If you use `tutorial-tools`, your host `quarkus-tutorial/work` contains the application code; point your IDE to this directory. 121 | 122 | == Live Coding (Development mode) 123 | 124 | Stop the Quarkus application that might be running in JVM or native mode. 125 | 126 | _Live Coding_ or _Development mode_ enables hot deployment with background compilation, which means that when you modify your Java files and/or your resource files and refresh your browser, these changes will automatically take effect. 127 | 128 | This is probably one of the best features of Quarkus: enabling a very fast and productive iterative feedback loop. 129 | 130 | Let's start the _Live Coding_ mode by using the `dev` command. You probably won't need to stop/start Quarkus again during this tutorial: 131 | 132 | [tabs%sync] 133 | ==== 134 | Maven:: 135 | + 136 | -- 137 | [.console-input] 138 | [source,bash,subs="+macros,+attributes"] 139 | ---- 140 | ./mvnw quarkus:dev 141 | ---- 142 | 143 | -- 144 | Quarkus CLI:: 145 | + 146 | -- 147 | [.console-input] 148 | [source,bash,subs="+macros,+attributes"] 149 | ---- 150 | quarkus dev 151 | ---- 152 | -- 153 | ==== 154 | 155 | NOTE: By default Quarkus uses 'localhost' for the dev mode host setting. If you are working in a remote development environment, you may need to set the host to 0.0.0.0 by appending `-D"quarkus.http.host=0.0.0.0"` to the above command. 156 | 157 | == Dev UI 158 | 159 | Quarkus ships with a Dev UI, which is available in dev mode at http://localhost:8080/q/dev[/q/dev] by default. 160 | It allows you to quickly visualize all the extensions currently loaded, see their status and go directly to their documentation. 161 | 162 | [.mt-4.center] 163 | image::Dev_UI.png[Dev UI,800,600,align="center"] 164 | 165 | == Continuous Testing (Development mode) 166 | 167 | Quarkus supports continuous testing, meaning that your tests will run immediately after code changes were saved. Your application should already be running in dev mode. 168 | 169 | At the bottom of the screen you should see the following: 170 | [.console-input] 171 | [source,bash] 172 | ---- 173 | Tests paused 174 | Press [r] to resume testing, [o] Toggle test output, [h] for more options> 175 | ---- 176 | 177 | Press `r` and the tests will start running. You should see the status change down the bottom of the screen as they are running, and it should finish similar to this: 178 | 179 | [.console-input] 180 | [source,bash] 181 | ---- 182 | All 1 test is passing (0 skipped), 1 test was run in 11705ms. Tests completed at 16:02:32. 183 | ---- 184 | 185 | TIP: If you don’t want to have continuous testing enabled, you can change this by adding `quarkus.test.continuous-testing=disabled` to your `src/main/resources/application.properties`. 186 | 187 | You can find more details about controlling continuous testing in https://quarkus.io/guides/continuous-testing#controlling-continuous-testing[this guide]. 188 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/03_configuration.adoc: -------------------------------------------------------------------------------- 1 | = Configuration 2 | 3 | Hardcoded values in your code is a no go, so let's see how to add configuration to your application. 4 | Quarkus relies on the MicroProfile Config specification and the main configuration file is `application.properties`. 5 | 6 | == Using a config property in your code 7 | 8 | Let's change our `GreetingResource` class to use a configuration property. Change its content to: 9 | 10 | [.console-input] 11 | [source,java] 12 | ---- 13 | package com.redhat.developers; 14 | 15 | import org.eclipse.microprofile.config.inject.ConfigProperty; 16 | 17 | import jakarta.ws.rs.GET; 18 | import jakarta.ws.rs.Path; 19 | import jakarta.ws.rs.Produces; 20 | import jakarta.ws.rs.core.MediaType; 21 | 22 | @Path("/hello") 23 | public class GreetingResource { 24 | 25 | @ConfigProperty(name = "greeting") 26 | String greeting; 27 | 28 | @GET 29 | @Produces(MediaType.TEXT_PLAIN) 30 | public String hello() { 31 | return greeting; 32 | } 33 | } 34 | ---- 35 | 36 | If you refresh your browser pointing to http://localhost:8080/hello[window="_blank"], you should see an error with a stacktrace like this: 37 | 38 | [.console-output] 39 | [source,text] 40 | ---- 41 | io.quarkus.runtime.configuration.ConfigurationException: Failed to load config value of type class java.lang.String for: greeting 42 | at io.quarkus.arc.runtime.ConfigRecorder.validateConfigProperties(ConfigRecorder.java:70) 43 | at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigValues1665125174.deploy_0(Unknown Source) 44 | at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigValues1665125174.deploy(Unknown Source) 45 | at io.quarkus.runner.ApplicationImpl.doStart(Unknown Source) 46 | at io.quarkus.runtime.Application.start(Application.java:101) 47 | at io.quarkus.runtime.ApplicationLifecycleManager.run(ApplicationLifecycleManager.java:111) 48 | at io.quarkus.runtime.Quarkus.run(Quarkus.java:71) 49 | at io.quarkus.runtime.Quarkus.run(Quarkus.java:44) 50 | at io.quarkus.runtime.Quarkus.run(Quarkus.java:124) 51 | at io.quarkus.runner.GeneratedMain.main(Unknown Source) 52 | ---- 53 | 54 | Which is expected, since we didn't provide the `greeting` property neither at the `application.properties` file nor with some runtime configuration. 55 | 56 | == Adding a config property to `application.properties` 57 | 58 | Since we requested a `greeting` property in our code, let's provide a value to it in our `application.properties` file available in `src/main/resources`: 59 | 60 | [.console-input] 61 | [source,properties] 62 | ---- 63 | # Configuration file 64 | # key = value 65 | greeting=Hello y'all! 66 | ---- 67 | 68 | Refresh your browser pointing to http://localhost:8080/hello[window="_blank"]. You should see the `Hello y'all!` string. 69 | 70 | You can also use `curl` to check the same result: 71 | 72 | [.console-input] 73 | [source,bash] 74 | ---- 75 | curl -w '\n' localhost:8080/hello 76 | ---- 77 | 78 | [.console-output] 79 | [source,text] 80 | ---- 81 | Hello y'all! 82 | ---- 83 | 84 | == Update your test 85 | 86 | Since we changed the output of our `/hello` endpoint, our test doesn't pass anymore. 87 | 88 | Let's update `GreetingResourceTest` to reflect our recent changes: 89 | 90 | [.console-input] 91 | [source,java] 92 | ---- 93 | package com.redhat.developers; 94 | 95 | import io.quarkus.test.junit.QuarkusTest; 96 | import org.junit.jupiter.api.Test; 97 | 98 | import static io.restassured.RestAssured.given; 99 | import static org.hamcrest.CoreMatchers.is; 100 | 101 | @QuarkusTest 102 | public class GreetingResourceTest { 103 | 104 | @Test 105 | public void testHelloEndpoint() { 106 | given() 107 | .when().get("/hello") 108 | .then() 109 | .statusCode(200) 110 | .body(is("Hello y'all!")); 111 | } 112 | 113 | } 114 | ---- 115 | 116 | NOTE: If you're still running in continuous testing mode, you will already see that the tests pass again and you can ignore the rest of this page and move on to the next step. 117 | 118 | Stop your current Live Coding session of Quarkus in the terminal by sending a `Ctrl+C`: 119 | 120 | 121 | 122 | [.console-output] 123 | [source,text] 124 | ---- 125 | ^C2020-05-11 08:33:41,865 INFO [io.quarkus] (Quarkus Main Thread) tutorial-app stopped in 0.007s 126 | ---- 127 | 128 | Now run your tests to check if everything is ok: 129 | 130 | [tabs] 131 | ==== 132 | Maven:: 133 | + 134 | -- 135 | [.console-input] 136 | [source,bash,subs="+macros,+attributes"] 137 | ---- 138 | ./mvnw clean test 139 | ---- 140 | 141 | -- 142 | Quarkus CLI:: 143 | + 144 | -- 145 | [.console-input] 146 | [source,bash,subs="+macros,+attributes"] 147 | ---- 148 | quarkus test 149 | ---- 150 | -- 151 | ==== 152 | 153 | [.console-output] 154 | [source,text] 155 | ---- 156 | 157 | [INFO] Results: 158 | [INFO] 159 | [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 160 | [INFO] 161 | [INFO] ------------------------------------------------------------------------ 162 | [INFO] BUILD SUCCESS 163 | [INFO] ------------------------------------------------------------------------ 164 | [INFO] Total time: 12.851 s 165 | [INFO] Finished at: 2020-05-11T21:31:31-04:00 166 | [INFO] ------------------------------------------------------------------------ 167 | ---- 168 | 169 | `BUILD SUCCESS`! You can now go back to the amazing _Live Coding_ mode of Quarkus: 170 | 171 | [tabs] 172 | ==== 173 | Maven:: 174 | + 175 | -- 176 | [.console-input] 177 | [source,bash,subs="+macros,+attributes"] 178 | ---- 179 | ./mvnw quarkus:dev 180 | ---- 181 | 182 | -- 183 | Quarkus CLI:: 184 | + 185 | -- 186 | [.console-input] 187 | [source,bash,subs="+macros,+attributes"] 188 | ---- 189 | quarkus dev 190 | ---- 191 | -- 192 | ==== 193 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/07_spring.adoc: -------------------------------------------------------------------------------- 1 | = Spring Compatibility 2 | 3 | Are you a Spring Framework user? Wouldn't it be wonderful if you could reuse your acquired Spring skills with live coding, small footprints, and fast time-to-first-response? With Quarkus, you don't have to give up one for the other: you can have it all! 4 | 5 | == Adding the Spring Compatibility Extensions 6 | 7 | Quarkus supports many of your favorite Spring Framework projects. In this chapter, we're going to use the Spring DI, Spring Web, and Spring Data APIs. Let's add the required extensions: 8 | 9 | [tabs] 10 | ==== 11 | Maven:: 12 | + 13 | -- 14 | [.console-input] 15 | [source,bash,subs="+macros,+attributes"] 16 | ---- 17 | ./mvnw quarkus:add-extension -D"extensions=spring-web,spring-data-jpa" 18 | ---- 19 | 20 | -- 21 | Quarkus CLI:: 22 | + 23 | -- 24 | [.console-input] 25 | [source,bash,subs="+macros,+attributes"] 26 | ---- 27 | quarkus ext add spring-web spring-data-jpa 28 | ---- 29 | -- 30 | ==== 31 | 32 | [.console-output] 33 | [source,text] 34 | ---- 35 | [INFO] Scanning for projects... 36 | [INFO] 37 | [INFO] -----------------< com.redhat.developers:tutorial-app >----------------- 38 | [INFO] Building tutorial-app 1.0-SNAPSHOT 39 | [INFO] --------------------------------[ jar ]--------------------------------- 40 | [INFO] 41 | [INFO] --- quarkus-maven-plugin:3.10.2:add-extension (default-cli) @ tutorial-app --- 42 | ✅ Adding extension io.quarkus:quarkus-spring-data-jpa 43 | ✅ Adding extension io.quarkus:quarkus-spring-web 44 | [INFO] ------------------------------------------------------------------------ 45 | [INFO] BUILD SUCCESS 46 | [INFO] ------------------------------------------------------------------------ 47 | [INFO] Total time: 2.052 s 48 | [INFO] Finished at: 2024-05-23T21:33:15-04:00 49 | [INFO] ------------------------------------------------------------------------ 50 | ---- 51 | 52 | Notice in the logs how Quarkus is reloading and the Spring compatibility extensions are now part of the `Installed features`. 53 | 54 | == Create a Spring Data Repository for Movie 55 | 56 | Spring Data is one of the most popular Spring APIs, so let's create a Spring Data JPA Repository for our `Movie` entity. Create the `SpringMovieRepository` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 57 | 58 | [.console-input] 59 | [source,java] 60 | ---- 61 | package com.redhat.developers; 62 | 63 | import java.util.List; 64 | 65 | import org.springframework.data.jpa.repository.JpaRepository; 66 | import org.springframework.stereotype.Repository; 67 | 68 | @Repository 69 | public interface SpringMovieRepository extends JpaRepository { 70 | 71 | public List findByYear(String year); 72 | 73 | } 74 | ---- 75 | 76 | == Create a Spring REST Controller 77 | 78 | Now let's create another REST endpoint for `Movie`, but now using the Spring Web APIs. Create the `MovieController` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 79 | 80 | [.console-input] 81 | [source,java] 82 | ---- 83 | package com.redhat.developers; 84 | 85 | import java.util.List; 86 | 87 | import org.springframework.web.bind.annotation.GetMapping; 88 | import org.springframework.web.bind.annotation.RequestMapping; 89 | import org.springframework.web.bind.annotation.RequestParam; 90 | import org.springframework.web.bind.annotation.RestController; 91 | 92 | @RestController 93 | @RequestMapping(path = "/spring-movie") 94 | public class MovieController { 95 | 96 | private SpringMovieRepository movieRepository; 97 | 98 | public MovieController(SpringMovieRepository movieRepository) { 99 | this.movieRepository = movieRepository; 100 | } 101 | 102 | @GetMapping 103 | public List movies(@RequestParam("year") String year) { 104 | if (year != null) { 105 | return movieRepository.findByYear(year); 106 | } 107 | return movieRepository.findAll(); 108 | } 109 | 110 | } 111 | ---- 112 | 113 | Let's try to filter only the movies with year *1980*. Don't forget to start Quarkus dev mode again if you stopped it. 114 | 115 | [.console-input] 116 | [source,bash] 117 | ---- 118 | curl -w '\n' localhost:8080/spring-movie?year=1980 119 | ---- 120 | 121 | [.console-output] 122 | [source,json] 123 | ---- 124 | [ 125 | { 126 | "id": 2, 127 | "title": "The Empire Strikes Back", 128 | "releaseDate": "1980-05-17" 129 | } 130 | ] 131 | ---- 132 | 133 | You'll see that the behavior is the same as the one provided with JAX-RS and Hibernate with Panache. 134 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/09_fault-tolerance.adoc: -------------------------------------------------------------------------------- 1 | = Fault Tolerance 2 | 3 | When trying to achieve the goal of resilience in our distributed systems, sometimes we need features like retries, fallbacks, and circuit breakers. Luckily for us, Quarkus provides these features for us through the _Fault Tolerance_ extension. 4 | 5 | == Add the Fault Tolerance extension 6 | 7 | Open a new terminal window, and make sure you're at the root of your `{project-name}` project, then run: 8 | 9 | [tabs] 10 | ==== 11 | Maven:: 12 | + 13 | -- 14 | [.console-input] 15 | [source,bash,subs="+macros,+attributes"] 16 | ---- 17 | ./mvnw quarkus:add-extension -D"extensions=quarkus-smallrye-fault-tolerance" 18 | ---- 19 | 20 | -- 21 | Quarkus CLI:: 22 | + 23 | -- 24 | [.console-input] 25 | [source,bash,subs="+macros,+attributes"] 26 | ---- 27 | quarkus extension add quarkus-smallrye-fault-tolerance 28 | ---- 29 | -- 30 | ==== 31 | 32 | 33 | == Add Retry to SwapiService 34 | 35 | Let's add a retry policy in `SwapiService`. 36 | 37 | Change the `SwapiService` Java interface in `src/main/java` in the `com.redhat.developers` package with the following contents: 38 | 39 | [.console-input] 40 | [source,java] 41 | ---- 42 | package com.redhat.developers; 43 | 44 | import java.util.List; 45 | 46 | import org.eclipse.microprofile.faulttolerance.Retry; 47 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 48 | 49 | import jakarta.ws.rs.GET; 50 | import jakarta.ws.rs.Path; 51 | import jakarta.ws.rs.Produces; 52 | import jakarta.ws.rs.QueryParam; 53 | import jakarta.ws.rs.core.MediaType; 54 | 55 | import com.redhat.developers.Swapi.Results; 56 | 57 | @RegisterRestClient 58 | public interface SwapiService { 59 | @GET 60 | @Path("/films/") 61 | @Produces(MediaType.APPLICATION_JSON) 62 | @Retry(maxRetries = 3, delay = 2000) 63 | public Swapi getFilmById(@PathParam("id") String id); 64 | } 65 | ---- 66 | 67 | Now in case of any error, 3 retries are done automatically, waiting for 2 seconds between retries. 68 | 69 | == Invoke the endpoint with Retry 70 | 71 | Run the following command: 72 | 73 | [.console-input] 74 | [source,bash] 75 | ---- 76 | curl -w '\n' localhost:8080/movie?year=1980 77 | ---- 78 | 79 | [.console-output] 80 | [source,json] 81 | ---- 82 | [ 83 | { 84 | "title": "The Empire Strikes Back", 85 | "releaseDate": "1980-05-17", 86 | "episodeId": 0, 87 | "producer": "Gary Kurtz, Rick McCallum", 88 | "director": "Irvin Kershner", 89 | "opening_crawl": "It is a dark time for the\r\nRebellion. Although the Death\r\nStar has been destroyed,\r\nImperial troops have driven the\r\nRebel forces from their hidden\r\nbase and pursued them across\r\nthe galaxy.\r\n\r\nEvading the dreaded Imperial\r\nStarfleet, a group of freedom\r\nfighters led by Luke Skywalker\r\nhas established a new secret\r\nbase on the remote ice world\r\nof Hoth.\r\n\r\nThe evil lord Darth Vader,\r\nobsessed with finding young\r\nSkywalker, has dispatched\r\nthousands of remote probes into\r\nthe far reaches of space...." 90 | } 91 | ] 92 | ---- 93 | 94 | No change from calls done previously, but now change the https://swapi.dev url in your application properties to https://swapii.dev (or some other random url that won't resolve). 95 | 96 | Run the following command again: 97 | 98 | [.console-input] 99 | [source,bash] 100 | ---- 101 | curl -w '\n' localhost:8080/movie?year=1980 102 | ---- 103 | 104 | Now after waiting 6 seconds (3 retries x 2 seconds), the next exception is thrown `java.net.UnknownHostException: swapii.dev`. 105 | 106 | == Add Fallback to Swapi Service 107 | 108 | We can handle unexpected issues with remote services more gracefully by adding a Fallback policy. Let's add a Fallback handler to `SwapiService`. 109 | 110 | Change the `SwapiService` Java interface in `src/main/java` in the `com.redhat.developers` package with the following contents: 111 | 112 | [.console-input] 113 | [source,java] 114 | ---- 115 | package com.redhat.developers; 116 | 117 | import jakarta.ws.rs.*; 118 | import org.eclipse.microprofile.faulttolerance.*; 119 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 120 | import jakarta.ws.rs.core.MediaType; 121 | 122 | @RegisterRestClient 123 | public interface SwapiService { 124 | 125 | @GET 126 | @Path("/films/{id}") 127 | @Produces(MediaType.APPLICATION_JSON) 128 | @Timeout(value = 2000L) 129 | @Fallback(SwapiFallback.class) 130 | public Swapi getFilmById(@PathParam("id") String id); 131 | 132 | public static class SwapiFallback implements FallbackHandler { 133 | 134 | private static final Swapi EMPTY_SWAPI = new Swapi("",0,"","",""); 135 | @Override 136 | public Swapi handle(ExecutionContext context) { 137 | return EMPTY_SWAPI; 138 | } 139 | 140 | } 141 | } 142 | 143 | ---- 144 | 145 | Now in case of any error, 3 retries are done automatically, waiting for 2 seconds between retries. 146 | 147 | If the error persists, then the fallback method is executed. 148 | 149 | Now after waiting for 6 seconds (3 retries x 2 seconds), an empty object is sent instead of an exception. 150 | 151 | == Invoke the endpoint with Retry and Fallback 152 | 153 | Run the following command: 154 | 155 | [.console-input] 156 | [source,bash] 157 | ---- 158 | curl -w '\n' localhost:8080/movie?year=1980 159 | ---- 160 | 161 | [.console-output] 162 | [source,json] 163 | ---- 164 | [ 165 | { 166 | "title": "The Empire Strikes Back", 167 | "releaseDate": "1980-05-17", 168 | "episodeId": 0, 169 | "producer": "", 170 | "director": "", 171 | "opening_crawl": "" 172 | } 173 | ] 174 | ---- 175 | 176 | Notice how we're still returning the results from our database, but the remote service values are now empty as they are set by our fallback method. 177 | 178 | == Add Circuit Breaker to Swapi Service 179 | 180 | Let's add the circuit breaker policy in `SwapiService`. 181 | 182 | Change the `SwapiService` Java interface in `src/main/java` in the `com.redhat.developers` package with the following contents: 183 | 184 | [.console-input] 185 | [source,java] 186 | ---- 187 | package com.redhat.developers; 188 | 189 | import jakarta.ws.rs.*; 190 | import org.eclipse.microprofile.faulttolerance.*; 191 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 192 | import jakarta.ws.rs.core.MediaType; 193 | 194 | @RegisterRestClient 195 | public interface SwapiService { 196 | @GET 197 | @Path("/films/{id}") 198 | @Produces(MediaType.APPLICATION_JSON) 199 | @Timeout(value = 2000L) 200 | @Fallback(SwapiFallback.class) 201 | @CircuitBreaker( 202 | requestVolumeThreshold = 4, 203 | failureRatio = .5, 204 | delay = 5000L, 205 | successThreshold = 2 206 | ) 207 | public Swapi getFilmById(@PathParam("id") String id); 208 | 209 | public static class SwapiFallback implements FallbackHandler { 210 | 211 | private static final Swapi EMPTY_SWAPI = new Swapi("",0,"","",""); 212 | @Override 213 | public Swapi handle(ExecutionContext context) { 214 | return EMPTY_SWAPI; 215 | } 216 | 217 | } 218 | } 219 | 220 | ---- 221 | 222 | Now, if 3 (4 x 0.75) failures occur among the rolling window of 4 consecutive invocations, then the circuit is opened for 5000 ms and then will be back to half open. 223 | If the invocation succeeds, then the circuit is back to closed again. 224 | 225 | Run the following command at least 5 times (without network connectivity): 226 | 227 | [.console-input] 228 | [source,bash] 229 | ---- 230 | curl -w '\n' localhost:8080/movie?year=1980 231 | ---- 232 | 233 | The output changes from `java.net.UnknownHostException: swapii.dev` (or any other network exception) in the first calls to `org.eclipse.microprofile.faulttolerance.exceptions.CircuitBreakerOpenException: getMovieByTitle` when the circuit is opened. 234 | 235 | The big difference between the first exception and the second one is that the first one occurs because the circuit is closed while the system is trying to reach the host, while in the second one, the circuit is closed and the exception is thrown automatically without trying to reach the host. 236 | 237 | TIP: You can use `@Retry` and `@Fallback` annotations together with `@CircuitBreaker` annotation. 238 | 239 | IMPORTANT: If you turned your network off for this chapter, remember to turn it back on again after you finish the exercises for this chapter. 240 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/10_health.adoc: -------------------------------------------------------------------------------- 1 | = Health Check 2 | 3 | Health Checks are important in platforms like Kubernetes because it allows the infrastructure to be aware of the state of the application. 4 | 5 | There are two different types of health checks: 6 | 7 | - *Liveness probes* tell your platform if your application is running ok or not. When your liveness probe is down, your platform might restart your instance to guarantee that you have the minimum required amount of running instances in production. 8 | 9 | - *Readiness probes* tell your platform if your application is _warm_ enough to reply to requests in a reasonable amount of time. Java applications, for example, might need some time to warm up, so the readiness probe should be up only when it's ready to reply to a request in a timely manner. Checks that depend on other services should be implemented as readiness probes: if a remote service is down, restarting your application won't fix the issue. 10 | 11 | == Add the Health extension 12 | 13 | Open a new terminal window, and make sure you're at the root of your `{project-name}` project, then run: 14 | 15 | [tabs] 16 | ==== 17 | Maven:: 18 | + 19 | -- 20 | [.console-input] 21 | [source,bash,subs="+macros,+attributes"] 22 | ---- 23 | ./mvnw quarkus:add-extension -D"extension=quarkus-smallrye-health" 24 | ---- 25 | 26 | -- 27 | Quarkus CLI:: 28 | + 29 | -- 30 | [.console-input] 31 | [source,bash,subs="+macros,+attributes"] 32 | ---- 33 | quarkus extension add quarkus-smallrye-health 34 | ---- 35 | -- 36 | ==== 37 | 38 | 39 | == Invoke the /health endpoint 40 | 41 | Just by adding the Health extension you'll have a `/q/health` endpoint providing a very trivial health check. 42 | 43 | Run the following command: 44 | 45 | [.console-input] 46 | [source,bash] 47 | ---- 48 | curl -w '\n' localhost:8080/q/health 49 | ---- 50 | 51 | [.console-output] 52 | [source,json] 53 | ---- 54 | { 55 | "status": "UP", 56 | "checks": [ 57 | { 58 | "name": "Database connections health check", 59 | "status": "UP" 60 | } 61 | ] 62 | } 63 | ---- 64 | 65 | Notice that since we're using the Hibernate and database extensions, Quarkus automatically added a readiness probe checking the status of our database connections. 66 | 67 | TIP: You can modify the default path by adding the following property in `applications.properties` : 68 | [.console-input] 69 | [source,properties] 70 | ---- 71 | quarkus.smallrye-health.root-path=/health 72 | ---- 73 | 74 | == Add a custom liveness probe 75 | 76 | Create a new `LivenessProbe` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 77 | 78 | [.console-input] 79 | [source,java] 80 | ---- 81 | package com.redhat.developers; 82 | 83 | import org.eclipse.microprofile.health.HealthCheck; 84 | import org.eclipse.microprofile.health.HealthCheckResponse; 85 | import org.eclipse.microprofile.health.Liveness; 86 | 87 | @Liveness 88 | public class LivenessProbe implements HealthCheck { 89 | @Override 90 | public HealthCheckResponse call() { 91 | return HealthCheckResponse.up("I'm alive"); 92 | } 93 | 94 | } 95 | ---- 96 | 97 | == Add a custom readiness probe 98 | 99 | In dev mode, all your health checks are visible in the Health UI: http://localhost:8080/q/health-ui/. 100 | 101 | Some extensions may provide default health checks, including that the extension will automatically register its health checks. 102 | For example, quarkus-agroal (which is used to manage Quarkus datasources) automatically registers a readiness health check that will validate each datasource. 103 | 104 | Since the readiness of the database is already assessed just by adding those extensions, we should look into defining health checks for other dependencies that we have. 105 | As we depend on the availability of https://swapi.dev, let's create a new `ReadinessProbe` to assess its availability prior to using it. 106 | 107 | We will define a new Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 108 | 109 | [.console-input] 110 | [source,java] 111 | ---- 112 | package com.redhat.developers; 113 | 114 | import io.smallrye.health.checks.UrlHealthCheck; 115 | import org.eclipse.microprofile.config.inject.ConfigProperty; 116 | import org.eclipse.microprofile.health.HealthCheck; 117 | import org.eclipse.microprofile.health.Readiness; 118 | 119 | import jakarta.enterprise.context.ApplicationScoped; 120 | import jakarta.ws.rs.HttpMethod; 121 | 122 | @ApplicationScoped 123 | public class CustomHealthCheck { 124 | 125 | @ConfigProperty(name = "quarkus.rest-client.\"com.redhat.developers.SwapiService\".url") 126 | String externalURL; 127 | 128 | @Readiness //<1> 129 | HealthCheck checkURL() { 130 | return new UrlHealthCheck(externalURL+"/api/films/") //<2> 131 | .name("ExternalURL health check").requestMethod(HttpMethod.GET).statusCode(200); 132 | } 133 | 134 | } 135 | ---- 136 | <1> Annotate the method with `org.eclipse.microprofile.health.Readiness` to signal its implementation. 137 | <2> `UrlHealthCheck` checks if host is reachable using a Http URL connection. 138 | 139 | == Invoke the /health endpoint with custom probes 140 | 141 | Run the following command: 142 | 143 | [.console-input] 144 | [source,bash] 145 | ---- 146 | curl -w '\n' localhost:8080/health 147 | ---- 148 | 149 | [.console-output] 150 | [source, json] 151 | ---- 152 | { 153 | "status": "UP", 154 | "checks": [ 155 | { 156 | "name": "I'm alive", 157 | "status": "UP" 158 | }, 159 | { 160 | "name": "ExternalURL health check", 161 | "status": "UP", 162 | "data": { 163 | "host": "GET https://swapi.dev/api/films" 164 | } 165 | }, 166 | { 167 | "name": "Database connections health check", 168 | "status": "UP" 169 | } 170 | ] 171 | } 172 | ---- 173 | 174 | You can see that the `/health` endpoint consolidates information from both the liveness and readiness probes. 175 | 176 | == Invoke the liveness endpoint 177 | 178 | Run the following command: 179 | 180 | [.console-input] 181 | [source,bash] 182 | ---- 183 | curl -w '\n' localhost:8080/health/live 184 | ---- 185 | 186 | [.console-output] 187 | [source, json] 188 | ---- 189 | { 190 | "status": "UP", 191 | "checks": [ 192 | { 193 | "name": "I'm alive", 194 | "status": "UP" 195 | } 196 | ] 197 | } 198 | ---- 199 | 200 | You can see that the liveness endpoint only returns information about the liveness probes. 201 | 202 | == Invoke the readiness endpoint 203 | 204 | Run the following command: 205 | 206 | [.console-input] 207 | [source,bash] 208 | ---- 209 | curl -w '\n' localhost:8080/health/ready 210 | ---- 211 | 212 | [.console-output] 213 | [source, json] 214 | ---- 215 | { 216 | "status": "UP", 217 | "checks": [ 218 | { 219 | "name": "ExternalURL health check", 220 | "status": "UP", 221 | "data": { 222 | "host": "GET https://swapi.dev/api/films" 223 | } 224 | }, 225 | { 226 | "name": "Database connections health check", 227 | "status": "UP" 228 | } 229 | ] 230 | } 231 | ---- 232 | 233 | You can see that the readiness endpoint only returns information about the readiness probes. 234 | 235 | == The Health extension and Kubernetes 236 | 237 | NOTE: If you're using the Quarkus Kubernetes extension, the liveness and readiness probes are automatically configured in your `Deployment` when you generate the Kubernetes YAML files. 238 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/11_observability.adoc: -------------------------------------------------------------------------------- 1 | = Observability 2 | 3 | When running applications in production we need to send telemetry (metrics and tracing information) to some services like Prometheus and Jaeger. 4 | 5 | Quarkus provides JVM and other statistics out-of-box with the Metrics extension, and it provides distributed tracing with the OpenTelemetry extensions. 6 | 7 | == Tracing 8 | 9 | Let's start with tracing. Tracing allows you to follow the flow of a request throughout your services. This becomes very important once you have more than one (micro) service running or are making external requests, eg. to a REST client or a database. 10 | 11 | === Add the OpenTelemetry extension 12 | 13 | In a new terminal window at the root of your `{project-name}` project, run: 14 | 15 | [tabs] 16 | ==== 17 | Maven:: 18 | + 19 | -- 20 | [.console-input] 21 | [source,bash,subs="+macros,+attributes"] 22 | ---- 23 | ./mvnw quarkus:add-extension -D"extensions=opentelemetry" 24 | ---- 25 | 26 | -- 27 | Quarkus CLI:: 28 | + 29 | -- 30 | [.console-input] 31 | [source,bash,subs="+macros,+attributes"] 32 | ---- 33 | quarkus extension add opentelemetry 34 | ---- 35 | -- 36 | ==== 37 | 38 | The opentelementry extension works out of the box and will start sending data to a gRPC receiver (by default localhost:4317) if we send new requests to the app. 39 | 40 | === Start an OpenTelemetry Collector 41 | 42 | To start collecting and inspecting telemetry, let's run a container that instantiates Jaeger (a tool to inspect telemetry data) and also acts as an OpenTelemetry collector and query service: 43 | 44 | [.console-input] 45 | [source,bash,subs="+macros,+attributes"] 46 | ---- 47 | docker run --name=jaeger -d -p 16686:16686 -p 4317:4317 -e COLLECTOR_OTLP_ENABLED=true jaegertracing/all-in-one:latest 48 | ---- 49 | 50 | Create a few requests so we have some telemetry data to work with by hitting our `/movie` endpoint a few times again: 51 | 52 | [.console-input] 53 | [source,bash,subs="+macros,+attributes"] 54 | ---- 55 | curl -w '\n' localhost:8080/movie?year=1980 56 | ---- 57 | 58 | === Trace down the requests in Jaeger 59 | 60 | Go to http://localhost:16686 in your browser. You should see the Jaeger UI, if not make sure the `docker run` command was executed successfully. To see the traces we just produced, select 'tutorial-app' from the Service dropdown and click 'Find Traces'. Depending on how many times you sent requests to the Movie endpoint, you'll see a number of traces that were collected. 61 | 62 | [.mt-4.center] 63 | image::Jaeger.png[Jaeger Tracing,800,500,align="center"] 64 | 65 | Click on the first "tutorial-app: GET /movie" trace to drill into the details of the latest request. Notice that without doing any further work, even data from the calls out to the Swapi REST service was collected, so we can see how long each request took: 66 | 67 | [.mt-4.center] 68 | image::Jaeger_Span.png[Jaeger Span,800,500,align="center"] 69 | 70 | === Tracing Database calls 71 | 72 | The keen observer (no pun intended) might have noticed that there is no tracing data for the database calls. To be able to do so, we'll need to add the opentelemetry-jdbc dependency to our application. This time we'll need to add the dependency directly to the .pom.xml file because it's not a quarkus extension. Add the following snippet to your pom.xml just *before* the `` line: 73 | 74 | [.console-input] 75 | [source,xml,subs="+macros,+attributes"] 76 | ---- 77 | 78 | io.opentelemetry.instrumentation 79 | opentelemetry-jdbc 80 | 81 | ---- 82 | 83 | We'll also need to add a new property to our `application.properties` file to enable telemetry for our datasource: 84 | 85 | [.console-input] 86 | [source,bash,subs="+macros,+attributes"] 87 | ---- 88 | # enable tracing 89 | quarkus.datasource.jdbc.telemetry=true 90 | ---- 91 | 92 | Create a few more requests to the movie resource: 93 | 94 | [.console-input] 95 | [source,bash,subs="+macros,+attributes"] 96 | ---- 97 | curl -w '\n' localhost:8080/movie?year=1980 98 | ---- 99 | 100 | ...And go back to the Jaeger UI at http://localhost:16686. Click the 'Find Traces' again (make sure the tutorial-app Service is selected) and click on the first trace from the top. You will now see 2 more spans, one with 'DataSource.getConnection' that shows how long it took for the database connection to get established, and one with 'SELECT quarkus.Movie' that shows the details of the database query and how long it took. 101 | 102 | [.mt-4.center] 103 | image::Jaeger_DataSource.png[Jaeger DataSource,800,500,align="center"] 104 | 105 | == Metrics 106 | 107 | Observability also means the ability to expose, collect and observe detailed metrics about your application and the JVM running underneath (if applicable). 108 | Let's add the metrics extension that enables this capability in Quarkus: 109 | 110 | === Add the Metrics extension 111 | 112 | In a terminal window at the root of your `{project-name}` project, run: 113 | 114 | [tabs] 115 | ==== 116 | Maven:: 117 | + 118 | -- 119 | [.console-input] 120 | [source,bash,subs="+macros,+attributes"] 121 | ---- 122 | ./mvnw quarkus:add-extension -D"extensions=quarkus-micrometer" 123 | ---- 124 | 125 | -- 126 | Quarkus CLI:: 127 | + 128 | -- 129 | [.console-input] 130 | [source,bash,subs="+macros,+attributes"] 131 | ---- 132 | quarkus extension add quarkus-micrometer 133 | ---- 134 | -- 135 | ==== 136 | 137 | You should also add the `quarkus-micrometer-registry-prometheus` extension which formats the metrics in format that Prometheus can easily ingest: 138 | 139 | [tabs] 140 | ==== 141 | Maven:: 142 | + 143 | -- 144 | [.console-input] 145 | [source,bash,subs="+macros,+attributes"] 146 | ---- 147 | ./mvnw quarkus:add-extension -D"extensions=quarkus-micrometer-registry-prometheus" 148 | ---- 149 | 150 | -- 151 | Quarkus CLI:: 152 | + 153 | -- 154 | [.console-input] 155 | [source,bash,subs="+macros,+attributes"] 156 | ---- 157 | quarkus extension add quarkus-micrometer-registry-prometheus 158 | ---- 159 | -- 160 | ==== 161 | 162 | By just adding these extensions, your application is now exposing metrics at the http://localhost:8080/q/metrics endpoint. You can also access the metrics by going to the http://localhost:8080/q/dev[Dev UI] where you will see a new card "Micrometer metrics" and a link in that card to a http://localhost:8080/q/dev-ui/io.quarkus.quarkus-micrometer/prometheus[Prometheus metrics page]. 163 | 164 | === Create TimeResource 165 | 166 | We can also generate custom metrics. Let's add a custom counter that counts how many times a particular method has been called. 167 | Create a new `TimeResource` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 168 | 169 | [.console-input] 170 | [source,java] 171 | ---- 172 | package com.redhat.developers; 173 | import java.time.Instant; 174 | import java.util.Calendar; 175 | import java.util.TimeZone; 176 | 177 | import jakarta.ws.rs.GET; 178 | import jakarta.ws.rs.Path; 179 | import jakarta.ws.rs.Produces; 180 | import jakarta.ws.rs.core.MediaType; 181 | 182 | import io.micrometer.core.annotation.Counted; 183 | import io.micrometer.core.instrument.MeterRegistry; 184 | 185 | @Path("/time") 186 | public class TimeResource { 187 | 188 | private final MeterRegistry registry; <1> 189 | 190 | TimeResource(MeterRegistry registry) { 191 | this.registry = registry; 192 | registry.gauge("offsetFromUTC", this, 193 | TimeResource::offsetFromUTC);<2> 194 | } 195 | 196 | @Counted(value = "time.now") <3> 197 | @GET 198 | @Produces(MediaType.TEXT_PLAIN) 199 | public Instant now() { 200 | return Instant.now(); 201 | } 202 | 203 | int offsetFromUTC() { 204 | return TimeZone.getDefault().getOffset(Calendar.ZONE_OFFSET)/(3600*1000); 205 | } 206 | } 207 | ---- 208 | <1> Meters in Micrometer are created from and contained in a MeterRegistry. 209 | <2> Add a gauge that returns a value computed by our application. 210 | <3> The `@Counted` annotation allows the Metrics extension to count the number of invocations to this method. 211 | 212 | === Invoke the endpoint multiple times 213 | 214 | We need to send some requests to our endpoint to increment our `@Counted` metrics, so use the following command: 215 | 216 | [.console-input] 217 | [source,bash] 218 | ---- 219 | for i in {1..5}; do curl -w '\n' localhost:8080/time; done 220 | ---- 221 | 222 | [.console-output] 223 | [source,bash] 224 | ---- 225 | 2020-05-12T22:38:10.546500Z 226 | 2020-05-12T22:38:10.869378Z 227 | 2020-05-12T22:38:11.188782Z 228 | 2020-05-12T22:38:11.510367Z 229 | 2020-05-12T22:38:11.832583Z 230 | ---- 231 | 232 | === Check the metrics 233 | 234 | By default the metrics are exposed in Prometheus format. You can check the output by pointing your browser to http://localhost:8080/q/metrics[window=_blank]. See if you can find the TimeResource counter result. 235 | 236 | [.mt-4.center] 237 | image::Timed_Resource.png[Micrometer Timed Resource,800,100,align="left"] 238 | 239 | NOTE: In this tutorial we consulted the results in raw format, however these metrics are meant to be consumed by a monitoring system such as Prometheus so you can produce meaningful dashboards or alerts instead of accessing the metrics endpoint directly. 240 | 241 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/12_security.adoc: -------------------------------------------------------------------------------- 1 | = Security with JWT RBAC 2 | 3 | == Securing endpoints with JWT RBAC 4 | 5 | In a microservices architecture, and generally speaking, any application, might need to be protected so only specific users can access the defined endpoint. 6 | Quarkus provides integration to the https://github.com/eclipse/microprofile-jwt-auth[MicroProfile JWT RBAC,window=_blank] specification. 7 | 8 | So let's see how you can start using JWT for _Role Based Access Control_ (RBAC) of endpoints. 9 | 10 | == Add the JWT extension 11 | 12 | Just open a new terminal window, and make sure you’re at the root of your `{project-name}` project, then run: 13 | 14 | [tabs] 15 | ==== 16 | Maven:: 17 | + 18 | -- 19 | [.console-input] 20 | [source,bash,subs="+macros,+attributes"] 21 | ---- 22 | ./mvnw quarkus:add-extension -D"extension=quarkus-smallrye-jwt" 23 | ---- 24 | 25 | -- 26 | Quarkus CLI:: 27 | + 28 | -- 29 | [.console-input] 30 | [source,bash,subs="+macros,+attributes"] 31 | ---- 32 | quarkus extension add quarkus-smallrye-jwt 33 | ---- 34 | -- 35 | ==== 36 | 37 | == Add the JWT properties 38 | 39 | Add the following properties to your `application.properties` in `src/main/resources`: 40 | 41 | [.console-input] 42 | [source,properties] 43 | ---- 44 | mp.jwt.verify.publickey.location=https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/jwt-token/quarkus.jwt.pub 45 | mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac 46 | 47 | #set jwt expiration duration 48 | #com.developers.redhat.jwt.duration=3600 49 | ---- 50 | 51 | We are providing a valid token that can be verified by the configured public key: 52 | 53 | [.console-output] 54 | [source,json] 55 | ---- 56 | { 57 | "kid": "/privateKey.pem", 58 | "typ": "JWT", 59 | "alg": "RS256" 60 | }, 61 | { 62 | "sub": "jdoe-using-jwt-rbac", 63 | "aud": "using-jwt-rbac", 64 | "upn": "jdoe@quarkus.io", 65 | "birthdate": "2001-07-13", 66 | "auth_time": 1570094171, 67 | "iss": "https://quarkus.io/using-jwt-rbac", // <1> 68 | "roleMappings": { 69 | "group2": "Group2MappedRole", 70 | "group1": "Group1MappedRole" 71 | }, 72 | "groups": [ // <2> 73 | "Echoer", 74 | "Tester", 75 | "Subscriber", 76 | "group2" 77 | ], 78 | "preferred_username": "jdoe", 79 | "exp": 2200814171, 80 | "iat": 1570094171, 81 | "jti": "a-123" 82 | } 83 | ---- 84 | <1> The issuer you set in `application.properties` 85 | <2> `groups` field is used by `MicroProfile JWT RBAC` to get the access groups (or roles) that the owner of the token has 86 | 87 | == Create SecureResource 88 | 89 | You can inject any defined claim into an object by using `@Claim` annotation: 90 | 91 | Create the `SecureResource` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 92 | 93 | [.console-input] 94 | [source,java] 95 | ---- 96 | package com.redhat.developers; 97 | 98 | import jakarta.ws.rs.GET; 99 | import jakarta.ws.rs.Path; 100 | 101 | import jakarta.enterprise.context.RequestScoped; 102 | 103 | import org.eclipse.microprofile.jwt.Claim; 104 | import org.eclipse.microprofile.jwt.Claims; 105 | 106 | @Path("secure") 107 | @RequestScoped 108 | public class SecureResource { 109 | 110 | @Claim(standard = Claims.preferred_username) 111 | String username; 112 | 113 | @GET 114 | @Path("claim") 115 | public String getClaim() { 116 | return username; 117 | } 118 | 119 | } 120 | ---- 121 | 122 | == Invoke the /secure/claim endpoint 123 | 124 | Run the following command in a bash shell: 125 | 126 | [.console-input] 127 | [source,bash] 128 | ---- 129 | token=$(curl https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/jwt-token/quarkus.jwt.token -s) 130 | curl -w '\n' -H "Authorization: Bearer $token" localhost:8080/secure/claim 131 | ---- 132 | 133 | You should see the `preferred_username` field for the given token (jdoe). 134 | 135 | [.console-output] 136 | [source,text] 137 | ---- 138 | jdoe 139 | ---- 140 | 141 | MicroProfile JWT RBAC spec is providing out-of-the-box validation of the given token. These validations include, for example, that the token has not been modified, has not expired, or the issuer is the expected one. 142 | 143 | To validate this, just invoke the service again, but change the token: 144 | 145 | [.console-input] 146 | [source,bash] 147 | ---- 148 | token=XXXX 149 | curl -v -w '\n' -H "Authorization: Bearer $token" localhost:8080/secure/claim 150 | ---- 151 | 152 | [.console-output] 153 | [source,text] 154 | ---- 155 | * Trying ::1... 156 | * TCP_NODELAY set 157 | * Connected to localhost (::1) port 8080 (#0) 158 | > GET /secure/claim HTTP/1.1 159 | > Host: localhost:8080 160 | > User-Agent: curl/7.64.1 161 | > Accept: */* 162 | > Authorization: Bearer XXXX 163 | > 164 | < HTTP/1.1 401 Unauthorized 165 | < www-authenticate: Bearer {token} 166 | < content-length: 0 167 | < 168 | * Connection #0 to host localhost left intact 169 | * Closing connection 0 170 | ---- 171 | 172 | You can check the `401 Unauthorized` response. 173 | 174 | == Add RBAC to SecureResource 175 | 176 | So far, you've seen how to get claims from the provided JWT token, but anyone could access that endpoint, so let's protect it with a role. 177 | For this case, you need to use a role that is defined in the JWT token inside the `groups` claim (ie `Subscriber`). 178 | 179 | Change the `SecureResource` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 180 | 181 | [.console-input] 182 | [source,java] 183 | ---- 184 | package com.redhat.developers; 185 | 186 | import jakarta.annotation.security.RolesAllowed; 187 | import jakarta.ws.rs.GET; 188 | import jakarta.ws.rs.Path; 189 | 190 | import jakarta.enterprise.context.RequestScoped; 191 | 192 | import org.eclipse.microprofile.jwt.Claim; 193 | import org.eclipse.microprofile.jwt.Claims; 194 | 195 | @Path("/secure") 196 | @RequestScoped 197 | public class SecureResource { 198 | 199 | @Claim(standard = Claims.preferred_username) 200 | String username; 201 | 202 | @RolesAllowed("Subscriber") 203 | @GET 204 | @Path("/claim") 205 | public String getClaim() { 206 | return username; 207 | } 208 | 209 | } 210 | ---- 211 | 212 | == Invoke the /secure/claim endpoint with RBAC 213 | 214 | Run the following command: 215 | 216 | [.console-input] 217 | [source,bash] 218 | ---- 219 | token=$(curl https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/jwt-token/quarkus.jwt.token -s) 220 | curl -w '\n' -H "Authorization: Bearer $token" localhost:8080/secure/claim 221 | ---- 222 | 223 | And you’ll see the `preferred_username` field for the given token (jdoe). 224 | 225 | [.console-output] 226 | [source,text] 227 | ---- 228 | jdoe 229 | ---- 230 | 231 | == Add incorrect RBAC to SecureResource 232 | 233 | [.console-input] 234 | [source,java] 235 | ---- 236 | package com.redhat.developers; 237 | 238 | import jakarta.annotation.security.RolesAllowed; 239 | import jakarta.ws.rs.GET; 240 | import jakarta.ws.rs.Path; 241 | 242 | import jakarta.enterprise.context.RequestScoped; 243 | 244 | import org.eclipse.microprofile.jwt.Claim; 245 | import org.eclipse.microprofile.jwt.Claims; 246 | 247 | @Path("/secure") 248 | @RequestScoped 249 | public class SecureResource { 250 | 251 | @Claim(standard = Claims.preferred_username) 252 | String username; 253 | 254 | @RolesAllowed("Not-Subscriber") 255 | @GET 256 | @Path("/claim") 257 | public String getClaim() { 258 | return username; 259 | } 260 | 261 | } 262 | ---- 263 | 264 | == Invoke the /secure/claim endpoint with incorrect RBAC 265 | 266 | Run the following command: 267 | 268 | [.console-input] 269 | [source,bash] 270 | ---- 271 | token=$(curl https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/jwt-token/quarkus.jwt.token -s) 272 | curl -v -w '\n' -H "Authorization: Bearer $token" localhost:8080/secure/claim 273 | ---- 274 | 275 | And you’ll see the preferred_username field for the given token (jdoe). 276 | 277 | [.console-output] 278 | [source,text] 279 | ---- 280 | * Trying ::1... 281 | * TCP_NODELAY set 282 | * Connected to localhost (::1) port 8080 (#0) 283 | > GET /secure/claim HTTP/1.1 284 | > Host: localhost:8080 285 | > User-Agent: curl/7.64.1 286 | > Accept: */* 287 | > Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTcwMDk0MTcxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MjIwMDgxNDE3MSwiaWF0IjoxNTcwMDk0MTcxLCJqdGkiOiJhLTEyMyJ9.Hzr41h3_uewy-g2B-sonOiBObtcpkgzqmF4bT3cO58v45AIOiegl7HIx7QgEZHRO4PdUtR34x9W23VJY7NJ545ucpCuKnEV1uRlspJyQevfI-mSRg1bHlMmdDt661-V3KmQES8WX2B2uqirykO5fCeCp3womboilzCq4VtxbmM2qgf6ag8rUNnTCLuCgEoulGwTn0F5lCrom-7dJOTryW1KI0qUWHMMwl4TX5cLmqJLgBzJapzc5_yEfgQZ9qXzvsT8zeOWSKKPLm7LFVt2YihkXa80lWcjewwt61rfQkpmqSzAHL0QIs7CsM9GfnoYc0j9po83-P3GJiBMMFmn-vg 288 | > 289 | < HTTP/1.1 403 Forbidden 290 | < Content-Length: 9 291 | < Content-Type: application/octet-stream 292 | < 293 | * Connection #0 to host localhost left intact 294 | Forbidden* Closing connection 0 295 | ---- 296 | 297 | You can notice the `403 Forbidden` response. 298 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/15_reactive-messaging.adoc: -------------------------------------------------------------------------------- 1 | = Reactive Messaging 2 | 3 | As a Java developer you're likely familiar with JMS, which is considered to be the standard when it comes to working with messages. 4 | JMS however is a blocking API, which prevents us from implementing the reactive principles. 5 | 6 | Quarkus has a "SmallRye Reactive Messaging" extension which is an implementation of the Eclipse MicroProfile Reactive Messaging specification. Reactive Messaging allows us to implement messaging in a non-blocking, reactive way. 7 | 8 | In this chapter we're going to use SmallRye Reactive Messaging to generate beers once again. This time we're going to add a (random) price to the beers. We're going to be using messages instead of synchronous calls to do this. 9 | 10 | To do so, we're going to use an in-memory channel. This means that messages are sent through the application using memory as transport channel of the messages. 11 | 12 | In the following section, we'll see what we need to change to start using an external broker for sending messages. 13 | 14 | == Add the Reactive Messaging extension 15 | 16 | Open a new terminal window, and make sure you’re at the root of your `{project-name}` project, then run: 17 | 18 | [tabs] 19 | ==== 20 | Maven:: 21 | + 22 | -- 23 | [.console-input] 24 | [source,bash,subs="+macros,+attributes"] 25 | ---- 26 | ./mvnw quarkus:add-extension -Dextension=messaging 27 | ---- 28 | 29 | -- 30 | Quarkus CLI:: 31 | + 32 | -- 33 | [.console-input] 34 | [source,bash,subs="+macros,+attributes"] 35 | ---- 36 | quarkus extension add messaging 37 | ---- 38 | -- 39 | ==== 40 | 41 | == Modify BeerResource 42 | 43 | Let's create a new endpoint that finds a beer and sends/emits a message to a `beers` channel. 44 | 45 | Open the `BeerResource` class and add the following code. 46 | 47 | In the imports section: 48 | 49 | [.console-input] 50 | [source,java] 51 | ---- 52 | import jakarta.ws.rs.core.Response; 53 | 54 | import org.eclipse.microprofile.reactive.messaging.Channel; 55 | import org.eclipse.microprofile.reactive.messaging.Emitter; 56 | ---- 57 | 58 | And the business code: 59 | 60 | [.console-input] 61 | [source,java] 62 | ---- 63 | @Channel("beers") 64 | Emitter emitter; 65 | 66 | @GET 67 | @Path("/emit/{beer}") 68 | public Response emitBeer(@PathParam("beer") int beerId) { 69 | beerService.getBeer(beerId) // <1> 70 | .map(beers -> beers.get(0).asJsonObject()) // <2> 71 | .subscribe().with(emitter::send); // <3> 72 | return Response.ok().build(); // <4> 73 | } 74 | ---- 75 | <1> Finds the beer 76 | <2> Gets the first beer 77 | <3> Emits the beer to `beers` channel 78 | <4> Sends an ack to caller 79 | 80 | The previous code sends the beer as a `JsonObject` to `beers` channel. 81 | Since we are using an in-memory channel, let's create a new Java class in the same project capturing the messages sent to the channel. 82 | 83 | This new class will send another event to a different channel which will be captured by yet another method. 84 | 85 | == Create BeerProcessor 86 | 87 | Create a new `BeerProcessor` Java class in `src/main/java` in the `org.acme` package with the following contents: 88 | 89 | [.console-input] 90 | [source,java] 91 | ---- 92 | package com.redhat.developers; 93 | 94 | import java.util.concurrent.ThreadLocalRandom; 95 | 96 | import org.eclipse.microprofile.reactive.messaging.Incoming; 97 | import org.eclipse.microprofile.reactive.messaging.Outgoing; 98 | 99 | import jakarta.enterprise.context.ApplicationScoped; 100 | import jakarta.json.Json; 101 | import jakarta.json.JsonObject; 102 | import jakarta.json.JsonObjectBuilder; 103 | import jakarta.json.bind.JsonbBuilder; 104 | 105 | 106 | @ApplicationScoped 107 | public class BeerProcessor { 108 | 109 | @Incoming("beers") // <1> 110 | @Outgoing("messages") // <2> 111 | public JsonObject processPrice(JsonObject beer) { // <3> 112 | JsonObjectBuilder beerWithPrice = Json.createObjectBuilder(beer).add("price", getPrice()); 113 | return beerWithPrice.build(); // <4> 114 | } 115 | 116 | private int getPrice() { 117 | return ThreadLocalRandom.current().nextInt(1, 10); 118 | } 119 | 120 | @Incoming("messages") // <5> 121 | public void print(JsonObject beer) { 122 | System.out.println(JsonbBuilder.create().toJson(beer)); 123 | } 124 | } 125 | ---- 126 | <1> Listen to events from `beers` channel 127 | <2> Sends/Emits the result of the method call to the `messages` channel 128 | <3> Argument is the message of the `beers` channel 129 | <4> Return object is sent to the `messages` channel 130 | <5> Captures `messages` event 131 | 132 | TIP: You can use the `@Retry` annotation from the `fault-tolerant` extension to add some resiliency. 133 | 134 | == Invoke the endpoint 135 | 136 | With all these changes done and having the application running (either in DevMode or as a packaged application) invoke the service and inspect the Quarkus console for the output: 137 | 138 | [.console-input] 139 | [source,bash] 140 | ---- 141 | curl localhost:8080/beer/emit/1 142 | ---- 143 | 144 | And in the Quarkus console, you'll see an output in JSON format of the beer with the price field added. 145 | 146 | [.console-output] 147 | [source,json] 148 | ---- 149 | {"id":1,"name":"Buzz","tagline":"A Real Bitter Experience."..."contributed_by":"Sam Mason ","price":8} 150 | ---- -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/16_kafka-and-streams.adoc: -------------------------------------------------------------------------------- 1 | = Apache Kafka with Reactive Streams 2 | 3 | Mutiny is just one part of the Reactive story. To complement it, we could use Reactive Streams too. An important service that can serve as the underlying implementation for our stream is http://kafka.apache.org[Apache Kafka,window=_blank]. 4 | 5 | In this chapter, we'll make a small change: We will send beers with a price to a Kafka broker instead of using an in-memory channel. 6 | 7 | == Add the Reactive Messaging Kafka extension 8 | 9 | Open a new terminal window, and make sure you’re at the root of your `{project-name}` project, then run: 10 | 11 | [tabs] 12 | ==== 13 | Maven:: 14 | + 15 | -- 16 | [.console-input] 17 | [source,bash,subs="+macros,+attributes"] 18 | ---- 19 | ./mvnw quarkus:add-extension -Dextensions=messaging-kafka 20 | ---- 21 | 22 | -- 23 | Quarkus CLI:: 24 | + 25 | -- 26 | [.console-input] 27 | [source,bash,subs="+macros,+attributes"] 28 | ---- 29 | quarkus extension add messaging-kafka 30 | ---- 31 | -- 32 | ==== 33 | 34 | == Modify BeerProcessor 35 | 36 | In the `BeerProcessor` Java class in `src/main/java` in the `org.acme` package should have the `print` method commented as it's not necessary anymore because the content of the `messages` channel will be send to a Kafka topic: 37 | 38 | [.console-input] 39 | [source,java] 40 | ---- 41 | /**@Incoming("messages") 42 | public void print(JsonObject beer) { 43 | System.out.println(JsonbBuilder.create().toJson(beer)); 44 | }**/ 45 | ---- 46 | 47 | == Add the Reactive Messaging Kafka properties 48 | 49 | Add the following properties to your `application.properties` in `src/main/resources` to configure `messages` channel to be backed by a Kafka topic instead of a memory channel: 50 | 51 | [.console-input] 52 | [source,properties] 53 | ---- 54 | mp.messaging.outgoing.messages.connector=smallrye-kafka// <1> 55 | mp.messaging.outgoing.messages.topic=pricedbeers// <2> 56 | ---- 57 | <1> `messages` channel is backed to Kafka 58 | <2> `messages` channel sends events to `pricedbeers` topic 59 | 60 | TIP: If all channels are backed to Kafka, it's not necessary to set the `connector` property. 61 | 62 | TIP: If the channel name is the same as the topic, it's not necessary to set the `topic` property. 63 | 64 | == Dev Services for Kafka 65 | 66 | Because starting a Kafka broker can be long and you need to develop fast in your local environment, Dev Services for Kafka is here to help you! 67 | 68 | Since we have added the `quarkus-messaging-kafka`, Quarkus Dev Services automatically starts a containerized Kafka broker in dev mode and when running tests. 69 | 70 | TIP: You can disable Dev Services for Kafka by adding `quarkus.kafka.devservices.enabled=false` or configuring `kafka.bootstrap.servers` in `application.properties`. 71 | 72 | == Invoke the endpoint 73 | 74 | There's not really any code to add at this point. Just by having Docker/Podman running on our computer, and starting the service in dev mode, we can now send the same request as in the previous chapter, but it will be sent to a Kafka topic instead of an in-memory channel. Let's try it: 75 | 76 | [.console-input] 77 | [source,bash] 78 | ---- 79 | curl -w '\n' localhost:8080/beer/emit/1 80 | ---- 81 | 82 | 83 | As you can see, nothing is shown in the return message, nor is there anything in the Quarkus terminal, because the event is simply sent to a Kafka topic. We could create some additional code to retrieve the message from Kafka, but in this case we're going to use the Dev UI interface where we can actually find the contents of the Kafka topic in the Kafka by pointing our browser to http://localhost:8080/q/dev-ui/io.quarkus.quarkus-kafka-client/topics[window=_blank] -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/17_ai_intro.adoc: -------------------------------------------------------------------------------- 1 | = Quarkus and AI 2 | 3 | AI is becoming an intrinsic part of software development. It can help us write, test and debug code. We can also infuse AI models directly into our applications. Inversely, we can also create functions/tools that can be called by AI agents to augment their capabilities and knowledge. 4 | 5 | Quarkus supports a few different ways to work with AI, mainly leveraging the LangChain4j extension. There are also other extensions such as the Quarkus MCP server which allows you to serve tools to be consumed by AI agents. 6 | 7 | In this chapter, we'll explore how to work with AI models. We'll cover: 8 | * Prompting AI models in your applications 9 | * Preserving state between calls 10 | * Creating Tools for use by AI Agents 11 | * Embedding Documents that can be queried by LLMs 12 | * Building a chatbot 13 | * Working with local models (using Podman Desktop AI Lab) 14 | 15 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/17_prompts.adoc: -------------------------------------------------------------------------------- 1 | = Working with prompts 2 | 3 | :project-ai-name: quarkus-langchain4j-app 4 | 5 | The Quarkus LangChain4j extension seamlessly integrates Large Language Models (LLMs) into Quarkus applications. LLMs are AI-based systems designed to understand, generate, and manipulate human language, showcasing advanced natural language processing capabilities. Thanks to this extension, we can enable the harnessing of LLM capabilities for the development of more intelligent applications. 6 | 7 | In this first chapter, we'll explore the simplest of interactions with an LLM: Prompting. It essentially means just asking questions to an LLM and receiving an answer in natural language from a given model, such as ChatGPT, Granite, Mistral, etc. 8 | 9 | 10 | == Creating a Quarkus & LangChain4j Application 11 | 12 | We're going to use the langchain4j-openai extension for our first interaction with models. 13 | The openai extension supports models that expose the open sourced OpenAI API specification. 14 | Several models and model providers expose this API specification. If you want to use 15 | a different API spec, then you can likely find a supported extension in the https://docs.quarkiverse.io/quarkus-langchain4j/dev/llms.html[Quarkus documentation]. 16 | 17 | 18 | [tabs%sync] 19 | ==== 20 | 21 | Maven:: 22 | + 23 | -- 24 | [.console-input] 25 | [source,bash,subs="+macros,+attributes"] 26 | ---- 27 | mvn "io.quarkus.platform:quarkus-maven-plugin:create" -DprojectGroupId="com.redhat.developers" -DprojectArtifactId="{project-ai-name}" -DprojectVersion="1.0-SNAPSHOT" -Dextensions=rest,langchain4j-openai 28 | cd {project-ai-name} 29 | ---- 30 | -- 31 | Quarkus CLI:: 32 | + 33 | -- 34 | 35 | [.console-input] 36 | [source,bash,subs="+macros,+attributes"] 37 | ---- 38 | quarkus create app -x rest -x langchain4j-openai com.redhat.developers:{project-ai-name}:1.0-SNAPSHOT 39 | cd {project-ai-name} 40 | ---- 41 | -- 42 | ==== 43 | 44 | IMPORTANT: All the remaining parts of this section assume that you'll be working inside the project folder that was just created. In this case, `{project-ai-name}`. 45 | 46 | == Connect to OpenAI 47 | 48 | LangChain4j provides you a proxy to connect your application to OpenAI by just adding a property to the `application.properties` file available in `src/main/resources`: 49 | 50 | [.console-input] 51 | [source,properties] 52 | ---- 53 | # Free demo key for basic usage of OpenAI ChatGPT 54 | quarkus.langchain4j.openai.api-key=demo 55 | # Change this URL to the model provider of your choice 56 | quarkus.langchain4j.openai.base-url=https://api.openai.com/v1 57 | # Set timeout explicitly (default is 10s) 58 | quarkus.langchain4j.openai.timeout=30s 59 | ---- 60 | 61 | 62 | == Create the AI service 63 | 64 | First we need to create an interface for our AI service. 65 | 66 | Create a new `Assistant` Java interface in `src/main/java` in the `com.redhat.developers` package with the following contents: 67 | 68 | [.console-input] 69 | [source,java] 70 | ---- 71 | package com.redhat.developers; 72 | 73 | import io.quarkiverse.langchain4j.RegisterAiService; 74 | 75 | @RegisterAiService 76 | public interface Assistant { 77 | String chat(String message); 78 | } 79 | ---- 80 | 81 | == Create the prompt-base resource 82 | 83 | Now we're going to implement a resource that sends prompts using the AI service. 84 | 85 | Create a new `ExistentialQuestionResource` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 86 | 87 | [.console-input] 88 | [source,java] 89 | ---- 90 | package com.redhat.developers; 91 | 92 | import jakarta.inject.Inject; 93 | import jakarta.ws.rs.GET; 94 | import jakarta.ws.rs.Path; 95 | import jakarta.ws.rs.Produces; 96 | import jakarta.ws.rs.core.MediaType; 97 | 98 | @Path("/earth") 99 | public class ExistentialQuestionResource { 100 | 101 | @Inject 102 | Assistant assistant; 103 | 104 | @GET 105 | @Path("/flat") 106 | @Produces(MediaType.TEXT_PLAIN) 107 | public String isEarthFlat() { 108 | return assistant.chat("Can you explain why the earth is flat?"); 109 | } 110 | } 111 | ---- 112 | 113 | == Invoke the endpoint 114 | 115 | Start the app in Quarkus dev mode: 116 | 117 | [tabs%sync] 118 | ==== 119 | 120 | Maven:: 121 | + 122 | -- 123 | [.console-input] 124 | [source,bash,subs="+macros,+attributes"] 125 | ---- 126 | ./mvnw quarkus:dev 127 | ---- 128 | -- 129 | Quarkus CLI:: 130 | + 131 | -- 132 | 133 | [.console-input] 134 | [source,bash,subs="+macros,+attributes"] 135 | ---- 136 | quarkus dev 137 | ---- 138 | -- 139 | ==== 140 | 141 | You can check your prompt implementation by pointing your browser to http://localhost:8080/earth/flat[window=_blank] 142 | 143 | You can also run the following command: 144 | 145 | [.console-input] 146 | [source,bash] 147 | ---- 148 | curl -w '\n' localhost:8080/earth/flat 149 | ---- 150 | 151 | An example of the output you might see (Yours will likely be slightly different 152 | depending on the response from the non-deterministic LLM): 153 | 154 | [.console-output] 155 | [source,text] 156 | ---- 157 | The Earth is not flat, it is an oblate spheroid, meaning it is mostly spherical in shape but slightly flattened at the poles and bulging at the equator. This shape is due to the Earth's rotation, which causes it to bulge slightly at the equator and flatten at the poles. The idea that the Earth is flat is a misconception that has been debunked by centuries of scientific evidence, including satellite imagery, photos from space, and measurements of the Earth's curvature. 158 | ---- 159 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/18_chains_memory.adoc: -------------------------------------------------------------------------------- 1 | = Chains and Memory 2 | 3 | :project-ai-name: quarkus-langchain4j-app 4 | 5 | So far we explored how to use prompts with LLMs, however to really leverage the power of LLMs it is essential that you can build a conversation by referring to previous questions and answers and manage concurrent interactions. 6 | 7 | In this section, we'll cover how we can achieve this with the LangChain4j extension in Quarkus. 8 | 9 | == Create an AI service with memory 10 | 11 | Let's create an interface for our AI service, but with memory this time. 12 | 13 | Create a new `AssistantWithMemory` Java interface in `src/main/java` in the `com.redhat.developers` package with the following contents: 14 | 15 | [.console-input] 16 | [source,java] 17 | ---- 18 | package com.redhat.developers; 19 | 20 | import dev.langchain4j.service.MemoryId; 21 | import dev.langchain4j.service.UserMessage; 22 | import io.quarkiverse.langchain4j.RegisterAiService; 23 | 24 | @RegisterAiService() 25 | public interface AssistantWithMemory { 26 | 27 | String chat(@MemoryId Integer id, @UserMessage String msg); 28 | 29 | } 30 | ---- 31 | 32 | == Create a Developer resource 33 | 34 | Now let's create a resource to help us write some code, and then ask the model to create a test for the code as well in a second request. Thanks to the memory feature, the model will remember what code we're referring to from the first request. 35 | 36 | Create a new `DeveloperResource` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 37 | 38 | [.console-input] 39 | [source,java] 40 | ---- 41 | package com.redhat.developers; 42 | 43 | import jakarta.inject.Inject; 44 | import jakarta.ws.rs.GET; 45 | import jakarta.ws.rs.Path; 46 | import jakarta.ws.rs.Produces; 47 | import jakarta.ws.rs.core.MediaType; 48 | 49 | @Path("/") 50 | public class DeveloperResource { 51 | 52 | @Inject 53 | private AssistantWithMemory ai; 54 | 55 | @GET 56 | @Path("/memory") 57 | @Produces(MediaType.TEXT_PLAIN) 58 | public String memory() { 59 | String msg1 = "How do I write a REST endpoint in Java using Quarkus?"; 60 | 61 | String response = "[User]: " + msg1 + "\n\n" + 62 | "[LLM]: "+ ai.chat(1, msg1) + "\n\n\n" + 63 | "------------------------------------------\n\n\n"; 64 | 65 | String msg2 = "Create a test of the first step. " + 66 | "Be short, 15 lines of code maximum."; 67 | 68 | response += "[User]: " + msg2 + "\n\n"+ 69 | "[LLM]: "+ ai.chat(1, msg2); 70 | 71 | return response; 72 | 73 | } 74 | 75 | } 76 | ---- 77 | 78 | == Invoke the endpoint 79 | 80 | You can check your prompt implementation by pointing your browser to http://localhost:8080/memory[window=_blank] 81 | 82 | You can also run the following command in your terminal: 83 | 84 | [.console-input] 85 | [source,bash] 86 | ---- 87 | curl localhost:8080/memory 88 | ---- 89 | 90 | An example of output (can vary on each prompt execution): 91 | 92 | [.console-output] 93 | [source,text] 94 | ---- 95 | [User]: How do I write a REST endpoint in Java using Quarkus? 96 | 97 | [LLM]: To create a REST endpoint in Java using Quarkus, you can follow these steps: 98 | 99 | 1. Create a new Quarkus project using the Quarkus Maven plugin or Quarkus CLI. 100 | 2. Create a new Java class for your REST endpoint. You can annotate this class with `@Path` to define the base URL path for your endpoint. 101 | 3. Add methods to your class and annotate them with `@GET`, `@POST`, `@PUT`, or `@DELETE` annotations to define the HTTP method for each endpoint. 102 | 4. Use the `@Produces` and `@Consumes` annotations to specify the content type of the responses and requests. 103 | 5. Use the `@PathParam` and `@QueryParam` annotations to capture path and query parameters in your endpoint methods. 104 | 6. Implement the logic for your endpoint methods. 105 | 7. Build and run your Quarkus project to start the application and test your REST endpoint. 106 | 107 | Here's an example of a simple REST endpoint class in Quarkus: 108 | 109 | ```java 110 | import javax.ws.rs.*; 111 | import javax.ws.rs.core.MediaType; 112 | 113 | @Path("/hello") 114 | @Produces(MediaType.APPLICATION_JSON) 115 | @Consumes(MediaType.APPLICATION_JSON) 116 | public class HelloResource { 117 | 118 | @GET 119 | public String sayHello() { 120 | return "Hello, World!"; 121 | } 122 | 123 | @GET 124 | @Path("/{name}") 125 | public String sayHelloTo(@PathParam("name") String name) { 126 | return "Hello, " + name + "!"; 127 | } 128 | } 129 | ``` 130 | 131 | This class defines two REST endpoints: `/hello` for saying hello to the world, and `/hello/{name}` for saying hello to a specific name. You can access these endpoints at `http://localhost:8080/hello` and `http://localhost:8080/hello/{name}` respectively. 132 | 133 | 134 | [User]: Create a test of the first step. Be short, 15 lines of code maximum. 135 | 136 | [LLM]: Here's an example of a simple test for the `sayHello` endpoint in Quarkus using JUnit: 137 | 138 | ```java 139 | import io.quarkus.test.junit.QuarkusTest; 140 | import io.restassured.RestAssured; 141 | import org.junit.jupiter.api.Test; 142 | 143 | import static io.restassured.RestAssured.given; 144 | import static org.hamcrest.CoreMatchers.is; 145 | 146 | @QuarkusTest 147 | public class HelloResourceTest { 148 | 149 | @Test 150 | public void testSayHelloEndpoint() { 151 | given() 152 | .when().get("/hello") 153 | .then() 154 | .statusCode(200) 155 | .body(is("Hello, World!")); 156 | } 157 | } 158 | ``` 159 | 160 | In this test, we are using the QuarkusTest annotation to run the test in the Quarkus test environment. The `testSayHelloEndpoint` method sends a GET request to the `/hello` endpoint and verifies that the response status code is 200 and that the response body is "Hello, World!". 161 | ``` 162 | 163 | ---- 164 | 165 | 166 | 167 | == How to index a conversation 168 | 169 | We can use the LangChain4j extension to index a conversation so we can reuse it, and keep multiple, parallel conversations separated. 170 | 171 | Let's add a new `guessWho()` method to our `DeveloperResource`: 172 | 173 | [.console-input] 174 | [source,java] 175 | ---- 176 | @GET 177 | @Path("/guess") 178 | @Produces(MediaType.TEXT_PLAIN) 179 | public String guess() { 180 | String msg1FromUser1 = "Hello, my name is Klaus and I'm a doctor"; 181 | 182 | String response = "[User1]: " + msg1FromUser1 + "\n\n" + 183 | "[LLM]: " + ai.chat(1, msg1FromUser1) + "\n\n\n" + 184 | "------------------------------------------\n\n\n"; 185 | 186 | String msg1FromUser2 = "Hi, I'm Francine and I'm a lawyer"; 187 | 188 | response += "[User2]: " + msg1FromUser2 + "\n\n" + 189 | "[LLM]: " + ai.chat(2, msg1FromUser2) + "\n\n\n" + 190 | "------------------------------------------\n\n\n"; 191 | 192 | String msg2FromUser2 = "What is my name?"; 193 | 194 | response += "[User2]: " + msg2FromUser2 + "\n\n" + 195 | "[LLM]: " + ai.chat(2, msg2FromUser2) + "\n\n\n" + 196 | "------------------------------------------\n\n\n"; 197 | 198 | String msg2FromUser1 = "What is my profession?"; 199 | 200 | response += "[User1]: " + msg2FromUser1 + "\n\n" + 201 | "[LLM]: " + ai.chat(1, msg2FromUser1) + "\n\n\n" + 202 | "------------------------------------------\n\n\n"; 203 | 204 | return response; 205 | } 206 | 207 | ---- 208 | 209 | == Invoke the endpoint 210 | 211 | You can check your implementation by pointing your browser to http://localhost:8080/guess[window=_blank] 212 | 213 | You can also run the following command: 214 | 215 | [.console-input] 216 | [source,bash] 217 | ---- 218 | curl localhost:8080/guess 219 | ---- 220 | 221 | The result will be at your Quarkus terminal. An example of output (it can vary on each prompt execution): 222 | 223 | [.console-output] 224 | [source,text] 225 | ---- 226 | [User1]: Hello, my name is Klaus and I'm a doctor 227 | 228 | [LLM]: Nice to meet you, Klaus! What field of medicine do you specialize in? 229 | 230 | 231 | ------------------------------------------ 232 | 233 | 234 | [User2]: Hi, I'm Francine and I'm a lawyer 235 | 236 | [LLM]: Hello Francine, nice to meet you. How can I assist you today? 237 | 238 | 239 | ------------------------------------------ 240 | 241 | 242 | [User2]: What is my name? 243 | 244 | [LLM]: Your name is Francine, and you mentioned earlier that you are a lawyer. How can I assist you today, Francine? 245 | 246 | 247 | ------------------------------------------ 248 | 249 | 250 | [User1]: What is my profession? 251 | 252 | [LLM]: Your profession is being a doctor, Klaus. How can I assist you today? 253 | 254 | 255 | ------------------------------------------ 256 | ---- 257 | 258 | NOTE: Take a close look at the IDs of our calls to the assistant. Do you notice that the last question was in fact directed to Klaus with ID=1? We were indeed able to maintain 2 separate and concurrent conversations with the LLM. 259 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/19_agents_tools.adoc: -------------------------------------------------------------------------------- 1 | = Agents/Tools 2 | 3 | :project-ai-name: quarkus-langchain4j-app 4 | 5 | Things become more interesting when you can bring the AI LLM into your application and get it to interact with specific functions you build for it. 6 | 7 | This section will use AI to trigger an email service from our local application. To do this, we'll use LangChain4j's concept of Agents and Tools. 8 | 9 | Agents operate by utilizing a language model to decipher a series of actions, unlike chains where actions are pre-programmed. Ie. agents leverage a language model as a cognitive engine to decide on the actions (tools) and their order. 10 | 11 | You can read more about this in the https://docs.quarkiverse.io/quarkus-langchain4j/dev/agent-and-tools.html[Quarkus LangChain4j Documentation] 12 | 13 | == Add the Mailer and Mailpit extensions 14 | 15 | Open a new terminal window, and make sure you’re at the root of your `{project-ai-name}` project, then run the following command to add emailing capabilities to our application: 16 | 17 | [tabs] 18 | ==== 19 | Maven:: 20 | + 21 | -- 22 | [.console-input] 23 | [source,bash,subs="+macros,+attributes"] 24 | ---- 25 | ./mvnw quarkus:add-extension -D"extensions=mailpit,mailer" 26 | ---- 27 | 28 | -- 29 | Quarkus CLI:: 30 | + 31 | -- 32 | [.console-input] 33 | [source,bash,subs="+macros,+attributes"] 34 | ---- 35 | quarkus extension add mailpit mailer 36 | ---- 37 | -- 38 | ==== 39 | 40 | == Create the email service 41 | 42 | Let's create a class for our email service. 43 | 44 | Create a new `EmailService` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 45 | 46 | [.console-input] 47 | [source,java] 48 | ---- 49 | package com.redhat.developers; 50 | 51 | import jakarta.enterprise.context.ApplicationScoped; 52 | import jakarta.inject.Inject; 53 | 54 | import dev.langchain4j.agent.tool.Tool; 55 | import io.quarkus.logging.Log; 56 | import io.quarkus.mailer.Mail; 57 | import io.quarkus.mailer.Mailer; 58 | 59 | @ApplicationScoped 60 | public class EmailService { 61 | 62 | @Inject 63 | Mailer mailer; 64 | 65 | @Tool("send the given content by email") 66 | public void sendAnEmail(String content) { 67 | Log.info("Sending an email: " + content); 68 | mailer.send(Mail 69 | .withText("sendMeALetter@quarkus.io", "A poem for you", content) 70 | .setFrom("origin@quarkus.io")); 71 | } 72 | 73 | } 74 | ---- 75 | 76 | 77 | == Create the AI service with prompt context 78 | 79 | Let's create an interface for our AI service, but with `SystemMessage` and `UserMessage` this time. 80 | `SystemMessage` gives context to the AI Model. 81 | In this case, we tell it that it should craft a message as if it is written by a professional poet. 82 | The `UserMessage` is the actual instruction/question we're sending to the AI model. 83 | As you can see in the example below, 84 | you can format and parameterize the `UserMessage`, translating structured content to text and vice-versa. 85 | 86 | Create a new `AssistantWithContext` Java interface in `src/main/java` in the `com.redhat.developers` package with the following contents: 87 | 88 | [.console-input] 89 | [source,java] 90 | ---- 91 | package com.redhat.developers; 92 | 93 | import dev.langchain4j.service.SystemMessage; 94 | import dev.langchain4j.service.UserMessage; 95 | import io.quarkiverse.langchain4j.RegisterAiService; 96 | 97 | @RegisterAiService(tools = EmailService.class) 98 | public interface AssistantWithContext { 99 | 100 | /** 101 | * Ask the LLM to create a poem about the given topic. 102 | * 103 | * @param topic the topic of the poem 104 | * @param lines the number of line of the poem 105 | * @return the poem 106 | */ 107 | @SystemMessage("You are a professional poet") 108 | @UserMessage("Write a poem about {topic}. The poem should be {lines} lines long. Then send this poem by email.") 109 | String writeAPoem(String topic, int lines); 110 | 111 | } 112 | ---- 113 | 114 | Note that this assistant references the email service as a tool. 115 | 116 | == Create a email sending resource 117 | 118 | Now we create a resource that builds the interaction and calls the service with the required parameters (topic and number of lines). 119 | 120 | Create a new `EmailMeAPoemResource` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 121 | 122 | [.console-input] 123 | [source,java] 124 | ---- 125 | package com.redhat.developers; 126 | 127 | import jakarta.ws.rs.GET; 128 | import jakarta.ws.rs.Path; 129 | 130 | @Path("/email-me-a-poem") 131 | public class EmailMeAPoemResource { 132 | 133 | private final AssistantWithContext service; 134 | 135 | public EmailMeAPoemResource(AssistantWithContext service) { 136 | this.service = service; 137 | } 138 | 139 | @GET 140 | public String emailMeAPoem() { 141 | return service.writeAPoem("Quarkus", 4); 142 | } 143 | 144 | } 145 | ---- 146 | 147 | == Modify application.properties to use the email Tools 148 | 149 | Tool calling is not supported with the OpenAI `demo` key so we will need to 150 | either use a real API key, or use a local model that supports tools.. 151 | If you want to use OpenAI's ChatGPT, you can create and fund an account at https://platform.openai.com/[OpenAI] and then set the openai-api-key to your key. 152 | 153 | We will use a local (free) open source model served with Ollama instead. 154 | To do this, you will need to https://ollama.com/download[download and install Ollama]. 155 | Once that's done, you will need to https://ollama.com/search?c=tools[download a model that supports tool calling], such as `granite3.1-dense:2b`. To do so, execute the command: 156 | 157 | [#quarkuspdb-dl-ollama] 158 | [.console-input] 159 | [source,config,subs="+macros,+attributes"] 160 | ---- 161 | ollama pull granite3.1-dense:2b 162 | ---- 163 | 164 | Update the following properties in your `application.properties` 165 | 166 | NOTE: If you do not want to go through the trouble of creating an OpenAI account or install Ollama, you can still test the below scenario, it just won't send an email since the "Tool" functionality unfortunately won't work. 167 | 168 | Modify the application.properties as below: 169 | 170 | [#quarkuspdb-update-props] 171 | [.console-input] 172 | [source,config,subs="+macros,+attributes"] 173 | ---- 174 | # Set OpenAI key if you want to use the API key 175 | # quarkus.langchain4j.openai.api-key=demo 176 | 177 | # With Ollama 178 | quarkus.langchain4j.openai.base-url=http://localhost:11434/v1 179 | # Configure server to use a specific model 180 | quarkus.langchain4j.openai.chat-model.model-name=granite3.1-dense:2b 181 | quarkus.langchain4j.openai.embedding-model.model-name=granite3.1-dense:2b 182 | 183 | quarkus.langchain4j.openai.log-requests=true 184 | quarkus.langchain4j.openai.log-responses=true 185 | quarkus.langchain4j.openai.timeout=60s 186 | 187 | %dev.quarkus.mailer.mock=false 188 | ---- 189 | 190 | Make sure your Quarkus Dev mode is still running. It should have reloaded with the new configuration. 191 | 192 | Because we haven't configured the local email service, Quarkus will also have started a Dev Service to instantiate and configure a local email service for you (in dev mode only!). 193 | 194 | You can check it running: 195 | 196 | [.console-input] 197 | [source,bash] 198 | ---- 199 | podman ps 200 | ---- 201 | 202 | And you should see something like this: 203 | 204 | [.console-output] 205 | [source,text] 206 | ---- 207 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 208 | e4a1d5aae322 docker.io/testcontainers/ryuk:0.6.0 /bin/ryuk 34 seconds ago Up 34 seconds 0.0.0.0:35965->8080/tcp testcontainers-ryuk-4cb568ec-9335-4e91-a6aa-60c5a631567a 209 | 729ad84b6561 docker.io/axllent/mailpit:latest 34 seconds ago Up 34 seconds 0.0.0.0:39141->1025/tcp, 0.0.0.0:45875->8025/tcp suspicious_hypatia 210 | ---- 211 | 212 | Which means that you have an email service up and running. 213 | 214 | == Invoke the endpoint 215 | 216 | You can check your prompt implementation by pointing your browser to http://localhost:8080/email-me-a-poem[window=_blank] 217 | 218 | You can also run the following command: 219 | 220 | [.console-input] 221 | [source,bash] 222 | ---- 223 | curl localhost:8080/email-me-a-poem 224 | ---- 225 | 226 | An example of output (will vary on each prompt execution): 227 | 228 | [.console-output] 229 | [source,text] 230 | ---- 231 | I have composed a poem about Quarkus. I have sent it to you via email. Let me know if you need anything else 232 | ---- 233 | 234 | If you have a tool calling model configured, you can check your inbox for the actual email: 235 | 236 | First, open the http://localhost:8080/q/dev-ui[DevUI, window=_blank] and click on the Mailpit arrow. 237 | 238 | image::devui-mailpit.png[] 239 | 240 | Now you can see the email that was sent: 241 | 242 | image::mailpit-email-sent.png[] 243 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/22_local_models.adoc: -------------------------------------------------------------------------------- 1 | = Working with local models 2 | 3 | :project-ollama-name: quarkus-ollama-app 4 | 5 | Throughout this tutorial, we've been working with remote or containerized models. Let's switch now to a model running natively on our local machine. 6 | 7 | There are various options out there. In our case we'll work with Ollama, an open-source project that serves as a powerful 8 | and user-friendly platform for running LLMs on your local machine. 9 | 10 | 11 | == Installing Ollama 12 | 13 | First, you must download and install the specific Ollama version on your operating system. https://ollama.com/download[The instructions can be found here, window="_blank"]. 14 | 15 | Once installed, go ahead and download the local model by running this command: 16 | 17 | [.console-input] 18 | [source,bash] 19 | ---- 20 | ollama pull llama3:latest 21 | ---- 22 | 23 | Now, let's run our model locally: 24 | 25 | [.console-input] 26 | [source,bash] 27 | ---- 28 | ollama serve 29 | ---- 30 | 31 | == Create a new project with the Ollama extension 32 | 33 | The Ollama extension isn't compatible with other extensions we have used in this tutorial, so we'll create a new project. 34 | 35 | [tabs%sync] 36 | ==== 37 | 38 | Maven:: 39 | + 40 | -- 41 | [.console-input] 42 | [source,bash,subs="+macros,+attributes"] 43 | ---- 44 | mvn "io.quarkus.platform:quarkus-maven-plugin:create" -DprojectGroupId="com.redhat.developers" -DprojectArtifactId="{project-ollama-name}" -DprojectVersion="1.0-SNAPSHOT" -Dextensions=rest,langchain4j-ollama 45 | cd {project-ollama-name} 46 | ---- 47 | -- 48 | Quarkus CLI:: 49 | + 50 | -- 51 | 52 | [.console-input] 53 | [source,bash,subs="+macros,+attributes"] 54 | ---- 55 | quarkus create app -x rest langchain4j-ollama com.redhat.developers:{project-ollama-name}:1.0-SNAPSHOT 56 | cd {project-ollama-name} 57 | ---- 58 | -- 59 | ==== 60 | 61 | == Connect to Ollama 62 | 63 | Just add these properties to the `application.properties` file available in `src/main/resources`: 64 | 65 | [.console-input] 66 | [source,properties] 67 | ---- 68 | quarkus.langchain4j.ollama.chat-model.model-id=llama3:latest 69 | quarkus.langchain4j.ollama.timeout=120s 70 | ---- 71 | 72 | == Create the AI service 73 | 74 | Let's create an interface for our AI service. 75 | 76 | Create a new `Assistant` Java interface in `src/main/java` in the `com.redhat.developers` package with the following contents: 77 | 78 | [.console-input] 79 | [source,java] 80 | ---- 81 | package com.redhat.developers; 82 | 83 | import io.quarkiverse.langchain4j.RegisterAiService; 84 | 85 | @RegisterAiService 86 | public interface Assistant { 87 | String chat(String message); 88 | } 89 | ---- 90 | 91 | == Create the prompt-base resource 92 | 93 | Now we're going to implement a resource that send prompts using the AI service. 94 | 95 | Create a new `ExistentialQuestionResource` Java class in `src/main/java` in the `com.redhat.developers` package with the following contents: 96 | 97 | [.console-input] 98 | [source,java] 99 | ---- 100 | package com.redhat.developers; 101 | 102 | import jakarta.inject.Inject; 103 | import jakarta.ws.rs.GET; 104 | import jakarta.ws.rs.Path; 105 | import jakarta.ws.rs.Produces; 106 | import jakarta.ws.rs.core.MediaType; 107 | 108 | @Path("/earth") 109 | public class ExistentialQuestionResource { 110 | 111 | @Inject 112 | Assistant assistant; 113 | 114 | @GET 115 | @Path("/flat") 116 | @Produces(MediaType.TEXT_PLAIN) 117 | public String isEarthFlat() { 118 | return assistant.chat("Can you explain why the earth is flat?"); 119 | } 120 | } 121 | ---- 122 | 123 | == Invoke the endpoint 124 | 125 | You can check your prompt implementation by pointing your browser to http://localhost:8080/earth/flat[window=_blank] 126 | 127 | You can also run the following command: 128 | 129 | [.console-input] 130 | [source,bash] 131 | ---- 132 | curl -w '\n' localhost:8080/earth/flat 133 | ---- 134 | 135 | An example of output (it can vary on each prompt execution): 136 | 137 | [.console-output] 138 | [source,text] 139 | ---- 140 | I think there may be a misunderstanding here! 141 | 142 | Actually, the scientific consensus is that the Earth is an oblate spheroid, meaning it's slightly flattened at the poles and bulging at the equator. The evidence from various fields of science, including astronomy, geology, and physics, all point to the fact that our planet is indeed round. 143 | 144 | Here are some reasons why we know the Earth is not flat: 145 | 146 | 1. **Ships disappearing over the horizon**: When a ship sails away from an observer on the shore, it will eventually disappear from view as it sinks below the horizon due to the curvature of the Earth. 147 | ---- -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/23_kubernetes_kafka_ai.adoc: -------------------------------------------------------------------------------- 1 | = Bringing Kubernetes and Kafka to the party 2 | 3 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/_partials/build.adoc: -------------------------------------------------------------------------------- 1 | NOTE: The instructions of this section assumes your project name to be `{quarkus-project-name}` 2 | 3 | #tag::build[] 4 | 5 | [tabs] 6 | ==== 7 | JVM mode:: 8 | + 9 | -- 10 | [#{section-k8s}-build-quarkus-jvm-app] 11 | [.console-input] 12 | [source,bash,subs="+macros,+attributes"] 13 | ---- 14 | ./mvnw -DskipTests clean package 15 | ---- 16 | 17 | After successful maven build, run the following command to build the container - choose the terminal of the target environment you wish to use (podman, minishift or minokube) as you want to build and publish the image in the right registry: 18 | 19 | [#{section-k8s}-build-quarkus-jvm-app-container] 20 | [.console-input] 21 | [source,bash,subs="+macros,+attributes"] 22 | ---- 23 | docker build -f src/main/docker/Dockerfile.jvm -t example/{quarkus-project-name}:1.0-SNAPSHOT . 24 | ---- 25 | 26 | -- 27 | Native mode:: 28 | + 29 | -- 30 | [#{section-k8s}-build-quarkus-native-app] 31 | [.console-input] 32 | [source,bash,subs="+macros,+attributes"] 33 | ---- 34 | ./mvnw -DskipTests clean package -Pnative -Dnative-image.docker-build=true <1> 35 | ---- 36 | <1> Using the `-Dnative-image.docker-build=true` is very important as need a linux native binary what will be containerized. 37 | 38 | NOTE: Native compilation will take few minutes to complete. 39 | 40 | After successful maven build, run the following command to build the container - choose the terminal of the target environment you wish to use (podman, minishift or minokube) as you want to build and publish the image in the right registry: 41 | 42 | [#{section-k8s}-build-quarkus-native-app-container] 43 | [.console-input] 44 | [source,bash,subs="+macros,+attributes"] 45 | ---- 46 | docker build -f src/main/docker/Dockerfile.native -t example/{quarkus-project-name}:1.0-SNAPSHOT . 47 | ---- 48 | 49 | -- 50 | ==== 51 | 52 | NOTE: For sake of simplicity the container name in both native and jvm mode are named identically. In real cases where you might need both containers, you'd like to name them differently to know exactly the package they are containing. 53 | 54 | #end::build[] 55 | 56 | 57 | #tag::run[] 58 | 59 | [#{doc-sec}-run-jvm-app] 60 | [.console-input] 61 | [source,bash,subs="+macros,+attributes"] 62 | ---- 63 | docker run -it --rm -p 8080:8080 example/{quarkus-project-name}:1.0-SNAPSHOT 64 | ---- 65 | 66 | #end::run[] 67 | 68 | #tag::tag-push[] 69 | 70 | [#{doc-sec}-tag-jvm-app] 71 | [.console-input] 72 | [source,bash,subs="+macros,+attributes"] 73 | ---- 74 | docker push example/{quarkus-project-name}:1.0-SNAPSHOT 75 | ---- 76 | 77 | #end::tag-push[] 78 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/_partials/compile-and-run.adoc: -------------------------------------------------------------------------------- 1 | [tabs%sync] 2 | ==== 3 | JVM mode:: 4 | + 5 | -- 6 | 7 | [%header,cols="1,1"] 8 | |===== 9 | 10 | |Maven 11 | 12 | |Quarkus CLI 13 | 14 | a| 15 | [#basics-build-quarkus-jvm-app-mvn] 16 | [.console-input] 17 | [source,bash,subs="+macros,+attributes"] 18 | ---- 19 | ./mvnw package 20 | ---- 21 | 22 | a| 23 | [#basics-build-quarkus-jvm-app-quarkus] 24 | [.console-input] 25 | [source,bash,subs="+macros,+attributes"] 26 | ---- 27 | quarkus build 28 | ---- 29 | 30 | |===== 31 | 32 | 33 | To run the application in jvm mode 34 | 35 | [#basics-build-run-jvm-app] 36 | [.console-input] 37 | [source,bash,subs="+macros,+attributes"] 38 | ---- 39 | java -jar target/quarkus-app/quarkus-run.jar 40 | ---- 41 | 42 | -- 43 | Native mode:: 44 | + 45 | -- 46 | 47 | Quarkus simplifies the https://quarkus.io/guides/building-native-image[compilation of Java applications down to a native binary] using GraalVM/Mandrel. To do so, add a "native" flag to your build command. 48 | 49 | NOTE: To force building a native binary in a GraalVM Mandrel container, add `-Dquarkus.native.container-build=true`. 50 | 51 | [%header,cols="1,1"] 52 | |===== 53 | 54 | |Maven 55 | 56 | |Quarkus CLI 57 | 58 | a| 59 | [#basics-build-quarkus-native-app-mvn] 60 | [.console-input] 61 | [source,bash,subs="+macros,+attributes"] 62 | ---- 63 | ./mvnw package -Dnative 64 | ---- 65 | 66 | a| 67 | [#basics-build-quarkus-native-app-quarkus] 68 | [.console-input] 69 | [source,bash,subs="+macros,+attributes"] 70 | ---- 71 | quarkus build --native 72 | ---- 73 | |===== 74 | 75 | 76 | NOTE: Native compilation will take few minutes to complete. 77 | 78 | To run the application in native mode 79 | 80 | [#basics-build-run-native-app] 81 | [.console-input] 82 | [source,bash,subs="+macros,+attributes"] 83 | ---- 84 | ./target/{project-name}-1.0-SNAPSHOT-runner 85 | ---- 86 | 87 | -- 88 | Native container-build mode:: 89 | + 90 | -- 91 | 92 | [%header,cols="1,1"] 93 | |===== 94 | |Maven 95 | 96 | |Quarkus CLI 97 | 98 | a| 99 | [#basics-build-quarkus-native-container-app-mvn] 100 | [.console-input] 101 | [source,bash,subs="+macros,+attributes"] 102 | ---- 103 | ./mvnw quarkus:image-build \ 104 | -Dnative 105 | ---- 106 | a| 107 | [#basics-build-quarkus-native-container-app-quarkus] 108 | [.console-input] 109 | [source,bash,subs="+macros,+attributes"] 110 | ---- 111 | quarkus image build --native 112 | ---- 113 | |===== 114 | 115 | NOTE: Native compilation will take few minutes to complete. 116 | 117 | The above command will build a native binary of the Java code and then copy it into a container image. 118 | Start the container that runs the native binary: 119 | 120 | [#basics-build-run-native-docker-app] 121 | [.console-input] 122 | [source,bash,subs="+macros,+attributes"] 123 | ---- 124 | docker run -it --rm -p 8080:8080 {project-name}:1.0-SNAPSHOT 125 | ---- 126 | 127 | -- 128 | ==== 129 | 130 | You'll see an output like this one: 131 | 132 | [.console-output] 133 | [source,text] 134 | ---- 135 | __ ____ __ _____ ___ __ ____ ______ 136 | --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 137 | -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ 138 | --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 139 | 2023-07-07 11:47:14,046 INFO [io.quarkus] (main) code-with-quarkus 1.0.0-SNAPSHOT native (powered by Quarkus 3.10.2) started in 0.019s. Listening on: http://0.0.0.0:8080 140 | 2023-07-07 11:47:14,047 INFO [io.quarkus] (main) Profile prod activated. 141 | 2023-07-07 11:47:14,047 INFO [io.quarkus] (main) Installed features: [cdi, -reactive, smallrye-context-propagation, vertx] 142 | ---- -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/_partials/invoke-service.adoc: -------------------------------------------------------------------------------- 1 | [k8s-env=''] 2 | [k8s-cli=''] 3 | [doc-sec=''] 4 | 5 | #tag::env[] 6 | 7 | [#{doc-sec}-{k8s-cli}-svc-gateway-env] 8 | [source,bash,subs="+macros,+attributes"] 9 | ---- 10 | IP_ADDRESS="$({k8s-env} ip):$({k8s-cli} get svc istio-ingressgateway --namespace istio-system --output 'jsonpath={.spec.ports[?(@.port==80)].nodePort}')" 11 | ---- 12 | 13 | #end::env[] 14 | 15 | #tag::call[] 16 | 17 | [#{doc-sec}-{k8s-cli}-svc-call] 18 | [.console-input] 19 | [source,bash,subs="+macros,+attributes"] 20 | ---- 21 | {cli-tool} {address}:8080/{path} 22 | ---- 23 | 24 | 25 | #end::call[] 26 | 27 | #tag::callToken[] 28 | 29 | [#{doc-sec}-{k8s-cli}-svc-call] 30 | [.console-input] 31 | [source,bash,subs="+macros,+attributes"] 32 | ---- 33 | token={token-value} 34 | {cli-tool} -H "Authorization: Bearer $token" {address}:8080/{path} 35 | ---- 36 | 37 | #end::callToken[] 38 | 39 | #tag::test[] 40 | When running this test, the application is started once, then all tests are executed, and finally, the application is stopped. 41 | Although it is not mandatory, by default http://rest-assured.io/[RestAssured] project is used to test Rest endpoints but it is up to you to change that. 42 | 43 | You can run the test either in IDE or by running Maven: 44 | 45 | [#basics-run-tests] 46 | [.console-input] 47 | [source,bash,subs="+macros,+attributes"] 48 | ---- 49 | ./mvnw clean compile test 50 | ---- 51 | #end::test[] 52 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/_partials/k8s-build-deploy.adoc: -------------------------------------------------------------------------------- 1 | [:quarkus-project-name=''] 2 | 3 | #tag::k8s-prep[] 4 | 5 | [IMPORTANT] 6 | ==== 7 | If you have not deployed the application previously, you need to add Quarkus Kubernetes extension. This extension uses https://github.com/dekorateio/dekorate[Dekorate] to generate a default Kubernetes resource template. 8 | 9 | [#qext-mvn-add-kubernetes-extension] 10 | [.console-input] 11 | [source,bash,subs="+macros,+attributes"] 12 | ---- 13 | ./mvnw quarkus:add-extension -Dextensions="quarkus-kubernetes" 14 | ---- 15 | 16 | You need to configure group and name of the container used to deploy into Kubernetes. 17 | 18 | Add the following properties (if you haven't already done it before) to pass:[$PROJECT_HOME]/src/main/resources/application.properties: 19 | 20 | [#quarkusk8s-update-props] 21 | [.console-input] 22 | [source,config,subs="+macros,+attributes"] 23 | ---- 24 | kubernetes.group=example 25 | quarkus.application.name={quarkus-project-name} 26 | ---- 27 | 28 | ==== 29 | 30 | Now you need to run Maven goal to generate Kubernetes resource. 31 | 32 | [#quarkusk8s-generate-kubernetes] 33 | [.console-input] 34 | [source,bash,subs="+macros,+attributes"] 35 | ---- 36 | ./mvnw package -DskipTests 37 | ---- 38 | 39 | You can inspect the generated file by accessing next file: 40 | 41 | [#quakrusk8s-generated-kubernetes-resource] 42 | [.console-input] 43 | [source,bash,subs="+macros,+attributes"] 44 | ---- 45 | cat target/kubernetes/kubernetes.yml 46 | ---- 47 | 48 | #end::k8s-prep[] 49 | 50 | #tag::k8s-deploy[] 51 | 52 | ifndef::workshop[] 53 | [tabs] 54 | ==== 55 | kubectl:: 56 | + 57 | -- 58 | [#{doc-sec}-run-deploy-k8s-app] 59 | [.console-input] 60 | [source,bash,subs="+macros,+attributes"] 61 | ---- 62 | kubectl apply -f pass:[$TUTORIAL_HOME]/target/kubernetes/kubernetes.yml 63 | ---- 64 | -- 65 | oc:: 66 | + 67 | -- 68 | endif::[] 69 | 70 | [#{doc-sec}-oc-run-deploy-k8s-app] 71 | [.console-input] 72 | [source,bash,subs="+macros,+attributes"] 73 | ---- 74 | oc apply -f pass:[$TUTORIAL_HOME]/target/kubernetes/kubernetes.yml 75 | ---- 76 | ifndef::workshop[] 77 | -- 78 | ==== 79 | endif::[] 80 | 81 | #end::k8s-deploy[] 82 | 83 | 84 | #tag::k8s-invoke-svc[] 85 | 86 | ifndef::workshop[] 87 | [tabs] 88 | ==== 89 | kubectl:: 90 | + 91 | -- 92 | 93 | You need to create a `NodePort` to expose the application if you are in minikube. 94 | As generated resources creates a `ClusterIP` service, you need to patch it: 95 | 96 | [#{doc-sec}-k8s-run-expose-svc-nodeport] 97 | [.console-input] 98 | [source,bash,subs="+macros,+attributes"] 99 | ---- 100 | kubectl patch svc {quarkus-project-name} --type='json' -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]' 101 | ---- 102 | 103 | [#{doc-sec}-run-deploy-k8s-service] 104 | [.console-input] 105 | [source,bash,subs="+macros,+attributes"] 106 | ---- 107 | SVC_URL=$(minikube service -n quarkustutorial {quarkus-project-name} --url) 108 | ---- 109 | 110 | -- 111 | oc:: 112 | + 113 | -- 114 | endif::[] 115 | With OpenShift we can create a route to make Quarkus application publicly accessible: 116 | 117 | [#{doc-sec}-oc-run-expose-svc-route] 118 | [.console-input] 119 | [source,bash,subs="+macros,+attributes"] 120 | ---- 121 | oc expose service {quarkus-project-name} 122 | ---- 123 | 124 | Once the service is exposed we can use the following command to get the public url: 125 | 126 | [#{doc-sec}-oc-run-get-route] 127 | [.console-input] 128 | [source,bash,subs="+macros,+attributes"] 129 | ---- 130 | SVC_URL=$(oc get routes {quarkus-project-name} -o jsonpath='{.spec.host}') 131 | ---- 132 | ifndef::workshop[] 133 | -- 134 | ==== 135 | endif::[] 136 | 137 | Now you can use the `SVC_URL` to call the service from the browser or via cli like: 138 | 139 | [#{doc-sec}-call-k8s-svc] 140 | [.console-input] 141 | [source,bash,subs="+macros,+attributes"] 142 | ---- 143 | curl $SVC_URL/{svc-path} 144 | ---- 145 | 146 | #end::k8s-invoke-svc[] 147 | 148 | #tag::k8s-delete[] 149 | 150 | ifndef::workshop[] 151 | [tabs] 152 | ==== 153 | kubectl:: 154 | + 155 | -- 156 | [#{section-k8s}k8s-delete-k8s-app] 157 | [.console-input] 158 | [source,bash,subs="+macros,+attributes"] 159 | ---- 160 | kubectl delete all --all 161 | ---- 162 | -- 163 | oc:: 164 | + 165 | -- 166 | endif::[] 167 | 168 | [#{section-k8s}k8s-delete-oc-app] 169 | [.console-input] 170 | [source,bash,subs="+macros,+attributes"] 171 | ---- 172 | oc delete all --all 173 | ---- 174 | ifndef::workshop[] 175 | -- 176 | ==== 177 | endif::[] 178 | 179 | #end::k8s-delete[] 180 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/_partials/prereq-cli.adoc: -------------------------------------------------------------------------------- 1 | 2 | The following checks ensure that each chapter exercises are done with the right environment settings. 3 | 4 | INFO: Make sure to use separate terminal windows or tabs between commands you run to target Podman for Mac/Windows and commands for Minikube/minishift as both will point to different container engines. 5 | 6 | ifdef::workshop[] 7 | 8 | include::ROOT:partial$openshift-prereq-cli.adoc[] 9 | 10 | endif::[] 11 | 12 | ifndef::workshop[] 13 | 14 | [tabs] 15 | ==== 16 | Minikube:: 17 | + 18 | -- 19 | * Set your local podman to use minikube container daemon 20 | 21 | [#minikube-set-env] 22 | [source,bash,subs="+macros,+attributes"] 23 | ---- 24 | eval $(minikube docker-env) 25 | ---- 26 | 27 | * Kubernetes should be v1.12+ 28 | 29 | [#kubectl-version] 30 | [source,bash,subs="+macros,+attributes"] 31 | ---- 32 | kubectl version 33 | ---- 34 | -- 35 | Minishift:: 36 | + 37 | -- 38 | * Set your local podman to use the minishift container daemon 39 | 40 | [#minishift-set-env] 41 | [source,bash,subs="+macros,+attributes"] 42 | ---- 43 | eval $(minishift docker-env) 44 | ---- 45 | 46 | include::ROOT:partial$openshift-prereq-cli.adoc[] 47 | 48 | -- 49 | ==== 50 | 51 | endif::[] 52 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/_partials/run-dev-mode.adoc: -------------------------------------------------------------------------------- 1 | :experimental: 2 | 3 | If you are not in dev mode, run the following command to start Quarkus application in development mode: 4 | 5 | [#{section-build-run}-build-run-dev] 6 | [.console-input] 7 | [source,bash,subs="+macros,+attributes"] 8 | ---- 9 | ./mvnw compile quarkus:dev 10 | ---- 11 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/_partials/set-env-vars.adoc: -------------------------------------------------------------------------------- 1 | .Environment Variables 2 | 3 | [cols="4*^,4*."] 4 | |=== 5 | |**Variable** |**Description** |**Default Value** | **e.g.** 6 | 7 | |REGISTRY_USERNAME 8 | |The Container Registry User Id that will be used to authenticate against the container registry `$REGISTRY_URL` 9 | | 10 | |demo 11 | 12 | |REGISTRY_PASSWORD 13 | |The Container Registry User Password that will be used to authenticate against the container registry `$REGISTRY_URL` 14 | | 15 | |demopassword 16 | 17 | |REGISTRY_URL 18 | |The Container Registry URL, defaults to https://index.docker.io 19 | |https://index.docker.io 20 | |https://quay.io/v2 21 | 22 | |DESTINATION_IMAGE_NAME 23 | |The fully qualified image name that will be built 24 | | 25 | | quay.io/foo/bar:v1.0 26 | |=== 27 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | = Welcome to Quarkus Tutorial 2 | :page-layout: home 3 | :!sectids: 4 | 5 | [.text-center.strong] 6 | == Supersonic Subatomic Java 7 | 8 | A Kubernetes Native Java stack tailored for GraalVM & OpenJDK HotSpot, crafted from the best of breed Java libraries and standards. 9 | 10 | [.mt-4.center] 11 | image::Developer_Joy.png[Developer Joy,400,400,align="center"] 12 | 13 | [.tiles.browse] 14 | == Browse modules 15 | 16 | include::../nav.adoc[] 17 | -------------------------------------------------------------------------------- /documentation/modules/_attributes.adoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/documentation/modules/_attributes.adoc -------------------------------------------------------------------------------- /github-pages.yml: -------------------------------------------------------------------------------- 1 | runtime: 2 | cache_dir: ./.cache/antora 3 | 4 | site: 5 | title: Quarkus Tutorial 6 | url: https://redhat-developer-demos.github.io/quarkus-tutorial 7 | start_page: quarkus-tutorial::index.adoc 8 | 9 | content: 10 | sources: 11 | - url: ./ 12 | start_path: documentation 13 | 14 | asciidoc: 15 | attributes: 16 | tutorial-namespace: quarkus-tutorial 17 | quarkus-version: 2.15.3.Final 18 | graalvm-version: 22.3.0 19 | project-name: tutorial-app 20 | page-pagination: true 21 | extensions: 22 | - ./lib/tab-block.js 23 | - ./lib/remote-include-processor.js 24 | ui: 25 | bundle: 26 | url: https://github.com/redhat-developer-demos/rhd-tutorial-ui/releases/download/v0.1.10/ui-bundle.zip 27 | supplemental_files: 28 | - path: ./supplemental-ui 29 | - path: .nojekyll 30 | - path: ui.yml 31 | contents: "static_files: [ .nojekyll ]" 32 | 33 | output: 34 | dir: ./gh-pages 35 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | 3 | import { series, watch } from "gulp"; 4 | import { remove } from "fs-extra"; 5 | import { readFileSync } from "fs"; 6 | import {load as yamlLoad} from "yaml-js"; 7 | import generator from "@antora/site-generator-default"; 8 | import browserSync from "browser-sync"; 9 | 10 | const filename = "github-pages.yml"; 11 | const server = browserSync.create(); 12 | const args = ["--playbook", filename]; 13 | 14 | //Watch Paths 15 | function watchGlobs() { 16 | let json_content = readFileSync(`${__dirname}/${filename}`, "UTF-8"); 17 | let yaml_content = yamlLoad(json_content); 18 | let dirs = yaml_content.content.sources.map(source => [ 19 | `**/*.yml`, 20 | `**/*.adoc`, 21 | `**/*.hbs` 22 | ]); 23 | dirs.push(["${filename}"]); 24 | dirs = [].concat(...dirs); 25 | //console.log(dirs); 26 | return dirs; 27 | } 28 | 29 | const siteWatch = () => watch(watchGlobs(), series(build, reload)); 30 | 31 | const removeSite = done => remove("gh-pages", done); 32 | const removeCache = done => remove(".cache", done); 33 | 34 | function build(done) { 35 | generator(args, process.env) 36 | .then(() => { 37 | done(); 38 | }) 39 | .catch(err => { 40 | console.log(err); 41 | done(); 42 | }); 43 | } 44 | 45 | function workshopSite(done){ 46 | generator(["--pull", "--stacktrace","--playbook","workshop-site.yaml"], process.env) 47 | .then(() => { 48 | done(); 49 | }) 50 | .catch(err => { 51 | console.log(err); 52 | done(); 53 | }); 54 | } 55 | 56 | function reload(done) { 57 | server.reload(); 58 | done(); 59 | } 60 | 61 | function serve(done) { 62 | server.init({ 63 | server: { 64 | baseDir: "./gh-pages" 65 | } 66 | }); 67 | done(); 68 | } 69 | 70 | const _build = build; 71 | export { _build as build }; 72 | const _clean = series(removeSite, removeCache); 73 | export { _clean as clean }; 74 | const _default = series(_clean, build, serve, siteWatch); 75 | export { _default as default }; 76 | //build workshop docs 77 | const _wsite = series(_clean, workshopSite); 78 | export { _wsite as workshopSite }; 79 | -------------------------------------------------------------------------------- /jwt-token/quarkus.jwt.pub: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | {"kty":"RSA","kid":"/privateKey.pem","e":"AQAB","n":"livFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm-ntyIv1p4kE1sPEQO73-HY8-Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF_F9-KEBWkwVta-PZ37bwqSE4sCb1soZFrVz_UT_LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv_3Lw50bAkbT4HeLFxTx4flEoZLKO_g0bAoV2uqBhkA9xnQ"} 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /jwt-token/quarkus.jwt.token: -------------------------------------------------------------------------------- 1 | eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTcwMDk0MTcxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MjIwMDgxNDE3MSwiaWF0IjoxNTcwMDk0MTcxLCJqdGkiOiJhLTEyMyJ9.Hzr41h3_uewy-g2B-sonOiBObtcpkgzqmF4bT3cO58v45AIOiegl7HIx7QgEZHRO4PdUtR34x9W23VJY7NJ545ucpCuKnEV1uRlspJyQevfI-mSRg1bHlMmdDt661-V3KmQES8WX2B2uqirykO5fCeCp3womboilzCq4VtxbmM2qgf6ag8rUNnTCLuCgEoulGwTn0F5lCrom-7dJOTryW1KI0qUWHMMwl4TX5cLmqJLgBzJapzc5_yEfgQZ9qXzvsT8zeOWSKKPLm7LFVt2YihkXa80lWcjewwt61rfQkpmqSzAHL0QIs7CsM9GfnoYc0j9po83-P3GJiBMMFmn-vg 2 | -------------------------------------------------------------------------------- /jwt-token/quarkus.keycloak.jwt.token: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjZklBRE5feHhDSm1Wa1d5Ti1QTlhFRXZNVVdzMnI2OEN4dG1oRUROelhVIn0.eyJleHAiOjE2Mjk5MDIxMDgsImlhdCI6MTYyOTkwMTgwOCwianRpIjoiZjZmNmI4MjItZTRkMC00NmZkLWFkZmQtZTRmYjRkZDY0NjAxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MTgwL2F1dGgvcmVhbG1zL3F1YXJrdXMiLCJzdWIiOiIxZWVkNmE4ZS1hODUzLTQ1OTctYjRjNi1jNGMyNTMzNTQ2YTAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJiYWNrZW5kLXNlcnZpY2UiLCJzZXNzaW9uX3N0YXRlIjoiODk3ZjMwYjctNWM5Ni00Njc3LTg0YTQtMzE4ZDc3YjczZWFhIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJTdWJzY3JpYmVyIiwidXNlciIsImNvbmZpZGVudGlhbCJdfSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiODk3ZjMwYjctNWM5Ni00Njc3LTg0YTQtMzE4ZDc3YjczZWFhIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqZG9lIn0.QeNM8f-w5i2OpCVeNcrrPHm7msvJtBiQVALTWn5BKb9CoAHC5hIKe3ClfqYmIHOO8DV3zLJvkqKfKs6fhl8crs-wzowMa04QVPKnOWTJHt79Db3nN7uzKK6P0iDAvZ4qEtuid2lE5fKxEaLhS58BQOqYJeHeiZXeyUumKKGqs_ddCbXgm2D4mnA_wSlpnexdoHu6QwEXYhx1G1eemuiGBQ8Rk9WSTTJeh2y7nZf4CAkq6HDOmX-8To-xm6TYJ04E11poMkcOWbrVT4ne_7LsCqMv8gXvlOtRtdbblWCHDorQxZyTvQi0RD9VbKHlcCnQrxGxnWxnK4FT714ejEfWvg -------------------------------------------------------------------------------- /lib/copy-to-clipboard.js: -------------------------------------------------------------------------------- 1 | const BlockCopyToClipboardMacro = (() => { 2 | const $context = Symbol("context"); 3 | const superclass = Opal.module(null, "Asciidoctor").Extensions 4 | .BlockMacroProcessor; 5 | const scope = Opal.klass( 6 | Opal.module(null, "Antora"), 7 | superclass, 8 | "BlockCopyToClipboardMacro", 9 | function() {} 10 | ); 11 | 12 | Opal.defn(scope, "$initialize", function initialize(name, config, context) { 13 | Opal.send( 14 | this, 15 | Opal.find_super_dispatcher(this, "initialize", initialize), 16 | [name, config] 17 | ); 18 | this[$context] = context; 19 | }); 20 | 21 | Opal.defn(scope, "$process", function(parent, target, attrs) { 22 | const t = target.startsWith(":") ? target.substr(1) : target; 23 | //console.log("target:", t); 24 | const createHtmlFragment = html => this.createBlock(parent, "pass", html); 25 | const html = `
`; 26 | parent.blocks.push(createHtmlFragment(html)); 27 | }); 28 | 29 | return scope; 30 | })(); 31 | 32 | module.exports.register = (registry, context) => { 33 | registry.blockMacro( 34 | BlockCopyToClipboardMacro.$new("copyToClipboard", Opal.hash(), context) 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /lib/remote-include-processor.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | this.includeProcessor(function () { 3 | this.$option('position', '>>') 4 | this.handles((target) => target.startsWith('http')) 5 | this.process((doc, reader, target, attrs) => { 6 | const contents = require('child_process').execFileSync('curl', ['--silent', '-L', target], { encoding: 'utf8' }) 7 | reader.pushInclude(contents, target, target, 1, attrs) 8 | }) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /lib/tab-block.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2018 OpenDevise, Inc. 2 | * 3 | * This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | /** 8 | * Extends the AsciiDoc syntax to support a tabset. The tabset is created from 9 | * a dlist enclosed in an example block that is marked with the tabs style. 10 | * 11 | * Usage: 12 | * 13 | * [tabs] 14 | * ==== 15 | * Tab A:: 16 | * + 17 | * -- 18 | * Contents of tab A. 19 | * -- 20 | * Tab B:: 21 | * + 22 | * -- 23 | * Contents of tab B. 24 | * -- 25 | * ==== 26 | * 27 | * @author Dan Allen 28 | */ 29 | const IdSeparatorCh = "-"; 30 | const ExtraIdSeparatorsRx = /^-+|-+$|-(-)+/g; 31 | const InvalidIdCharsRx = /[^a-zA-Z0-9_]/g; 32 | const List = Opal.const_get_local(Opal.module(null, "Asciidoctor"), "List"); 33 | const ListItem = Opal.const_get_local( 34 | Opal.module(null, "Asciidoctor"), 35 | "ListItem" 36 | ); 37 | 38 | const generateId = (str, idx) => 39 | `tabset${idx}_${str 40 | .toLowerCase() 41 | .replace(InvalidIdCharsRx, IdSeparatorCh) 42 | .replace(ExtraIdSeparatorsRx, "$1")}`; 43 | 44 | function tabsBlock() { 45 | this.onContext("example"); 46 | this.process((parent, reader, attrs) => { 47 | const createHtmlFragment = html => this.createBlock(parent, "pass", html); 48 | const tabsetIdx = parent.getDocument().counter("idx-tabset"); 49 | const nodes = []; 50 | nodes.push(createHtmlFragment('
')); 51 | const container = this.parseContent( 52 | this.createBlock(parent, "open"), 53 | reader 54 | ); 55 | const sourceTabs = container.getBlocks()[0]; 56 | if ( 57 | !( 58 | sourceTabs && 59 | sourceTabs.getContext() === "dlist" && 60 | sourceTabs.getItems().length 61 | ) 62 | ) 63 | return; 64 | const tabs = List.$new(parent, "ulist"); 65 | tabs.addRole("tabs"); 66 | const panes = {}; 67 | sourceTabs.getItems().forEach(([[title], details]) => { 68 | const tab = ListItem.$new(tabs); 69 | tabs.$append(tab); 70 | const id = generateId(title.getText(), tabsetIdx); 71 | tab.text = `[[${id}]]${title.text}`; 72 | let blocks = details.getBlocks(); 73 | const numBlocks = blocks.length; 74 | if (numBlocks) { 75 | if (blocks[0].context === "open" && numBlocks === 1) 76 | blocks = blocks[0].getBlocks(); 77 | panes[id] = blocks.map(block => (block.parent = parent) && block); 78 | } 79 | }); 80 | nodes.push(tabs); 81 | nodes.push(createHtmlFragment('
')); 82 | Object.entries(panes).forEach(([id, blocks]) => { 83 | nodes.push( 84 | createHtmlFragment(`
`) 85 | ); 86 | nodes.push(...blocks); 87 | nodes.push(createHtmlFragment("
")); 88 | }); 89 | nodes.push(createHtmlFragment("
")); 90 | nodes.push(createHtmlFragment("
")); 91 | parent.blocks.push(...nodes); 92 | }); 93 | } 94 | 95 | function register(registry, context) { 96 | registry.block("tabs", tabsBlock); 97 | } 98 | 99 | module.exports.register = register; 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quarkus-tutorial-site", 3 | "description": "Quarkus Tutorial Documentation", 4 | "homepage": "https://redhat-developer-demos.github.io/quarkus-tutorial", 5 | "author": { 6 | "email": "rhd@redhat.com", 7 | "name": "Red Hat Developer", 8 | "url": "https://developers.redhat.com" 9 | }, 10 | "dependencies": { 11 | "@antora/cli": "2.3.1", 12 | "@antora/site-generator-default": "2.3.1", 13 | "@babel/cli": "^7.5.5", 14 | "@babel/core": "^7.5.5", 15 | "@babel/polyfill": "^7.4.4", 16 | "@babel/preset-env": "^7.5.5", 17 | "@babel/register": "^7.5.5", 18 | "browser-sync": "^2.26.7", 19 | "fs-extra": "^8.1.0", 20 | "gulp": "^4.0.2", 21 | "yaml-js": "^0.2.3" 22 | }, 23 | "devDependencies": {}, 24 | "scripts": { 25 | "dev": "gulp", 26 | "clean": "gulp clean", 27 | "workshop": "gulp workshopSite" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/redhat-developer-demos/quarkus-tutorial.git" 32 | }, 33 | "license": "Apache-2.0", 34 | "babel": { 35 | "presets": [ 36 | "@babel/preset-env" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /supplemental-ui/img/clippy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /supplemental-ui/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/48ccaabad4b77732c223bd93079ef169afdecc2a/supplemental-ui/img/favicon.ico -------------------------------------------------------------------------------- /supplemental-ui/partials/footer-content.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/app.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: $APP_NAME 6 | spec: 7 | selector: 8 | app: $APP_NAME 9 | ports: 10 | - port: 8080 11 | targetPort: 8080 12 | type: NodePort 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: $APP_NAME 18 | spec: 19 | selector: 20 | matchLabels: 21 | app: $APP_NAME 22 | template: 23 | metadata: 24 | labels: 25 | app: $APP_NAME 26 | spec: 27 | containers: 28 | - name: quarkus 29 | image: docker.io/$REGISTRY_USER_NAME/$APP_IMAGE 30 | ports: 31 | - name: http-web 32 | containerPort: 8080 33 | resources: 34 | limits: 35 | memory: "128Mi" 36 | cpu: "500m" 37 | -------------------------------------------------------------------------------- /templates/custom-probes.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | template: 3 | spec: 4 | containers: 5 | - name: quarkus 6 | livenessProbe: 7 | httpGet: 8 | path: /health/live 9 | port: 8080 10 | readinessProbe: 11 | httpGet: 12 | path: /health/ready 13 | port: 8080 -------------------------------------------------------------------------------- /templates/probes.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | template: 3 | spec: 4 | containers: 5 | - name: quarkus 6 | livenessProbe: 7 | httpGet: 8 | path: /health 9 | port: 8080 10 | readinessProbe: 11 | httpGet: 12 | path: /health 13 | port: 8080 --------------------------------------------------------------------------------