├── .github ├── dependabot.yml └── workflows │ └── gradle.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── openapi-route-definition-locator.common-java-library.gradle.kts ├── docs └── images │ └── overview.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── openapi-route-definition-locator-bom └── build.gradle.kts ├── openapi-route-definition-locator-core ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── net │ │ └── bretti │ │ └── openapi │ │ └── route │ │ └── definition │ │ └── locator │ │ └── core │ │ ├── config │ │ ├── OpenApiRouteDefinitionLocatorProperties.java │ │ └── validation │ │ │ ├── OnlyUniqueServiceIds.java │ │ │ ├── OnlyUniqueServiceIdsValidator.java │ │ │ ├── ValidBaseUri.java │ │ │ ├── ValidBaseUriValidator.java │ │ │ ├── ValidOpenApiDefinitionUri.java │ │ │ └── ValidOpenApiDefinitionUriValidator.java │ │ ├── customizer │ │ └── OpenApiRouteDefinitionCustomizer.java │ │ └── impl │ │ ├── OpenApiDefinitionRepository.java │ │ ├── OpenApiDefinitionUpdateScheduler.java │ │ ├── OpenApiOperation.java │ │ ├── OpenApiRouteDefinitionLocator.java │ │ ├── OpenApiRouteDefinitionLocatorMetrics.java │ │ ├── OpenApiRouteDefinitionLocatorTimedMetrics.java │ │ ├── OpenApiRouteDefinitionPublishException.java │ │ └── utils │ │ ├── MapMerge.java │ │ └── Optionals.java │ └── test │ └── groovy │ └── net │ └── bretti │ └── openapi │ └── route │ └── definition │ └── locator │ └── core │ └── impl │ └── utils │ └── MapMergeTest.groovy ├── openapi-route-definition-locator-spring-cloud-starter ├── build.gradle.kts └── src │ ├── main │ ├── java │ │ └── net │ │ │ └── bretti │ │ │ └── openapi │ │ │ └── route │ │ │ └── definition │ │ │ └── locator │ │ │ └── autoconfigure │ │ │ ├── OpenApiRouteDefinitionLocatorAutoConfiguration.java │ │ │ └── OpenApiRouteDefinitionLocatorMetricsAutoConfiguration.java │ └── resources │ │ └── META-INF │ │ └── spring │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── test │ ├── groovy │ ├── componenttest │ │ ├── CustomGlobalOpenApiDefinitionUrlCompTest.groovy │ │ ├── OpenApiRouteDefinitionLocatorCompTest.groovy │ │ └── setup │ │ │ ├── app │ │ │ ├── TestApiGatewayApplication.groovy │ │ │ └── XAuthTypeRouteDefinitionCustomizer.groovy │ │ │ ├── basetest │ │ │ └── BaseCompTest.groovy │ │ │ └── wiremock │ │ │ ├── BaseWireMock.groovy │ │ │ ├── OpenapiDefinitionServedFromDifferentHostServiceMock1.groovy │ │ │ ├── OpenapiDefinitionServedFromDifferentHostServiceMock2.groovy │ │ │ ├── OrderServiceMock.groovy │ │ │ └── UserServiceMock.groovy │ └── net │ │ └── bretti │ │ └── openapi │ │ └── route │ │ └── definition │ │ └── locator │ │ └── autoconfigure │ │ ├── OpenApiRouteDefinitionLocatorAutoConfigurationTest.groovy │ │ └── OpenApiRouteDefinitionLocatorMetricsAutoConfigurationTest.groovy │ └── resources │ ├── application-custom-global-openapi-definition-url.yml │ ├── application.yml │ ├── openapi-definition-in-classpath-service │ └── openapi.public.yaml │ └── wiremock │ └── __files │ ├── openapi-definition-served-from-different-host-service │ └── openapi.public.yaml │ ├── order-service │ ├── openapi.public.unknown-filter.yaml │ └── openapi.public.yaml │ └── user-service │ └── openapi.public.yaml ├── sample-apps ├── README.md ├── Taskfile.yml ├── api-gateway │ ├── build.gradle.kts │ ├── helm │ │ ├── Chart.yaml │ │ ├── files │ │ │ └── dashboards │ │ │ │ ├── spring-boot-dashboard.json │ │ │ │ └── spring-cloud-gateway-dashboard.json │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── configmap.yaml │ │ │ ├── deployment.yaml │ │ │ ├── grafana-dashboards.yaml │ │ │ ├── ingress.yaml │ │ │ ├── service.yaml │ │ │ └── servicemonitor.yaml │ │ └── values.yaml │ └── src │ │ └── main │ │ ├── java │ │ └── net │ │ │ └── bretti │ │ │ └── sample │ │ │ └── apigateway │ │ │ ├── ApiGatewayApplication.java │ │ │ └── customizer │ │ │ └── SampleOpenApiRouteDefinitionCustomizer.java │ │ └── resources │ │ └── application.yml ├── service-orders │ ├── build.gradle.kts │ ├── helm │ │ ├── Chart.yaml │ │ ├── templates │ │ │ ├── configmap.yaml │ │ │ ├── deployment.yaml │ │ │ ├── service.yaml │ │ │ └── servicemonitor.yaml │ │ └── values.yaml │ └── src │ │ └── main │ │ ├── java │ │ └── net │ │ │ └── bretti │ │ │ └── sample │ │ │ └── service │ │ │ └── orders │ │ │ ├── ServiceOrdersApplication.java │ │ │ ├── controller │ │ │ ├── OpenApiDefinitionController.java │ │ │ └── OrdersController.java │ │ │ └── dto │ │ │ ├── Order.java │ │ │ └── OrderItem.java │ │ └── resources │ │ ├── application.yml │ │ └── openapi.public.yaml └── service-users │ ├── build.gradle.kts │ ├── helm │ ├── Chart.yaml │ ├── templates │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── service.yaml │ │ └── servicemonitor.yaml │ └── values.yaml │ └── src │ └── main │ ├── java │ └── net │ │ └── bretti │ │ └── sample │ │ └── service │ │ └── users │ │ ├── ServiceUsersApplication.java │ │ ├── controller │ │ ├── OpenApiDefinitionController.java │ │ └── UsersController.java │ │ └── dto │ │ └── User.java │ └── resources │ ├── application.yml │ └── openapi.public.yaml └── settings.gradle.kts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 7 | 8 | name: Java CI with Gradle 9 | 10 | on: 11 | push: 12 | branches: [ "master" ] 13 | pull_request: 14 | branches: [ "master" ] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up JDK 17 27 | uses: actions/setup-java@v3 28 | with: 29 | java-version: '17' 30 | distribution: 'temurin' 31 | - name: Build with Gradle 32 | uses: gradle/gradle-build-action@v2 33 | with: 34 | arguments: build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | build/ 4 | out/ 5 | classes/ 6 | gradle.properties 7 | *.jfr 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jan Bretschneider 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | defaultTasks("clean", "build") 2 | 3 | tasks.wrapper { 4 | gradleVersion = "8.11.1" 5 | distributionType = Wrapper.DistributionType.ALL 6 | } 7 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | } 8 | 9 | dependencies { 10 | implementation("org.springframework.boot:spring-boot-gradle-plugin:3.4.0") 11 | implementation("io.spring.gradle:dependency-management-plugin:1.0.11.RELEASE") 12 | implementation("com.github.ben-manes:gradle-versions-plugin:0.39.0") 13 | } 14 | 15 | java { 16 | toolchain { 17 | // Keep the same Java compatibility as Spring Cloud Gateway. 18 | languageVersion.set(JavaLanguageVersion.of(17)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/openapi-route-definition-locator.common-java-library.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | `maven-publish` 4 | signing 5 | idea 6 | groovy 7 | id("com.github.ben-manes.versions") 8 | } 9 | 10 | apply(plugin = "io.spring.dependency-management") 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | the().apply { 17 | imports { 18 | mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) 19 | } 20 | } 21 | 22 | val springCloudDependenciesVersion = "2024.0.0" 23 | val lombokVersion = "1.18.36" 24 | val spockVersion = "2.4-M1-groovy-4.0" 25 | 26 | dependencies { 27 | implementation(platform("org.springframework.cloud:spring-cloud-dependencies:${springCloudDependenciesVersion}")) 28 | 29 | compileOnly("org.projectlombok:lombok:${lombokVersion}") 30 | annotationProcessor("org.projectlombok:lombok:${lombokVersion}") 31 | testImplementation("org.projectlombok:lombok:${lombokVersion}") 32 | testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") 33 | } 34 | 35 | java { 36 | group = "net.bretti.openapi-route-definition-locator" 37 | version = "1.0.1-sc-2024.0-SNAPSHOT" 38 | toolchain { 39 | // Keep the same Java compatibility as Spring Cloud Gateway. 40 | languageVersion.set(JavaLanguageVersion.of(17)) 41 | } 42 | withJavadocJar() 43 | withSourcesJar() 44 | } 45 | 46 | testing { 47 | suites { 48 | val test by getting(JvmTestSuite::class) { 49 | useJUnitJupiter() 50 | 51 | dependencies { 52 | implementation("org.springframework.boot:spring-boot-starter-test") 53 | implementation(platform("org.spockframework:spock-bom:${spockVersion}")) 54 | implementation("org.spockframework:spock-spring") 55 | implementation("org.apache.groovy:groovy-json") 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/49c80e2e2a242896f757528f011404028972c473/docs/images/overview.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/49c80e2e2a242896f757528f011404028972c473/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-bom/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-platform` 3 | `maven-publish` 4 | signing 5 | } 6 | 7 | javaPlatform { 8 | allowDependencies() 9 | } 10 | 11 | dependencies { 12 | constraints { 13 | api(project(":openapi-route-definition-locator-core")) 14 | api(project(":openapi-route-definition-locator-spring-cloud-starter")) 15 | } 16 | } 17 | 18 | javaPlatform { 19 | group = "net.bretti.openapi-route-definition-locator" 20 | version = "1.0.1-sc-2024.0-SNAPSHOT" 21 | } 22 | 23 | publishing { 24 | publications { 25 | create("mavenJava") { 26 | artifactId = "openapi-route-definition-locator-bom" 27 | from(components["javaPlatform"]) 28 | versionMapping { 29 | usage("java-runtime") { 30 | fromResolutionResult() 31 | } 32 | } 33 | pom { 34 | name.set("openapi-route-definition-locator-bom") 35 | description.set("Bill of materials for the OpenAPI Route Definition Locator") 36 | url.set("https://github.com/jbretsch/openapi-route-definition-locator") 37 | licenses { 38 | license { 39 | name.set("MIT License") 40 | url.set("https://github.com/jbretsch/openapi-route-definition-locator/blob/master/LICENSE") 41 | } 42 | } 43 | developers { 44 | developer { 45 | id.set("jbretsch") 46 | name.set("Jan Bretschneider") 47 | email.set("mail@jan-bretschneider.de") 48 | } 49 | } 50 | scm { 51 | connection.set("scm:git:git://github.com/jbretsch/openapi-route-definition-locator.git") 52 | developerConnection.set("scm:git:ssh://github.com/jbretsch/openapi-route-definition-locator.git") 53 | url.set("https://github.com/jbretsch/openapi-route-definition-locator") 54 | } 55 | } 56 | } 57 | } 58 | repositories { 59 | maven { 60 | name = "ossrh" 61 | credentials(PasswordCredentials::class) 62 | val releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 63 | val snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" 64 | url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) 65 | } 66 | } 67 | } 68 | 69 | signing { 70 | sign(publishing.publications["mavenJava"]) 71 | } 72 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("openapi-route-definition-locator.common-java-library") 3 | } 4 | 5 | dependencies { 6 | compileOnly("io.micrometer:micrometer-core") 7 | implementation("org.springframework.cloud:spring-cloud-gateway-server") 8 | implementation("org.springframework:spring-webflux") 9 | implementation("io.swagger.parser.v3:swagger-parser:2.1.24") 10 | annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") 11 | } 12 | 13 | publishing { 14 | publications { 15 | create("mavenJava") { 16 | artifactId = "openapi-route-definition-locator-core" 17 | from(components["java"]) 18 | versionMapping { 19 | usage("java-api") { 20 | fromResolutionOf("runtimeClasspath") 21 | } 22 | usage("java-runtime") { 23 | fromResolutionResult() 24 | } 25 | } 26 | pom { 27 | name.set("openapi-route-definition-locator-core") 28 | description.set("Core library for the OpenAPI Route Definition Locator") 29 | url.set("https://github.com/jbretsch/openapi-route-definition-locator") 30 | licenses { 31 | license { 32 | name.set("MIT License") 33 | url.set("https://github.com/jbretsch/openapi-route-definition-locator/blob/master/LICENSE") 34 | } 35 | } 36 | developers { 37 | developer { 38 | id.set("jbretsch") 39 | name.set("Jan Bretschneider") 40 | email.set("mail@jan-bretschneider.de") 41 | } 42 | } 43 | scm { 44 | connection.set("scm:git:git://github.com/jbretsch/openapi-route-definition-locator.git") 45 | developerConnection.set("scm:git:ssh://github.com/jbretsch/openapi-route-definition-locator.git") 46 | url.set("https://github.com/jbretsch/openapi-route-definition-locator") 47 | } 48 | } 49 | } 50 | } 51 | repositories { 52 | maven { 53 | name = "ossrh" 54 | credentials(PasswordCredentials::class) 55 | val releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 56 | val snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" 57 | url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) 58 | } 59 | } 60 | } 61 | 62 | signing { 63 | sign(publishing.publications["mavenJava"]) 64 | } 65 | 66 | // Remove entries from published POM. 67 | // Inspired by . 68 | tasks.withType().all { 69 | doLast { 70 | val file = layout.buildDirectory.file("publications/mavenJava/pom-default.xml").get().asFile 71 | var text = file.readText() 72 | val regex = "(?s)(.+?)(.+?)(.+?)".toRegex() 73 | val matcher = regex.find(text) 74 | if (matcher != null) { 75 | text = regex.replace(text, "") 76 | } 77 | file.writeText(text) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/config/OpenApiRouteDefinitionLocatorProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.config; 20 | 21 | import lombok.Data; 22 | import net.bretti.openapi.route.definition.locator.core.config.validation.OnlyUniqueServiceIds; 23 | import net.bretti.openapi.route.definition.locator.core.config.validation.ValidBaseUri; 24 | import net.bretti.openapi.route.definition.locator.core.config.validation.ValidOpenApiDefinitionUri; 25 | import org.springframework.boot.context.properties.ConfigurationProperties; 26 | import org.springframework.cloud.gateway.filter.FilterDefinition; 27 | import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; 28 | import org.springframework.cloud.gateway.route.RouteDefinition; 29 | import org.springframework.validation.annotation.Validated; 30 | 31 | import jakarta.validation.Valid; 32 | import jakarta.validation.constraints.NotBlank; 33 | import jakarta.validation.constraints.NotNull; 34 | import java.net.URI; 35 | import java.time.Duration; 36 | import java.time.temporal.ChronoUnit; 37 | import java.util.ArrayList; 38 | import java.util.HashMap; 39 | import java.util.List; 40 | import java.util.Map; 41 | import java.util.Optional; 42 | 43 | @ConfigurationProperties(prefix = "openapi-route-definition-locator") 44 | @Validated 45 | @Data 46 | public class OpenApiRouteDefinitionLocatorProperties { 47 | 48 | private static final String DEFAULT_OPENAPI_DEFINITION_URI = "/internal/openapi-definition"; 49 | 50 | /** 51 | * List of services for routes should be registered in the gateway based on their 52 | * OpenAPI definitions. 53 | */ 54 | @Valid 55 | @OnlyUniqueServiceIds 56 | private List services = new ArrayList<>(); 57 | 58 | /** 59 | * Settings that should be added to all {@link RouteDefinition}s created for the configured 60 | * {@link OpenApiRouteDefinitionLocatorProperties#services}. 61 | */ 62 | @Valid 63 | private DefaultRouteSettings defaultRouteSettings = new DefaultRouteSettings(); 64 | 65 | /** 66 | * Configures the scheduler which periodically retrieves the OpenAPI definitions from 67 | * the configured services. 68 | */ 69 | @Valid 70 | private UpdateScheduler updateScheduler = new UpdateScheduler(); 71 | 72 | /** 73 | * The URI of the OpenAPI definitions to be retrieved from the configured services. 74 | * This generally is a relative URI; relative to the base URI of each configured service. 75 | * The default is "/internal/openapi-definition". 76 | */ 77 | @ValidOpenApiDefinitionUri 78 | private URI openapiDefinitionUri = URI.create(DEFAULT_OPENAPI_DEFINITION_URI); 79 | 80 | @Data 81 | public static class Service { 82 | 83 | /** 84 | * Identifier of the service. 85 | */ 86 | @NotBlank 87 | private String id; 88 | 89 | /** 90 | * Base URI of the service. 91 | */ 92 | @NotNull 93 | @ValidBaseUri 94 | private URI uri; 95 | 96 | /** 97 | * The URI of the OpenAPI definition to be retrieved from the service. 98 | * This generally is a relative URI; relative to the service's base URI. 99 | * But it can also be an absolute URI. As the OpenAPI definition is loaded 100 | * via Spring's 101 | * ResourceLoader, you can use schemas such as {@code http:}, {@code https:}, {@code file:} or 102 | * {@code classpath:}. The default is the value of the property 103 | * {@code openapi-route-definition-locator.openapi-definition-uri}. 104 | */ 105 | @ValidOpenApiDefinitionUri 106 | private URI openapiDefinitionUri; 107 | 108 | /** 109 | * Settings that should be applied to all {@link RouteDefinition}s created for this service. 110 | */ 111 | @Valid 112 | private DefaultRouteSettings defaultRouteSettings = new DefaultRouteSettings(); 113 | } 114 | 115 | @Data 116 | public static class UpdateScheduler { 117 | 118 | /** 119 | * Fixed delay between runs to retrieve the services' OpenAPI definitions. 120 | * If no timeunit is given, milliseconds are used. 121 | */ 122 | @NotNull 123 | private Duration fixedDelay = Duration.of(5, ChronoUnit.MINUTES); 124 | 125 | /** 126 | * When an error occurs while retrieving a service's OpenAPI definition, its registered routes/operations 127 | * are not immediately de-registered. They are only de-registered if there was no successful retrieval 128 | * for the amount of time configured here. If no timeunit is given, milliseconds are used. 129 | */ 130 | @NotNull 131 | private Duration removeRoutesOnUpdateFailuresAfter = Duration.of(15, ChronoUnit.MINUTES); 132 | } 133 | 134 | /** 135 | * Settings that should be applied to all created {@link RouteDefinition}s. Contains a subset of the attributes of a 136 | * {@link RouteDefinition}. 137 | */ 138 | @Data 139 | public static class DefaultRouteSettings { 140 | /** 141 | * The predicates that should be added to the created {@link RouteDefinition}s. 142 | */ 143 | @Valid 144 | private List predicates = new ArrayList<>(); 145 | 146 | /** 147 | * The filters that should be added to the created {@link RouteDefinition}s. 148 | */ 149 | @Valid 150 | private List filters = new ArrayList<>(); 151 | 152 | /** 153 | * The metadata that should be added to the created {@link RouteDefinition}s. 154 | */ 155 | private Map metadata = new HashMap<>(); 156 | 157 | /** 158 | * The order that should be applied to the created {@link RouteDefinition}s. 159 | */ 160 | private Optional order = Optional.empty(); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/config/validation/OnlyUniqueServiceIds.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.config.validation; 20 | 21 | import jakarta.validation.Constraint; 22 | import jakarta.validation.Payload; 23 | import java.lang.annotation.Documented; 24 | import java.lang.annotation.ElementType; 25 | import java.lang.annotation.Retention; 26 | import java.lang.annotation.RetentionPolicy; 27 | import java.lang.annotation.Target; 28 | 29 | @Documented 30 | @Constraint(validatedBy = OnlyUniqueServiceIdsValidator.class) 31 | @Target({ElementType.FIELD}) 32 | @Retention(RetentionPolicy.RUNTIME) 33 | public @interface OnlyUniqueServiceIds { 34 | String message() default "Contains duplicate service ids."; 35 | Class[] groups() default {}; 36 | Class[] payload() default {}; 37 | } 38 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/config/validation/OnlyUniqueServiceIdsValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.config.validation; 20 | 21 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties.Service; 22 | 23 | import jakarta.validation.ConstraintValidator; 24 | import jakarta.validation.ConstraintValidatorContext; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.stream.Collectors; 28 | 29 | import static java.lang.String.format; 30 | 31 | public class OnlyUniqueServiceIdsValidator implements ConstraintValidator> { 32 | @Override 33 | public boolean isValid(List services, ConstraintValidatorContext context) { 34 | Map countByServiceId = services.stream().collect( 35 | Collectors.groupingBy(Service::getId, Collectors.counting())); 36 | 37 | String duplicateServiceIds = countByServiceId.entrySet().stream() 38 | .filter(entry -> entry.getValue() > 1) 39 | .map(Map.Entry::getKey) 40 | .collect(Collectors.joining(",")); 41 | 42 | if (duplicateServiceIds.isEmpty()) { 43 | return true; 44 | } 45 | 46 | context.disableDefaultConstraintViolation(); 47 | context.buildConstraintViolationWithTemplate(format("Contains duplicate service ids: %s", duplicateServiceIds)) 48 | .addConstraintViolation(); 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/config/validation/ValidBaseUri.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.config.validation; 20 | 21 | import jakarta.validation.Constraint; 22 | import jakarta.validation.Payload; 23 | import java.lang.annotation.Documented; 24 | import java.lang.annotation.ElementType; 25 | import java.lang.annotation.Retention; 26 | import java.lang.annotation.RetentionPolicy; 27 | import java.lang.annotation.Target; 28 | 29 | @Documented 30 | @Constraint(validatedBy = ValidBaseUriValidator.class) 31 | @Target({ElementType.FIELD}) 32 | @Retention(RetentionPolicy.RUNTIME) 33 | public @interface ValidBaseUri { 34 | String message() default "Is invalid base URI."; 35 | Class[] groups() default {}; 36 | Class[] payload() default {}; 37 | } 38 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/config/validation/ValidBaseUriValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.config.validation; 20 | 21 | import jakarta.validation.ConstraintValidator; 22 | import jakarta.validation.ConstraintValidatorContext; 23 | import java.net.URI; 24 | 25 | import static org.apache.commons.lang3.StringUtils.isNotEmpty; 26 | 27 | public class ValidBaseUriValidator implements ConstraintValidator { 28 | @Override 29 | public boolean isValid(URI uri, ConstraintValidatorContext context) { 30 | if (uri == null) { 31 | // Covered by @NotNull. 32 | return true; 33 | } 34 | 35 | if (!uri.isAbsolute()) { 36 | setConstraintViolation(context, "Must be an absolute URI."); 37 | return false; 38 | } 39 | 40 | if (isNotEmpty(uri.getPath()) && !"/".equals(uri.getPath())) { 41 | setConstraintViolation(context, "Path must be empty or '/'."); 42 | return false; 43 | } 44 | 45 | if (isNotEmpty(uri.getQuery())) { 46 | setConstraintViolation(context, "Must have no query parameters."); 47 | return false; 48 | } 49 | 50 | if (isNotEmpty(uri.getFragment())) { 51 | setConstraintViolation(context, "Must have no fragment part."); 52 | return false; 53 | } 54 | 55 | return true; 56 | } 57 | 58 | private static void setConstraintViolation(ConstraintValidatorContext context, String messageTemplate) { 59 | context.disableDefaultConstraintViolation(); 60 | context.buildConstraintViolationWithTemplate(messageTemplate) 61 | .addConstraintViolation(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/config/validation/ValidOpenApiDefinitionUri.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.config.validation; 20 | 21 | import jakarta.validation.Constraint; 22 | import jakarta.validation.Payload; 23 | import java.lang.annotation.Documented; 24 | import java.lang.annotation.ElementType; 25 | import java.lang.annotation.Retention; 26 | import java.lang.annotation.RetentionPolicy; 27 | import java.lang.annotation.Target; 28 | 29 | @Documented 30 | @Constraint(validatedBy = ValidOpenApiDefinitionUriValidator.class) 31 | @Target({ElementType.FIELD}) 32 | @Retention(RetentionPolicy.RUNTIME) 33 | public @interface ValidOpenApiDefinitionUri { 34 | String message() default "Is invalid OpenAPI definition URI."; 35 | Class[] groups() default {}; 36 | Class[] payload() default {}; 37 | } 38 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/config/validation/ValidOpenApiDefinitionUriValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.config.validation; 20 | 21 | import jakarta.validation.ConstraintValidator; 22 | import jakarta.validation.ConstraintValidatorContext; 23 | import java.net.URI; 24 | 25 | public class ValidOpenApiDefinitionUriValidator implements ConstraintValidator { 26 | @Override 27 | public boolean isValid(URI uri, ConstraintValidatorContext context) { 28 | if (uri == null) { 29 | // Covered by @NotNull. 30 | return true; 31 | } 32 | 33 | if (uri.isAbsolute() || uri.getPath().startsWith("/")) { 34 | return true; 35 | } 36 | 37 | setConstraintViolation(context, "Must be absolute or start with '/'."); 38 | return false; 39 | } 40 | 41 | private static void setConstraintViolation(ConstraintValidatorContext context, String messageTemplate) { 42 | context.disableDefaultConstraintViolation(); 43 | context.buildConstraintViolationWithTemplate(messageTemplate) 44 | .addConstraintViolation(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/customizer/OpenApiRouteDefinitionCustomizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.customizer; 20 | 21 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 22 | import org.springframework.cloud.gateway.route.RouteDefinition; 23 | 24 | import java.util.Map; 25 | 26 | @FunctionalInterface 27 | public interface OpenApiRouteDefinitionCustomizer { 28 | void customize(RouteDefinition routeDefinition, 29 | OpenApiRouteDefinitionLocatorProperties.Service service, 30 | Map openApiGlobalExtensions, 31 | Map openApiOperationExtensions); 32 | } 33 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/OpenApiDefinitionRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl; 20 | 21 | import io.swagger.parser.OpenAPIParser; 22 | import io.swagger.v3.oas.models.OpenAPI; 23 | import io.swagger.v3.oas.models.PathItem; 24 | import io.swagger.v3.parser.core.models.SwaggerParseResult; 25 | import lombok.Getter; 26 | import lombok.RequiredArgsConstructor; 27 | import lombok.extern.slf4j.Slf4j; 28 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 29 | import net.bretti.openapi.route.definition.locator.core.impl.utils.MapMerge; 30 | import org.apache.commons.lang3.StringUtils; 31 | import org.springframework.cloud.gateway.event.RefreshRoutesEvent; 32 | import org.springframework.cloud.gateway.event.RefreshRoutesResultEvent; 33 | import org.springframework.cloud.gateway.filter.FilterDefinition; 34 | import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; 35 | import org.springframework.context.ApplicationEventPublisher; 36 | import org.springframework.context.ApplicationListener; 37 | import org.springframework.core.io.Resource; 38 | import org.springframework.core.io.ResourceLoader; 39 | import org.springframework.http.HttpMethod; 40 | import org.springframework.lang.NonNull; 41 | import org.springframework.util.CollectionUtils; 42 | import org.springframework.util.StreamUtils; 43 | 44 | import java.io.IOException; 45 | import java.io.InputStream; 46 | import java.net.URI; 47 | import java.nio.charset.StandardCharsets; 48 | import java.time.Duration; 49 | import java.time.Instant; 50 | import java.util.ArrayList; 51 | import java.util.Arrays; 52 | import java.util.Collections; 53 | import java.util.List; 54 | import java.util.Map; 55 | import java.util.Optional; 56 | import java.util.concurrent.ConcurrentHashMap; 57 | import java.util.concurrent.TimeUnit; 58 | import java.util.function.Function; 59 | import java.util.stream.Collectors; 60 | 61 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_NAME_UPDATES; 62 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPDATE_RESULT; 63 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPDATE_RESULT_DETAILED; 64 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPDATE_RESULT_DETAILED_FAILURE_PUBLICATION; 65 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPDATE_RESULT_DETAILED_FAILURE_RETRIEVAL; 66 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPDATE_RESULT_DETAILED_SUCCESS_WITHOUT_CHANGES; 67 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPDATE_RESULT_DETAILED_SUCCESS_WITH_CHANGES; 68 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPDATE_RESULT_FAILURE; 69 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPDATE_RESULT_SUCCESS; 70 | import static net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics.METRIC_TAG_UPSTREAM_SERVICE; 71 | import static org.apache.commons.lang3.ObjectUtils.firstNonNull; 72 | 73 | @RequiredArgsConstructor 74 | @Slf4j 75 | public class OpenApiDefinitionRepository implements ApplicationListener { 76 | private static final String X_GATEWAY_ROUTE_SETTINGS = "x-gateway-route-settings"; 77 | private static final String FILTERS = "filters"; 78 | private static final String PREDICATES = "predicates"; 79 | private static final String ORDER = "order"; 80 | private static final String METADATA = "metadata"; 81 | 82 | private final OpenApiRouteDefinitionLocatorProperties config; 83 | 84 | @Getter 85 | private final ConcurrentHashMap> operations; 86 | private final ConcurrentHashMap firstRetrievalFailures; 87 | private final ApplicationEventPublisher applicationEventPublisher; 88 | private final Optional metrics; 89 | private final ResourceLoader resourceLoader; 90 | private Throwable lastRouteDefinitionPublicationFailureCause; 91 | 92 | void getOpenApiDefinitions() { 93 | config.getServices().forEach(this::getAndUpdateOperationsSafely); 94 | } 95 | 96 | int getRegisteredOperationsCount(OpenApiRouteDefinitionLocatorProperties.Service service) { 97 | return Optional.ofNullable(operations.get(service)).orElse(Collections.emptyList()).size(); 98 | } 99 | 100 | private void getAndUpdateOperationsSafely(OpenApiRouteDefinitionLocatorProperties.Service service) { 101 | try { 102 | getAndUpdateOperations(service); 103 | } catch (Exception e) { 104 | log.error("Unexpected error while retrieving and publishing REST operations for {}", service.getId(), e); 105 | } 106 | } 107 | 108 | private void getAndUpdateOperations(OpenApiRouteDefinitionLocatorProperties.Service service) { 109 | long start = System.nanoTime(); 110 | List oldOpenApiOperations = operations.get(service); 111 | try { 112 | log.info("Getting list of operations for {}", service.getId()); 113 | List newOpenApiOperations = getOperations(service); 114 | 115 | if (newOpenApiOperations.equals(oldOpenApiOperations)) { 116 | log.info("List of {} operations is unchanged for {}", oldOpenApiOperations.size(), service.getId()); 117 | firstRetrievalFailures.remove(service); 118 | metricsRecordRetrievalResult(service, METRIC_TAG_UPDATE_RESULT_SUCCESS, 119 | METRIC_TAG_UPDATE_RESULT_DETAILED_SUCCESS_WITHOUT_CHANGES, start); 120 | return; 121 | } 122 | 123 | log.info("Got new list of {} operations for {}", newOpenApiOperations.size(), service.getId()); 124 | operations.put(service, newOpenApiOperations); 125 | publishNewOpenApiOperationsAndRollbackOnFailure(service, oldOpenApiOperations); 126 | 127 | // Only reached if no rollback was performed. 128 | firstRetrievalFailures.remove(service); 129 | metricsRecordRetrievalResult(service, METRIC_TAG_UPDATE_RESULT_SUCCESS, 130 | METRIC_TAG_UPDATE_RESULT_DETAILED_SUCCESS_WITH_CHANGES, start); 131 | } catch (Exception e) { 132 | String updateResultFailureDetailed = e instanceof OpenApiRouteDefinitionPublishException 133 | ? METRIC_TAG_UPDATE_RESULT_DETAILED_FAILURE_PUBLICATION 134 | : METRIC_TAG_UPDATE_RESULT_DETAILED_FAILURE_RETRIEVAL; 135 | metricsRecordRetrievalResult(service, METRIC_TAG_UPDATE_RESULT_FAILURE, updateResultFailureDetailed, start); 136 | log.error("Error while retrieving and publishing REST operations for {}", service.getId(), e); 137 | Instant now = Instant.now(); 138 | Instant firstRetrievalFailure = firstRetrievalFailures.computeIfAbsent(service, k -> now); 139 | 140 | if (CollectionUtils.isEmpty(oldOpenApiOperations)) { 141 | log.error("Retrieving and publishing operations for {} keeps failing since {}. Currently, no operations for this " + 142 | "service are registered.", service.getId(), firstRetrievalFailure); 143 | return; 144 | } 145 | 146 | Duration removeAfterDuration = config.getUpdateScheduler().getRemoveRoutesOnUpdateFailuresAfter(); 147 | Instant removeAfterInstant = firstRetrievalFailure.plus(removeAfterDuration); 148 | 149 | if (now.isAfter(removeAfterInstant)) { 150 | operations.remove(service); 151 | log.error("De-registering operations of {}. First retrieval/publishing failure was at {}. " + 152 | "That is more than {} ago.", service.getId(), firstRetrievalFailure, removeAfterDuration); 153 | publishNewOpenApiOperations(service); 154 | return; 155 | } 156 | 157 | log.error("Keeping operations of {} despite retrieval/publishing failure. First failure was at {}. " + 158 | "That is less than {} ago. If attempts keep failing, operations of that service will be " + 159 | "de-registered after {}.", service.getId(), firstRetrievalFailure, removeAfterDuration, 160 | removeAfterInstant); 161 | } 162 | } 163 | 164 | private void metricsRecordRetrievalResult( 165 | OpenApiRouteDefinitionLocatorProperties.Service service, 166 | String metricUpdateResult, 167 | String metricUpdateResultDetailed, 168 | long startNanoTime 169 | ) { 170 | metrics.ifPresent(metrics1 -> { 171 | long endNanoTime = System.nanoTime(); 172 | metrics1.recordTime(METRIC_NAME_UPDATES, (endNanoTime- startNanoTime), TimeUnit.NANOSECONDS, 173 | METRIC_TAG_UPDATE_RESULT, metricUpdateResult, 174 | METRIC_TAG_UPDATE_RESULT_DETAILED, metricUpdateResultDetailed, 175 | METRIC_TAG_UPSTREAM_SERVICE, service.getId()); 176 | }); 177 | } 178 | 179 | private List getOperations(OpenApiRouteDefinitionLocatorProperties.Service service) { 180 | String yaml = getOpenApiDefinitionAsYamlString(service); 181 | OpenAPI openApi = parseOpenApiDefinition(yaml, service); 182 | return getOperations(service, openApi); 183 | } 184 | 185 | private String getOpenApiDefinitionAsYamlString(OpenApiRouteDefinitionLocatorProperties.Service service) { 186 | URI openApiDefinitionUri = firstNonNull(service.getOpenapiDefinitionUri(), config.getOpenapiDefinitionUri()); 187 | URI fullOpenApiDefinitionUri = service.getUri().resolve(openApiDefinitionUri); 188 | 189 | log.info("Retrieving OpenAPI definition for {} from '{}'", service.getId(), fullOpenApiDefinitionUri); 190 | Resource resource = resourceLoader.getResource(fullOpenApiDefinitionUri.toString()); 191 | try (InputStream is = resource.getInputStream()) { 192 | return StreamUtils.copyToString(is, StandardCharsets.UTF_8); 193 | } catch (IOException e) { 194 | throw new RuntimeException(String.format("Error loading '%s'", fullOpenApiDefinitionUri), e); 195 | } 196 | } 197 | 198 | private static OpenAPI parseOpenApiDefinition(String yaml, OpenApiRouteDefinitionLocatorProperties.Service service) { 199 | SwaggerParseResult result = new OpenAPIParser().readContents(yaml, null, null); 200 | String messages = StringUtils.defaultString(StringUtils.join(result.getMessages(), "; ")); 201 | OpenAPI openAPI = result.getOpenAPI(); 202 | if (openAPI == null) { 203 | throw new IllegalArgumentException("Error while parsing OpenAPI definition: " + messages); 204 | } 205 | if (StringUtils.isNoneBlank(messages)) { 206 | log.warn("Warnings while parsing OpenAPI definition of {}: {}", service, messages); 207 | } 208 | return openAPI; 209 | } 210 | 211 | private static List getOperations(OpenApiRouteDefinitionLocatorProperties.Service service, OpenAPI openApi) { 212 | Optional> globalGatewayRouteSettings = getGatewayRouteSettings(openApi.getExtensions()); 213 | 214 | List result = new ArrayList<>(); 215 | openApi.getPaths().forEach((path, pathItem) -> 216 | pathItem.readOperationsMap().forEach((httpMethod, openApiOperation) -> { 217 | Optional> operationGatewayRouteSettings = getGatewayRouteSettings(openApiOperation.getExtensions()); 218 | Optional> gatewayRouteSettings = MapMerge.deepMerge(globalGatewayRouteSettings, operationGatewayRouteSettings); 219 | 220 | List filters = getFilters(gatewayRouteSettings); 221 | List predicates = getPredicates(gatewayRouteSettings); 222 | Optional> metadata = getMetadata(gatewayRouteSettings); 223 | Optional order = getOrder(gatewayRouteSettings); 224 | 225 | OpenApiOperation operation = OpenApiOperation.builder() 226 | .baseUri(service.getUri()) 227 | .httpMethod(map(httpMethod)) 228 | .path(path) 229 | .filters(filters) 230 | .predicates(predicates) 231 | .metadata(metadata) 232 | .order(order) 233 | .openApiExtension(firstNonNull(openApi.getExtensions(), Collections.emptyMap())) 234 | .openApiOperationExtension(firstNonNull(openApiOperation.getExtensions(), Collections.emptyMap())) 235 | .build(); 236 | 237 | result.add(operation); 238 | }) 239 | ); 240 | return result; 241 | } 242 | 243 | private static HttpMethod map(PathItem.HttpMethod method) { 244 | return HttpMethod.valueOf(method.name()); 245 | } 246 | 247 | private static Optional> getGatewayRouteSettings(Map extensions) { 248 | if (extensions == null) { 249 | return Optional.empty(); 250 | } 251 | 252 | Object gatewayRouteSettings = extensions.get(X_GATEWAY_ROUTE_SETTINGS); 253 | if (!(gatewayRouteSettings instanceof Map)) { 254 | return Optional.empty(); 255 | } 256 | 257 | return Optional.of((Map)gatewayRouteSettings); 258 | } 259 | 260 | private static List getFilters(Optional> gatewayRouteSettings) { 261 | return getGatewayRouteSettingsAtKey(gatewayRouteSettings, FILTERS, OpenApiDefinitionRepository::toFilterDefinition); 262 | } 263 | 264 | private static Optional toFilterDefinition(Object filterDefinitionObj) { 265 | if (filterDefinitionObj instanceof String) { 266 | return Optional.of(new FilterDefinition((String)filterDefinitionObj)); 267 | } 268 | 269 | if (filterDefinitionObj instanceof Map) { 270 | Map m = (Map)filterDefinitionObj; 271 | String name = (String)m.get("name"); 272 | Map argsOrig = (Map)m.getOrDefault("args", Collections.emptyMap()); 273 | Map args = argsOrig.entrySet() 274 | .stream() 275 | .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())); 276 | FilterDefinition filterDefinition = new FilterDefinition(); 277 | filterDefinition.setName(name); 278 | filterDefinition.setArgs(args); 279 | return Optional.of(filterDefinition); 280 | } 281 | 282 | log.error("Error while parsing '{}'", filterDefinitionObj); 283 | return Optional.empty(); 284 | } 285 | 286 | private static List getPredicates(Optional> gatewayRouteSettings) { 287 | return getGatewayRouteSettingsAtKey(gatewayRouteSettings, PREDICATES, OpenApiDefinitionRepository::toPredicateDefinition); 288 | } 289 | 290 | private static List getGatewayRouteSettingsAtKey( 291 | Optional> gatewayRouteSettings, 292 | String key, 293 | Function> mapper 294 | ) { 295 | if (!gatewayRouteSettings.isPresent()) { 296 | return Collections.emptyList(); 297 | } 298 | 299 | Object gatewayRouteSettingsAtKey = gatewayRouteSettings.get().get(key); 300 | if (!(gatewayRouteSettingsAtKey instanceof List)) { 301 | return Collections.emptyList(); 302 | } 303 | 304 | List gatewayRouteSettingsAtKeyAsList = (List)gatewayRouteSettingsAtKey; 305 | 306 | return gatewayRouteSettingsAtKeyAsList.stream() 307 | .map(mapper) 308 | .filter(Optional::isPresent) 309 | .map(Optional::get) 310 | .collect(Collectors.toList()); 311 | } 312 | 313 | private static Optional toPredicateDefinition(Object predicateDefinitionObj) { 314 | if (predicateDefinitionObj instanceof String) { 315 | return Optional.of(new PredicateDefinition((String)predicateDefinitionObj)); 316 | } 317 | 318 | if (predicateDefinitionObj instanceof Map) { 319 | Map m = (Map)predicateDefinitionObj; 320 | String name = (String)m.get("name"); 321 | Map argsOrig = (Map)m.getOrDefault("args", Collections.emptyMap()); 322 | Map args = argsOrig.entrySet() 323 | .stream() 324 | .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())); 325 | PredicateDefinition predicateDefinition = new PredicateDefinition(); 326 | predicateDefinition.setName(name); 327 | predicateDefinition.setArgs(args); 328 | return Optional.of(predicateDefinition); 329 | } 330 | 331 | log.error("Error while parsing '{}'", predicateDefinitionObj); 332 | return Optional.empty(); 333 | } 334 | 335 | private static Optional getOrder(Optional> gatewayRouteSettings) { 336 | if (!gatewayRouteSettings.isPresent()) { 337 | return Optional.empty(); 338 | } 339 | 340 | Object gatewayOrder = gatewayRouteSettings.get().get(ORDER); 341 | if (!(gatewayOrder instanceof Integer)) { 342 | return Optional.empty(); 343 | } 344 | 345 | return Optional.of((Integer)gatewayOrder); 346 | } 347 | 348 | private static Optional> getMetadata(Optional> gatewayRouteSettings) { 349 | if (!gatewayRouteSettings.isPresent()) { 350 | return Optional.empty(); 351 | } 352 | 353 | Object gatewayMetadata = gatewayRouteSettings.get().get(METADATA); 354 | if (!(gatewayMetadata instanceof Map)) { 355 | return Optional.empty(); 356 | } 357 | 358 | return Optional.of((Map)gatewayMetadata); 359 | } 360 | 361 | private void publishNewOpenApiOperationsAndRollbackOnFailure( 362 | OpenApiRouteDefinitionLocatorProperties.Service service, 363 | List oldOpenApiOperations 364 | ) { 365 | try { 366 | publishNewOpenApiOperations(service); 367 | } catch (Exception e) { 368 | if (oldOpenApiOperations == null) { 369 | operations.remove(service); 370 | } else { 371 | operations.put(service, oldOpenApiOperations); 372 | } 373 | publishNewOpenApiOperations(service); 374 | throw e; 375 | } 376 | } 377 | 378 | private void publishNewOpenApiOperations(OpenApiRouteDefinitionLocatorProperties.Service service) { 379 | lastRouteDefinitionPublicationFailureCause = null; 380 | applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this)); 381 | if (lastRouteDefinitionPublicationFailureCause != null) { 382 | throw new OpenApiRouteDefinitionPublishException(String.format("Error while publishing route" + 383 | " definitions for %s", service.getId()), lastRouteDefinitionPublicationFailureCause); 384 | } 385 | } 386 | 387 | @Override 388 | public void onApplicationEvent(@NonNull RefreshRoutesResultEvent event) { 389 | if (event.isSuccess()) { 390 | return; 391 | } 392 | 393 | boolean isErrorCausedByThisClass = Arrays.stream(event.getThrowable().getStackTrace()) 394 | .anyMatch(t -> t.getClassName().equals(this.getClass().getCanonicalName())); 395 | 396 | if (isErrorCausedByThisClass) { 397 | lastRouteDefinitionPublicationFailureCause = event.getThrowable(); 398 | } 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/OpenApiDefinitionUpdateScheduler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl; 20 | 21 | import lombok.RequiredArgsConstructor; 22 | import org.springframework.scheduling.annotation.Scheduled; 23 | 24 | @RequiredArgsConstructor 25 | public class OpenApiDefinitionUpdateScheduler { 26 | 27 | private final OpenApiDefinitionRepository openApiDefinitionRepository; 28 | 29 | @Scheduled(fixedDelayString = "#{@openApiRouteDefinitionLocatorProperties.getUpdateScheduler().getFixedDelay().toMillis()}") 30 | private void getOpenApiDefinitions() { 31 | openApiDefinitionRepository.getOpenApiDefinitions(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/OpenApiOperation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl; 20 | 21 | import lombok.Builder; 22 | import lombok.Value; 23 | import org.springframework.cloud.gateway.filter.FilterDefinition; 24 | import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; 25 | import org.springframework.http.HttpMethod; 26 | 27 | import java.net.URI; 28 | import java.util.ArrayList; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.Optional; 33 | 34 | @Value 35 | @Builder 36 | public class OpenApiOperation { 37 | URI baseUri; 38 | String path; 39 | HttpMethod httpMethod; 40 | 41 | @Builder.Default 42 | List filters = new ArrayList<>(); 43 | 44 | @Builder.Default 45 | List predicates = new ArrayList<>(); 46 | 47 | @Builder.Default 48 | Optional order = Optional.empty(); 49 | 50 | @Builder.Default 51 | Optional> metadata = Optional.empty(); 52 | 53 | @Builder.Default 54 | Map openApiExtension = new HashMap<>(); 55 | 56 | @Builder.Default 57 | Map openApiOperationExtension = new HashMap<>(); 58 | } 59 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/OpenApiRouteDefinitionLocator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl; 20 | 21 | import lombok.RequiredArgsConstructor; 22 | import lombok.extern.slf4j.Slf4j; 23 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 24 | import net.bretti.openapi.route.definition.locator.core.customizer.OpenApiRouteDefinitionCustomizer; 25 | import net.bretti.openapi.route.definition.locator.core.impl.utils.MapMerge; 26 | import org.springframework.cloud.gateway.filter.FilterDefinition; 27 | import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; 28 | import org.springframework.cloud.gateway.route.RouteDefinition; 29 | import org.springframework.cloud.gateway.route.RouteDefinitionLocator; 30 | import reactor.core.publisher.Flux; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | import java.util.Map; 35 | import java.util.Optional; 36 | import java.util.UUID; 37 | 38 | import static net.bretti.openapi.route.definition.locator.core.impl.utils.Optionals.firstPresent; 39 | 40 | @RequiredArgsConstructor 41 | @Slf4j 42 | public class OpenApiRouteDefinitionLocator implements RouteDefinitionLocator { 43 | 44 | private final OpenApiDefinitionRepository repository; 45 | 46 | private final List openApiRouteDefinitionCustomizers; 47 | 48 | private final OpenApiRouteDefinitionLocatorProperties properties; 49 | 50 | @Override 51 | public Flux getRouteDefinitions() { 52 | List routeDefinitions = new ArrayList<>(); 53 | repository.getOperations().forEach((service, operations) -> operations.forEach(operation -> { 54 | RouteDefinition routeDefinition = new RouteDefinition(); 55 | routeDefinition.setId(UUID.randomUUID().toString()); 56 | routeDefinition.setUri(operation.getBaseUri()); 57 | 58 | PredicateDefinition pathPredicate = new PredicateDefinition("Path=" + operation.getPath()); 59 | PredicateDefinition methodPredicate = new PredicateDefinition("Method=" + operation.getHttpMethod()); 60 | 61 | List predicates = new ArrayList<>(); 62 | predicates.add(methodPredicate); 63 | predicates.add(pathPredicate); 64 | predicates.addAll(properties.getDefaultRouteSettings().getPredicates()); 65 | predicates.addAll(service.getDefaultRouteSettings().getPredicates()); 66 | predicates.addAll(operation.getPredicates()); 67 | routeDefinition.setPredicates(predicates); 68 | 69 | List filters = new ArrayList<>(); 70 | filters.addAll(properties.getDefaultRouteSettings().getFilters()); 71 | filters.addAll(service.getDefaultRouteSettings().getFilters()); 72 | filters.addAll(operation.getFilters()); 73 | routeDefinition.setFilters(filters); 74 | 75 | firstPresent( 76 | operation.getOrder(), 77 | service.getDefaultRouteSettings().getOrder(), 78 | properties.getDefaultRouteSettings().getOrder() 79 | ).ifPresent(routeDefinition::setOrder); 80 | 81 | Optional> metaData = MapMerge.deepMerge( 82 | Optional.of(properties.getDefaultRouteSettings().getMetadata()), 83 | Optional.of(service.getDefaultRouteSettings().getMetadata()), 84 | operation.getMetadata() 85 | ); 86 | metaData.ifPresent(routeDefinition::setMetadata); 87 | 88 | openApiRouteDefinitionCustomizers.forEach(customizer -> 89 | customizer.customize(routeDefinition, service, operation.getOpenApiExtension(), 90 | operation.getOpenApiOperationExtension()) 91 | ); 92 | 93 | routeDefinitions.add(routeDefinition); 94 | })); 95 | 96 | return Flux.fromIterable(routeDefinitions); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/OpenApiRouteDefinitionLocatorMetrics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl; 20 | 21 | import io.micrometer.core.instrument.Gauge; 22 | import io.micrometer.core.instrument.MeterRegistry; 23 | import io.micrometer.core.instrument.Timer; 24 | import lombok.RequiredArgsConstructor; 25 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 26 | 27 | import jakarta.annotation.PostConstruct; 28 | import java.util.Arrays; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | 33 | @RequiredArgsConstructor 34 | public class OpenApiRouteDefinitionLocatorMetrics { 35 | static final String METRIC_NAME_UPDATES = "openapi_route_definition_locator_openapi_definition_updates"; 36 | private static final String METRIC_DESCRIPTION_UPDATES = "Time and count of attempts to update the route definitions for registered services based on their OpenAPI definitions."; 37 | 38 | private static final String METRIC_NAME_ROUTES = "openapi_route_definition_locator_routes_count"; 39 | private static final String METRIC_DESCRIPTION_ROUTES = "Number of routes managed by the OpenAPI Route Definition Locator"; 40 | 41 | static final String METRIC_TAG_UPSTREAM_SERVICE = "upstream_service"; 42 | 43 | static final String METRIC_TAG_UPDATE_RESULT = "update_result"; 44 | static final String METRIC_TAG_UPDATE_RESULT_SUCCESS = "success"; 45 | static final String METRIC_TAG_UPDATE_RESULT_FAILURE = "failure"; 46 | 47 | static final String METRIC_TAG_UPDATE_RESULT_DETAILED = "update_result_detailed"; 48 | static final String METRIC_TAG_UPDATE_RESULT_DETAILED_SUCCESS_WITHOUT_CHANGES = "success_without_route_changes"; 49 | static final String METRIC_TAG_UPDATE_RESULT_DETAILED_SUCCESS_WITH_CHANGES = "success_with_route_changes"; 50 | static final String METRIC_TAG_UPDATE_RESULT_DETAILED_FAILURE_RETRIEVAL = "failure_retrieval"; 51 | static final String METRIC_TAG_UPDATE_RESULT_DETAILED_FAILURE_PUBLICATION = "failure_publication"; 52 | 53 | private final MeterRegistry meterRegistry; 54 | private final OpenApiRouteDefinitionLocatorProperties config; 55 | private final OpenApiDefinitionRepository openApiDefinitionRepository; 56 | 57 | @PostConstruct 58 | private void postConstruct() { 59 | Map> updateResults = new HashMap<>(); 60 | updateResults.put(METRIC_TAG_UPDATE_RESULT_SUCCESS, Arrays.asList( 61 | METRIC_TAG_UPDATE_RESULT_DETAILED_SUCCESS_WITHOUT_CHANGES, 62 | METRIC_TAG_UPDATE_RESULT_DETAILED_SUCCESS_WITH_CHANGES 63 | )); 64 | updateResults.put(METRIC_TAG_UPDATE_RESULT_FAILURE, Arrays.asList( 65 | METRIC_TAG_UPDATE_RESULT_DETAILED_FAILURE_RETRIEVAL, 66 | METRIC_TAG_UPDATE_RESULT_DETAILED_FAILURE_PUBLICATION 67 | )); 68 | 69 | config.getServices().forEach(service -> { 70 | updateResults.forEach((updateResult, updateResultDetails) -> 71 | updateResultDetails.forEach(updateResultDetail -> 72 | Timer.builder(METRIC_NAME_UPDATES) 73 | .description(METRIC_DESCRIPTION_UPDATES) 74 | .tags(METRIC_TAG_UPSTREAM_SERVICE, service.getId(), 75 | METRIC_TAG_UPDATE_RESULT, updateResult, 76 | METRIC_TAG_UPDATE_RESULT_DETAILED, updateResultDetail) 77 | .publishPercentiles(0.5, 0.8, 0.95, 0.98) 78 | .register(meterRegistry) 79 | ) 80 | ); 81 | 82 | Gauge.builder(METRIC_NAME_ROUTES, () -> openApiDefinitionRepository.getRegisteredOperationsCount(service)) 83 | .description(METRIC_DESCRIPTION_ROUTES) 84 | .tag(METRIC_TAG_UPSTREAM_SERVICE, service.getId()) 85 | .strongReference(true) 86 | .register(meterRegistry); 87 | }); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/OpenApiRouteDefinitionLocatorTimedMetrics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl; 20 | 21 | import io.micrometer.core.instrument.MeterRegistry; 22 | import lombok.RequiredArgsConstructor; 23 | 24 | import java.util.concurrent.TimeUnit; 25 | 26 | @RequiredArgsConstructor 27 | public class OpenApiRouteDefinitionLocatorTimedMetrics { 28 | private final MeterRegistry meterRegistry; 29 | 30 | void recordTime(String name, long amount, TimeUnit unit, String... tags) { 31 | meterRegistry.timer(name, tags).record(amount, unit); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/OpenApiRouteDefinitionPublishException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl; 20 | 21 | public class OpenApiRouteDefinitionPublishException extends RuntimeException { 22 | public OpenApiRouteDefinitionPublishException(String message, Throwable cause) { 23 | super(message, cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/utils/MapMerge.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl.utils; 20 | 21 | import lombok.experimental.UtilityClass; 22 | 23 | import java.util.ArrayList; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Optional; 28 | import java.util.stream.Collectors; 29 | import java.util.stream.Stream; 30 | 31 | @UtilityClass 32 | public class MapMerge { 33 | public static Optional> deepMerge(Optional> original, Optional> patch) { 34 | if (!patch.isPresent()) { 35 | return original.map(it -> deepCopy(it, true)); 36 | } 37 | 38 | if (!original.isPresent()) { 39 | return patch.map(it -> deepCopy(it, false)); 40 | } 41 | 42 | return Optional.of(deepMerge(original.get(), patch.get())); 43 | } 44 | 45 | @SafeVarargs 46 | public static Optional> deepMerge(Optional> original, Optional>... patches) { 47 | if (patches.length == 0) { 48 | return original.map(it -> deepCopy(it, true)); 49 | } 50 | 51 | Optional> result = original; 52 | for (Optional> patch: patches) { 53 | result = deepMerge(result, patch); 54 | } 55 | return result; 56 | } 57 | 58 | /** 59 | * Deep merge Maps with semantics almost as defined in 60 | * https://datatracker.ietf.org/doc/html/rfc7386. 61 | * There is one exception: Merging two lists is done by concatenating them. 62 | * Returns the result. 63 | */ 64 | private static Map deepMerge(Map original, Map patch) { 65 | Map result = deepCopy(original, true); 66 | for (Map.Entry patchEntry : patch.entrySet()) { 67 | String key = patchEntry.getKey(); 68 | Object originalValue = original.get(key); 69 | Object patchValue = patchEntry.getValue(); 70 | if (patchValue == null) { 71 | result.remove(key); 72 | continue; 73 | } 74 | 75 | if (originalValue instanceof Map && patchValue instanceof Map) { 76 | result.put(key, deepMerge((Map)originalValue, (Map)patchValue)); 77 | continue; 78 | } 79 | 80 | if (originalValue instanceof List && patchValue instanceof List) { 81 | result.put(key, union(deepCopy((List) originalValue, true), deepCopy((List) patchValue, false))); 82 | continue; 83 | } 84 | 85 | if (patchValue instanceof Map) { 86 | result.put(key, deepCopy((Map)patchValue, false)); 87 | continue; 88 | } 89 | 90 | if (patchValue instanceof List) { 91 | result.put(key, deepCopy((List)patchValue, false)); 92 | continue; 93 | } 94 | 95 | result.put(key, patchValue); 96 | } 97 | return result; 98 | } 99 | 100 | @SafeVarargs 101 | private static List union(List... lists) { 102 | return Stream.of(lists).flatMap(List::stream).collect(Collectors.toList()); 103 | } 104 | 105 | private static List deepCopy(List list, boolean keepNullValuesInMaps) { 106 | List result = new ArrayList<>(); 107 | for (Object item : list) { 108 | if (item instanceof Map) { 109 | result.add(deepCopy((Map) item, keepNullValuesInMaps)); 110 | continue; 111 | } 112 | 113 | if (item instanceof List) { 114 | result.add(deepCopy((List) item, keepNullValuesInMaps)); 115 | continue; 116 | } 117 | 118 | result.add(item); 119 | } 120 | return result; 121 | } 122 | 123 | private static Map deepCopy(Map map, boolean keepNullValuesInMaps) { 124 | Map result = new HashMap<>(); 125 | for (Map.Entry entry : map.entrySet()) { 126 | String key = entry.getKey(); 127 | Object value = entry.getValue(); 128 | 129 | if (value == null && !keepNullValuesInMaps) { 130 | continue; 131 | } 132 | 133 | if (value instanceof Map) { 134 | result.put(key, deepCopy((Map) value, keepNullValuesInMaps)); 135 | continue; 136 | } 137 | 138 | if (value instanceof List) { 139 | result.put(key, deepCopy((List) value, keepNullValuesInMaps)); 140 | continue; 141 | } 142 | 143 | result.put(key, value); 144 | } 145 | return result; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/utils/Optionals.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl.utils; 20 | 21 | import lombok.experimental.UtilityClass; 22 | 23 | import java.util.Optional; 24 | import java.util.stream.Stream; 25 | 26 | @UtilityClass 27 | public class Optionals { 28 | @SafeVarargs 29 | public static Optional firstPresent(Optional... optionals) { 30 | return Stream.of(optionals) 31 | .filter(Optional::isPresent) 32 | .findFirst() 33 | .orElseGet(Optional::empty); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/test/groovy/net/bretti/openapi/route/definition/locator/core/impl/utils/MapMergeTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.core.impl.utils 20 | 21 | import spock.lang.Specification 22 | 23 | class MapMergeTest extends Specification { 24 | def "deepMerge merges two maps correctly"() { 25 | expect: 26 | MapMerge.deepMerge(Optional.ofNullable(original), Optional.ofNullable(patch)) == Optional.ofNullable(expectedResult) 27 | 28 | where: 29 | original | patch | expectedResult 30 | [a: 'b'] | [a: 'c'] | [a: 'c'] 31 | [a: 'b'] | [b: 'c'] | [a: 'b', b: 'c'] 32 | [a: 'b'] | [a: null] | [:] 33 | [a: 'b', b: 'c'] | [a: null] | [b: 'c'] 34 | [a: ['b']] | [a: 'c'] | [a: 'c'] 35 | [a: 'c'] | [a: ['b']] | [a: ['b']] 36 | [a: [b: 'c']] | [a: [b: 'd', c: null]] | [a: [b: 'd']] 37 | [a: [[b: 'c']]] | [a: [1]] | [a: [[b: 'c'], 1]] // Deviation from RFC7386. 38 | [a: ['a', 'b']] | [a: ['c', 'd']] | [a: ['a', 'b', 'c', 'd']] // Deviation from RFC7386. 39 | [a: [a: 'b']] | [a: ['c']] | [a: ['c']] 40 | [a: [a: 'foo']] | [a: null] | [:] 41 | [a: [a: 'foo']] | [a: 'bar'] | [a: 'bar'] 42 | [e: null] | [a: 1] | [e: null, a: 1] 43 | [e: null] | null | [e: null] 44 | [a: [[e: null]]] | [a: [[f: null]]] | [a: [[e: null], [:]]] 45 | [:] | [a: [[f: null]]] | [a: [[:]]] 46 | null | [e: null] | [:] 47 | [a: [1, 2]] | [a: [a: 'b', c: null]] | [a: [a: 'b']] 48 | [:] | [a: [bb: [ccc: null]]] | [a: [bb: [:]]] 49 | } 50 | 51 | def "deepMerge merges three maps correctly"() { 52 | expect: 53 | MapMerge.deepMerge(Optional.of(original), Optional.of(patch1), Optional.of(patch2)) == Optional.of(expectedResult) 54 | 55 | where: 56 | original | patch1 | patch2 | expectedResult 57 | [a: 'b'] | [a: 'c'] | [a: 'd'] | [a: 'd'] 58 | [a: 'b'] | [b: 'c'] | [c: 'd'] | [a: 'b', b: 'c', c: 'd'] 59 | [a: 'b'] | [a: null] | [b: 'c'] | [b: 'c'] 60 | } 61 | 62 | // It is important that deepMerge() returns a deep copy of the input maps because metadata maps can be arbitrarily 63 | // modified via a `OpenApiRouteDefinitionCustomizer` implementation and there should be no interference whatsoever 64 | // between the metadata maps of different API operations. 65 | def "deepMerge(original, patch) returns a deep copy"() { 66 | when: 'we merge an original map and a patch map' 67 | Map originalPatched = MapMerge.deepMerge(Optional.ofNullable(original), Optional.ofNullable(patch)).get() 68 | 69 | then: 'we get some expected result' 70 | originalPatched == originalPatchedExpected 71 | 72 | when: 'we modify that result, e.g. via a `OpenApiRouteDefinitionCustomizer` implementation' 73 | originalPatchModifier.call(originalPatched) 74 | 75 | and: 'merge the original map and patch map again' 76 | Map originalPatched2 = MapMerge.deepMerge(Optional.ofNullable(original), Optional.ofNullable(patch)).get() 77 | 78 | then: 'the patch result should be the same as the result of the first merge operation' 79 | originalPatched2 == originalPatchedExpected 80 | 81 | where: 82 | original | patch | originalPatchedExpected | originalPatchModifier 83 | [:] | [key1: [key11: 'value11']] | [key1: [key11: 'value11']] | { it.key1.key12 = 'value12' } 84 | [:] | [key1: ['value11']] | [key1: ['value11']] | { it.key1.add('value12') } 85 | [key1: [key11: 'value11']] | null | [key1: [key11: 'value11']] | { it.key1.key12 = 'value12' } 86 | null | [key1: [key11: 'value11']] | [key1: [key11: 'value11']] | { it.key1.key12 = 'value12' } 87 | [key1: [key11: 'value11']] | [:] | [key1: [key11: 'value11']] | { it.key1.key12 = 'value12' } 88 | [key1: [[key101: 'value101']]] | [key1: [[key111: 'value111']]] | [key1: [[key101: 'value101'], [key111: 'value111']]] | { it.key1[0].key102 = 'value102' } 89 | [key1: [[key101: 'value101']]] | [key1: [[key111: 'value111']]] | [key1: [[key101: 'value101'], [key111: 'value111']]] | { it.key1[1].key112 = 'value112' } 90 | } 91 | 92 | def "deepMerge(original) returns a deep copy"() { 93 | when: 'we merge an original map with an empty list of patch maps' 94 | Map originalPatched = MapMerge.deepMerge(Optional.ofNullable(original)).get() 95 | 96 | then: 'we get some expected result' 97 | originalPatched == originalPatchedExpected 98 | 99 | when: 'we modify that result, e.g. via a `OpenApiRouteDefinitionCustomizer` implementation' 100 | originalPatchModifier.call(originalPatched) 101 | 102 | and: 'merge the original map with an empty list of patch maps again' 103 | Map originalPatched2 = MapMerge.deepMerge(Optional.ofNullable(original)).get() 104 | 105 | then: 'the patch result should be the same as the result of the first merge operation' 106 | originalPatched2 == originalPatchedExpected 107 | 108 | where: 109 | original | originalPatchedExpected | originalPatchModifier 110 | [:] | [:] | { it.key1 = 'value1' } 111 | [e: null] | [e: null] | { it.key1 = 'value1' } 112 | [:] | [:] | { it.key1 = ['value1'] } 113 | [key1: [key11: 'value11']] | [key1: [key11: 'value11']] | { it.key1.key12 = 'value12' } 114 | [key1: ['value11']] | [key1: ['value11']] | { it.key1.add('value12') } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("openapi-route-definition-locator.common-java-library") 3 | } 4 | 5 | dependencies { 6 | api(project(":openapi-route-definition-locator-core")) 7 | implementation("org.springframework.cloud:spring-cloud-gateway-server") 8 | annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor") 9 | compileOnly("io.micrometer:micrometer-core") 10 | 11 | testImplementation("org.springframework.cloud:spring-cloud-starter-gateway") 12 | testImplementation("org.springframework.boot:spring-boot-actuator-autoconfigure") 13 | testRuntimeOnly("org.springframework.boot:spring-boot-starter-actuator") 14 | testRuntimeOnly("io.micrometer:micrometer-registry-prometheus") 15 | testImplementation("org.wiremock:wiremock-standalone:3.10.0") 16 | testImplementation("org.apache.commons:commons-lang3") 17 | } 18 | 19 | publishing { 20 | publications { 21 | create("mavenJava") { 22 | artifactId = "openapi-route-definition-locator-spring-cloud-starter" 23 | from(components["java"]) 24 | versionMapping { 25 | usage("java-api") { 26 | fromResolutionOf("runtimeClasspath") 27 | } 28 | usage("java-runtime") { 29 | fromResolutionResult() 30 | } 31 | } 32 | pom { 33 | name.set("openapi-route-definition-locator-spring-cloud-starter") 34 | description.set("Spring Cloud starter for the OpenAPI Route Definition Locator") 35 | url.set("https://github.com/jbretsch/openapi-route-definition-locator") 36 | licenses { 37 | license { 38 | name.set("MIT License") 39 | url.set("https://github.com/jbretsch/openapi-route-definition-locator/blob/master/LICENSE") 40 | } 41 | } 42 | developers { 43 | developer { 44 | id.set("jbretsch") 45 | name.set("Jan Bretschneider") 46 | email.set("mail@jan-bretschneider.de") 47 | } 48 | } 49 | scm { 50 | connection.set("scm:git:git://github.com/jbretsch/openapi-route-definition-locator.git") 51 | developerConnection.set("scm:git:ssh://github.com/jbretsch/openapi-route-definition-locator.git") 52 | url.set("https://github.com/jbretsch/openapi-route-definition-locator") 53 | } 54 | } 55 | } 56 | } 57 | repositories { 58 | maven { 59 | name = "ossrh" 60 | credentials(PasswordCredentials::class) 61 | val releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 62 | val snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" 63 | url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) 64 | } 65 | } 66 | } 67 | 68 | signing { 69 | sign(publishing.publications["mavenJava"]) 70 | } 71 | 72 | // Remove entries from published POM. 73 | // Inspired by . 74 | tasks.withType().all { 75 | doLast { 76 | val file = layout.buildDirectory.file("publications/mavenJava/pom-default.xml").get().asFile 77 | var text = file.readText() 78 | val regex = "(?s)(.+?)(.+?)(.+?)".toRegex() 79 | val matcher = regex.find(text) 80 | if (matcher != null) { 81 | text = regex.replace(text, "") 82 | } 83 | file.writeText(text) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/main/java/net/bretti/openapi/route/definition/locator/autoconfigure/OpenApiRouteDefinitionLocatorAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.autoconfigure; 20 | 21 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 22 | import net.bretti.openapi.route.definition.locator.core.customizer.OpenApiRouteDefinitionCustomizer; 23 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiDefinitionRepository; 24 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiDefinitionUpdateScheduler; 25 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocator; 26 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorTimedMetrics; 27 | import org.springframework.boot.autoconfigure.AutoConfiguration; 28 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 29 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 30 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 31 | import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; 32 | import org.springframework.context.ApplicationEventPublisher; 33 | import org.springframework.context.annotation.Bean; 34 | import org.springframework.core.io.ResourceLoader; 35 | import org.springframework.scheduling.annotation.EnableScheduling; 36 | 37 | import java.util.List; 38 | import java.util.Optional; 39 | import java.util.concurrent.ConcurrentHashMap; 40 | 41 | @AutoConfiguration(after = GatewayAutoConfiguration.class) 42 | @ConditionalOnBean(GatewayAutoConfiguration.class) 43 | @ConditionalOnProperty(value = "openapi-route-definition-locator.enabled", matchIfMissing = true) 44 | @EnableConfigurationProperties 45 | @EnableScheduling 46 | public class OpenApiRouteDefinitionLocatorAutoConfiguration { 47 | @Bean 48 | public OpenApiDefinitionRepository openApiDefinitionRepository( 49 | OpenApiRouteDefinitionLocatorProperties config, 50 | ApplicationEventPublisher applicationEventPublisher, 51 | Optional metrics, 52 | ResourceLoader resourceLoader) { 53 | return new OpenApiDefinitionRepository(config, new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), 54 | applicationEventPublisher, metrics, resourceLoader); 55 | } 56 | 57 | @Bean 58 | public OpenApiRouteDefinitionLocatorProperties openApiRouteDefinitionLocatorProperties() { 59 | return new OpenApiRouteDefinitionLocatorProperties(); 60 | } 61 | 62 | @Bean 63 | public OpenApiRouteDefinitionLocator openApiRouteDefinitionLocator( 64 | OpenApiDefinitionRepository openApiDefinitionRepository, 65 | List openApiRouteDefinitionCustomizers, 66 | OpenApiRouteDefinitionLocatorProperties openApiRouteDefinitionLocatorProperties 67 | ) { 68 | return new OpenApiRouteDefinitionLocator(openApiDefinitionRepository, openApiRouteDefinitionCustomizers, 69 | openApiRouteDefinitionLocatorProperties); 70 | } 71 | 72 | @Bean 73 | public OpenApiDefinitionUpdateScheduler openApiDefinitionUpdateScheduler( 74 | OpenApiDefinitionRepository openApiDefinitionRepository 75 | ) { 76 | return new OpenApiDefinitionUpdateScheduler(openApiDefinitionRepository); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/main/java/net/bretti/openapi/route/definition/locator/autoconfigure/OpenApiRouteDefinitionLocatorMetricsAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.autoconfigure; 20 | 21 | import io.micrometer.core.instrument.MeterRegistry; 22 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 23 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiDefinitionRepository; 24 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics; 25 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorTimedMetrics; 26 | import org.springframework.boot.autoconfigure.AutoConfiguration; 27 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 28 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 29 | import org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration; 30 | import org.springframework.cloud.gateway.route.RouteDefinitionMetrics; 31 | import org.springframework.context.annotation.Bean; 32 | 33 | @AutoConfiguration(after = {OpenApiRouteDefinitionLocatorAutoConfiguration.class, GatewayMetricsAutoConfiguration.class}) 34 | @ConditionalOnProperty(name = "openapi-route-definition-locator.metrics.enabled", matchIfMissing = true) 35 | @ConditionalOnBean({ OpenApiDefinitionRepository.class, OpenApiRouteDefinitionLocatorProperties.class, 36 | RouteDefinitionMetrics.class, MeterRegistry.class }) 37 | public class OpenApiRouteDefinitionLocatorMetricsAutoConfiguration { 38 | 39 | @Bean 40 | public OpenApiRouteDefinitionLocatorMetrics openApiRouteDefinitionLocatorMetrics( 41 | MeterRegistry meterRegistry, 42 | OpenApiRouteDefinitionLocatorProperties config, 43 | OpenApiDefinitionRepository openApiDefinitionRepository) { 44 | return new OpenApiRouteDefinitionLocatorMetrics(meterRegistry, config, openApiDefinitionRepository); 45 | } 46 | 47 | @Bean 48 | public OpenApiRouteDefinitionLocatorTimedMetrics openApiRouteDefinitionLocatorTimedMetrics( 49 | MeterRegistry meterRegistry) { 50 | return new OpenApiRouteDefinitionLocatorTimedMetrics(meterRegistry); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | net.bretti.openapi.route.definition.locator.autoconfigure.OpenApiRouteDefinitionLocatorAutoConfiguration 2 | net.bretti.openapi.route.definition.locator.autoconfigure.OpenApiRouteDefinitionLocatorMetricsAutoConfiguration 3 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/CustomGlobalOpenApiDefinitionUrlCompTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest 20 | 21 | import componenttest.setup.basetest.BaseCompTest 22 | import componenttest.setup.wiremock.OpenapiDefinitionServedFromDifferentHostServiceMock1 23 | import componenttest.setup.wiremock.OpenapiDefinitionServedFromDifferentHostServiceMock2 24 | import componenttest.setup.wiremock.OrderServiceMock 25 | import componenttest.setup.wiremock.UserServiceMock 26 | import org.springframework.test.context.ActiveProfiles 27 | import org.springframework.test.web.reactive.server.FluxExchangeResult 28 | 29 | @ActiveProfiles("custom-global-openapi-definition-url") 30 | class CustomGlobalOpenApiDefinitionUrlCompTest extends BaseCompTest { 31 | 32 | def "API Gateway routes requests according to OpenAPI definitions"() { 33 | given: 34 | waitForRemovalOfAllRoutes() 35 | 36 | and: 37 | UserServiceMock.instance.mockOpenApiDefinition("/global-custom-path-to/openapi-definition") 38 | UserServiceMock.instance.mockGetUsers() 39 | UserServiceMock.instance.mockGetUser() 40 | 41 | and: 42 | OrderServiceMock.instance.mockOpenApiDefinition() 43 | OrderServiceMock.instance.mockGetOrders() 44 | OrderServiceMock.instance.mockGetOrder() 45 | OrderServiceMock.instance.mockPostOrder() 46 | 47 | and: 48 | OpenapiDefinitionServedFromDifferentHostServiceMock1.instance.mockGetThings() 49 | OpenapiDefinitionServedFromDifferentHostServiceMock2.instance.mockOpenApiDefinition() 50 | 51 | when: 52 | waitForRouteAddition { 53 | assert getRoutesFromActuatorEndpoint().size() == 7 54 | } 55 | 56 | and: 57 | List routes = getRoutesFromActuatorEndpoint() 58 | 59 | then: 60 | Map getUsersRoute = extractRoute(routes, "GET", "/users") 61 | getUsersRoute.predicate == "((Methods: [GET] && Paths: [/users], match trailing slash: true) && Header: Authorization regexp=null)" 62 | getUsersRoute.route_id != null 63 | getUsersRoute.filters == [ 64 | "[[AddResponseHeader X-Response-FromGlobalConfig = 'global-sample-value'], order = 1]", 65 | "[[AddResponseHeader X-Response-DefaultForAllServices = 'sample-value-all'], order = 1]", 66 | "[[AddResponseHeader X-Response-DefaultForOneService = 'sample-value-one'], order = 2]", 67 | "[[AddResponseHeader X-Auth-Type-Was = 'Application'], order = 3]", 68 | ] 69 | getUsersRoute.uri == "http://localhost:9091" 70 | getUsersRoute.order == 6 71 | getUsersRoute.metadata == [ 72 | defaultForAllServices : 'OptionValueAll', 73 | defaultForOneService : 'OptionValueOne', 74 | AddedByXAuthTypeRouteDefinitionCustomizer: 'Application', 75 | ] 76 | getUsersRoute.size() == 6 77 | 78 | and: 79 | Map getUserRoute = extractRoute(routes, "GET", "/users/{userId}") 80 | getUserRoute.predicate == 81 | "((((Methods: [GET] && Paths: [/users/{userId}], match trailing slash: true) && " + 82 | "After: 2022-01-20T17:42:47.789+01:00[Europe/Berlin]) && " + 83 | "Header: Required-Test-Header regexp=required-test-header-.*) && " + 84 | "Header: Authorization regexp=null)" 85 | getUserRoute.route_id != null 86 | getUserRoute.filters == [ 87 | "[[AddResponseHeader X-Response-FromGlobalConfig = 'global-sample-value'], order = 1]", 88 | "[[AddResponseHeader X-Response-DefaultForAllServices = 'sample-value-all'], order = 1]", 89 | "[[AddResponseHeader X-Response-DefaultForOneService = 'sample-value-one'], order = 2]", 90 | "[[AddResponseHeader X-Auth-Type-Was = 'Application User'], order = 3]", 91 | ] 92 | getUserRoute.uri == "http://localhost:9091" 93 | getUserRoute.order == 6 94 | getUserRoute.metadata == [ 95 | defaultForAllServices : 'OptionValueAll', 96 | defaultForOneService : 'OptionValueOne', 97 | AddedByXAuthTypeRouteDefinitionCustomizer: 'Application User', 98 | ] 99 | getUserRoute.size() == 6 100 | 101 | and: 102 | Map getOrdersRoute = extractRoute(routes, "GET", "/users/{userId}/orders") 103 | getOrdersRoute.predicate == "(Methods: [GET] && Paths: [/users/{userId}/orders], match trailing slash: true)" 104 | getOrdersRoute.route_id != null 105 | getOrdersRoute.filters == [ 106 | "[[AddResponseHeader X-Response-FromGlobalConfig = 'global-sample-value'], order = 1]", 107 | "[[AddResponseHeader X-Response-DefaultForAllServices = 'sample-value-all'], order = 1]", 108 | "[[PrefixPath prefix = '/api'], order = 2]", 109 | "[[AddResponseHeader X-Response-FromOpenApiDefinition = 'sample-value'], order = 3]", 110 | "[[SetStatus status = '418'], order = 4]", 111 | ] 112 | getOrdersRoute.uri == "http://localhost:9092" 113 | getOrdersRoute.order == 1 114 | getOrdersRoute.metadata == [ 115 | optionName : "OptionValue", 116 | compositeObject : [ 117 | name : "value", 118 | otherName: 2, 119 | ], 120 | aList : ["foo", "bar", "quuz"], 121 | defaultForAllServices: 'OptionValueAll', 122 | iAmNumber : 1, 123 | ] 124 | getOrdersRoute.size() == 6 125 | 126 | and: 127 | Map getOrderRoute = extractRoute(routes, "GET", "/users/{userId}/orders/{orderId}") 128 | getOrderRoute.predicate == "(Methods: [GET] && Paths: [/users/{userId}/orders/{orderId}], match trailing slash: true)" 129 | getOrderRoute.route_id != null 130 | getOrderRoute.filters == [ 131 | "[[AddResponseHeader X-Response-FromGlobalConfig = 'global-sample-value'], order = 1]", 132 | "[[AddResponseHeader X-Response-DefaultForAllServices = 'sample-value-all'], order = 1]", 133 | "[[PrefixPath prefix = '/api'], order = 2]", 134 | "[[AddResponseHeader X-Response-FromOpenApiDefinition = 'sample-value'], order = 3]", 135 | "[[SetStatus status = '418'], order = 4]", 136 | ] 137 | getOrderRoute.uri == "http://localhost:9092" 138 | getOrderRoute.order == 1 139 | getOrderRoute.metadata == [ 140 | optionName : "OptionValue", 141 | compositeObject : [name: "value"], 142 | aList : ["foo", "bar"], 143 | defaultForAllServices: 'OptionValueAll', 144 | iAmNumber : 1, 145 | ] 146 | getOrderRoute.size() == 6 147 | 148 | and: 149 | Map postOrderRoute = extractRoute(routes, "POST", "/users/{userId}/orders") 150 | postOrderRoute.predicate == "(Methods: [POST] && Paths: [/users/{userId}/orders], match trailing slash: true)" 151 | postOrderRoute.route_id != null 152 | postOrderRoute.filters == [ 153 | "[[AddResponseHeader X-Response-FromGlobalConfig = 'global-sample-value'], order = 1]", 154 | "[[AddResponseHeader X-Response-DefaultForAllServices = 'sample-value-all'], order = 1]", 155 | "[[PrefixPath prefix = '/api'], order = 2]", 156 | "[[AddResponseHeader X-Response-FromOpenApiDefinition = 'sample-value'], order = 3]", 157 | ] 158 | postOrderRoute.uri == "http://localhost:9092" 159 | postOrderRoute.order == 1 160 | postOrderRoute.metadata == [ 161 | optionName : "OptionValue", 162 | compositeObject : [name: "value"], 163 | aList : ["foo", "bar"], 164 | defaultForAllServices: 'OptionValueAll', 165 | iAmNumber : 1, 166 | ] 167 | postOrderRoute.size() == 6 168 | 169 | and: 170 | Map getThingsRoute = extractRoute(routes, "GET", "/things") 171 | getThingsRoute.predicate == "(Methods: [GET] && Paths: [/things], match trailing slash: true)" 172 | getThingsRoute.route_id != null 173 | getThingsRoute.filters == [ 174 | "[[AddResponseHeader X-Response-FromGlobalConfig = 'global-sample-value'], order = 1]", 175 | "[[AddResponseHeader X-Response-DefaultForAllServices = 'sample-value-all'], order = 1]", 176 | ] 177 | getThingsRoute.uri == "http://localhost:9093" 178 | getThingsRoute.order == 5 179 | getThingsRoute.metadata == [defaultForAllServices: 'OptionValueAll'] 180 | getThingsRoute.size() == 6 181 | 182 | and: 183 | Map getOpenApiInClassPathEntitiesRoute = extractRoute(routes, "GET", "/entities-of-service-with-openapi-definition-in-classpath") 184 | getOpenApiInClassPathEntitiesRoute.predicate == "(Methods: [GET] && Paths: [/entities-of-service-with-openapi-definition-in-classpath], match trailing slash: true)" 185 | getOpenApiInClassPathEntitiesRoute.route_id != null 186 | getOpenApiInClassPathEntitiesRoute.filters == [ 187 | "[[AddResponseHeader X-Response-FromGlobalConfig = 'global-sample-value'], order = 1]", 188 | "[[AddResponseHeader X-Response-DefaultForAllServices = 'sample-value-all'], order = 1]", 189 | ] 190 | getOpenApiInClassPathEntitiesRoute.uri == "http://localhost:9095" 191 | getOpenApiInClassPathEntitiesRoute.order == 5 192 | getOpenApiInClassPathEntitiesRoute.metadata == [defaultForAllServices: 'OptionValueAll'] 193 | getOpenApiInClassPathEntitiesRoute.size() == 6 194 | 195 | when: 196 | FluxExchangeResult getUsersWithoutHeaderResponse = webTestClient 197 | .get().uri("http://localhost:${localServerPort}/users") 198 | .exchange().returnResult(String) 199 | 200 | then: 201 | getUsersWithoutHeaderResponse.status.value() == 404 202 | String getUsersWithoutHeaderResponseBody = getUsersWithoutHeaderResponse.getResponseBody().blockFirst() 203 | Map getUsersWithoutHeaderResponseBodyJson = jsonSlurper.parseText(getUsersWithoutHeaderResponseBody) as Map 204 | getUsersWithoutHeaderResponseBodyJson.timestamp != null 205 | getUsersWithoutHeaderResponseBodyJson.path == "/users" 206 | getUsersWithoutHeaderResponseBodyJson.status == 404 207 | getUsersWithoutHeaderResponseBodyJson.error == "Not Found" 208 | getUsersWithoutHeaderResponseBodyJson.message == null 209 | getUsersWithoutHeaderResponseBodyJson.requestId != null 210 | 211 | when: 212 | FluxExchangeResult getUsersResponse = webTestClient 213 | .get().uri("http://localhost:${localServerPort}/users") 214 | .header("Authorization", "Bearer someToken") 215 | .exchange().returnResult(String) 216 | 217 | then: 218 | getUsersResponse.status.value() == 200 219 | getUsersResponse.getResponseBody().blockFirst() == '[{"id": "user-id-1"}]' 220 | 221 | 222 | when: 223 | FluxExchangeResult getUserWithoutHeaderResponse = webTestClient 224 | .get().uri("http://localhost:${localServerPort}/users/${USER_ID}") 225 | .exchange().returnResult(String) 226 | 227 | then: 228 | getUserWithoutHeaderResponse.status.value() == 404 229 | String getUserWithoutHeaderResponseBody = getUserWithoutHeaderResponse.getResponseBody().blockFirst() 230 | Map getUserWithoutHeaderResponseBodyJson = jsonSlurper.parseText(getUserWithoutHeaderResponseBody) as Map 231 | getUserWithoutHeaderResponseBodyJson.timestamp != null 232 | getUserWithoutHeaderResponseBodyJson.path == "/users/${USER_ID}" 233 | getUserWithoutHeaderResponseBodyJson.status == 404 234 | getUserWithoutHeaderResponseBodyJson.error == "Not Found" 235 | getUserWithoutHeaderResponseBodyJson.message == null 236 | getUserWithoutHeaderResponseBodyJson.requestId != null 237 | 238 | when: 239 | FluxExchangeResult getUserResponse = webTestClient 240 | .get().uri("http://localhost:${localServerPort}/users/${USER_ID}") 241 | .header("Authorization", "Bearer someToken") 242 | .header("Required-Test-Header", "required-test-header-value") 243 | .exchange().returnResult(String) 244 | 245 | then: 246 | getUserResponse.status.value() == 200 247 | getUserResponse.getResponseBody().blockFirst() == '{"id": "user-id-1"}' 248 | 249 | when: 250 | FluxExchangeResult getOrdersResponse = webTestClient 251 | .get().uri("http://localhost:${localServerPort}/users/${USER_ID}/orders") 252 | .exchange().returnResult(String) 253 | 254 | then: 255 | getOrdersResponse.status.value() == 418 256 | getOrdersResponse.getResponseBody().blockFirst() == '[{"id": "order-id-1"}]' 257 | 258 | when: 259 | FluxExchangeResult getOrderResponse = webTestClient 260 | .get().uri("http://localhost:${localServerPort}/users/${USER_ID}/orders/${ORDER_ID}") 261 | .exchange().returnResult(String) 262 | 263 | then: 264 | getOrderResponse.status.value() == 418 265 | getOrderResponse.getResponseBody().blockFirst() == '{"id": "order-id-1"}' 266 | 267 | when: 268 | FluxExchangeResult postOrderResponse = webTestClient 269 | .post().uri("http://localhost:${localServerPort}/users/${USER_ID}/orders") 270 | .exchange().returnResult(String) 271 | 272 | then: 273 | postOrderResponse.status.value() == 201 274 | postOrderResponse.getResponseBody().blockFirst() == '{"id": "order-id-1"}' 275 | 276 | when: 277 | FluxExchangeResult getContextInBaseUriThingsResponse = webTestClient 278 | .get().uri("http://localhost:${localServerPort}/things") 279 | .exchange().returnResult(String) 280 | 281 | then: 282 | getContextInBaseUriThingsResponse.status.value() == 200 283 | getContextInBaseUriThingsResponse.getResponseBody().blockFirst() == '[{"id": "thing-id-1"}]' 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/setup/app/TestApiGatewayApplication.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest.setup.app 20 | 21 | import org.springframework.boot.SpringApplication 22 | import org.springframework.boot.autoconfigure.SpringBootApplication 23 | import org.springframework.test.context.ActiveProfiles 24 | 25 | @ActiveProfiles("test") 26 | @SpringBootApplication 27 | class TestApiGatewayApplication { 28 | static void main(String[] args) { 29 | SpringApplication.run(TestApiGatewayApplication, args) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/setup/app/XAuthTypeRouteDefinitionCustomizer.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest.setup.app 20 | 21 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties 22 | import net.bretti.openapi.route.definition.locator.core.customizer.OpenApiRouteDefinitionCustomizer 23 | import org.apache.commons.lang3.ObjectUtils 24 | import org.springframework.cloud.gateway.filter.FilterDefinition 25 | import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition 26 | import org.springframework.cloud.gateway.route.RouteDefinition 27 | import org.springframework.stereotype.Component 28 | 29 | @Component 30 | class XAuthTypeRouteDefinitionCustomizer implements OpenApiRouteDefinitionCustomizer { 31 | @Override 32 | void customize( 33 | RouteDefinition routeDefinition, 34 | OpenApiRouteDefinitionLocatorProperties.Service service, 35 | Map openApiGlobalExtensions, 36 | Map openApiOperationExtensions 37 | ) { 38 | Object xAuthType = ObjectUtils.firstNonNull(openApiOperationExtensions['x-auth-type'], openApiGlobalExtensions['x-auth-type']) 39 | if (!(xAuthType instanceof String)) { 40 | return 41 | } 42 | 43 | // We add a filter, a predicate, and some metadata here to make sure that the respective lists and maps are 44 | // mutable. 45 | routeDefinition.getFilters().add(new FilterDefinition("AddResponseHeader=X-Auth-Type-Was, ${xAuthType}")) 46 | routeDefinition.getPredicates().add(new PredicateDefinition("Header=Authorization")) 47 | routeDefinition.getMetadata().put("AddedByXAuthTypeRouteDefinitionCustomizer", xAuthType) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/setup/basetest/BaseCompTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest.setup.basetest 20 | 21 | import componenttest.setup.app.TestApiGatewayApplication 22 | import componenttest.setup.wiremock.OpenapiDefinitionServedFromDifferentHostServiceMock1 23 | import componenttest.setup.wiremock.OpenapiDefinitionServedFromDifferentHostServiceMock2 24 | import componenttest.setup.wiremock.OrderServiceMock 25 | import componenttest.setup.wiremock.UserServiceMock 26 | import groovy.json.JsonSlurper 27 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties 28 | import org.springframework.beans.factory.annotation.Autowired 29 | import org.springframework.boot.test.context.SpringBootTest 30 | import org.springframework.boot.test.web.server.LocalServerPort 31 | import org.springframework.test.web.reactive.server.WebTestClient 32 | import spock.lang.Specification 33 | import spock.util.concurrent.PollingConditions 34 | 35 | import java.time.Duration 36 | 37 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT 38 | 39 | @SpringBootTest(webEnvironment = RANDOM_PORT, classes = TestApiGatewayApplication) 40 | abstract class BaseCompTest extends Specification { 41 | 42 | static final String USER_ID = "user-id-1" 43 | static final String ORDER_ID = "order-id-1" 44 | 45 | JsonSlurper jsonSlurper = new JsonSlurper() 46 | 47 | @LocalServerPort 48 | int localServerPort 49 | 50 | @Autowired 51 | WebTestClient webTestClient 52 | 53 | @Autowired 54 | OpenApiRouteDefinitionLocatorProperties locatorProperties 55 | 56 | Duration maxWaitTimeForRouteAddition 57 | Duration maxWaitTimeForRouteRemoval 58 | 59 | def setup() { 60 | assert locatorProperties.getUpdateScheduler().getFixedDelay() == Duration.ofSeconds(1) 61 | assert locatorProperties.getUpdateScheduler().getRemoveRoutesOnUpdateFailuresAfter() == Duration.ofSeconds(5) 62 | maxWaitTimeForRouteAddition = locatorProperties.getUpdateScheduler().getFixedDelay().plusSeconds(1) 63 | maxWaitTimeForRouteRemoval = locatorProperties.getUpdateScheduler().getRemoveRoutesOnUpdateFailuresAfter() + maxWaitTimeForRouteAddition 64 | UserServiceMock.instance.resetAll() 65 | OrderServiceMock.instance.resetAll() 66 | OpenapiDefinitionServedFromDifferentHostServiceMock1.instance.resetAll() 67 | OpenapiDefinitionServedFromDifferentHostServiceMock2.instance.resetAll() 68 | } 69 | 70 | Map extractRoute(List routes, String httpMethod, String path) { 71 | return routes.find {it.predicate.contains("[${httpMethod}]") && it.predicate.contains("[${path}]") } as Map 72 | } 73 | 74 | List getRoutesFromActuatorEndpoint() { 75 | String routesJson = webTestClient.get().uri("http://localhost:${localServerPort}/actuator/gateway/routes") 76 | .exchange() 77 | .returnResult(String) 78 | .getResponseBody() 79 | .blockFirst() 80 | 81 | return jsonSlurper.parseText(routesJson) as List 82 | } 83 | 84 | void waitForRouteAddition(Closure conditions) { 85 | new PollingConditions(timeout: maxWaitTimeForRouteAddition.getSeconds()).eventually(conditions) 86 | } 87 | 88 | void waitForRouteRemoval(Closure conditions) { 89 | new PollingConditions(timeout: maxWaitTimeForRouteRemoval.getSeconds()).eventually(conditions) 90 | } 91 | 92 | void waitForRemovalOfAllRoutes() { 93 | waitForRouteRemoval { 94 | // One route remains because it comes from an OpenAPI definition read from the classpath. 95 | assert getRoutesFromActuatorEndpoint().size() == 1 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/setup/wiremock/BaseWireMock.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest.setup.wiremock 20 | 21 | import com.github.tomakehurst.wiremock.WireMockServer 22 | import com.github.tomakehurst.wiremock.common.Slf4jNotifier 23 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration 24 | 25 | abstract class BaseWireMock extends WireMockServer { 26 | 27 | BaseWireMock(int port) { 28 | super(WireMockConfiguration.options() 29 | .port(port) 30 | .bindAddress("127.0.0.1") 31 | .usingFilesUnderDirectory("src/test/resources/wiremock") 32 | .notifier(new Slf4jNotifier(true))) 33 | 34 | start() 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/setup/wiremock/OpenapiDefinitionServedFromDifferentHostServiceMock1.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest.setup.wiremock 20 | 21 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse 22 | import static com.github.tomakehurst.wiremock.client.WireMock.get 23 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo 24 | 25 | @Singleton(strict = false) 26 | class OpenapiDefinitionServedFromDifferentHostServiceMock1 extends BaseWireMock { 27 | 28 | OpenapiDefinitionServedFromDifferentHostServiceMock1() { 29 | super(9093) 30 | } 31 | 32 | void mockGetThings() { 33 | client.register(get(urlEqualTo("/things")) 34 | .willReturn(aResponse() 35 | .withStatus(200) 36 | .withHeader("Content-Type", "application/json") 37 | .withBody('[{"id": "thing-id-1"}]') 38 | ) 39 | ) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/setup/wiremock/OpenapiDefinitionServedFromDifferentHostServiceMock2.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest.setup.wiremock 20 | 21 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse 22 | import static com.github.tomakehurst.wiremock.client.WireMock.get 23 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo 24 | 25 | @Singleton(strict = false) 26 | class OpenapiDefinitionServedFromDifferentHostServiceMock2 extends BaseWireMock { 27 | 28 | OpenapiDefinitionServedFromDifferentHostServiceMock2() { 29 | super(9094) 30 | } 31 | 32 | void mockOpenApiDefinition() { 33 | client.register(get(urlPathEqualTo("/custom-path-to/openapi-definition")) 34 | .willReturn(aResponse() 35 | .withStatus(200) 36 | .withHeader("Content-Type", "application/yaml") 37 | .withBodyFile("openapi-definition-served-from-different-host-service/openapi.public.yaml") 38 | ) 39 | ) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/setup/wiremock/OrderServiceMock.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest.setup.wiremock 20 | 21 | 22 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse 23 | import static com.github.tomakehurst.wiremock.client.WireMock.get 24 | import static com.github.tomakehurst.wiremock.client.WireMock.post 25 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo 26 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching 27 | 28 | @Singleton(strict = false) 29 | class OrderServiceMock extends BaseWireMock { 30 | 31 | OrderServiceMock() { 32 | super(9092) 33 | } 34 | 35 | void mockOpenApiDefinition() { 36 | client.register(get(urlPathEqualTo("/custom-path-to/openapi-definition")) 37 | .willReturn(aResponse() 38 | .withStatus(200) 39 | .withHeader("Content-Type", "application/yaml") 40 | .withBodyFile("order-service/openapi.public.yaml") 41 | ) 42 | ) 43 | } 44 | 45 | void mockOpenApiDefinitionContainingUnknownFilter() { 46 | client.register(get(urlPathEqualTo("/custom-path-to/openapi-definition")) 47 | .willReturn(aResponse() 48 | .withStatus(200) 49 | .withHeader("Content-Type", "application/yaml") 50 | .withBodyFile("order-service/openapi.public.unknown-filter.yaml") 51 | ) 52 | ) 53 | } 54 | 55 | void mockGetOrders() { 56 | client.register(get(urlPathMatching("/api/users/.*?/orders")) 57 | .willReturn(aResponse() 58 | .withStatus(200) 59 | .withHeader("Content-Type", "application/json") 60 | .withBody('[{"id": "order-id-1"}]') 61 | ) 62 | ) 63 | } 64 | 65 | void mockGetOrder() { 66 | client.register(get(urlPathMatching("/api/users/.*?/orders/.*?")) 67 | .willReturn(aResponse() 68 | .withStatus(200) 69 | .withHeader("Content-Type", "application/json") 70 | .withBody('{"id": "order-id-1"}') 71 | ) 72 | ) 73 | } 74 | 75 | void mockPostOrder() { 76 | client.register(post(urlPathMatching("/api/users/.*?/orders")) 77 | .willReturn(aResponse() 78 | .withStatus(201) 79 | .withHeader("Content-Type", "application/json") 80 | .withBody('{"id": "order-id-1"}') 81 | ) 82 | ) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/setup/wiremock/UserServiceMock.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package componenttest.setup.wiremock 20 | 21 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse 22 | import static com.github.tomakehurst.wiremock.client.WireMock.get 23 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo 24 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo 25 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching 26 | 27 | @Singleton(strict = false) 28 | class UserServiceMock extends BaseWireMock { 29 | 30 | UserServiceMock() { 31 | super(9091) 32 | } 33 | 34 | void mockOpenApiDefinition(String path = "/internal/openapi-definition") { 35 | client.register(get(urlPathEqualTo(path)) 36 | .willReturn(aResponse() 37 | .withStatus(200) 38 | .withHeader("Content-Type", "application/yaml") 39 | .withBodyFile("user-service/openapi.public.yaml") 40 | ) 41 | ) 42 | } 43 | 44 | void mockGetUsers() { 45 | client.register(get(urlEqualTo("/users")) 46 | .willReturn(aResponse() 47 | .withStatus(200) 48 | .withHeader("Content-Type", "application/json") 49 | .withBody('[{"id": "user-id-1"}]') 50 | ) 51 | ) 52 | } 53 | 54 | void mockGetUser() { 55 | client.register(get(urlPathMatching("/users/.*?")) 56 | .willReturn(aResponse() 57 | .withStatus(200) 58 | .withHeader("Content-Type", "application/json") 59 | .withBody('{"id": "user-id-1"}') 60 | ) 61 | ) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/net/bretti/openapi/route/definition/locator/autoconfigure/OpenApiRouteDefinitionLocatorAutoConfigurationTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.autoconfigure 20 | 21 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties 22 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiDefinitionRepository 23 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiDefinitionUpdateScheduler 24 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocator 25 | import org.assertj.core.api.Assertions 26 | import org.springframework.boot.autoconfigure.AutoConfigurations 27 | import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration 28 | import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration 29 | import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner 30 | import org.springframework.cloud.gateway.config.GatewayAutoConfiguration 31 | import spock.lang.Specification 32 | 33 | class OpenApiRouteDefinitionLocatorAutoConfigurationTest extends Specification { 34 | private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() 35 | 36 | def "OpenAPI Route Definition Locator is active if GatewayAutoConfiguration is present"() { 37 | expect: 38 | contextRunner 39 | .withConfiguration(AutoConfigurations.of( 40 | OpenApiRouteDefinitionLocatorAutoConfiguration, 41 | GatewayAutoConfiguration, 42 | WebFluxAutoConfiguration, 43 | SslAutoConfiguration 44 | )) 45 | .run({ context -> 46 | Assertions.assertThat(context).hasSingleBean(OpenApiDefinitionRepository) 47 | Assertions.assertThat(context).hasSingleBean(OpenApiRouteDefinitionLocatorProperties) 48 | Assertions.assertThat(context).hasSingleBean(OpenApiRouteDefinitionLocator) 49 | Assertions.assertThat(context).hasSingleBean(OpenApiDefinitionUpdateScheduler) 50 | }) 51 | } 52 | 53 | def "OpenAPI Route Definition Locator is inactive if GatewayAutoConfiguration is present but OpenApiRouteDefinitionLocator is disabled"() { 54 | expect: 55 | contextRunner 56 | .withConfiguration(AutoConfigurations.of( 57 | OpenApiRouteDefinitionLocatorAutoConfiguration, 58 | GatewayAutoConfiguration, 59 | WebFluxAutoConfiguration, 60 | SslAutoConfiguration 61 | )) 62 | .withPropertyValues("openapi-route-definition-locator.enabled=false") 63 | .run({ context -> 64 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionRepository) 65 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorProperties) 66 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocator) 67 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionUpdateScheduler) 68 | }) 69 | } 70 | 71 | def "OpenAPI Route Definition Locator is inactive if Spring Cloud Gateway is disabled"() { 72 | expect: 73 | contextRunner 74 | .withConfiguration(AutoConfigurations.of( 75 | OpenApiRouteDefinitionLocatorAutoConfiguration, 76 | GatewayAutoConfiguration, 77 | WebFluxAutoConfiguration, 78 | )) 79 | .withPropertyValues("spring.cloud.gateway.enabled=false") 80 | .run({ context -> 81 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionRepository) 82 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorProperties) 83 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocator) 84 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionUpdateScheduler) 85 | }) 86 | } 87 | 88 | def "OpenAPI Route Definition Locator is inactive if GatewayAutoConfiguration is absent"() { 89 | expect: 90 | contextRunner 91 | .withConfiguration(AutoConfigurations.of( 92 | OpenApiRouteDefinitionLocatorAutoConfiguration, 93 | )) 94 | .withPropertyValues("openapi-route-definition-locator.enabled=false") 95 | .run({ context -> 96 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionRepository) 97 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorProperties) 98 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocator) 99 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionUpdateScheduler) 100 | }) 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/net/bretti/openapi/route/definition/locator/autoconfigure/OpenApiRouteDefinitionLocatorMetricsAutoConfigurationTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.openapi.route.definition.locator.autoconfigure 20 | 21 | 22 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorMetrics 23 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorTimedMetrics 24 | import org.assertj.core.api.Assertions 25 | import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration 26 | import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration 27 | import org.springframework.boot.autoconfigure.AutoConfigurations 28 | import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener 29 | import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration 30 | import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration 31 | import org.springframework.boot.logging.LogLevel 32 | import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner 33 | import org.springframework.cloud.gateway.config.GatewayAutoConfiguration 34 | import org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration 35 | import spock.lang.Specification 36 | 37 | class OpenApiRouteDefinitionLocatorMetricsAutoConfigurationTest extends Specification { 38 | private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() 39 | 40 | def "OpenAPI Route Definition Locator metrics are active if GatewayMetricsAutoConfiguration is present"() { 41 | expect: 42 | contextRunner 43 | .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) 44 | .withConfiguration(AutoConfigurations.of( 45 | OpenApiRouteDefinitionLocatorMetricsAutoConfiguration, 46 | OpenApiRouteDefinitionLocatorAutoConfiguration, 47 | GatewayAutoConfiguration, 48 | GatewayMetricsAutoConfiguration, 49 | WebFluxAutoConfiguration, 50 | SslAutoConfiguration, 51 | MetricsAutoConfiguration, 52 | CompositeMeterRegistryAutoConfiguration, 53 | )) 54 | .run({ context -> 55 | Assertions.assertThat(context).hasSingleBean(OpenApiRouteDefinitionLocatorMetrics) 56 | Assertions.assertThat(context).hasSingleBean(OpenApiRouteDefinitionLocatorTimedMetrics) 57 | }) 58 | } 59 | 60 | def "OpenAPI Route Definition Locator metrics are inactive if they are explicitly disabled"() { 61 | expect: 62 | contextRunner 63 | .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) 64 | .withConfiguration(AutoConfigurations.of( 65 | OpenApiRouteDefinitionLocatorMetricsAutoConfiguration, 66 | OpenApiRouteDefinitionLocatorAutoConfiguration, 67 | GatewayAutoConfiguration, 68 | GatewayMetricsAutoConfiguration, 69 | WebFluxAutoConfiguration, 70 | SslAutoConfiguration, 71 | MetricsAutoConfiguration, 72 | CompositeMeterRegistryAutoConfiguration, 73 | )) 74 | .withPropertyValues("openapi-route-definition-locator.metrics.enabled=false") 75 | .run({ context -> 76 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorMetrics) 77 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorTimedMetrics) 78 | }) 79 | } 80 | 81 | def "OpenAPI Route Definition Locator metrics are inactive if the OpenAPI Route Definition Locator is explicitly disabled"() { 82 | expect: 83 | contextRunner 84 | .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) 85 | .withConfiguration(AutoConfigurations.of( 86 | OpenApiRouteDefinitionLocatorMetricsAutoConfiguration, 87 | OpenApiRouteDefinitionLocatorAutoConfiguration, 88 | GatewayAutoConfiguration, 89 | GatewayMetricsAutoConfiguration, 90 | WebFluxAutoConfiguration, 91 | SslAutoConfiguration, 92 | MetricsAutoConfiguration, 93 | CompositeMeterRegistryAutoConfiguration, 94 | )) 95 | .withPropertyValues("openapi-route-definition-locator.enabled=false") 96 | .run({ context -> 97 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorMetrics) 98 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorTimedMetrics) 99 | }) 100 | } 101 | 102 | def "OpenAPI Route Definition Locator metrics are inactive if Spring Cloud metrics are explicitly disabled"() { 103 | expect: 104 | contextRunner 105 | .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) 106 | .withConfiguration(AutoConfigurations.of( 107 | OpenApiRouteDefinitionLocatorMetricsAutoConfiguration, 108 | OpenApiRouteDefinitionLocatorAutoConfiguration, 109 | GatewayAutoConfiguration, 110 | GatewayMetricsAutoConfiguration, 111 | WebFluxAutoConfiguration, 112 | SslAutoConfiguration, 113 | MetricsAutoConfiguration, 114 | CompositeMeterRegistryAutoConfiguration, 115 | )) 116 | .withPropertyValues("spring.cloud.gateway.metrics.enabled=false") 117 | .run({ context -> 118 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorMetrics) 119 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorTimedMetrics) 120 | }) 121 | } 122 | 123 | def "OpenAPI Route Definition Locator metrics are inactive if metrics are globally absent"() { 124 | expect: 125 | contextRunner 126 | .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) 127 | .withConfiguration(AutoConfigurations.of( 128 | OpenApiRouteDefinitionLocatorMetricsAutoConfiguration, 129 | OpenApiRouteDefinitionLocatorAutoConfiguration, 130 | GatewayAutoConfiguration, 131 | GatewayMetricsAutoConfiguration, 132 | WebFluxAutoConfiguration, 133 | SslAutoConfiguration, 134 | )) 135 | .run({ context -> 136 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorMetrics) 137 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorTimedMetrics) 138 | }) 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/resources/application-custom-global-openapi-definition-url.yml: -------------------------------------------------------------------------------- 1 | openapi-route-definition-locator: 2 | openapi-definition-uri: /global-custom-path-to/openapi-definition 3 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: api-manager 4 | cloud: 5 | gateway: 6 | default-filters: 7 | - AddResponseHeader=X-Response-FromGlobalConfig, global-sample-value 8 | httpserver: 9 | wiretap: true 10 | httpclient: 11 | pool: 12 | metrics: true 13 | wiretap: true 14 | 15 | logging: 16 | level: 17 | root: info 18 | org.springframework.cloud.gateway: debug 19 | org.springframework.http.server.reactive: debug 20 | org.springframework.web.reactive: info 21 | org.springframework.boot.autoconfigure.web: debug 22 | reactor.netty: info 23 | redisratelimiter: debug 24 | 25 | management: 26 | endpoint: 27 | gateway: 28 | enabled: true 29 | endpoints: 30 | web: 31 | exposure: 32 | include: "*" 33 | 34 | openapi-route-definition-locator: 35 | # enabled: false 36 | default-route-settings: 37 | filters: 38 | - AddResponseHeader=X-Response-DefaultForAllServices, sample-value-all 39 | order: 5 40 | metadata: 41 | defaultForAllServices: "OptionValueAll" 42 | services: 43 | - id: user-service 44 | uri: http://localhost:9091 45 | default-route-settings: 46 | filters: 47 | - AddResponseHeader=X-Response-DefaultForOneService, sample-value-one 48 | order: 6 49 | metadata: 50 | defaultForOneService: "OptionValueOne" 51 | - id: order-service 52 | uri: http://localhost:9092 53 | openapi-definition-uri: /custom-path-to/openapi-definition 54 | - id: openapi-definition-served-from-different-host-service 55 | uri: http://localhost:9093 56 | openapi-definition-uri: http://localhost:9094/custom-path-to/openapi-definition 57 | - id: openapi-definition-in-classpath-service 58 | uri: http://localhost:9095 59 | openapi-definition-uri: "classpath:openapi-definition-in-classpath-service/openapi.public.yaml" 60 | update-scheduler: 61 | fixed-delay: 1s 62 | remove-routes-on-update-failures-after: 5s 63 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/resources/openapi-definition-in-classpath-service/openapi.public.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: API of Service which has its OpenAPI definition in the classpath of the API Gateway 4 | version: 0.1.0 5 | servers: 6 | - url: http://localhost:8080 7 | paths: 8 | /entities-of-service-with-openapi-definition-in-classpath: 9 | get: 10 | summary: Test resource 11 | tags: 12 | - Test 13 | responses: 14 | 200: 15 | description: An array of test entities 16 | content: 17 | application/json: 18 | schema: 19 | type: array 20 | items: 21 | $ref: '#/components/schemas/TestEntity' 22 | components: 23 | schemas: 24 | TestEntity: 25 | type: object 26 | properties: 27 | id: 28 | type: string 29 | required: 30 | - id 31 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/resources/wiremock/__files/openapi-definition-served-from-different-host-service/openapi.public.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Service with OpenAPI definition served from different host 4 | version: 0.1.0 5 | servers: 6 | - url: http://localhost:8080 7 | paths: 8 | /things: 9 | get: 10 | summary: Returns a list of things. 11 | tags: 12 | - Things 13 | responses: 14 | 200: 15 | description: An array of things 16 | content: 17 | application/json: 18 | schema: 19 | type: array 20 | items: 21 | $ref: '#/components/schemas/Thing' 22 | components: 23 | schemas: 24 | Thing: 25 | type: object 26 | properties: 27 | id: 28 | type: string 29 | required: 30 | - id 31 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/resources/wiremock/__files/order-service/openapi.public.unknown-filter.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Orders API 4 | version: 0.1.0 5 | servers: 6 | - url: http://localhost:8080 7 | x-gateway-route-settings: 8 | filters: 9 | - PrefixPath=/api 10 | - AddResponseHeader=X-Response-FromOpenApiDefinition, sample-value 11 | order: 1 12 | metadata: 13 | optionName: "OptionValue" 14 | compositeObject: 15 | name: "value" 16 | aList: 17 | - foo 18 | - bar 19 | iAmNumber: 1 20 | paths: 21 | /users/{userId}/orders: 22 | get: 23 | summary: Returns a list of orders. 24 | tags: 25 | - Orders 26 | parameters: 27 | - $ref: '#/components/parameters/UserId' 28 | responses: 29 | 200: 30 | description: An array of orders 31 | content: 32 | application/json: 33 | schema: 34 | type: array 35 | items: 36 | $ref: '#/components/schemas/Order' 37 | x-gateway-route-settings: 38 | filters: 39 | - UnknownFilter= 40 | - name: SetStatus 41 | args: 42 | status: 418 43 | metadata: 44 | compositeObject: 45 | otherName: 2 46 | aList: 47 | - quuz 48 | post: 49 | summary: Creates an order. 50 | tags: 51 | - Orders 52 | parameters: 53 | - $ref: '#/components/parameters/UserId' 54 | requestBody: 55 | content: 56 | application/json: 57 | schema: 58 | $ref: '#/components/schemas/Order' 59 | responses: 60 | 201: 61 | description: An order 62 | content: 63 | application/json: 64 | schema: 65 | $ref: '#/components/schemas/Order' 66 | /users/{userId}/orders/{orderId}: 67 | get: 68 | summary: Returns an order. 69 | tags: 70 | - Orders 71 | parameters: 72 | - $ref: '#/components/parameters/UserId' 73 | - $ref: '#/components/parameters/OrderId' 74 | responses: 75 | 200: 76 | description: An order 77 | content: 78 | application/json: 79 | schema: 80 | $ref: '#/components/schemas/Order' 81 | x-gateway-route-settings: 82 | filters: 83 | - name: SetStatus 84 | args: 85 | status: 418 86 | components: 87 | parameters: 88 | UserId: 89 | name: userId 90 | in: path 91 | schema: 92 | type: string 93 | format: uuid 94 | required: true 95 | OrderId: 96 | name: orderId 97 | in: path 98 | schema: 99 | type: string 100 | format: uuid 101 | required: true 102 | schemas: 103 | Order: 104 | type: object 105 | properties: 106 | id: 107 | type: string 108 | items: 109 | type: array 110 | items: 111 | type: object 112 | properties: 113 | article: 114 | type: string 115 | example: Bread 116 | amount: 117 | type: integer 118 | example: 2 119 | required: 120 | - article 121 | - amount 122 | required: 123 | - id 124 | - items 125 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/resources/wiremock/__files/order-service/openapi.public.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Orders API 4 | version: 0.1.0 5 | servers: 6 | - url: http://localhost:8080 7 | x-gateway-route-settings: 8 | filters: 9 | - PrefixPath=/api 10 | - AddResponseHeader=X-Response-FromOpenApiDefinition, sample-value 11 | order: 1 12 | metadata: 13 | optionName: "OptionValue" 14 | compositeObject: 15 | name: "value" 16 | aList: 17 | - foo 18 | - bar 19 | iAmNumber: 1 20 | paths: 21 | /users/{userId}/orders: 22 | get: 23 | summary: Returns a list of orders. 24 | tags: 25 | - Orders 26 | parameters: 27 | - $ref: '#/components/parameters/UserId' 28 | responses: 29 | 200: 30 | description: An array of orders 31 | content: 32 | application/json: 33 | schema: 34 | type: array 35 | items: 36 | $ref: '#/components/schemas/Order' 37 | x-gateway-route-settings: 38 | filters: 39 | - name: SetStatus 40 | args: 41 | status: 418 42 | metadata: 43 | compositeObject: 44 | otherName: 2 45 | aList: 46 | - quuz 47 | post: 48 | summary: Creates an order. 49 | tags: 50 | - Orders 51 | parameters: 52 | - $ref: '#/components/parameters/UserId' 53 | requestBody: 54 | content: 55 | application/json: 56 | schema: 57 | $ref: '#/components/schemas/Order' 58 | responses: 59 | 201: 60 | description: An order 61 | content: 62 | application/json: 63 | schema: 64 | $ref: '#/components/schemas/Order' 65 | /users/{userId}/orders/{orderId}: 66 | get: 67 | summary: Returns an order. 68 | tags: 69 | - Orders 70 | parameters: 71 | - $ref: '#/components/parameters/UserId' 72 | - $ref: '#/components/parameters/OrderId' 73 | responses: 74 | 200: 75 | description: An order 76 | content: 77 | application/json: 78 | schema: 79 | $ref: '#/components/schemas/Order' 80 | x-gateway-route-settings: 81 | filters: 82 | - name: SetStatus 83 | args: 84 | status: 418 85 | components: 86 | parameters: 87 | UserId: 88 | name: userId 89 | in: path 90 | schema: 91 | type: string 92 | format: uuid 93 | required: true 94 | OrderId: 95 | name: orderId 96 | in: path 97 | schema: 98 | type: string 99 | format: uuid 100 | required: true 101 | schemas: 102 | Order: 103 | type: object 104 | properties: 105 | id: 106 | type: string 107 | items: 108 | type: array 109 | items: 110 | type: object 111 | properties: 112 | article: 113 | type: string 114 | example: Bread 115 | amount: 116 | type: integer 117 | example: 2 118 | required: 119 | - article 120 | - amount 121 | required: 122 | - id 123 | - items 124 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/resources/wiremock/__files/user-service/openapi.public.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Users API 4 | version: 0.1.0 5 | servers: 6 | - url: http://localhost:8080 7 | x-auth-type: Application 8 | paths: 9 | /users: 10 | get: 11 | summary: Returns a list of users. 12 | tags: 13 | - Users 14 | responses: 15 | 200: 16 | description: An array of users 17 | content: 18 | application/json: 19 | schema: 20 | type: array 21 | items: 22 | $ref: '#/components/schemas/User' 23 | /users/{userId}: 24 | get: 25 | summary: Returns a list of users. 26 | tags: 27 | - Users 28 | parameters: 29 | - name: userId 30 | in: path 31 | schema: 32 | type: string 33 | format: uuid 34 | required: true 35 | responses: 36 | 200: 37 | description: A user 38 | content: 39 | application/json: 40 | schema: 41 | $ref: '#/components/schemas/User' 42 | x-gateway-route-settings: 43 | predicates: 44 | - After=2022-01-20T17:42:47.789+01:00[Europe/Berlin] 45 | - name: Header 46 | args: 47 | header: Required-Test-Header 48 | regexp: required-test-header-.* 49 | x-auth-type: Application User 50 | components: 51 | schemas: 52 | User: 53 | type: object 54 | properties: 55 | id: 56 | type: string 57 | name: 58 | type: string 59 | example: John Doe 60 | required: 61 | - id 62 | - name 63 | -------------------------------------------------------------------------------- /sample-apps/README.md: -------------------------------------------------------------------------------- 1 | # Sample Apps using the OpenAPI Route Definition Locator 2 | 3 | Here you can find an example of running two microservices behind a Spring Cloud Gateway using 4 | the [OpenAPI Route Definition Locator](../README.md) in a Kubernetes cluster. This example 5 | includes Grafana dashboards for monitoring the OpenAPI Route Definition Locator. 6 | 7 | ## Prerequisites 8 | 9 | Install the following software. Make sure their installed binaries are in your `$PATH`. 10 | 11 | 1. [Kubernetes](https://kubernetes.io) (e.g. via [Docker Desktop](https://www.docker.com/products/docker-desktop/)) 12 | 2. [Helm](https://helm.sh) 13 | 3. [Task](https://taskfile.dev) 14 | 15 | For a nice terminal based user interface to manage your Kubernetes cluster, you may want to install 16 | [k9s](https://k9scli.io). 17 | 18 | ## Build and Deploy 19 | 20 | Run in your shell: 21 | ```shell 22 | cd sample-apps 23 | task build deploy 24 | ``` 25 | 26 | See the [troubleshooting](#troubleshooting) tips if the Helm deployments fail. 27 | 28 | After the deployment succeeded you will see output like this: 29 | ``` 30 | API Gateway: 31 | Base URL: http://api.127.0.0.1.nip.io 32 | 33 | Try: 34 | curl -v http://api.127.0.0.1.nip.io/users | jq . 35 | curl -v http://api.127.0.0.1.nip.io/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868 | jq . 36 | curl -v http://api.127.0.0.1.nip.io/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868/orders | jq . 37 | curl -v http://api.127.0.0.1.nip.io/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868/orders/271acbc1-50b0-45ae-ad04-a231f1057714 | jq . 38 | curl -v http://api.127.0.0.1.nip.io/actuator/gateway/routes | jq . 39 | 40 | Grafana: 41 | URL : http://grafana.127.0.0.1.nip.io/ 42 | Login credentials: admin // admin 43 | Gateway Dashboard: http://grafana.127.0.0.1.nip.io/d/c09a9f35 44 | Service Dashboard: http://grafana.127.0.0.1.nip.io/d/179dd90b 45 | ``` 46 | 47 | ## API Requests via API Gateway 48 | 49 | You can send some API requests via the API gateway to the example services: 50 | ```shell 51 | curl -v http://api.127.0.0.1.nip.io/users 52 | curl -v http://api.127.0.0.1.nip.io/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868 53 | curl -v http://api.127.0.0.1.nip.io/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868/orders 54 | curl -v http://api.127.0.0.1.nip.io/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868/orders/271acbc1-50b0-45ae-ad04-a231f1057714 55 | ``` 56 | 57 | ## Grafana Dashboards 58 | 59 | There are Grafana dashboards you can look at. Open . Login with 60 | the credentials `admin` / `admin`. 61 | 62 | There is a [Spring Boot Dashboard](http://grafana.127.0.0.1.nip.io/d/179dd90b) and 63 | a [Spring Cloud Gateway Dashboard](http://grafana.127.0.0.1.nip.io/d/c09a9f35). 64 | 65 | ## Troubleshooting 66 | 67 | The Helm deployment of 68 | [kube-prometheus-stack](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack) 69 | may fail if you have incompatible versions of the CRDs created by this chart installed in your 70 | Kubernetes cluster. Run `task clean` to have them deleted. Then run `task deploy` again. 71 | 72 | 73 | -------------------------------------------------------------------------------- /sample-apps/Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | 5 | build: 6 | desc: Build the Docker images for all sample services. 7 | dir: .. 8 | cmds: 9 | - ./gradlew bootBuildImage 10 | 11 | deploy: 12 | desc: Deploy all sample services. 13 | deps: 14 | - deploy-services 15 | cmds: 16 | - task: deploy-api-gateway 17 | 18 | deploy-services: 19 | deps: 20 | - deploy-nginx-controller 21 | - deploy-prometheus 22 | cmds: 23 | - task: deploy-service-orders 24 | - task: deploy-service-users 25 | 26 | deploy-debug: 27 | deps: 28 | - deploy-debug-nginx-controller 29 | - deploy-debug-service-orders 30 | - deploy-debug-service-users 31 | - deploy-debug-api-gateway 32 | 33 | undeploy: 34 | desc: Undeploy all sample services. 35 | ignore_error: true 36 | deps: 37 | - undeploy-nginx-controller 38 | - undeploy-prometheus 39 | - undeploy-service-orders 40 | - undeploy-service-users 41 | - undeploy-api-gateway 42 | 43 | clean: 44 | desc: Undeploys all sample services and cleans up Kubernetes (e.g. delete installed CRDs) 45 | ignore_error: true 46 | deps: 47 | - undeploy 48 | - clean-prometheus 49 | - clean-nginx-controller 50 | 51 | deploy-nginx-controller: 52 | cmds: 53 | - helm repo add nginx-stable https://helm.nginx.com/stable 54 | - helm repo update 55 | - helm upgrade --install nginx-controller nginx-stable/nginx-ingress --version 1.0.0 56 | 57 | deploy-debug-nginx-controller: 58 | cmds: 59 | - helm repo add nginx-stable https://helm.nginx.com/stable 60 | - helm repo update 61 | - helm upgrade --dry-run --debug --install nginx-controller nginx-stable/nginx-ingress --version 1.0.0 62 | 63 | undeploy-nginx-controller: 64 | ignore_error: true 65 | cmds: 66 | - helm delete nginx-controller 67 | 68 | clean-nginx-controller: 69 | ignore_error: true 70 | deps: 71 | - undeploy-nginx-controller 72 | cmds: 73 | - kubectl delete crd apdoslogconfs.appprotectdos.f5.com 74 | - kubectl delete crd apdospolicies.appprotectdos.f5.com 75 | - kubectl delete crd aplogconfs.appprotect.f5.com 76 | - kubectl delete crd appolicies.appprotect.f5.com 77 | - kubectl delete crd apusersigs.appprotect.f5.com 78 | - kubectl delete crd dnsendpoints.externaldns.nginx.org 79 | - kubectl delete crd dosprotectedresources.appprotectdos.f5.com 80 | - kubectl delete crd globalconfigurations.k8s.nginx.org 81 | - kubectl delete crd policies.k8s.nginx.org 82 | - kubectl delete crd transportservers.k8s.nginx.org 83 | - kubectl delete crd virtualserverroutes.k8s.nginx.org 84 | - kubectl delete crd virtualservers.k8s.nginx.org 85 | deploy-api-gateway: 86 | dir: api-gateway/helm 87 | cmds: 88 | - helm upgrade --install api-gateway . 89 | 90 | deploy-debug-api-gateway: 91 | dir: api-gateway/helm 92 | cmds: 93 | - helm upgrade --dry-run --debug --install api-gateway . 94 | 95 | undeploy-api-gateway: 96 | ignore_error: true 97 | cmds: 98 | - helm delete api-gateway 99 | 100 | deploy-service-orders: 101 | dir: service-orders/helm 102 | cmds: 103 | - helm upgrade --install service-orders . 104 | 105 | deploy-debug-service-orders: 106 | dir: service-orders/helm 107 | cmds: 108 | - helm upgrade --dry-run --debug --install service-orders . 109 | 110 | undeploy-service-orders: 111 | ignore_error: true 112 | cmds: 113 | - helm delete service-orders 114 | 115 | deploy-service-users: 116 | dir: service-users/helm 117 | cmds: 118 | - helm upgrade --install service-users . 119 | 120 | deploy-debug-service-users: 121 | dir: service-users/helm 122 | cmds: 123 | - helm upgrade --dry-run --debug --install service-users . 124 | 125 | undeploy-service-users: 126 | ignore_error: true 127 | cmds: 128 | - helm delete service-users 129 | 130 | deploy-prometheus: 131 | cmds: 132 | - helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 133 | - helm repo update 134 | - | 135 | helm upgrade --install prometheus prometheus-community/kube-prometheus-stack --version 51.2.0 \ 136 | --set prometheus-node-exporter.hostRootFsMount.enabled=false \ 137 | --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \ 138 | --set grafana.ingress.enabled=true \ 139 | --set grafana.ingress.ingressClassName=nginx \ 140 | --set 'grafana.ingress.hosts={"grafana.127.0.0.1.nip.io"}' \ 141 | --set grafana.adminPassword=admin \ 142 | --set grafana.sidecar.dashboards.enabled=true 143 | 144 | undeploy-prometheus: 145 | ignore_error: true 146 | cmds: 147 | - helm delete prometheus 148 | 149 | clean-prometheus: 150 | ignore_error: true 151 | deps: 152 | - undeploy-prometheus 153 | cmds: 154 | - kubectl delete crd alertmanagerconfigs.monitoring.coreos.com 155 | - kubectl delete crd alertmanagers.monitoring.coreos.com 156 | - kubectl delete crd podmonitors.monitoring.coreos.com 157 | - kubectl delete crd probes.monitoring.coreos.com 158 | - kubectl delete crd prometheusagents.monitoring.coreos.com 159 | - kubectl delete crd prometheuses.monitoring.coreos.com 160 | - kubectl delete crd prometheusrules.monitoring.coreos.com 161 | - kubectl delete crd scrapeconfigs.monitoring.coreos.com 162 | - kubectl delete crd servicemonitors.monitoring.coreos.com 163 | - kubectl delete crd thanosrulers.monitoring.coreos.com 164 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | idea 4 | id("org.springframework.boot") 5 | id("com.github.ben-manes.versions") 6 | } 7 | 8 | apply(plugin = "io.spring.dependency-management") 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | tasks.jar { 15 | archiveFileName.set("api-gateway.jar") 16 | } 17 | 18 | dependencies { 19 | implementation(platform("org.springframework.cloud:spring-cloud-dependencies:2024.0.0")) 20 | implementation("org.springframework.cloud:spring-cloud-starter-gateway") 21 | implementation(project(":openapi-route-definition-locator-spring-cloud-starter")) 22 | runtimeOnly("org.springframework.boot:spring-boot-starter-actuator") 23 | runtimeOnly("io.micrometer:micrometer-registry-prometheus") 24 | } 25 | 26 | java { 27 | toolchain { 28 | // Keep the same Java compatibility as Spring Cloud Gateway. 29 | languageVersion.set(JavaLanguageVersion.of(17)) 30 | } 31 | } 32 | 33 | tasks.getByName("bootBuildImage") { 34 | environment.set(mapOf("BP_JVM_VERSION" to "17")) 35 | imageName.set("bretti.net/sample-api-gateway:latest") 36 | } 37 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: Deploys the API gateway. 3 | name: api-gateway 4 | version: 0.1.0 5 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | API Gateway: 3 | Base URL: http://{{ .Values.ingress.host }} 4 | 5 | Try: 6 | curl -v http://{{ .Values.ingress.host }}/users | jq . 7 | curl -v http://{{ .Values.ingress.host }}/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868 | jq . 8 | curl -v http://{{ .Values.ingress.host }}/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868/orders | jq . 9 | curl -v http://{{ .Values.ingress.host }}/users/6ac8d69c-7a8c-4ce3-854a-2a51f8bbd868/orders/271acbc1-50b0-45ae-ad04-a231f1057714 | jq . 10 | curl -v http://{{ .Values.ingress.host }}/actuator/gateway/routes | jq . 11 | 12 | Grafana: 13 | URL : http://grafana.127.0.0.1.nip.io/ 14 | Login credentials: admin // admin 15 | Gateway Dashboard: http://grafana.127.0.0.1.nip.io/d/c09a9f35 16 | Service Dashboard: http://grafana.127.0.0.1.nip.io/d/179dd90b 17 | -------------------------------------------------------------------------------- 18 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | data: 6 | spring.application.json: | 7 | {{ .Values.appConfig | toJson }} 8 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | spec: 6 | replicas: {{ .Values.replicaCount }} 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/name: "{{ .Release.Name }}" 10 | template: 11 | metadata: 12 | labels: 13 | app.kubernetes.io/name: "{{ .Release.Name }}" 14 | annotations: 15 | checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 16 | spec: 17 | containers: 18 | - name: main 19 | image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" 20 | imagePullPolicy: {{ .Values.image.pullPolicy }} 21 | ports: 22 | - name: http 23 | containerPort: {{ .Values.appConfig.server.port }} 24 | env: 25 | - name: SPRING_APPLICATION_JSON 26 | valueFrom: 27 | configMapKeyRef: 28 | name: "{{ .Release.Name }}" 29 | key: spring.application.json 30 | - name: BPL_JVM_THREAD_COUNT 31 | value: "50" 32 | - name: JAVA_TOOL_OPTIONS 33 | value: "-Xss256k -XX:ReservedCodeCacheSize=16M -XX:MaxMetaspaceSize=64M -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10" 34 | - name: MALLOC_ARENA_MAX 35 | value: "1" 36 | resources: 37 | limits: 38 | memory: 384Mi 39 | startupProbe: 40 | httpGet: 41 | path: /actuator/health/readiness 42 | port: {{ .Values.appConfig.server.port }} 43 | periodSeconds: 1 44 | failureThreshold: 20 45 | livenessProbe: 46 | httpGet: 47 | path: /actuator/health/liveness 48 | port: {{ .Values.appConfig.server.port }} 49 | readinessProbe: 50 | httpGet: 51 | path: /actuator/health/readiness 52 | port: {{ .Values.appConfig.server.port }} 53 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/templates/grafana-dashboards.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: "{{ .Release.Name }}-grafana-dashboards" 5 | labels: 6 | grafana_dashboard: '1' 7 | data: 8 | spring-boot-dashboard.json: |- 9 | {{ .Files.Get "files/dashboards/spring-boot-dashboard.json" | indent 4 }} 10 | spring-cloud-gateway-dashboard.json: |- 11 | {{ .Files.Get "files/dashboards/spring-cloud-gateway-dashboard.json" | indent 4 }} 12 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | spec: 6 | ingressClassName: nginx 7 | rules: 8 | - host: "{{ .Values.ingress.host }}" 9 | http: 10 | paths: 11 | - path: / 12 | pathType: Prefix 13 | backend: 14 | service: 15 | name: "{{ .Release.Name }}" 16 | port: 17 | name: http 18 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | labels: 6 | app.kubernetes.io/name: "{{ .Release.Name }}" 7 | application: "{{ .Release.Name }}" 8 | spec: 9 | ports: 10 | - port: {{ .Values.appConfig.server.port }} 11 | targetPort: http 12 | name: http 13 | selector: 14 | app.kubernetes.io/name: "{{ .Release.Name }}" 15 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | labels: 6 | app.kubernetes.io/name: "{{ .Release.Name }}" 7 | spec: 8 | selector: 9 | matchLabels: 10 | app.kubernetes.io/name: "{{ .Release.Name }}" 11 | endpoints: 12 | - port: http 13 | scheme: "http" 14 | path: "/actuator/prometheus" 15 | interval: 5s 16 | targetLabels: 17 | - application 18 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/helm/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | image: 3 | name: bretti.net/sample-api-gateway 4 | tag: latest 5 | pullPolicy: IfNotPresent 6 | 7 | ingress: 8 | host: api.127.0.0.1.nip.io 9 | 10 | appConfig: 11 | server: 12 | port: 8080 13 | openapi-route-definition-locator: 14 | default-route-settings: 15 | filters: 16 | - AddResponseHeader=X-Response-DefaultForAllServices, sample-value-all 17 | services: 18 | - id: service-users 19 | uri: http://service-users:8080 20 | default-route-settings: 21 | filters: 22 | - AddResponseHeader=X-Response-DefaultForOneService, sample-value-one 23 | - id: service-orders 24 | uri: http://service-orders:8080 25 | openapi-definition-uri: /custom-path-to/openapi-definition 26 | update-scheduler: 27 | fixed-delay: 30s 28 | remove-routes-on-update-failures-after: 120s 29 | 30 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/src/main/java/net/bretti/sample/apigateway/ApiGatewayApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.apigateway; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | 24 | @SpringBootApplication 25 | public class ApiGatewayApplication { 26 | public static void main(String[] args) { 27 | SpringApplication.run(ApiGatewayApplication.class, args); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/src/main/java/net/bretti/sample/apigateway/customizer/SampleOpenApiRouteDefinitionCustomizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.apigateway.customizer; 20 | 21 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 22 | import net.bretti.openapi.route.definition.locator.core.customizer.OpenApiRouteDefinitionCustomizer; 23 | import org.springframework.cloud.gateway.filter.FilterDefinition; 24 | import org.springframework.cloud.gateway.route.RouteDefinition; 25 | import org.springframework.stereotype.Component; 26 | 27 | import java.util.Map; 28 | 29 | @Component 30 | public class SampleOpenApiRouteDefinitionCustomizer implements OpenApiRouteDefinitionCustomizer { 31 | @Override 32 | public void customize( 33 | RouteDefinition routeDefinition, 34 | OpenApiRouteDefinitionLocatorProperties.Service service, 35 | Map openApiGlobalExtensions, 36 | Map openApiOperationExtensions 37 | ) { 38 | Object xSampleKeyValue = openApiOperationExtensions.get("x-sample-key"); 39 | if (!(xSampleKeyValue instanceof String)) { 40 | return; 41 | } 42 | 43 | FilterDefinition filter = new FilterDefinition("AddResponseHeader=X-Sample-Key-Was, " + xSampleKeyValue); 44 | routeDefinition.getFilters().add(filter); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: api-manager 4 | cloud: 5 | gateway: 6 | default-filters: 7 | - AddResponseHeader=X-Response-FromGlobalConfig, global-sample-value 8 | httpserver: 9 | wiretap: true 10 | httpclient: 11 | pool: 12 | metrics: true 13 | wiretap: true 14 | 15 | logging: 16 | level: 17 | root: info 18 | org.springframework.cloud.gateway: debug 19 | org.springframework.http.server.reactive: debug 20 | org.springframework.web.reactive: info 21 | org.springframework.boot.autoconfigure.web: debug 22 | reactor.netty: info 23 | redisratelimiter: debug 24 | 25 | management: 26 | endpoint: 27 | gateway: 28 | enabled: true 29 | endpoints: 30 | web: 31 | exposure: 32 | include: "*" 33 | 34 | openapi-route-definition-locator: 35 | #enabled: false 36 | #metrics: 37 | # enabled: false 38 | default-route-settings: 39 | filters: 40 | - AddResponseHeader=X-Response-DefaultForAllServices, sample-value-all 41 | services: 42 | - id: service1 43 | uri: http://localhost:8081 44 | default-route-settings: 45 | filters: 46 | - AddResponseHeader=X-Response-DefaultForOneService, sample-value-one 47 | - id: service2 48 | uri: http://localhost:8082 49 | openapi-definition-uri: /custom-path-to/openapi-definition 50 | update-scheduler: 51 | fixed-delay: 30s 52 | remove-routes-on-update-failures-after: 120s 53 | -------------------------------------------------------------------------------- /sample-apps/service-orders/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | idea 4 | id("org.springframework.boot") 5 | id("com.github.ben-manes.versions") 6 | } 7 | 8 | apply(plugin = "io.spring.dependency-management") 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | tasks.jar { 15 | archiveFileName.set("service-orders") 16 | } 17 | 18 | dependencies { 19 | implementation("org.springframework.boot:spring-boot-starter-web") 20 | runtimeOnly("org.springframework.boot:spring-boot-starter-actuator") 21 | runtimeOnly("io.micrometer:micrometer-registry-prometheus") 22 | implementation("org.projectlombok:lombok:1.18.36") 23 | annotationProcessor("org.projectlombok:lombok:1.18.36") 24 | testImplementation("org.projectlombok:lombok:1.18.36") 25 | testAnnotationProcessor("org.projectlombok:lombok:1.18.36") 26 | } 27 | 28 | java { 29 | toolchain { 30 | languageVersion.set(JavaLanguageVersion.of(17)) 31 | } 32 | } 33 | 34 | tasks.getByName("bootBuildImage") { 35 | environment.set(mapOf("BP_JVM_VERSION" to "17")) 36 | imageName.set("bretti.net/sample-service-orders:latest") 37 | } 38 | -------------------------------------------------------------------------------- /sample-apps/service-orders/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: Deploys the orders service. 3 | name: service-orders 4 | version: 0.1.0 5 | -------------------------------------------------------------------------------- /sample-apps/service-orders/helm/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | data: 6 | spring.application.json: | 7 | {{ .Values.appConfig | toJson }} 8 | -------------------------------------------------------------------------------- /sample-apps/service-orders/helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | spec: 6 | replicas: {{ .Values.replicaCount }} 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/name: "{{ .Release.Name }}" 10 | template: 11 | metadata: 12 | labels: 13 | app.kubernetes.io/name: "{{ .Release.Name }}" 14 | annotations: 15 | checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 16 | spec: 17 | containers: 18 | - name: main 19 | image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" 20 | imagePullPolicy: {{ .Values.image.pullPolicy }} 21 | ports: 22 | - name: http 23 | containerPort: {{ .Values.appConfig.server.port }} 24 | env: 25 | - name: SPRING_APPLICATION_JSON 26 | valueFrom: 27 | configMapKeyRef: 28 | name: "{{ .Release.Name }}" 29 | key: spring.application.json 30 | - name: BPL_JVM_THREAD_COUNT 31 | value: "50" 32 | - name: JAVA_TOOL_OPTIONS 33 | value: "-Xss256k -XX:ReservedCodeCacheSize=16M -XX:MaxMetaspaceSize=64M -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10" 34 | - name: MALLOC_ARENA_MAX 35 | value: "1" 36 | resources: 37 | limits: 38 | memory: 320Mi 39 | startupProbe: 40 | httpGet: 41 | path: /actuator/health/readiness 42 | port: {{ .Values.appConfig.server.port }} 43 | periodSeconds: 1 44 | failureThreshold: 20 45 | livenessProbe: 46 | httpGet: 47 | path: /actuator/health/liveness 48 | port: {{ .Values.appConfig.server.port }} 49 | readinessProbe: 50 | httpGet: 51 | path: /actuator/health/readiness 52 | port: {{ .Values.appConfig.server.port }} 53 | -------------------------------------------------------------------------------- /sample-apps/service-orders/helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | labels: 6 | app.kubernetes.io/name: "{{ .Release.Name }}" 7 | application: "{{ .Release.Name }}" 8 | spec: 9 | ports: 10 | - port: {{ .Values.appConfig.server.port }} 11 | targetPort: http 12 | name: http 13 | selector: 14 | app.kubernetes.io/name: "{{ .Release.Name }}" 15 | -------------------------------------------------------------------------------- /sample-apps/service-orders/helm/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | labels: 6 | app.kubernetes.io/name: "{{ .Release.Name }}" 7 | spec: 8 | selector: 9 | matchLabels: 10 | app.kubernetes.io/name: "{{ .Release.Name }}" 11 | endpoints: 12 | - port: http 13 | scheme: "http" 14 | path: "/actuator/prometheus" 15 | interval: 5s 16 | targetLabels: 17 | - application 18 | -------------------------------------------------------------------------------- /sample-apps/service-orders/helm/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | image: 3 | name: bretti.net/sample-service-orders 4 | tag: latest 5 | pullPolicy: IfNotPresent 6 | 7 | appConfig: 8 | server: 9 | port: 8080 10 | -------------------------------------------------------------------------------- /sample-apps/service-orders/src/main/java/net/bretti/sample/service/orders/ServiceOrdersApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.orders; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | 24 | @SpringBootApplication 25 | public class ServiceOrdersApplication { 26 | public static void main(String[] args) { 27 | SpringApplication.run(ServiceOrdersApplication.class, args); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample-apps/service-orders/src/main/java/net/bretti/sample/service/orders/controller/OpenApiDefinitionController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.orders.controller; 20 | 21 | import org.springframework.core.io.ClassPathResource; 22 | import org.springframework.http.ResponseEntity; 23 | import org.springframework.web.bind.annotation.GetMapping; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.RestController; 26 | 27 | @RequestMapping(path = "/custom-path-to/openapi-definition") 28 | @RestController 29 | public class OpenApiDefinitionController { 30 | @GetMapping 31 | public ResponseEntity get() { 32 | return ResponseEntity.ok(new ClassPathResource("openapi.public.yaml")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample-apps/service-orders/src/main/java/net/bretti/sample/service/orders/controller/OrdersController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.orders.controller; 20 | 21 | import net.bretti.sample.service.orders.dto.Order; 22 | import net.bretti.sample.service.orders.dto.OrderItem; 23 | import org.springframework.web.bind.annotation.GetMapping; 24 | import org.springframework.web.bind.annotation.PathVariable; 25 | import org.springframework.web.bind.annotation.RequestMapping; 26 | import org.springframework.web.bind.annotation.ResponseBody; 27 | import org.springframework.web.bind.annotation.RestController; 28 | 29 | import java.util.Arrays; 30 | import java.util.List; 31 | import java.util.UUID; 32 | 33 | @RestController 34 | @RequestMapping(path = "/api/users/{userId}/orders") 35 | public class OrdersController { 36 | @GetMapping 37 | @ResponseBody 38 | public List get() { 39 | Order order1 = Order.builder() 40 | .id(UUID.randomUUID()) 41 | .items(Arrays.asList( 42 | OrderItem.builder().article("Bread").amount(2).build(), 43 | OrderItem.builder().article("Butter").amount(1).build() 44 | )) 45 | .build(); 46 | Order order2 = Order.builder() 47 | .id(UUID.randomUUID()) 48 | .items(Arrays.asList( 49 | OrderItem.builder().article("Potatoes").amount(1).build(), 50 | OrderItem.builder().article("Sour Creme").amount(2).build() 51 | )) 52 | .build(); 53 | return Arrays.asList(order1, order2); 54 | } 55 | 56 | @GetMapping(path = "/{orderId}") 57 | public Order getOrder(@PathVariable UUID orderId) { 58 | return Order.builder() 59 | .id(orderId) 60 | .items(Arrays.asList( 61 | OrderItem.builder().article("Bread").amount(2).build(), 62 | OrderItem.builder().article("Butter").amount(1).build() 63 | )) 64 | .build(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /sample-apps/service-orders/src/main/java/net/bretti/sample/service/orders/dto/Order.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.orders.dto; 20 | 21 | import lombok.Builder; 22 | import lombok.Data; 23 | 24 | import java.util.List; 25 | import java.util.UUID; 26 | 27 | @Data 28 | @Builder 29 | public class Order { 30 | private final UUID id; 31 | private final List items; 32 | } 33 | -------------------------------------------------------------------------------- /sample-apps/service-orders/src/main/java/net/bretti/sample/service/orders/dto/OrderItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.orders.dto; 20 | 21 | import lombok.Builder; 22 | import lombok.Data; 23 | 24 | @Data 25 | @Builder 26 | public class OrderItem { 27 | private final String article; 28 | private final int amount; 29 | } 30 | -------------------------------------------------------------------------------- /sample-apps/service-orders/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8082 3 | tomcat: 4 | mbeanregistry: 5 | enabled: true 6 | 7 | management: 8 | endpoints: 9 | web: 10 | exposure: 11 | include: 12 | - health 13 | - info 14 | - prometheus 15 | -------------------------------------------------------------------------------- /sample-apps/service-orders/src/main/resources/openapi.public.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Orders API 4 | version: 0.1.0 5 | servers: 6 | - url: http://localhost:8080 7 | x-gateway-route-settings: 8 | filters: 9 | - PrefixPath=/api 10 | - AddResponseHeader=X-Response-FromOpenApiDefinition, sample-value 11 | - name: SetStatus 12 | args: 13 | status: 418 14 | order: 1 15 | metadata: 16 | optionName: "OptionValue" 17 | compositeObject: 18 | name: "value" 19 | aList: 20 | - foo 21 | - bar 22 | iAmNumber: 1 23 | paths: 24 | /users/{userId}/orders: 25 | get: 26 | summary: Returns a list of orders. 27 | tags: 28 | - Orders 29 | parameters: 30 | - $ref: '#/components/parameters/UserId' 31 | responses: 32 | 200: 33 | description: An array of orders 34 | content: 35 | application/json: 36 | schema: 37 | type: array 38 | items: 39 | $ref: '#/components/schemas/Order' 40 | x-gateway-route-settings: 41 | metadata: 42 | compositeObject: 43 | otherName: 2 44 | aList: 45 | - quuz 46 | /users/{userId}/orders/{orderId}: 47 | get: 48 | summary: Returns an order. 49 | tags: 50 | - Orders 51 | parameters: 52 | - $ref: '#/components/parameters/UserId' 53 | - $ref: '#/components/parameters/OrderId' 54 | responses: 55 | 200: 56 | description: An order 57 | content: 58 | application/json: 59 | schema: 60 | $ref: '#/components/schemas/Order' 61 | components: 62 | parameters: 63 | UserId: 64 | name: userId 65 | in: path 66 | schema: 67 | type: string 68 | format: uuid 69 | required: true 70 | OrderId: 71 | name: orderId 72 | in: path 73 | schema: 74 | type: string 75 | format: uuid 76 | required: true 77 | schemas: 78 | Order: 79 | type: object 80 | properties: 81 | id: 82 | type: string 83 | format: uuid 84 | items: 85 | type: array 86 | items: 87 | type: object 88 | properties: 89 | article: 90 | type: string 91 | example: Bread 92 | amount: 93 | type: integer 94 | example: 2 95 | required: 96 | - article 97 | - amount 98 | required: 99 | - id 100 | - items 101 | -------------------------------------------------------------------------------- /sample-apps/service-users/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | idea 4 | id("org.springframework.boot") 5 | id("com.github.ben-manes.versions") 6 | } 7 | 8 | apply(plugin = "io.spring.dependency-management") 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | tasks.jar { 15 | archiveFileName.set("service-users") 16 | } 17 | 18 | dependencies { 19 | implementation("org.springframework.boot:spring-boot-starter-web") 20 | runtimeOnly("org.springframework.boot:spring-boot-starter-actuator") 21 | runtimeOnly("io.micrometer:micrometer-registry-prometheus") 22 | implementation("org.projectlombok:lombok:1.18.36") 23 | annotationProcessor("org.projectlombok:lombok:1.18.36") 24 | testImplementation("org.projectlombok:lombok:1.18.36") 25 | testAnnotationProcessor("org.projectlombok:lombok:1.18.36") 26 | } 27 | 28 | java { 29 | toolchain { 30 | languageVersion.set(JavaLanguageVersion.of(17)) 31 | } 32 | } 33 | 34 | tasks.getByName("bootBuildImage") { 35 | environment.set(mapOf("BP_JVM_VERSION" to "17")) 36 | imageName.set("bretti.net/sample-service-users:latest") 37 | } 38 | -------------------------------------------------------------------------------- /sample-apps/service-users/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: Deploys the users service. 3 | name: service-users 4 | version: 0.1.0 5 | -------------------------------------------------------------------------------- /sample-apps/service-users/helm/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | data: 6 | spring.application.json: | 7 | {{ .Values.appConfig | toJson }} 8 | -------------------------------------------------------------------------------- /sample-apps/service-users/helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | spec: 6 | replicas: {{ .Values.replicaCount }} 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/name: "{{ .Release.Name }}" 10 | template: 11 | metadata: 12 | labels: 13 | app.kubernetes.io/name: "{{ .Release.Name }}" 14 | annotations: 15 | checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 16 | spec: 17 | containers: 18 | - name: main 19 | image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" 20 | imagePullPolicy: {{ .Values.image.pullPolicy }} 21 | ports: 22 | - name: http 23 | containerPort: {{ .Values.appConfig.server.port }} 24 | env: 25 | - name: SPRING_APPLICATION_JSON 26 | valueFrom: 27 | configMapKeyRef: 28 | name: "{{ .Release.Name }}" 29 | key: spring.application.json 30 | - name: BPL_JVM_THREAD_COUNT 31 | value: "50" 32 | - name: JAVA_TOOL_OPTIONS 33 | value: "-Xss256k -XX:ReservedCodeCacheSize=16M -XX:MaxMetaspaceSize=64M -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10" 34 | - name: MALLOC_ARENA_MAX 35 | value: "1" 36 | resources: 37 | limits: 38 | memory: 320Mi 39 | startupProbe: 40 | httpGet: 41 | path: /actuator/health/readiness 42 | port: {{ .Values.appConfig.server.port }} 43 | periodSeconds: 1 44 | failureThreshold: 20 45 | livenessProbe: 46 | httpGet: 47 | path: /actuator/health/liveness 48 | port: {{ .Values.appConfig.server.port }} 49 | readinessProbe: 50 | httpGet: 51 | path: /actuator/health/readiness 52 | port: {{ .Values.appConfig.server.port }} 53 | -------------------------------------------------------------------------------- /sample-apps/service-users/helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | labels: 6 | app.kubernetes.io/name: "{{ .Release.Name }}" 7 | application: "{{ .Release.Name }}" 8 | spec: 9 | ports: 10 | - port: {{ .Values.appConfig.server.port }} 11 | targetPort: http 12 | name: http 13 | selector: 14 | app.kubernetes.io/name: "{{ .Release.Name }}" 15 | -------------------------------------------------------------------------------- /sample-apps/service-users/helm/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: "{{ .Release.Name }}" 5 | labels: 6 | app.kubernetes.io/name: "{{ .Release.Name }}" 7 | spec: 8 | selector: 9 | matchLabels: 10 | app.kubernetes.io/name: "{{ .Release.Name }}" 11 | endpoints: 12 | - port: http 13 | scheme: "http" 14 | path: "/actuator/prometheus" 15 | interval: 5s 16 | targetLabels: 17 | - application 18 | -------------------------------------------------------------------------------- /sample-apps/service-users/helm/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | image: 3 | name: bretti.net/sample-service-users 4 | tag: latest 5 | pullPolicy: IfNotPresent 6 | 7 | appConfig: 8 | server: 9 | port: 8080 10 | -------------------------------------------------------------------------------- /sample-apps/service-users/src/main/java/net/bretti/sample/service/users/ServiceUsersApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.users; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | 24 | @SpringBootApplication 25 | public class ServiceUsersApplication { 26 | public static void main(String[] args) { 27 | SpringApplication.run(ServiceUsersApplication.class, args); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample-apps/service-users/src/main/java/net/bretti/sample/service/users/controller/OpenApiDefinitionController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.users.controller; 20 | 21 | import org.springframework.core.io.ClassPathResource; 22 | import org.springframework.http.ResponseEntity; 23 | import org.springframework.web.bind.annotation.GetMapping; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.RestController; 26 | 27 | @RequestMapping(path = "/internal/openapi-definition") 28 | @RestController 29 | public class OpenApiDefinitionController { 30 | @GetMapping 31 | public ResponseEntity get() { 32 | return ResponseEntity.ok(new ClassPathResource("openapi.public.yaml")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample-apps/service-users/src/main/java/net/bretti/sample/service/users/controller/UsersController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.users.controller; 20 | 21 | import net.bretti.sample.service.users.dto.User; 22 | import org.springframework.web.bind.annotation.GetMapping; 23 | import org.springframework.web.bind.annotation.PathVariable; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.ResponseBody; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | import java.util.Arrays; 29 | import java.util.List; 30 | import java.util.UUID; 31 | 32 | @RestController 33 | @RequestMapping(path = "/users") 34 | public class UsersController { 35 | @GetMapping 36 | @ResponseBody 37 | public List getUsers() { 38 | User user1 = User.builder().id(UUID.randomUUID()).name("John Doe").build(); 39 | User user2 = User.builder().id(UUID.randomUUID()).name("Jane Doe").build(); 40 | return Arrays.asList(user1, user2); 41 | } 42 | 43 | @GetMapping(path = "/{userId}") 44 | @ResponseBody 45 | public User getUser(@PathVariable UUID userId) { 46 | return User.builder().id(userId).name("John Doe").build(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sample-apps/service-users/src/main/java/net/bretti/sample/service/users/dto/User.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Jan Bretschneider 3 | * 4 | * Licensed under the MIT License (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You can find the License in the `LICENSE` file at the top level of 7 | * this repository or may obtain a copy at 8 | * 9 | * https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/master/LICENSE 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package net.bretti.sample.service.users.dto; 20 | 21 | import lombok.Builder; 22 | import lombok.Data; 23 | 24 | import java.util.UUID; 25 | 26 | @Data 27 | @Builder 28 | public class User { 29 | private final UUID id; 30 | private final String name; 31 | } 32 | -------------------------------------------------------------------------------- /sample-apps/service-users/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | tomcat: 4 | mbeanregistry: 5 | enabled: true 6 | 7 | management: 8 | endpoints: 9 | web: 10 | exposure: 11 | include: 12 | - health 13 | - info 14 | - prometheus 15 | -------------------------------------------------------------------------------- /sample-apps/service-users/src/main/resources/openapi.public.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Users API 4 | version: 0.1.0 5 | servers: 6 | - url: http://localhost:8080 7 | paths: 8 | /users: 9 | get: 10 | summary: Returns a list of users. 11 | tags: 12 | - Users 13 | responses: 14 | 200: 15 | description: An array of users 16 | content: 17 | application/json: 18 | schema: 19 | type: array 20 | items: 21 | $ref: '#/components/schemas/User' 22 | /users/{userId}: 23 | get: 24 | summary: Returns a list of users. 25 | tags: 26 | - Users 27 | parameters: 28 | - name: userId 29 | in: path 30 | schema: 31 | type: string 32 | format: uuid 33 | required: true 34 | responses: 35 | 200: 36 | description: A user 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/User' 41 | x-sample-key: x-sample-key-value 42 | components: 43 | schemas: 44 | User: 45 | type: object 46 | properties: 47 | id: 48 | type: string 49 | format: uuid 50 | name: 51 | type: string 52 | example: John Doe 53 | required: 54 | - id 55 | - name 56 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "openapi-route-definition-locator" 2 | include( 3 | "openapi-route-definition-locator-bom", 4 | "openapi-route-definition-locator-core", 5 | "openapi-route-definition-locator-spring-cloud-starter", 6 | "sample-apps:api-gateway", 7 | "sample-apps:service-orders", 8 | "sample-apps:service-users", 9 | ) 10 | --------------------------------------------------------------------------------