├── .gitignore ├── docs └── images │ └── overview.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── sample-apps ├── api-gateway │ ├── helm │ │ ├── Chart.yaml │ │ ├── templates │ │ │ ├── configmap.yaml │ │ │ ├── service.yaml │ │ │ ├── grafana-dashboards.yaml │ │ │ ├── ingress.yaml │ │ │ ├── servicemonitor.yaml │ │ │ ├── NOTES.txt │ │ │ └── deployment.yaml │ │ └── values.yaml │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── net │ │ │ │ └── bretti │ │ │ │ └── sample │ │ │ │ └── apigateway │ │ │ │ ├── ApiGatewayApplication.java │ │ │ │ ├── customizer │ │ │ │ └── SampleOpenApiRouteDefinitionCustomizer.java │ │ │ │ └── filter │ │ │ │ └── SampleOpenApiRouteDefinitionFilter.java │ │ │ └── resources │ │ │ └── application.yml │ └── build.gradle.kts ├── service-users │ ├── helm │ │ ├── Chart.yaml │ │ ├── templates │ │ │ ├── configmap.yaml │ │ │ ├── service.yaml │ │ │ ├── servicemonitor.yaml │ │ │ └── deployment.yaml │ │ └── values.yaml │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── application.yml │ │ │ └── openapi.public.yaml │ │ │ └── java │ │ │ └── net │ │ │ └── bretti │ │ │ └── sample │ │ │ └── service │ │ │ └── users │ │ │ ├── dto │ │ │ └── User.java │ │ │ ├── ServiceUsersApplication.java │ │ │ └── controller │ │ │ ├── OpenApiDefinitionController.java │ │ │ └── UsersController.java │ └── build.gradle.kts ├── service-orders │ ├── helm │ │ ├── Chart.yaml │ │ ├── templates │ │ │ ├── configmap.yaml │ │ │ ├── service.yaml │ │ │ ├── servicemonitor.yaml │ │ │ └── deployment.yaml │ │ └── values.yaml │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── application.yml │ │ │ └── openapi.public.yaml │ │ │ └── java │ │ │ └── net │ │ │ └── bretti │ │ │ └── sample │ │ │ └── service │ │ │ └── orders │ │ │ ├── dto │ │ │ ├── OrderItem.java │ │ │ └── Order.java │ │ │ ├── ServiceOrdersApplication.java │ │ │ └── controller │ │ │ ├── OpenApiDefinitionController.java │ │ │ └── OrdersController.java │ └── build.gradle.kts ├── README.md └── Taskfile.yml ├── openapi-route-definition-locator-spring-cloud-starter ├── src │ ├── test │ │ ├── resources │ │ │ ├── application-with-gateway-name.yml │ │ │ ├── application-custom-global-openapi-definition-url.yml │ │ │ ├── application-route-definition-filtering.yml │ │ │ ├── wiremock │ │ │ │ └── __files │ │ │ │ │ ├── openapi-definition-served-from-different-host-service │ │ │ │ │ └── openapi.public.yaml │ │ │ │ │ ├── user-service │ │ │ │ │ └── openapi.public.yaml │ │ │ │ │ └── order-service │ │ │ │ │ ├── openapi.public.unknown-filter.yaml │ │ │ │ │ └── openapi.public.yaml │ │ │ ├── openapi-definition-in-classpath-service │ │ │ │ └── openapi.public.yaml │ │ │ └── application.yml │ │ └── groovy │ │ │ ├── componenttest │ │ │ ├── setup │ │ │ │ ├── app │ │ │ │ │ ├── TestApiGatewayApplication.groovy │ │ │ │ │ ├── EnvironmentRouteDefinitionFilter.groovy │ │ │ │ │ └── XAuthTypeRouteDefinitionCustomizer.groovy │ │ │ │ ├── wiremock │ │ │ │ │ ├── BaseWireMock.groovy │ │ │ │ │ ├── OpenapiDefinitionServedFromDifferentHostServiceMock1.groovy │ │ │ │ │ ├── OpenapiDefinitionServedFromDifferentHostServiceMock2.groovy │ │ │ │ │ ├── UserServiceMock.groovy │ │ │ │ │ └── OrderServiceMock.groovy │ │ │ │ └── basetest │ │ │ │ │ └── BaseCompTest.groovy │ │ │ └── RouteDefinitionFilteringCompTest.groovy │ │ │ └── net │ │ │ └── bretti │ │ │ └── openapi │ │ │ └── route │ │ │ └── definition │ │ │ └── locator │ │ │ └── autoconfigure │ │ │ ├── FilterOrderingTest.groovy │ │ │ ├── OpenApiRouteDefinitionLocatorAutoConfigurationTest.groovy │ │ │ └── OpenApiRouteDefinitionLocatorMetricsAutoConfigurationTest.groovy │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── spring │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── java │ │ └── net │ │ └── bretti │ │ └── openapi │ │ └── route │ │ └── definition │ │ └── locator │ │ └── autoconfigure │ │ ├── OpenApiRouteDefinitionLocatorMetricsAutoConfiguration.java │ │ └── OpenApiRouteDefinitionLocatorAutoConfiguration.java └── build.gradle.kts ├── settings.gradle.kts ├── .github ├── dependabot.yml └── workflows │ └── gradle.yml ├── .editorconfig ├── LICENSE ├── openapi-route-definition-locator-core ├── src │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── bretti │ │ │ └── openapi │ │ │ └── route │ │ │ └── definition │ │ │ └── locator │ │ │ └── core │ │ │ ├── impl │ │ │ ├── OpenApiRouteDefinitionPublishException.java │ │ │ ├── utils │ │ │ │ ├── Optionals.java │ │ │ │ ├── GatewayRouteSettingsUtil.java │ │ │ │ └── MapMerge.java │ │ │ ├── OpenApiRouteDefinitionLocatorTimedMetrics.java │ │ │ ├── OpenApiDefinitionUpdateScheduler.java │ │ │ ├── OpenApiOperation.java │ │ │ ├── validator │ │ │ │ └── NullOrNotBlank.java │ │ │ ├── filter │ │ │ │ ├── EnabledFlagFilter.java │ │ │ │ └── GatewayNameFilter.java │ │ │ ├── OpenApiRouteDefinitionLocatorMetrics.java │ │ │ └── OpenApiRouteDefinitionLocator.java │ │ │ ├── filter │ │ │ └── OpenApiRouteDefinitionFilter.java │ │ │ ├── customizer │ │ │ └── OpenApiRouteDefinitionCustomizer.java │ │ │ └── config │ │ │ ├── validation │ │ │ ├── ValidBaseUri.java │ │ │ ├── OnlyUniqueServiceIds.java │ │ │ ├── ValidOpenApiDefinitionUri.java │ │ │ ├── ValidOpenApiDefinitionUriValidator.java │ │ │ ├── OnlyUniqueServiceIdsValidator.java │ │ │ └── ValidBaseUriValidator.java │ │ │ └── OpenApiRouteDefinitionLocatorProperties.java │ └── test │ │ └── groovy │ │ └── net │ │ └── bretti │ │ └── openapi │ │ └── route │ │ └── definition │ │ └── locator │ │ └── core │ │ ├── config │ │ └── OpenApiRouteDefinitionLocatorPropertiesValidationTest.groovy │ │ └── impl │ │ └── utils │ │ └── MapMergeTest.groovy └── build.gradle.kts ├── openapi-route-definition-locator-bom └── build.gradle.kts └── gradlew.bat /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | build/ 4 | out/ 5 | classes/ 6 | gradle.properties 7 | *.jfr 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /docs/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/HEAD/docs/images/overview.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbretsch/openapi-route-definition-locator/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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/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-orders/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: Deploys the orders service. 3 | name: service-orders 4 | version: 0.1.0 5 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/resources/application-with-gateway-name.yml: -------------------------------------------------------------------------------- 1 | openapi-route-definition-locator: 2 | gateway-name: gateway-02-active 3 | -------------------------------------------------------------------------------- /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/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-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-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-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 | -------------------------------------------------------------------------------- /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-route-definition-filtering.yml: -------------------------------------------------------------------------------- 1 | openapi-route-definition-locator: 2 | gateway-name: test-gateway 3 | services: 4 | - id: route-definition-filtering-service 5 | uri: http://localhost:9096 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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-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/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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | max_line_length = 120 8 | tab_width = 4 9 | ij_continuation_indent_size = 8 10 | ij_formatter_off_tag = @formatter:off 11 | ij_formatter_on_tag = @formatter:on 12 | ij_formatter_tags_enabled = true 13 | ij_smart_tabs = false 14 | ij_visual_guides = 15 | ij_wrap_on_typing = false 16 | 17 | [*.java] 18 | ij_java_class_count_to_use_import_on_demand = 99 19 | ij_java_names_count_to_use_import_on_demand = 99 20 | ij_java_continuation_indent_size = 4 21 | 22 | [*.groovy] 23 | ij_groovy_class_count_to_use_import_on_demand = 99 24 | ij_groovy_names_count_to_use_import_on_demand = 99 25 | ij_groovy_continuation_indent_size = 4 26 | 27 | [{*.yaml,*.yml}] 28 | indent_size = 2 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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.42") 23 | annotationProcessor("org.projectlombok:lombok:1.18.42") 24 | testImplementation("org.projectlombok:lombok:1.18.42") 25 | testAnnotationProcessor("org.projectlombok:lombok:1.18.42") 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-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.42") 23 | annotationProcessor("org.projectlombok:lombok:1.18.42") 24 | testImplementation("org.projectlombok:lombok:1.18.42") 25 | testAnnotationProcessor("org.projectlombok:lombok:1.18.42") 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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-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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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:2025.1.0")) 20 | implementation("org.springframework.cloud:spring-cloud-starter-gateway-server-webflux") 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 | // TODO: Possibly remove this dependency again after 26 | // TODO: has been resolved. 27 | runtimeOnly("org.springframework.boot:spring-boot-micrometer-tracing") 28 | } 29 | 30 | java { 31 | toolchain { 32 | // Keep the same Java compatibility as Spring Cloud Gateway. 33 | languageVersion.set(JavaLanguageVersion.of(17)) 34 | } 35 | } 36 | 37 | tasks.getByName("bootBuildImage") { 38 | environment.set(mapOf("BP_JVM_VERSION" to "17")) 39 | imageName.set("bretti.net/sample-api-gateway:latest") 40 | } 41 | -------------------------------------------------------------------------------- /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/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-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-core/src/main/java/net/bretti/openapi/route/definition/locator/core/filter/OpenApiRouteDefinitionFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.filter; 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 OpenApiRouteDefinitionFilter { 28 | boolean test(RouteDefinition routeDefinition, 29 | OpenApiRouteDefinitionLocatorProperties.Service service, 30 | Map openApiGlobalExtensions, 31 | Map openApiOperationExtensions); 32 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/api-gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: api-manager 4 | cloud: 5 | gateway: 6 | server: 7 | webflux: 8 | default-filters: 9 | - AddResponseHeader=X-Response-FromGlobalConfig, global-sample-value 10 | httpserver: 11 | wiretap: true 12 | httpclient: 13 | pool: 14 | metrics: true 15 | wiretap: true 16 | 17 | logging: 18 | level: 19 | root: info 20 | org.springframework.cloud.gateway: debug 21 | org.springframework.http.server.reactive: debug 22 | org.springframework.web.reactive: info 23 | org.springframework.boot.autoconfigure.web: debug 24 | reactor.netty: info 25 | redisratelimiter: debug 26 | 27 | management: 28 | endpoint: 29 | gateway: 30 | access: read_only 31 | endpoints: 32 | web: 33 | exposure: 34 | include: "*" 35 | 36 | openapi-route-definition-locator: 37 | #enabled: false 38 | #metrics: 39 | # enabled: false 40 | default-route-settings: 41 | filters: 42 | - AddResponseHeader=X-Response-DefaultForAllServices, sample-value-all 43 | services: 44 | - id: service1 45 | uri: http://localhost:8081 46 | default-route-settings: 47 | filters: 48 | - AddResponseHeader=X-Response-DefaultForOneService, sample-value-one 49 | - id: service2 50 | uri: http://localhost:8082 51 | openapi-definition-uri: /custom-path-to/openapi-definition 52 | update-scheduler: 53 | fixed-delay: 30s 54 | remove-routes-on-update-failures-after: 120s 55 | -------------------------------------------------------------------------------- /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/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/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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | /not-published-because-of-custom-filter: 43 | # See `sample-apps/api-gateway/src/main/java/net/bretti/sample/apigateway/filter/SampleOpenApiRouteDefinitionFilter.java`. 44 | get: 45 | summary: Work in progress 46 | tags: 47 | - Misc 48 | responses: 49 | 200: 50 | description: Work in progress 51 | x-environment: dev 52 | components: 53 | schemas: 54 | User: 55 | type: object 56 | properties: 57 | id: 58 | type: string 59 | format: uuid 60 | name: 61 | type: string 62 | example: John Doe 63 | required: 64 | - id 65 | - name 66 | -------------------------------------------------------------------------------- /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/app/EnvironmentRouteDefinitionFilter.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.filter.OpenApiRouteDefinitionFilter 23 | import org.apache.commons.lang3.ObjectUtils 24 | import org.springframework.cloud.gateway.route.RouteDefinition 25 | import org.springframework.stereotype.Component 26 | 27 | @Component 28 | class EnvironmentRouteDefinitionFilter implements OpenApiRouteDefinitionFilter { 29 | @Override 30 | boolean test( 31 | RouteDefinition routeDefinition, 32 | OpenApiRouteDefinitionLocatorProperties.Service service, 33 | Map openApiGlobalExtensions, 34 | Map openApiOperationExtensions 35 | ) { 36 | Object apiOperationEnv = ObjectUtils.firstNonNull(openApiOperationExtensions['x-environment'], openApiGlobalExtensions['x-environment']) 37 | return apiOperationEnv == null || apiOperationEnv == "dev" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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/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-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/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 | -------------------------------------------------------------------------------- /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/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-spring-cloud-starter/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: api-manager 4 | cloud: 5 | gateway: 6 | server: 7 | webflux: 8 | default-filters: 9 | - AddResponseHeader=X-Response-FromGlobalConfig, global-sample-value 10 | httpserver: 11 | wiretap: true 12 | httpclient: 13 | pool: 14 | metrics: true 15 | wiretap: true 16 | logging: 17 | level: 18 | root: info 19 | net.bretti.openapi.route.definition.locator: debug 20 | org.springframework.cloud.gateway: debug 21 | org.springframework.http.server.reactive: debug 22 | org.springframework.web.reactive: info 23 | org.springframework.boot.autoconfigure.web: debug 24 | reactor.netty: info 25 | redisratelimiter: debug 26 | 27 | management: 28 | endpoint: 29 | gateway: 30 | access: read_only 31 | endpoints: 32 | web: 33 | exposure: 34 | include: "*" 35 | 36 | openapi-route-definition-locator: 37 | # enabled: false 38 | default-route-settings: 39 | filters: 40 | - AddResponseHeader=X-Response-DefaultForAllServices, sample-value-all 41 | order: 5 42 | metadata: 43 | defaultForAllServices: "OptionValueAll" 44 | services: 45 | - id: user-service 46 | uri: http://localhost:9091 47 | default-route-settings: 48 | filters: 49 | - AddResponseHeader=X-Response-DefaultForOneService, sample-value-one 50 | order: 6 51 | metadata: 52 | defaultForOneService: "OptionValueOne" 53 | - id: order-service 54 | uri: http://localhost:9092 55 | openapi-definition-uri: /custom-path-to/openapi-definition 56 | - id: openapi-definition-served-from-different-host-service 57 | uri: http://localhost:9093 58 | openapi-definition-uri: http://localhost:9094/custom-path-to/openapi-definition 59 | - id: openapi-definition-in-classpath-service 60 | uri: http://localhost:9095 61 | openapi-definition-uri: "classpath:openapi-definition-in-classpath-service/openapi.public.yaml" 62 | update-scheduler: 63 | fixed-delay: 1s 64 | remove-routes-on-update-failures-after: 5s 65 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/validator/NullOrNotBlank.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.validator; 20 | 21 | import org.hibernate.validator.constraints.ConstraintComposition; 22 | 23 | import jakarta.validation.Constraint; 24 | import jakarta.validation.Payload; 25 | import jakarta.validation.ReportAsSingleViolation; 26 | import jakarta.validation.constraints.NotBlank; 27 | import jakarta.validation.constraints.Null; 28 | import java.lang.annotation.Retention; 29 | import java.lang.annotation.Target; 30 | 31 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 32 | import static java.lang.annotation.ElementType.CONSTRUCTOR; 33 | import static java.lang.annotation.ElementType.FIELD; 34 | import static java.lang.annotation.ElementType.METHOD; 35 | import static java.lang.annotation.ElementType.PARAMETER; 36 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 37 | import static org.hibernate.validator.constraints.CompositionType.OR; 38 | 39 | @ConstraintComposition(OR) 40 | @Null 41 | @NotBlank 42 | @ReportAsSingleViolation 43 | @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) 44 | @Retention(RUNTIME) 45 | @Constraint(validatedBy = {}) 46 | public @interface NullOrNotBlank { 47 | String message() default "null or not blank"; 48 | 49 | Class[] groups() default {}; 50 | 51 | Class[] payload() default {}; 52 | } 53 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/utils/GatewayRouteSettingsUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.Map; 24 | import java.util.Optional; 25 | 26 | @UtilityClass 27 | public final class GatewayRouteSettingsUtil { 28 | 29 | public static final String X_GATEWAY_ROUTE_SETTINGS = "x-gateway-route-settings"; 30 | 31 | public static Optional> getGatewayRouteSettings(Map globalExtensions, Map operationExtensions) { 32 | Optional> globalGatewayRouteSettings = getGatewayRouteSettings(globalExtensions); 33 | Optional> operationGatewayRouteSettings = getGatewayRouteSettings(operationExtensions); 34 | 35 | return MapMerge.deepMerge(globalGatewayRouteSettings, operationGatewayRouteSettings); 36 | } 37 | 38 | public static Optional> getGatewayRouteSettings(Map extensions) { 39 | if (extensions == null) { 40 | return Optional.empty(); 41 | } 42 | 43 | Object gatewayRouteSettings = extensions.get(X_GATEWAY_ROUTE_SETTINGS); 44 | if (!(gatewayRouteSettings instanceof Map)) { 45 | return Optional.empty(); 46 | } 47 | 48 | return Optional.of((Map) gatewayRouteSettings); 49 | } 50 | } -------------------------------------------------------------------------------- /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 net.bretti.openapi.route.definition.locator.core.impl.utils.MapMerge; 24 | import org.springframework.cloud.gateway.filter.FilterDefinition; 25 | import org.springframework.cloud.gateway.route.RouteDefinition; 26 | import org.springframework.stereotype.Component; 27 | 28 | import java.util.Map; 29 | 30 | @Component 31 | public class SampleOpenApiRouteDefinitionCustomizer implements OpenApiRouteDefinitionCustomizer { 32 | @Override 33 | public void customize( 34 | RouteDefinition routeDefinition, 35 | OpenApiRouteDefinitionLocatorProperties.Service service, 36 | Map openApiGlobalExtensions, 37 | Map openApiOperationExtensions 38 | ) { 39 | Map openApiExtensions = MapMerge.deepMerge(openApiGlobalExtensions, openApiOperationExtensions); 40 | Object xSampleKeyValue = openApiExtensions.get("x-sample-key"); 41 | if (!(xSampleKeyValue instanceof String)) { 42 | return; 43 | } 44 | 45 | FilterDefinition filter = new FilterDefinition("AddResponseHeader=X-Sample-Key-Was, " + xSampleKeyValue); 46 | routeDefinition.getFilters().add(filter); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample-apps/api-gateway/src/main/java/net/bretti/sample/apigateway/filter/SampleOpenApiRouteDefinitionFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.filter; 20 | 21 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 22 | import net.bretti.openapi.route.definition.locator.core.filter.OpenApiRouteDefinitionFilter; 23 | import net.bretti.openapi.route.definition.locator.core.impl.utils.MapMerge; 24 | import org.springframework.cloud.gateway.route.RouteDefinition; 25 | import org.springframework.stereotype.Component; 26 | 27 | import java.util.Map; 28 | import java.util.Objects; 29 | 30 | @Component 31 | public class SampleOpenApiRouteDefinitionFilter implements OpenApiRouteDefinitionFilter { 32 | 33 | @Override 34 | public boolean test(RouteDefinition routeDefinition, 35 | OpenApiRouteDefinitionLocatorProperties.Service service, 36 | Map openApiGlobalExtensions, 37 | Map openApiOperationExtensions) { 38 | 39 | // Example: Only publish operations marked for the current environment. 40 | Map openApiExtensions = MapMerge.deepMerge(openApiGlobalExtensions, openApiOperationExtensions); 41 | Object apiOperationEnv = openApiExtensions.get("x-environment"); 42 | if (apiOperationEnv instanceof String) { 43 | String currentEnv = System.getenv("DEPLOY_ENV"); 44 | return Objects.equals(currentEnv, apiOperationEnv.toString()); 45 | } 46 | 47 | // Publish API operation if it specifies no environment. 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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-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-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-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.1.1-sc-2025.1-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://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" 63 | val snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/filter/EnabledFlagFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.filter; 20 | 21 | import lombok.extern.slf4j.Slf4j; 22 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties; 23 | import net.bretti.openapi.route.definition.locator.core.filter.OpenApiRouteDefinitionFilter; 24 | import net.bretti.openapi.route.definition.locator.core.impl.utils.GatewayRouteSettingsUtil; 25 | import org.springframework.cloud.gateway.route.RouteDefinition; 26 | import org.springframework.core.annotation.Order; 27 | 28 | import java.util.Map; 29 | import java.util.Optional; 30 | 31 | @Slf4j 32 | @Order(100) 33 | public class EnabledFlagFilter implements OpenApiRouteDefinitionFilter { 34 | 35 | private static final String ENABLED = "enabled"; 36 | 37 | @Override 38 | public boolean test(RouteDefinition routeDefinition, 39 | OpenApiRouteDefinitionLocatorProperties.Service service, 40 | Map openApiGlobalExtensions, 41 | Map openApiOperationExtensions) { 42 | 43 | Optional> gatewayRouteSettings = GatewayRouteSettingsUtil.getGatewayRouteSettings(openApiGlobalExtensions, openApiOperationExtensions); 44 | 45 | boolean enabled = getEnabled(gatewayRouteSettings); 46 | if (!enabled) { 47 | log.info("Route is filtered out because of 'enabled: false' on API operation, service={}, route-predicates={}", 48 | service.getId(), routeDefinition.getPredicates()); 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | private boolean getEnabled(Optional> gatewayRouteSettings) { 56 | if (!gatewayRouteSettings.isPresent()) { 57 | return true; // Default: enabled 58 | } 59 | 60 | Object enabled = gatewayRouteSettings.get().get(ENABLED); 61 | if (enabled instanceof Boolean) { 62 | return (Boolean) enabled; 63 | } 64 | 65 | return true; // Default: enabled 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /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-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-webflux") 8 | implementation("org.springframework:spring-webflux") 9 | implementation("io.swagger.parser.v3:swagger-parser:2.1.36") 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://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" 56 | val snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-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-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 | gateway-names: 39 | - gateway-01-inactive 40 | - gateway-02-active 41 | filters: 42 | - name: SetStatus 43 | args: 44 | status: 418 45 | metadata: 46 | compositeObject: 47 | otherName: 2 48 | aList: 49 | - quuz 50 | post: 51 | summary: Creates an order. 52 | tags: 53 | - Orders 54 | parameters: 55 | - $ref: '#/components/parameters/UserId' 56 | requestBody: 57 | content: 58 | application/json: 59 | schema: 60 | $ref: '#/components/schemas/Order' 61 | responses: 62 | 201: 63 | description: An order 64 | content: 65 | application/json: 66 | schema: 67 | $ref: '#/components/schemas/Order' 68 | x-gateway-route-settings: 69 | gateway-names: 70 | - gateway-01-inactive 71 | /users/{userId}/orders/{orderId}: 72 | get: 73 | summary: Returns an order. 74 | tags: 75 | - Orders 76 | parameters: 77 | - $ref: '#/components/parameters/UserId' 78 | - $ref: '#/components/parameters/OrderId' 79 | responses: 80 | 200: 81 | description: An order 82 | content: 83 | application/json: 84 | schema: 85 | $ref: '#/components/schemas/Order' 86 | x-gateway-route-settings: 87 | enabled: false 88 | filters: 89 | - name: SetStatus 90 | args: 91 | status: 418 92 | components: 93 | parameters: 94 | UserId: 95 | name: userId 96 | in: path 97 | schema: 98 | type: string 99 | format: uuid 100 | required: true 101 | OrderId: 102 | name: orderId 103 | in: path 104 | schema: 105 | type: string 106 | format: uuid 107 | required: true 108 | schemas: 109 | Order: 110 | type: object 111 | properties: 112 | id: 113 | type: string 114 | items: 115 | type: array 116 | items: 117 | type: object 118 | properties: 119 | article: 120 | type: string 121 | example: Bread 122 | amount: 123 | type: integer 124 | example: 2 125 | required: 126 | - article 127 | - amount 128 | required: 129 | - id 130 | - items 131 | -------------------------------------------------------------------------------- /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-webflux") 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-server-webflux") 12 | testImplementation("org.springframework.boot:spring-boot-actuator-autoconfigure") 13 | testImplementation("org.springframework.boot:spring-boot-starter-micrometer-metrics") 14 | testImplementation("org.springframework.boot:spring-boot-webtestclient") 15 | testRuntimeOnly("org.springframework.boot:spring-boot-starter-actuator") 16 | testRuntimeOnly("io.micrometer:micrometer-registry-prometheus") 17 | testImplementation("org.wiremock:wiremock-standalone:3.13.2") 18 | testImplementation("org.apache.commons:commons-lang3") 19 | } 20 | 21 | publishing { 22 | publications { 23 | create("mavenJava") { 24 | artifactId = "openapi-route-definition-locator-spring-cloud-starter" 25 | from(components["java"]) 26 | versionMapping { 27 | usage("java-api") { 28 | fromResolutionOf("runtimeClasspath") 29 | } 30 | usage("java-runtime") { 31 | fromResolutionResult() 32 | } 33 | } 34 | pom { 35 | name.set("openapi-route-definition-locator-spring-cloud-starter") 36 | description.set("Spring Cloud starter for the OpenAPI Route Definition Locator") 37 | url.set("https://github.com/jbretsch/openapi-route-definition-locator") 38 | licenses { 39 | license { 40 | name.set("MIT License") 41 | url.set("https://github.com/jbretsch/openapi-route-definition-locator/blob/master/LICENSE") 42 | } 43 | } 44 | developers { 45 | developer { 46 | id.set("jbretsch") 47 | name.set("Jan Bretschneider") 48 | email.set("mail@jan-bretschneider.de") 49 | } 50 | } 51 | scm { 52 | connection.set("scm:git:git://github.com/jbretsch/openapi-route-definition-locator.git") 53 | developerConnection.set("scm:git:ssh://github.com/jbretsch/openapi-route-definition-locator.git") 54 | url.set("https://github.com/jbretsch/openapi-route-definition-locator") 55 | } 56 | } 57 | } 58 | } 59 | repositories { 60 | maven { 61 | name = "ossrh" 62 | credentials(PasswordCredentials::class) 63 | val releasesRepoUrl = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" 64 | val snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-snapshots/" 65 | url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) 66 | } 67 | } 68 | } 69 | 70 | signing { 71 | sign(publishing.publications["mavenJava"]) 72 | } 73 | 74 | // Remove entries from published POM. 75 | // Inspired by . 76 | tasks.withType().all { 77 | doLast { 78 | val file = layout.buildDirectory.file("publications/mavenJava/pom-default.xml").get().asFile 79 | var text = file.readText() 80 | val regex = "(?s)(.+?)(.+?)(.+?)".toRegex() 81 | val matcher = regex.find(text) 82 | if (matcher != null) { 83 | text = regex.replace(text, "") 84 | } 85 | file.writeText(text) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/test/groovy/net/bretti/openapi/route/definition/locator/core/config/OpenApiRouteDefinitionLocatorPropertiesValidationTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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 spock.lang.Specification 22 | 23 | import jakarta.validation.ConstraintViolation 24 | import jakarta.validation.Validation 25 | import jakarta.validation.Validator 26 | import jakarta.validation.ValidatorFactory 27 | 28 | class OpenApiRouteDefinitionLocatorPropertiesValidationTest extends Specification { 29 | 30 | private Validator validator 31 | 32 | def setup() { 33 | ValidatorFactory factory = Validation.buildDefaultValidatorFactory() 34 | validator = factory.getValidator() 35 | } 36 | 37 | def "gateway name validation passes for null value"() { 38 | given: 39 | OpenApiRouteDefinitionLocatorProperties properties = new OpenApiRouteDefinitionLocatorProperties() 40 | properties.gatewayName = null 41 | 42 | when: 43 | Set> violations = validator.validate(properties) 44 | 45 | then: 46 | violations.isEmpty() 47 | } 48 | 49 | def "gateway name validation passes for valid non-blank string"() { 50 | given: 51 | OpenApiRouteDefinitionLocatorProperties properties = new OpenApiRouteDefinitionLocatorProperties() 52 | properties.gatewayName = "valid-gateway-name" 53 | 54 | when: 55 | Set> violations = validator.validate(properties) 56 | 57 | then: 58 | violations.isEmpty() 59 | } 60 | 61 | def "gateway name validation fails for blank string"() { 62 | given: 63 | OpenApiRouteDefinitionLocatorProperties properties = new OpenApiRouteDefinitionLocatorProperties() 64 | properties.gatewayName = "" 65 | 66 | when: 67 | Set> violations = validator.validate(properties) 68 | 69 | then: 70 | violations.size() == 1 71 | violations.first().message == "null or not blank" 72 | violations.first().propertyPath.toString() == "gatewayName" 73 | } 74 | 75 | def "gateway name validation fails for whitespace-only string"() { 76 | given: 77 | OpenApiRouteDefinitionLocatorProperties properties = new OpenApiRouteDefinitionLocatorProperties() 78 | properties.gatewayName = " " 79 | 80 | when: 81 | Set> violations = validator.validate(properties) 82 | 83 | then: 84 | violations.size() == 1 85 | violations.first().message == "null or not blank" 86 | violations.first().propertyPath.toString() == "gatewayName" 87 | } 88 | 89 | def "gateway name validation passes for string with content and whitespace"() { 90 | given: 91 | OpenApiRouteDefinitionLocatorProperties properties = new OpenApiRouteDefinitionLocatorProperties() 92 | properties.gatewayName = " valid-gateway " 93 | 94 | when: 95 | Set> violations = validator.validate(properties) 96 | 97 | then: 98 | violations.isEmpty() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-core/src/main/java/net/bretti/openapi/route/definition/locator/core/impl/filter/GatewayNameFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.filter; 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.filter.OpenApiRouteDefinitionFilter; 25 | import org.springframework.cloud.gateway.route.RouteDefinition; 26 | import org.springframework.core.annotation.Order; 27 | import org.springframework.util.StringUtils; 28 | 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.Optional; 32 | 33 | import static net.bretti.openapi.route.definition.locator.core.impl.utils.GatewayRouteSettingsUtil.getGatewayRouteSettings; 34 | 35 | @RequiredArgsConstructor 36 | @Slf4j 37 | @Order(200) 38 | public class GatewayNameFilter implements OpenApiRouteDefinitionFilter { 39 | 40 | private static final String GATEWAY_NAMES = "gateway-names"; 41 | 42 | private final OpenApiRouteDefinitionLocatorProperties properties; 43 | 44 | @Override 45 | public boolean test(RouteDefinition routeDefinition, 46 | OpenApiRouteDefinitionLocatorProperties.Service service, 47 | Map openApiGlobalExtensions, 48 | Map openApiOperationExtensions) { 49 | 50 | Optional> gatewayRouteSettings = getGatewayRouteSettings(openApiGlobalExtensions, openApiOperationExtensions); 51 | 52 | // Check gateway-names filtering 53 | Optional> gatewayNames = getGatewayNames(gatewayRouteSettings); 54 | String configuredGatewayName = properties.getGatewayName(); 55 | 56 | if (StringUtils.hasText(configuredGatewayName)) { 57 | if (gatewayNames.isPresent()) { 58 | // gateway-names specified: only allow if configured gateway name is in the list 59 | boolean allowed = gatewayNames.get().contains(configuredGatewayName); 60 | if (!allowed) { 61 | log.info("Route is filtered out because gateway-names of API operation does not contain name of " + 62 | "running gateway, gateway-name={}, API operation gateway-names={}, service={}, " + 63 | "route-predicates={}", configuredGatewayName, gatewayNames.get(), service.getId(), 64 | routeDefinition.getPredicates()); 65 | } 66 | return allowed; 67 | } else { 68 | // no gateway-names specified: allow (publish in all gateways) 69 | return true; 70 | } 71 | } else { 72 | // no gateway name configured: allow all routes 73 | return true; 74 | } 75 | } 76 | 77 | 78 | private Optional> getGatewayNames(Optional> gatewayRouteSettings) { 79 | if (!gatewayRouteSettings.isPresent()) { 80 | return Optional.empty(); 81 | } 82 | 83 | Object gatewayNames = gatewayRouteSettings.get().get(GATEWAY_NAMES); 84 | if (gatewayNames instanceof List) { 85 | List result = new java.util.ArrayList<>(); 86 | for (Object item : (List) gatewayNames) { 87 | if (item instanceof String) { 88 | result.add((String) item); 89 | } 90 | } 91 | return result.isEmpty() ? Optional.empty() : Optional.of(result); 92 | } 93 | 94 | return Optional.empty(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /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-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.RouteDefinitionFilteringServiceMock 26 | import componenttest.setup.wiremock.UserServiceMock 27 | import groovy.json.JsonSlurper 28 | import net.bretti.openapi.route.definition.locator.core.config.OpenApiRouteDefinitionLocatorProperties 29 | import org.springframework.beans.factory.annotation.Autowired 30 | import org.springframework.boot.test.context.SpringBootTest 31 | import org.springframework.boot.test.web.server.LocalServerPort 32 | import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient 33 | import org.springframework.test.web.reactive.server.WebTestClient 34 | import spock.lang.Specification 35 | import spock.util.concurrent.PollingConditions 36 | 37 | import java.time.Duration 38 | 39 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT 40 | 41 | @SpringBootTest(webEnvironment = RANDOM_PORT, classes = TestApiGatewayApplication) 42 | @AutoConfigureWebTestClient 43 | abstract class BaseCompTest extends Specification { 44 | 45 | static final String USER_ID = "user-id-1" 46 | static final String ORDER_ID = "order-id-1" 47 | 48 | JsonSlurper jsonSlurper = new JsonSlurper() 49 | 50 | @LocalServerPort 51 | int localServerPort 52 | 53 | @Autowired 54 | WebTestClient webTestClient 55 | 56 | @Autowired 57 | OpenApiRouteDefinitionLocatorProperties locatorProperties 58 | 59 | Duration maxWaitTimeForRouteAddition 60 | Duration maxWaitTimeForRouteRemoval 61 | 62 | def setup() { 63 | assert locatorProperties.getUpdateScheduler().getFixedDelay() == Duration.ofSeconds(1) 64 | assert locatorProperties.getUpdateScheduler().getRemoveRoutesOnUpdateFailuresAfter() == Duration.ofSeconds(5) 65 | maxWaitTimeForRouteAddition = locatorProperties.getUpdateScheduler().getFixedDelay().plusSeconds(1) 66 | maxWaitTimeForRouteRemoval = locatorProperties.getUpdateScheduler().getRemoveRoutesOnUpdateFailuresAfter() + maxWaitTimeForRouteAddition 67 | UserServiceMock.instance.resetAll() 68 | OrderServiceMock.instance.resetAll() 69 | OpenapiDefinitionServedFromDifferentHostServiceMock1.instance.resetAll() 70 | OpenapiDefinitionServedFromDifferentHostServiceMock2.instance.resetAll() 71 | RouteDefinitionFilteringServiceMock.instance.resetAll() 72 | } 73 | 74 | Map extractRoute(List routes, String httpMethod, String path) { 75 | return routes.find {it.predicate.contains("[${httpMethod}]") && it.predicate.contains("[${path}]") } as Map 76 | } 77 | 78 | List getRoutesFromActuatorEndpoint() { 79 | String routesJson = webTestClient.get().uri("http://localhost:${localServerPort}/actuator/gateway/routes") 80 | .exchange() 81 | .returnResult(String) 82 | .getResponseBody() 83 | .blockFirst() 84 | 85 | return jsonSlurper.parseText(routesJson) as List 86 | } 87 | 88 | void waitForRouteAddition(Closure conditions) { 89 | new PollingConditions(timeout: maxWaitTimeForRouteAddition.getSeconds()).eventually(conditions) 90 | } 91 | 92 | void waitForRouteRemoval(Closure conditions) { 93 | new PollingConditions(timeout: maxWaitTimeForRouteRemoval.getSeconds()).eventually(conditions) 94 | } 95 | 96 | void waitForRemovalOfAllRoutes() { 97 | waitForRouteRemoval { 98 | assert getRoutesFromActuatorEndpoint().size() == 0 99 | } 100 | } 101 | 102 | void waitForRemovalOfAllRoutesExceptThoseReadFromClasspath() { 103 | waitForRouteRemoval { 104 | // One route remains because it comes from an OpenAPI definition read from the classpath. 105 | assert getRoutesFromActuatorEndpoint().size() == 1 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /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.filter.EnabledFlagFilter; 24 | import net.bretti.openapi.route.definition.locator.core.impl.filter.GatewayNameFilter; 25 | import net.bretti.openapi.route.definition.locator.core.filter.OpenApiRouteDefinitionFilter; 26 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiDefinitionRepository; 27 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiDefinitionUpdateScheduler; 28 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocator; 29 | import net.bretti.openapi.route.definition.locator.core.impl.OpenApiRouteDefinitionLocatorTimedMetrics; 30 | import org.springframework.boot.autoconfigure.AutoConfiguration; 31 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 32 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 33 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 34 | import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; 35 | import org.springframework.context.ApplicationEventPublisher; 36 | import org.springframework.context.annotation.Bean; 37 | import org.springframework.core.io.ResourceLoader; 38 | import org.springframework.scheduling.annotation.EnableScheduling; 39 | 40 | import java.util.List; 41 | import java.util.Optional; 42 | import java.util.concurrent.ConcurrentHashMap; 43 | 44 | @AutoConfiguration(after = GatewayAutoConfiguration.class) 45 | @ConditionalOnBean(GatewayAutoConfiguration.class) 46 | @ConditionalOnProperty(value = "openapi-route-definition-locator.enabled", matchIfMissing = true) 47 | @EnableConfigurationProperties 48 | @EnableScheduling 49 | public class OpenApiRouteDefinitionLocatorAutoConfiguration { 50 | @Bean 51 | public OpenApiDefinitionRepository openApiDefinitionRepository( 52 | OpenApiRouteDefinitionLocatorProperties config, 53 | ApplicationEventPublisher applicationEventPublisher, 54 | Optional metrics, 55 | ResourceLoader resourceLoader) { 56 | return new OpenApiDefinitionRepository(config, new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), 57 | applicationEventPublisher, metrics, resourceLoader); 58 | } 59 | 60 | @Bean 61 | public OpenApiRouteDefinitionLocatorProperties openApiRouteDefinitionLocatorProperties() { 62 | return new OpenApiRouteDefinitionLocatorProperties(); 63 | } 64 | 65 | @Bean 66 | @ConditionalOnProperty(value = "openapi-route-definition-locator.internal.filters.enabled-flag-filter.enabled", matchIfMissing = true) 67 | public EnabledFlagFilter enabledFlagFilter() { 68 | return new EnabledFlagFilter(); 69 | } 70 | 71 | @Bean 72 | @ConditionalOnProperty(value = "openapi-route-definition-locator.internal.filters.gateway-name-filter.enabled", matchIfMissing = true) 73 | public GatewayNameFilter gatewayNameFilter(OpenApiRouteDefinitionLocatorProperties properties) { 74 | return new GatewayNameFilter(properties); 75 | } 76 | 77 | @Bean 78 | public OpenApiRouteDefinitionLocator openApiRouteDefinitionLocator( 79 | OpenApiDefinitionRepository openApiDefinitionRepository, 80 | List openApiRouteDefinitionFilters, 81 | List openApiRouteDefinitionCustomizers, 82 | OpenApiRouteDefinitionLocatorProperties openApiRouteDefinitionLocatorProperties 83 | ) { 84 | return new OpenApiRouteDefinitionLocator(openApiDefinitionRepository, openApiRouteDefinitionFilters, 85 | openApiRouteDefinitionCustomizers, openApiRouteDefinitionLocatorProperties); 86 | } 87 | 88 | @Bean 89 | public OpenApiDefinitionUpdateScheduler openApiDefinitionUpdateScheduler( 90 | OpenApiDefinitionRepository openApiDefinitionRepository 91 | ) { 92 | return new OpenApiDefinitionUpdateScheduler(openApiDefinitionRepository); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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.filter.OpenApiRouteDefinitionFilter; 26 | import net.bretti.openapi.route.definition.locator.core.impl.utils.MapMerge; 27 | import org.springframework.cloud.gateway.filter.FilterDefinition; 28 | import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; 29 | import org.springframework.cloud.gateway.route.RouteDefinition; 30 | import org.springframework.cloud.gateway.route.RouteDefinitionLocator; 31 | import reactor.core.publisher.Flux; 32 | 33 | import java.util.ArrayList; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.Optional; 37 | import java.util.UUID; 38 | 39 | import static net.bretti.openapi.route.definition.locator.core.impl.utils.Optionals.firstPresent; 40 | 41 | @RequiredArgsConstructor 42 | @Slf4j 43 | public class OpenApiRouteDefinitionLocator implements RouteDefinitionLocator { 44 | 45 | private final OpenApiDefinitionRepository repository; 46 | 47 | private final List openApiRouteDefinitionFilters; 48 | 49 | private final List openApiRouteDefinitionCustomizers; 50 | 51 | private final OpenApiRouteDefinitionLocatorProperties properties; 52 | 53 | @Override 54 | public Flux getRouteDefinitions() { 55 | List routeDefinitions = new ArrayList<>(); 56 | repository.getOperations().forEach((service, operations) -> operations.forEach(operation -> { 57 | RouteDefinition routeDefinition = new RouteDefinition(); 58 | routeDefinition.setId(UUID.randomUUID().toString()); 59 | routeDefinition.setUri(operation.getBaseUri()); 60 | 61 | PredicateDefinition pathPredicate = new PredicateDefinition("Path=" + operation.getPath()); 62 | PredicateDefinition methodPredicate = new PredicateDefinition("Method=" + operation.getHttpMethod()); 63 | 64 | List predicates = new ArrayList<>(); 65 | predicates.add(methodPredicate); 66 | predicates.add(pathPredicate); 67 | predicates.addAll(properties.getDefaultRouteSettings().getPredicates()); 68 | predicates.addAll(service.getDefaultRouteSettings().getPredicates()); 69 | predicates.addAll(operation.getPredicates()); 70 | routeDefinition.setPredicates(predicates); 71 | 72 | List filters = new ArrayList<>(); 73 | filters.addAll(properties.getDefaultRouteSettings().getFilters()); 74 | filters.addAll(service.getDefaultRouteSettings().getFilters()); 75 | filters.addAll(operation.getFilters()); 76 | routeDefinition.setFilters(filters); 77 | 78 | firstPresent( 79 | operation.getOrder(), 80 | service.getDefaultRouteSettings().getOrder(), 81 | properties.getDefaultRouteSettings().getOrder() 82 | ).ifPresent(routeDefinition::setOrder); 83 | 84 | Optional> metaData = MapMerge.deepMerge( 85 | Optional.of(properties.getDefaultRouteSettings().getMetadata()), 86 | Optional.of(service.getDefaultRouteSettings().getMetadata()), 87 | operation.getMetadata() 88 | ); 89 | metaData.ifPresent(routeDefinition::setMetadata); 90 | 91 | // Apply filters before customizers. 92 | boolean shouldInclude = openApiRouteDefinitionFilters.stream() 93 | .allMatch(filter -> filter.test(routeDefinition, service, operation.getOpenApiExtension(), 94 | operation.getOpenApiOperationExtension())); 95 | 96 | if (!shouldInclude) { 97 | return; // Skip this route. 98 | } 99 | 100 | openApiRouteDefinitionCustomizers.forEach(customizer -> 101 | customizer.customize(routeDefinition, service, operation.getOpenApiExtension(), 102 | operation.getOpenApiOperationExtension()) 103 | ); 104 | 105 | routeDefinitions.add(routeDefinition); 106 | })); 107 | 108 | return Flux.fromIterable(routeDefinitions); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | public 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-spring-cloud-starter/src/test/groovy/net/bretti/openapi/route/definition/locator/autoconfigure/FilterOrderingTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.impl.filter.EnabledFlagFilter 22 | import net.bretti.openapi.route.definition.locator.core.impl.filter.GatewayNameFilter 23 | import net.bretti.openapi.route.definition.locator.core.filter.OpenApiRouteDefinitionFilter 24 | import org.assertj.core.api.Assertions 25 | import org.springframework.boot.autoconfigure.AutoConfigurations 26 | import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration 27 | import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration 28 | import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner 29 | import org.springframework.cloud.gateway.config.GatewayAutoConfiguration 30 | import spock.lang.Specification 31 | 32 | class FilterOrderingTest extends Specification { 33 | 34 | private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() 35 | 36 | def "Filter ordering is correct - EnabledFlagFilter runs before GatewayNameFilter"() { 37 | expect: 38 | contextRunner 39 | .withConfiguration(AutoConfigurations.of( 40 | OpenApiRouteDefinitionLocatorAutoConfiguration, 41 | GatewayAutoConfiguration, 42 | WebFluxAutoConfiguration, 43 | SslAutoConfiguration 44 | )) 45 | .run({ context -> 46 | List filters = context.getBeansOfType(OpenApiRouteDefinitionFilter).values().toList() 47 | 48 | // Verify we have both filters 49 | Assertions.assertThat(filters).hasSize(2) 50 | 51 | // Verify ordering: EnabledFlagFilter (order 100) should come before GatewayNameFilter (order 200) 52 | Assertions.assertThat(filters[0]).isInstanceOf(EnabledFlagFilter) 53 | Assertions.assertThat(filters[1]).isInstanceOf(GatewayNameFilter) 54 | }) 55 | } 56 | 57 | def "EnabledFlagFilter is not created when disabled"() { 58 | expect: 59 | contextRunner 60 | .withConfiguration(AutoConfigurations.of( 61 | OpenApiRouteDefinitionLocatorAutoConfiguration, 62 | GatewayAutoConfiguration, 63 | WebFluxAutoConfiguration, 64 | SslAutoConfiguration 65 | )) 66 | .withPropertyValues("openapi-route-definition-locator.internal.filters.enabled-flag-filter.enabled=false") 67 | .run({ context -> 68 | List filters = context.getBeansOfType(OpenApiRouteDefinitionFilter).values().toList() 69 | 70 | // Verify we have only the GatewayNameFilter 71 | Assertions.assertThat(filters).hasSize(1) 72 | Assertions.assertThat(filters[0]).isInstanceOf(GatewayNameFilter) 73 | 74 | // Verify EnabledFlagFilter is not present 75 | Assertions.assertThat(context.getBeansOfType(EnabledFlagFilter)).isEmpty() 76 | }) 77 | } 78 | 79 | def "GatewayNameFilter is not created when disabled"() { 80 | expect: 81 | contextRunner 82 | .withConfiguration(AutoConfigurations.of( 83 | OpenApiRouteDefinitionLocatorAutoConfiguration, 84 | GatewayAutoConfiguration, 85 | WebFluxAutoConfiguration, 86 | SslAutoConfiguration 87 | )) 88 | .withPropertyValues("openapi-route-definition-locator.internal.filters.gateway-name-filter.enabled=false") 89 | .run({ context -> 90 | List filters = context.getBeansOfType(OpenApiRouteDefinitionFilter).values().toList() 91 | 92 | // Verify we have only the EnabledFlagFilter 93 | Assertions.assertThat(filters).hasSize(1) 94 | Assertions.assertThat(filters[0]).isInstanceOf(EnabledFlagFilter) 95 | 96 | // Verify GatewayNameFilter is not present 97 | Assertions.assertThat(context.getBeansOfType(GatewayNameFilter)).isEmpty() 98 | }) 99 | } 100 | 101 | def "Both filters are not created when both are disabled"() { 102 | expect: 103 | contextRunner 104 | .withConfiguration(AutoConfigurations.of( 105 | OpenApiRouteDefinitionLocatorAutoConfiguration, 106 | GatewayAutoConfiguration, 107 | WebFluxAutoConfiguration, 108 | SslAutoConfiguration 109 | )) 110 | .withPropertyValues( 111 | "openapi-route-definition-locator.internal.filters.enabled-flag-filter.enabled=false", 112 | "openapi-route-definition-locator.internal.filters.gateway-name-filter.enabled=false" 113 | ) 114 | .run({ context -> 115 | List filters = context.getBeansOfType(OpenApiRouteDefinitionFilter).values().toList() 116 | 117 | // Verify no filters are present 118 | Assertions.assertThat(filters).isEmpty() 119 | 120 | // Verify specific filter beans are not present 121 | Assertions.assertThat(context.getBeansOfType(EnabledFlagFilter)).isEmpty() 122 | Assertions.assertThat(context.getBeansOfType(GatewayNameFilter)).isEmpty() 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /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.webflux.autoconfigure.WebFluxAutoConfiguration 29 | import org.springframework.boot.context.properties.bind.validation.BindValidationException 30 | import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner 31 | import org.springframework.cloud.gateway.config.GatewayAutoConfiguration 32 | import spock.lang.Specification 33 | 34 | class OpenApiRouteDefinitionLocatorAutoConfigurationTest extends Specification { 35 | private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() 36 | 37 | def "OpenAPI Route Definition Locator is active if GatewayAutoConfiguration is present"() { 38 | expect: 39 | contextRunner 40 | .withConfiguration(AutoConfigurations.of( 41 | OpenApiRouteDefinitionLocatorAutoConfiguration, 42 | GatewayAutoConfiguration, 43 | WebFluxAutoConfiguration, 44 | SslAutoConfiguration 45 | )) 46 | .run({ context -> 47 | Assertions.assertThat(context).hasSingleBean(OpenApiDefinitionRepository) 48 | Assertions.assertThat(context).hasSingleBean(OpenApiRouteDefinitionLocatorProperties) 49 | Assertions.assertThat(context).hasSingleBean(OpenApiRouteDefinitionLocator) 50 | Assertions.assertThat(context).hasSingleBean(OpenApiDefinitionUpdateScheduler) 51 | }) 52 | } 53 | 54 | def "OpenAPI Route Definition Locator is inactive if GatewayAutoConfiguration is present but OpenApiRouteDefinitionLocator is disabled"() { 55 | expect: 56 | contextRunner 57 | .withConfiguration(AutoConfigurations.of( 58 | OpenApiRouteDefinitionLocatorAutoConfiguration, 59 | GatewayAutoConfiguration, 60 | WebFluxAutoConfiguration, 61 | SslAutoConfiguration 62 | )) 63 | .withPropertyValues("openapi-route-definition-locator.enabled=false") 64 | .run({ context -> 65 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionRepository) 66 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorProperties) 67 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocator) 68 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionUpdateScheduler) 69 | }) 70 | } 71 | 72 | def "OpenAPI Route Definition Locator is inactive if Spring Cloud Gateway is disabled"() { 73 | expect: 74 | contextRunner 75 | .withConfiguration(AutoConfigurations.of( 76 | OpenApiRouteDefinitionLocatorAutoConfiguration, 77 | GatewayAutoConfiguration, 78 | WebFluxAutoConfiguration, 79 | )) 80 | .withPropertyValues("spring.cloud.gateway.server.webflux.enabled=false") 81 | .run({ context -> 82 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionRepository) 83 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocatorProperties) 84 | Assertions.assertThat(context).doesNotHaveBean(OpenApiRouteDefinitionLocator) 85 | Assertions.assertThat(context).doesNotHaveBean(OpenApiDefinitionUpdateScheduler) 86 | }) 87 | } 88 | 89 | def "OpenAPI Route Definition Locator is inactive if GatewayAutoConfiguration is absent"() { 90 | expect: 91 | contextRunner 92 | .withConfiguration(AutoConfigurations.of( 93 | OpenApiRouteDefinitionLocatorAutoConfiguration, 94 | )) 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 | def "Startup fails if configured gateway name is blank"() { 104 | expect: 105 | contextRunner 106 | .withConfiguration(AutoConfigurations.of( 107 | OpenApiRouteDefinitionLocatorAutoConfiguration, 108 | GatewayAutoConfiguration, 109 | WebFluxAutoConfiguration, 110 | SslAutoConfiguration 111 | )) 112 | .withPropertyValues("openapi-route-definition-locator.gateway-name= ") 113 | .run({ context -> 114 | Assertions.assertThat(context).hasFailed() 115 | Assertions.assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(BindValidationException.class) 116 | Assertions.assertThat(context.getStartupFailure()).rootCause().hasMessageContaining( 117 | "Field error in object 'openapi-route-definition-locator' on field 'gatewayName'" 118 | ) 119 | }) 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /openapi-route-definition-locator-spring-cloud-starter/src/test/groovy/componenttest/RouteDefinitionFilteringCompTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 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.RouteDefinitionFilteringServiceMock 23 | import org.springframework.test.context.ActiveProfiles 24 | 25 | @ActiveProfiles("route-definition-filtering") 26 | class RouteDefinitionFilteringCompTest extends BaseCompTest { 27 | 28 | def "Routes with invalid enabled values default to enabled"() { 29 | given: 30 | waitForRemovalOfAllRoutes() 31 | 32 | and: 33 | RouteDefinitionFilteringServiceMock.instance.mockOpenApiDefinitionWithInvalidEnabledValues() 34 | 35 | when: 36 | waitForRouteAddition { 37 | // Routes with invalid enabled values should default to enabled (true) 38 | // Only the route with explicitly "enabled: false" should be filtered out 39 | assert getRoutesFromActuatorEndpoint().size() == 3 40 | } 41 | 42 | and: 43 | List routes = getRoutesFromActuatorEndpoint() 44 | 45 | then: "Route with enabled: 'invalid-string' should be included (defaults to true)" 46 | extractRoute(routes, "GET", "/test-invalid-enabled-string") != null 47 | 48 | and: "Route with enabled: 123 should be included (defaults to true)" 49 | extractRoute(routes, "GET", "/test-invalid-enabled-number") != null 50 | 51 | and: "Route with enabled: null should be included (defaults to true)" 52 | extractRoute(routes, "GET", "/test-null-enabled") != null 53 | 54 | and: "Route with enabled: false should be filtered out" 55 | extractRoute(routes, "GET", "/test-disabled") == null 56 | } 57 | 58 | def "Routes with invalid gateway-names values are handled gracefully"() { 59 | given: 60 | waitForRemovalOfAllRoutes() 61 | 62 | and: 63 | RouteDefinitionFilteringServiceMock.instance.mockOpenApiDefinitionWithInvalidGatewayNames() 64 | 65 | when: 66 | waitForRouteAddition { 67 | // All routes should be included since invalid gateway-names are ignored 68 | assert getRoutesFromActuatorEndpoint().size() == 4 69 | } 70 | 71 | and: 72 | List routes = getRoutesFromActuatorEndpoint() 73 | 74 | then: "Route with gateway-names as string should be included (ignored)" 75 | extractRoute(routes, "GET", "/test-gateway-names-string") != null 76 | 77 | and: "Route with gateway-names as number should be included (ignored)" 78 | extractRoute(routes, "GET", "/test-gateway-names-number") != null 79 | 80 | and: "Route with gateway-names as mixed array should be included (only strings processed)" 81 | extractRoute(routes, "GET", "/test-gateway-names-mixed-array") != null 82 | 83 | and: "Route with gateway-names as empty array should be included" 84 | extractRoute(routes, "GET", "/test-gateway-names-empty-array") != null 85 | } 86 | 87 | def "Mixed filtering scenarios work correctly"() { 88 | given: 89 | waitForRemovalOfAllRoutes() 90 | 91 | and: 92 | RouteDefinitionFilteringServiceMock.instance.mockOpenApiDefinitionWithMixedFiltering() 93 | 94 | when: 95 | waitForRouteAddition { 96 | // Only routes that pass both enabled and gateway-name filters should be included 97 | assert getRoutesFromActuatorEndpoint().size() == 2 98 | } 99 | 100 | and: 101 | List routes = getRoutesFromActuatorEndpoint() 102 | 103 | then: "Route that is enabled and has no gateway-names should be included" 104 | extractRoute(routes, "GET", "/test-enabled-no-gateway") != null 105 | 106 | and: "Route that is disabled should be filtered out (even with matching gateway name)" 107 | extractRoute(routes, "GET", "/test-disabled-matching-gateway") == null 108 | 109 | and: "Route that is enabled but has non-matching gateway-names should be filtered out" 110 | extractRoute(routes, "GET", "/test-enabled-wrong-gateway") == null 111 | 112 | and: "Route that is enabled with matching gateway-names should be included" 113 | extractRoute(routes, "GET", "/test-enabled-matching-gateway") != null 114 | } 115 | 116 | def "Global vs operation-level settings precedence works correctly"() { 117 | given: 118 | waitForRemovalOfAllRoutes() 119 | 120 | and: 121 | RouteDefinitionFilteringServiceMock.instance.mockOpenApiDefinitionWithSettingsPrecedence() 122 | 123 | when: 124 | waitForRouteAddition { 125 | // Test operation-level settings override global settings 126 | assert getRoutesFromActuatorEndpoint().size() == 1 127 | } 128 | 129 | and: 130 | List routes = getRoutesFromActuatorEndpoint() 131 | 132 | then: "Operation-level enabled: false should override global enabled: true" 133 | extractRoute(routes, "GET", "/test-operation-overrides-global-enabled") == null 134 | 135 | and: "Operation-level gateway-names should override global gateway-names" 136 | extractRoute(routes, "GET", "/test-operation-overrides-global-gateway") != null 137 | } 138 | 139 | def "EnvironmentRouteDefinitionFilter correctly filters routes based on x-environment"() { 140 | given: 141 | waitForRemovalOfAllRoutes() 142 | 143 | and: 144 | RouteDefinitionFilteringServiceMock.instance.mockOpenApiDefinitionWithEnvironmentFiltering() 145 | 146 | when: 147 | waitForRouteAddition { 148 | // Only routes with no x-environment or x-environment: dev should be included 149 | assert getRoutesFromActuatorEndpoint().size() == 2 150 | } 151 | 152 | and: 153 | List routes = getRoutesFromActuatorEndpoint() 154 | 155 | then: "Route with no x-environment should be included (null case)" 156 | extractRoute(routes, "GET", "/test-no-environment") != null 157 | 158 | and: "Route with x-environment: dev should be included" 159 | extractRoute(routes, "GET", "/test-environment-dev") != null 160 | 161 | and: "Route with x-environment: prod should be filtered out" 162 | extractRoute(routes, "GET", "/test-environment-prod") == null 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /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-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 net.bretti.openapi.route.definition.locator.core.impl.filter.GatewayNameFilter; 26 | import net.bretti.openapi.route.definition.locator.core.impl.validator.NullOrNotBlank; 27 | import org.springframework.boot.context.properties.ConfigurationProperties; 28 | import org.springframework.cloud.gateway.filter.FilterDefinition; 29 | import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; 30 | import org.springframework.cloud.gateway.route.RouteDefinition; 31 | import org.springframework.validation.annotation.Validated; 32 | 33 | import jakarta.validation.Valid; 34 | import jakarta.validation.constraints.NotBlank; 35 | import jakarta.validation.constraints.NotNull; 36 | import java.net.URI; 37 | import java.time.Duration; 38 | import java.time.temporal.ChronoUnit; 39 | import java.util.ArrayList; 40 | import java.util.HashMap; 41 | import java.util.List; 42 | import java.util.Map; 43 | import java.util.Optional; 44 | 45 | @ConfigurationProperties(prefix = "openapi-route-definition-locator") 46 | @Validated 47 | @Data 48 | public class OpenApiRouteDefinitionLocatorProperties { 49 | 50 | private static final String DEFAULT_OPENAPI_DEFINITION_URI = "/internal/openapi-definition"; 51 | 52 | /** 53 | * List of services for routes should be registered in the gateway based on their 54 | * OpenAPI definitions. 55 | */ 56 | @Valid 57 | @OnlyUniqueServiceIds 58 | private List services = new ArrayList<>(); 59 | 60 | /** 61 | * Settings that should be added to all {@link RouteDefinition}s created for the configured 62 | * {@link OpenApiRouteDefinitionLocatorProperties#services}. 63 | */ 64 | @Valid 65 | private DefaultRouteSettings defaultRouteSettings = new DefaultRouteSettings(); 66 | 67 | /** 68 | * Configures the scheduler which periodically retrieves the OpenAPI definitions from 69 | * the configured services. 70 | */ 71 | @Valid 72 | private UpdateScheduler updateScheduler = new UpdateScheduler(); 73 | 74 | /** 75 | * The URI of the OpenAPI definitions to be retrieved from the configured services. 76 | * This generally is a relative URI; relative to the base URI of each configured service. 77 | * The default is "/internal/openapi-definition". 78 | */ 79 | @ValidOpenApiDefinitionUri 80 | private URI openapiDefinitionUri = URI.create(DEFAULT_OPENAPI_DEFINITION_URI); 81 | 82 | /** 83 | * The name of the gateway. This name is used by the 84 | * {@link GatewayNameFilter}. 85 | * If specified, only routes for OpenAPI operations which specify this gateway name or no gateway name will be 86 | * created in this gateway. If not specified, all routes for all enabled OpenAPI operations will be created. 87 | */ 88 | @NullOrNotBlank 89 | private String gatewayName; 90 | 91 | @Data 92 | public static class Service { 93 | 94 | /** 95 | * Identifier of the service. 96 | */ 97 | @NotBlank 98 | private String id; 99 | 100 | /** 101 | * Base URI of the service. 102 | */ 103 | @NotNull 104 | @ValidBaseUri 105 | private URI uri; 106 | 107 | /** 108 | * The URI of the OpenAPI definition to be retrieved from the service. 109 | * This generally is a relative URI; relative to the service's base URI. 110 | * But it can also be an absolute URI. As the OpenAPI definition is loaded 111 | * via Spring's 112 | * ResourceLoader, you can use schemas such as {@code http:}, {@code https:}, {@code file:} or 113 | * {@code classpath:}. The default is the value of the property 114 | * {@code openapi-route-definition-locator.openapi-definition-uri}. 115 | */ 116 | @ValidOpenApiDefinitionUri 117 | private URI openapiDefinitionUri; 118 | 119 | /** 120 | * Settings that should be applied to all {@link RouteDefinition}s created for this service. 121 | */ 122 | @Valid 123 | private DefaultRouteSettings defaultRouteSettings = new DefaultRouteSettings(); 124 | } 125 | 126 | @Data 127 | public static class UpdateScheduler { 128 | 129 | /** 130 | * Fixed delay between runs to retrieve the services' OpenAPI definitions. 131 | * If no timeunit is given, milliseconds are used. 132 | */ 133 | @NotNull 134 | private Duration fixedDelay = Duration.of(5, ChronoUnit.MINUTES); 135 | 136 | /** 137 | * When an error occurs while retrieving a service's OpenAPI definition, its registered routes/operations 138 | * are not immediately de-registered. They are only de-registered if there was no successful retrieval 139 | * for the amount of time configured here. If no timeunit is given, milliseconds are used. 140 | */ 141 | @NotNull 142 | private Duration removeRoutesOnUpdateFailuresAfter = Duration.of(15, ChronoUnit.MINUTES); 143 | } 144 | 145 | /** 146 | * Settings that should be applied to all created {@link RouteDefinition}s. Contains a subset of the attributes of a 147 | * {@link RouteDefinition}. 148 | */ 149 | @Data 150 | public static class DefaultRouteSettings { 151 | /** 152 | * The predicates that should be added to the created {@link RouteDefinition}s. 153 | */ 154 | @Valid 155 | private List predicates = new ArrayList<>(); 156 | 157 | /** 158 | * The filters that should be added to the created {@link RouteDefinition}s. 159 | */ 160 | @Valid 161 | private List filters = new ArrayList<>(); 162 | 163 | /** 164 | * The metadata that should be added to the created {@link RouteDefinition}s. 165 | */ 166 | private Map metadata = new HashMap<>(); 167 | 168 | /** 169 | * The order that should be applied to the created {@link RouteDefinition}s. 170 | */ 171 | private Optional order = Optional.empty(); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /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.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration 26 | import org.springframework.boot.micrometer.metrics.autoconfigure.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.webflux.autoconfigure.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.server.webflux.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 | --------------------------------------------------------------------------------