├── .DS_Store ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── Tiltfile ├── api-specs ├── bruno │ ├── AccessToken.bru │ ├── Add Course.bru │ ├── Add Review.bru │ ├── Course-Composite.bru │ ├── bruno.json │ └── environments │ │ ├── docker.bru │ │ └── kubernetes.bru ├── openapi-specs │ └── OpenApi.yml └── postman │ └── postman-apis.json ├── build-images.sh ├── course-management-realm-realm.json ├── create-project.sh ├── docker ├── dashboard-1.yml ├── docker-compose-base.yml ├── docker-compose-infra.yml ├── docker-compose-observability.yml ├── fluent-bit │ └── fluent-bit.conf ├── loki │ └── loki-config.yaml ├── postgresql │ └── init.sql ├── prometheus │ └── prometheus.yml └── tempo │ └── tempo.yml ├── grafana-dashboard ├── Spring Boot 3.x Statistics.json └── Spring Boot Observability.json ├── kubernetes └── infrastructure │ ├── fluent-bit │ └── fluent-bit.yml │ ├── grafana │ └── grafana.yml │ ├── keycloak │ ├── course-management-realm.json │ └── keycloak.yml │ ├── loki │ └── loki.yml │ ├── mongodb │ └── mongodb.yml │ ├── postgres │ └── postgres.yml │ ├── prometheus │ └── prometheus.yml │ └── tempo │ └── tempo.yml ├── microservices ├── course-composite-service │ ├── .gitattributes │ ├── .gitignore │ ├── .mvn │ │ └── wrapper │ │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── kubernetes │ │ ├── deployment.yml │ │ └── service.yml │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── io │ │ │ │ └── javatab │ │ │ │ └── microservices │ │ │ │ └── composite │ │ │ │ └── course │ │ │ │ ├── CourseCompositeServiceApplication.java │ │ │ │ ├── MetricsController.java │ │ │ │ ├── config │ │ │ │ ├── MetricsConfig.java │ │ │ │ └── SecurityConfig.java │ │ │ │ └── web │ │ │ │ ├── Course.java │ │ │ │ ├── CourseAggregate.java │ │ │ │ ├── CourseAggregateController.java │ │ │ │ ├── CourseCompositeIntegration.java │ │ │ │ ├── Review.java │ │ │ │ └── Vote.java │ │ └── resources │ │ │ └── application.yml │ │ └── test │ │ └── java │ │ └── io │ │ └── javatab │ │ └── microservices │ │ └── composite │ │ └── course │ │ └── CourseCompositeServiceApplicationTests.java ├── course-service │ ├── .gitattributes │ ├── .gitignore │ ├── .mvn │ │ └── wrapper │ │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── kubernetes │ │ ├── deployment.yml │ │ └── service.yml │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── io │ │ │ │ └── javatab │ │ │ │ └── microservices │ │ │ │ └── core │ │ │ │ └── course │ │ │ │ ├── CourseServiceApplication.java │ │ │ │ ├── config │ │ │ │ ├── DatabaseConfig.java │ │ │ │ └── SecurityConfig.java │ │ │ │ ├── domain │ │ │ │ ├── Course.java │ │ │ │ ├── CourseAlreadyExitsException.java │ │ │ │ ├── CourseNotFoundException.java │ │ │ │ ├── CourseRepository.java │ │ │ │ └── CourseService.java │ │ │ │ └── web │ │ │ │ ├── CourseController.java │ │ │ │ └── CourseControllerAdvice.java │ │ └── resources │ │ │ ├── application.yml │ │ │ └── db │ │ │ └── migration │ │ │ └── V1__Initial_schema.sql │ │ └── test │ │ └── java │ │ └── io │ │ └── javatab │ │ └── microservices │ │ └── core │ │ └── course │ │ └── CourseServiceApplicationTests.java └── review-service │ ├── .gitattributes │ ├── .gitignore │ ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── Tiltfile │ ├── kubernetes │ ├── deployment.yml │ └── service.yml │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── javatab │ │ │ └── microservices │ │ │ └── core │ │ │ └── review │ │ │ ├── ReviewServiceApplication.java │ │ │ ├── config │ │ │ └── SecurityConfig.java │ │ │ ├── domain │ │ │ ├── Review.java │ │ │ ├── ReviewNotFoundException.java │ │ │ ├── ReviewRepository.java │ │ │ └── ReviewService.java │ │ │ └── web │ │ │ ├── ReviewController.java │ │ │ ├── ReviewControllerAdvice.java │ │ │ ├── ReviewDTO.java │ │ │ └── ReviewResponse.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── io │ └── javatab │ └── microservices │ └── core │ └── review │ └── ReviewServiceApplicationTests.java ├── mvnw ├── mvnw.cmd ├── notes ├── archtecture.md └── images │ ├── component-level3.png │ ├── container-level2.png │ ├── context-level1.png │ ├── deployment-level4.png │ ├── jwt.png │ ├── keycloak.png │ ├── loki.png │ ├── metrics.png │ ├── observability.png │ ├── prometheus.png │ ├── statistic.png │ ├── tempo1.png │ ├── tempo2.png │ └── tilt.png ├── pom.xml ├── run.sh ├── spring-cloud └── gateway-service │ ├── .gitattributes │ ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── kubernetes │ ├── deployment.yml │ ├── ingress.yml │ └── service.yml │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── springcloud │ │ │ └── gateway │ │ │ ├── GatewayServiceApplication.java │ │ │ └── config │ │ │ └── SecurityConfig.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com │ └── example │ └── springcloud │ └── gateway │ └── GatewayServiceApplicationTests.java └── util ├── .gitattributes ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src └── main ├── java └── io │ └── javatab │ └── util │ └── http │ └── NetworkUtility.java └── resources └── application.yml /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .idea 3 | .vscode -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | # Define and build infrastructure services 2 | k8s_yaml([ 3 | "kubernetes/infrastructure/keycloak/keycloak.yml", 4 | "kubernetes/infrastructure/postgres/postgres.yml", 5 | "kubernetes/infrastructure/mongodb/mongodb.yml", 6 | "kubernetes/infrastructure/prometheus/prometheus.yml", 7 | "kubernetes/infrastructure/fluent-bit/fluent-bit.yml", 8 | "kubernetes/infrastructure/loki/loki.yml", 9 | "kubernetes/infrastructure/tempo/tempo.yml", 10 | "kubernetes/infrastructure/grafana/grafana.yml" 11 | 12 | ]) 13 | 14 | # Define infrastructure resources 15 | k8s_resource("keycloak", labels=["infra"], auto_init=True) 16 | k8s_resource("prometheus", labels=["observability"], auto_init=True) 17 | k8s_resource("fluent-bit", labels=["observability"], auto_init=True) 18 | k8s_resource("loki", labels=["observability"], auto_init=True) 19 | k8s_resource("tempo", labels=["observability"], auto_init=True) 20 | k8s_resource("grafana", labels=["observability"], auto_init=True) 21 | k8s_resource("course-postgres", labels=["infra"], auto_init=True) 22 | k8s_resource("review-mongodb", labels=["infra"], auto_init=True) 23 | 24 | # Define and build course-service 25 | docker_build( 26 | "course-service", 27 | context="./microservices/course-service", 28 | dockerfile="./microservices/course-service/Dockerfile", 29 | live_update=[ 30 | sync("./microservices/course-service/src", "/application/src"), 31 | run("mvn package -DskipTests", trigger=["/application/src"]), 32 | ] 33 | ) 34 | k8s_yaml([ 35 | "microservices/course-service/kubernetes/deployment.yml", 36 | "microservices/course-service/kubernetes/service.yml" 37 | ]) 38 | k8s_resource( 39 | "course-service", 40 | port_forwards="9001:9001", 41 | labels=["services"] 42 | ) 43 | 44 | # Define and build review-service 45 | docker_build( 46 | "review-service", 47 | context="./microservices/review-service", 48 | dockerfile="./microservices/review-service/Dockerfile", 49 | live_update=[ 50 | sync("./microservices/review-service/src", "/application/src"), 51 | run("mvn package -DskipTests", trigger=["/application/src"]), 52 | ] 53 | ) 54 | k8s_yaml([ 55 | "microservices/review-service/kubernetes/deployment.yml", 56 | "microservices/review-service/kubernetes/service.yml" 57 | ]) 58 | k8s_resource( 59 | "review-service", 60 | port_forwards="9002:9002", 61 | labels=["services"] 62 | ) 63 | 64 | # Define and build course-composite-service 65 | docker_build( 66 | "course-composite-service", 67 | context="./microservices/course-composite-service", 68 | dockerfile="./microservices/course-composite-service/Dockerfile", 69 | live_update=[ 70 | sync("./microservices/course-composite-service/src", "/application/src"), 71 | run("mvn package -DskipTests", trigger=["/application/src"]), 72 | ] 73 | ) 74 | k8s_yaml([ 75 | "microservices/course-composite-service/kubernetes/deployment.yml", 76 | "microservices/course-composite-service/kubernetes/service.yml" 77 | ]) 78 | k8s_resource( 79 | "course-composite-service", 80 | port_forwards="5000:5000", 81 | labels=["services"] 82 | ) 83 | 84 | # Define and build gateway-service 85 | docker_build( 86 | "gateway-service", 87 | context="./spring-cloud/gateway-service", 88 | dockerfile="./spring-cloud/gateway-service/Dockerfile", 89 | live_update=[ 90 | sync("./spring-cloud/gateway-service/src", "/application/src"), 91 | run("mvn package -DskipTests", trigger=["/application/src"]), 92 | ] 93 | ) 94 | k8s_yaml([ 95 | "spring-cloud/gateway-service/kubernetes/deployment.yml", 96 | "spring-cloud/gateway-service/kubernetes/service.yml", 97 | "spring-cloud/gateway-service/kubernetes/ingress.yml" 98 | ]) 99 | k8s_resource( 100 | "gateway-service", 101 | labels=["services"] 102 | ) -------------------------------------------------------------------------------- /api-specs/bruno/AccessToken.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: AccessToken 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: http://localhost:8081/realms/course-management-realm/protocol/openid-connect/token 9 | body: formUrlEncoded 10 | auth: none 11 | } 12 | 13 | body:form-urlencoded { 14 | grant_type: password 15 | client_id: course-app 16 | client_secret: v1sCIPjANbvyJ87RsTkYeI9xHonDqZh7 17 | username: nasruddin 18 | password: password 19 | scope: openid roles 20 | } 21 | -------------------------------------------------------------------------------- /api-specs/bruno/Add Course.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Add Course 3 | type: http 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: http://localhost:9000/courses 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0T1lYQVlZUXF6T2ZibE1wRjF0dmNvLW1UY2dEODVjai1Qak4xVnhuUExzIn0.eyJleHAiOjE3NDE0NTA1NzEsImlhdCI6MTc0MTQxNDU3MSwianRpIjoiMWE1Y2I3NWYtNWMxYy00ZDE4LWJjNTEtNjczYWMzMjZjNTJhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL3JlYWxtcy9jb3Vyc2UtbWFuYWdlbWVudC1yZWFsbSIsImF1ZCI6InJldmlldy1hcHAiLCJzdWIiOiIzNzY2NmQ1Ni1jYTJkLTRkNDQtOTRmMS1kNDk4ZTdhZmVhOTUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjb3Vyc2UtYXBwIiwic2lkIjoiZTk5ZDZiOWQtZDdmNC00YzUxLWI1NDItOGFjNTU1MTU3ZDI0IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiR1VFU1QiLCJBRE1JTiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InJldmlldy1hcHAiOnsicm9sZXMiOlsiUkVWSUVXLVJFQUQiLCJSRVZJRVctV1JJVEUiXX0sImNvdXJzZS1hcHAiOnsicm9sZXMiOlsiQ09VUlNFLVdSSVRFIiwiQ09VUlNFLVJFQUQiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiTmFzcnVkZGluIE5hc3J1ZGRpbiIsInByZWZlcnJlZF91c2VybmFtZSI6Im5hc3J1ZGRpbiIsImdpdmVuX25hbWUiOiJOYXNydWRkaW4iLCJmYW1pbHlfbmFtZSI6Ik5hc3J1ZGRpbiIsImVtYWlsIjoibmFzcnVkZGluQGdtYWlsLmNvbSJ9.H2zm8j1_VPNUxSlBBSlARWYBWsb8RUUcFjZHHqAAvBojIRi73uOJQ2Xf30EAOj2bpjsDLukBCyZYbhJmaNDWAfiG2_q8wL8xvflpnHdBlAIdrnJ3OU2qFSDYnB7kWblqbeYFsbYZS72CO9K2BYPieViRi_eWUF61AneFJRqIyBMMmwXTcnwuX10ImEIpzQ8OdakVMBxrP1eZlUGAXGjHpRuzfJYSwQhwphVvOt1tUhEfRawu9ihJLvLRADcZXxS-5qvHJDMpDJuHKQL2HvfJLdmseIO4l7lYi2QoqsC56R7wSORioaP0lxxD6DXeEIvaor6oKuRXyZ-Txon4P40iMQ 15 | } 16 | 17 | body:json { 18 | { 19 | "title": "Microservices with Quarkus", 20 | "author": "John Doe", 21 | "price": 29.79, 22 | "publisher": "Whatsapp" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api-specs/bruno/Add Review.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Add Review 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: http://localhost:9000/reviews 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0T1lYQVlZUXF6T2ZibE1wRjF0dmNvLW1UY2dEODVjai1Qak4xVnhuUExzIn0.eyJleHAiOjE3NDE0NTA1NzEsImlhdCI6MTc0MTQxNDU3MSwianRpIjoiMWE1Y2I3NWYtNWMxYy00ZDE4LWJjNTEtNjczYWMzMjZjNTJhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL3JlYWxtcy9jb3Vyc2UtbWFuYWdlbWVudC1yZWFsbSIsImF1ZCI6InJldmlldy1hcHAiLCJzdWIiOiIzNzY2NmQ1Ni1jYTJkLTRkNDQtOTRmMS1kNDk4ZTdhZmVhOTUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjb3Vyc2UtYXBwIiwic2lkIjoiZTk5ZDZiOWQtZDdmNC00YzUxLWI1NDItOGFjNTU1MTU3ZDI0IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiR1VFU1QiLCJBRE1JTiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InJldmlldy1hcHAiOnsicm9sZXMiOlsiUkVWSUVXLVJFQUQiLCJSRVZJRVctV1JJVEUiXX0sImNvdXJzZS1hcHAiOnsicm9sZXMiOlsiQ09VUlNFLVdSSVRFIiwiQ09VUlNFLVJFQUQiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiTmFzcnVkZGluIE5hc3J1ZGRpbiIsInByZWZlcnJlZF91c2VybmFtZSI6Im5hc3J1ZGRpbiIsImdpdmVuX25hbWUiOiJOYXNydWRkaW4iLCJmYW1pbHlfbmFtZSI6Ik5hc3J1ZGRpbiIsImVtYWlsIjoibmFzcnVkZGluQGdtYWlsLmNvbSJ9.H2zm8j1_VPNUxSlBBSlARWYBWsb8RUUcFjZHHqAAvBojIRi73uOJQ2Xf30EAOj2bpjsDLukBCyZYbhJmaNDWAfiG2_q8wL8xvflpnHdBlAIdrnJ3OU2qFSDYnB7kWblqbeYFsbYZS72CO9K2BYPieViRi_eWUF61AneFJRqIyBMMmwXTcnwuX10ImEIpzQ8OdakVMBxrP1eZlUGAXGjHpRuzfJYSwQhwphVvOt1tUhEfRawu9ihJLvLRADcZXxS-5qvHJDMpDJuHKQL2HvfJLdmseIO4l7lYi2QoqsC56R7wSORioaP0lxxD6DXeEIvaor6oKuRXyZ-Txon4P40iMQ 15 | } 16 | 17 | body:json { 18 | { 19 | "courseId": 2, 20 | "author": "John Doe", 21 | "content": "Amazing book and loved reading it", 22 | "email": "abc@xyz.com" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api-specs/bruno/Course-Composite.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Course-Composite 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: http://localhost:9000/course-aggregate/4/with-details 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | headers { 14 | : 15 | } 16 | 17 | auth:bearer { 18 | token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0T1lYQVlZUXF6T2ZibE1wRjF0dmNvLW1UY2dEODVjai1Qak4xVnhuUExzIn0.eyJleHAiOjE3NDE0NTA1NzEsImlhdCI6MTc0MTQxNDU3MSwianRpIjoiMWE1Y2I3NWYtNWMxYy00ZDE4LWJjNTEtNjczYWMzMjZjNTJhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL3JlYWxtcy9jb3Vyc2UtbWFuYWdlbWVudC1yZWFsbSIsImF1ZCI6InJldmlldy1hcHAiLCJzdWIiOiIzNzY2NmQ1Ni1jYTJkLTRkNDQtOTRmMS1kNDk4ZTdhZmVhOTUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjb3Vyc2UtYXBwIiwic2lkIjoiZTk5ZDZiOWQtZDdmNC00YzUxLWI1NDItOGFjNTU1MTU3ZDI0IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiR1VFU1QiLCJBRE1JTiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InJldmlldy1hcHAiOnsicm9sZXMiOlsiUkVWSUVXLVJFQUQiLCJSRVZJRVctV1JJVEUiXX0sImNvdXJzZS1hcHAiOnsicm9sZXMiOlsiQ09VUlNFLVdSSVRFIiwiQ09VUlNFLVJFQUQiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiTmFzcnVkZGluIE5hc3J1ZGRpbiIsInByZWZlcnJlZF91c2VybmFtZSI6Im5hc3J1ZGRpbiIsImdpdmVuX25hbWUiOiJOYXNydWRkaW4iLCJmYW1pbHlfbmFtZSI6Ik5hc3J1ZGRpbiIsImVtYWlsIjoibmFzcnVkZGluQGdtYWlsLmNvbSJ9.H2zm8j1_VPNUxSlBBSlARWYBWsb8RUUcFjZHHqAAvBojIRi73uOJQ2Xf30EAOj2bpjsDLukBCyZYbhJmaNDWAfiG2_q8wL8xvflpnHdBlAIdrnJ3OU2qFSDYnB7kWblqbeYFsbYZS72CO9K2BYPieViRi_eWUF61AneFJRqIyBMMmwXTcnwuX10ImEIpzQ8OdakVMBxrP1eZlUGAXGjHpRuzfJYSwQhwphVvOt1tUhEfRawu9ihJLvLRADcZXxS-5qvHJDMpDJuHKQL2HvfJLdmseIO4l7lYi2QoqsC56R7wSORioaP0lxxD6DXeEIvaor6oKuRXyZ-Txon4P40iMQ 19 | } 20 | -------------------------------------------------------------------------------- /api-specs/bruno/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "apis", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ] 9 | } -------------------------------------------------------------------------------- /api-specs/bruno/environments/docker.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | keycloakBaseUrl: http://localhost:8081 3 | baseUrl: http://localhost:9000 4 | } 5 | -------------------------------------------------------------------------------- /api-specs/bruno/environments/kubernetes.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | keycloakBaseUrl: http://keycloak.local 3 | baseUrl: http://127.0.0.1 4 | } 5 | -------------------------------------------------------------------------------- /api-specs/openapi-specs/OpenApi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: apis 4 | version: 1.0.0 5 | contact: {} 6 | servers: 7 | - url: http://localhost:8081 8 | - url: http://localhost:9000 9 | paths: 10 | /realms/course-management-realm/protocol/openid-connect/token: 11 | post: 12 | summary: AccessToken 13 | description: '' 14 | operationId: accesstoken 15 | requestBody: 16 | content: 17 | application/x-www-form-urlencoded: 18 | schema: 19 | type: object 20 | properties: 21 | client_id: 22 | type: string 23 | example: course-app 24 | client_secret: 25 | type: string 26 | example: v1sCIPjANbvyJ87RsTkYeI9xHonDqZh7 27 | grant_type: 28 | type: string 29 | example: password 30 | password: 31 | type: string 32 | example: password 33 | scope: 34 | type: string 35 | example: openid roles 36 | username: 37 | type: string 38 | example: nasruddin 39 | examples: 40 | AccessToken: 41 | value: 42 | client_id: course-app 43 | client_secret: v1sCIPjANbvyJ87RsTkYeI9xHonDqZh7 44 | grant_type: password 45 | password: password 46 | scope: openid roles 47 | username: nasruddin 48 | responses: 49 | '200': 50 | description: '' 51 | security: 52 | - {} 53 | /courses: 54 | post: 55 | summary: Add Course 56 | description: '' 57 | operationId: addCourse 58 | requestBody: 59 | content: 60 | application/json: 61 | schema: 62 | type: object 63 | properties: 64 | author: 65 | type: string 66 | example: John Doe 67 | price: 68 | type: number 69 | example: 29.79 70 | publisher: 71 | type: string 72 | example: Whatsapp 73 | title: 74 | type: string 75 | example: Microservices with Quarkus 76 | examples: 77 | Add Course: 78 | value: 79 | author: John Doe 80 | price: 29.79 81 | publisher: Whatsapp 82 | title: Microservices with Quarkus 83 | responses: 84 | '200': 85 | description: '' 86 | security: 87 | - bearerAuth: [] 88 | /reviews: 89 | post: 90 | summary: Add Review 91 | description: '' 92 | operationId: addReview 93 | requestBody: 94 | content: 95 | application/json: 96 | schema: 97 | type: object 98 | properties: 99 | author: 100 | type: string 101 | example: John Doe 102 | content: 103 | type: string 104 | example: Amazing book and loved reading it 105 | courseId: 106 | type: number 107 | example: 2 108 | email: 109 | type: string 110 | example: abc@xyz.com 111 | examples: 112 | Add Review: 113 | value: 114 | author: John Doe 115 | content: Amazing book and loved reading it 116 | courseId: 2 117 | email: abc@xyz.com 118 | responses: 119 | '200': 120 | description: '' 121 | security: 122 | - bearerAuth: [] 123 | /course-aggregate/4/with-details: 124 | get: 125 | summary: Course-Composite 126 | description: '' 127 | operationId: courseComposite 128 | parameters: 129 | - name: '' 130 | in: header 131 | schema: 132 | type: string 133 | example: '' 134 | responses: 135 | '200': 136 | description: '' 137 | security: 138 | - bearerAuth: [] 139 | components: 140 | securitySchemes: 141 | bearerAuth: 142 | type: http 143 | scheme: bearer 144 | tags: [] 145 | -------------------------------------------------------------------------------- /build-images.sh: -------------------------------------------------------------------------------- 1 | echo "Building Docker images for Kubernetes using Minikube..." 2 | 3 | # Define an array of services 4 | SERVICES=( 5 | "microservices/course-composite-service:course-composite-service" 6 | "microservices/course-service:course-service" 7 | "microservices/review-service:review-service" 8 | "spring-cloud/gateway-service:gateway-service" 9 | ) 10 | 11 | eval $(minikube docker-env --profile microservice-deployment) 12 | 13 | # Iterate over services and build each one 14 | for SERVICE in "${SERVICES[@]}"; do 15 | IFS=":" read -r DIR IMAGE <<< "$SERVICE" 16 | echo "Building $IMAGE..." 17 | cd "$DIR" || { echo "Failed to enter directory $DIR"; exit 1; } 18 | docker build -t "$IMAGE" . 19 | cd - >/dev/null || exit 1 20 | echo "$IMAGE built successfully!" 21 | done 22 | 23 | echo "All images built successfully!" 24 | -------------------------------------------------------------------------------- /create-project.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Common configuration 4 | SPRING_BOOT_VERSION="3.4.3" 5 | JAVA_VERSION="17" 6 | BUILD_TOOL="maven" 7 | PACKAGING="jar" 8 | BASE_VERSION="1.0.0" 9 | BASE_GROUP="io.javatab.microservices" 10 | DEPENDENCIES="actuator,webflux" 11 | 12 | # Function to initialize a Spring Boot microservice 13 | init_microservice() { 14 | local service_name=$1 15 | local package_suffix=$2 16 | 17 | spring init \ 18 | --boot-version="$SPRING_BOOT_VERSION" \ 19 | --build="$BUILD_TOOL" \ 20 | --java-version="$JAVA_VERSION" \ 21 | --packaging="$PACKAGING" \ 22 | --name="${service_name}-service" \ 23 | --package-name="${BASE_GROUP}.${package_suffix}" \ 24 | --groupId="${BASE_GROUP}.${package_suffix}" \ 25 | --dependencies="$DEPENDENCIES" \ 26 | --version="$BASE_VERSION" \ 27 | "${service_name}-service" 28 | } 29 | 30 | # Main execution 31 | main() { 32 | # Create and enter microservices directory 33 | mkdir -p microservices 34 | cd microservices || exit 1 35 | 36 | # Initialize each microservice 37 | init_microservice "course" "core.course" 38 | init_microservice "review" "core.review" 39 | init_microservice "course-composite" "composite.course" 40 | 41 | # Return to parent directory 42 | cd .. || exit 1 43 | } 44 | 45 | # Execute main function 46 | main -------------------------------------------------------------------------------- /docker/docker-compose-base.yml: -------------------------------------------------------------------------------- 1 | services: 2 | course: 3 | build: ../microservices/course-service 4 | mem_limit: 512m 5 | environment: 6 | - SPRING_PROFILES_ACTIVE=docker 7 | - JAVA_TOOL_OPTIONS="-javaagent:/application/BOOT-INF/lib/opentelemetry-javaagent-2.13.3.jar" #maven will place jar in BOOT-INF as per dep mentioned in pom. This will instruct the JVM to run the OpenTelemetry Java agent from the path. 8 | - OTEL_TRACES_EXPORTER=otlp 9 | - OTEL_METRICS_EXPORTER=none 10 | - OTEL_LOGS_EXPORTER=none 11 | - OTEL_METRIC_EXPORT_INTERVAL=15000 12 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318 13 | logging: 14 | driver: fluentd 15 | options: 16 | fluentd-address: 127.0.0.1:24224 17 | networks: 18 | - shared-network 19 | #depends_on: 20 | # postgres: 21 | # condition: service_healthy 22 | 23 | review: 24 | build: ../microservices/review-service 25 | mem_limit: 512m 26 | environment: 27 | - SPRING_PROFILES_ACTIVE=docker 28 | - JAVA_TOOL_OPTIONS="-javaagent:/application/BOOT-INF/lib/opentelemetry-javaagent-2.13.3.jar" #maven will place jar in BOOT-INF as per dep mentioned in pom. This will instruct the JVM to run the OpenTelemetry Java agent from the path. 29 | - OTEL_TRACES_EXPORTER=otlp 30 | - OTEL_METRICS_EXPORTER=none 31 | - OTEL_LOGS_EXPORTER=none 32 | - OTEL_METRIC_EXPORT_INTERVAL=15000 33 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318 34 | logging: 35 | driver: fluentd 36 | options: 37 | fluentd-address: 127.0.0.1:24224 38 | networks: 39 | - shared-network 40 | #depends_on: 41 | # mongodb: 42 | # condition: service_healthy 43 | 44 | course-composite: 45 | build: ../microservices/course-composite-service 46 | mem_limit: 512m 47 | ports: 48 | - "8080:8080" 49 | environment: 50 | - SPRING_PROFILES_ACTIVE=docker 51 | - JAVA_TOOL_OPTIONS="-javaagent:/application/BOOT-INF/lib/opentelemetry-javaagent-2.13.3.jar" #maven will place jar in BOOT-INF as per dep mentioned in pom. This will instruct the JVM to run the OpenTelemetry Java agent from the path. 52 | - OTEL_TRACES_EXPORTER=otlp 53 | - OTEL_METRICS_EXPORTER=none 54 | - OTEL_LOGS_EXPORTER=none 55 | - OTEL_METRIC_EXPORT_INTERVAL=15000 56 | - OTEL_INSTRUMENTATION_SPRING_WEB_EXCLUDE_PATTERNS=/actuator/prometheus 57 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318 58 | 59 | logging: 60 | driver: fluentd 61 | options: 62 | fluentd-address: 127.0.0.1:24224 63 | networks: 64 | - shared-network 65 | 66 | gateway-service: 67 | build: ../spring-cloud/gateway-service 68 | mem_limit: 512m 69 | ports: 70 | - "9000:9000" 71 | environment: 72 | - SPRING_PROFILES_ACTIVE=docker 73 | - JAVA_TOOL_OPTIONS="-javaagent:/application/BOOT-INF/lib/opentelemetry-javaagent-2.13.3.jar" #maven will place jar in BOOT-INF as per dep mentioned in pom. This will instruct the JVM to run the OpenTelemetry Java agent from the path. 74 | - OTEL_TRACES_EXPORTER=otlp 75 | - OTEL_METRICS_EXPORTER=none 76 | - OTEL_LOGS_EXPORTER=none 77 | - OTEL_METRIC_EXPORT_INTERVAL=15000 78 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318 79 | logging: 80 | driver: fluentd 81 | options: 82 | fluentd-address: 127.0.0.1:24224 83 | networks: 84 | - shared-network 85 | 86 | networks: 87 | shared-network: 88 | driver: bridge -------------------------------------------------------------------------------- /docker/docker-compose-infra.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mongodb: 3 | image: mongo:6.0.4 4 | container_name: "mongodb" 5 | mem_limit: 512m 6 | ports: 7 | - "27017:27017" 8 | command: mongod 9 | healthcheck: 10 | test: "mongostat -n 1" 11 | networks: 12 | - shared-network 13 | 14 | postgres: 15 | image: "postgres:17.4" 16 | container_name: "postgres" 17 | ports: 18 | - 5432:5432 19 | environment: 20 | - POSTGRES_USER=user 21 | - POSTGRES_PASSWORD=pwd 22 | - POSTGRES_DB=course_db 23 | healthcheck: 24 | test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ] 25 | interval: 10s 26 | timeout: 5s 27 | retries: 5 28 | start_period: 30s 29 | volumes: 30 | - ./postgresql/init.sql:/docker-entrypoint-initdb.d/init.sql 31 | networks: 32 | - shared-network 33 | 34 | keycloak: 35 | image: quay.io/keycloak/keycloak:latest 36 | container_name: keycloak 37 | ports: 38 | - "8081:8080" 39 | environment: 40 | KEYCLOAK_ADMIN: admin 41 | KEYCLOAK_ADMIN_PASSWORD: admin 42 | command: [ "start-dev" ] 43 | networks: 44 | - shared-network 45 | 46 | networks: 47 | shared-network: 48 | driver: bridge 49 | volumes: 50 | prometheus-data: 51 | grafana-data: 52 | loki-data: -------------------------------------------------------------------------------- /docker/docker-compose-observability.yml: -------------------------------------------------------------------------------- 1 | services: 2 | prometheus: 3 | image: prom/prometheus:latest 4 | volumes: 5 | - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 6 | - prometheus-data:/prometheus 7 | command: 8 | - '--config.file=/etc/prometheus/prometheus.yml' 9 | - '--storage.tsdb.path=/prometheus' 10 | ports: 11 | - "9090:9090" 12 | networks: 13 | - shared-network 14 | 15 | grafana: 16 | image: grafana/grafana:latest 17 | volumes: 18 | - grafana-data:/var/lib/grafana 19 | ports: 20 | - "3000:3000" 21 | environment: 22 | - GF_SECURITY_ADMIN_PASSWORD=admin 23 | depends_on: 24 | - prometheus 25 | networks: 26 | - shared-network 27 | 28 | loki: 29 | image: grafana/loki:3.0.0 30 | ports: 31 | - "3100:3100" 32 | command: -config.file=/etc/loki/local-config.yaml 33 | volumes: 34 | - ./loki/loki-config.yaml:/etc/loki/local-config.yaml 35 | - loki-data:/loki 36 | networks: 37 | - shared-network 38 | 39 | fluent-bit: 40 | image: grafana/fluent-bit-plugin-loki:2.9.8 41 | container_name: fluent-bit 42 | ports: 43 | - "24224:24224" 44 | environment: 45 | - LOKI_URL=http://loki:3100/loki/api/v1/push 46 | volumes: 47 | - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf 48 | networks: 49 | - shared-network 50 | tempo: 51 | image: grafana/tempo:latest 52 | command: -config.file /etc/tempo-config.yml 53 | ports: 54 | - "4317:4317" # OTLP gRPC 55 | - "4318:4318" # OTLP HTTP 56 | - "3200:3200" # Tempo HTTP server (for querying) 57 | volumes: 58 | - ./tempo/tempo.yml:/etc/tempo-config.yml 59 | networks: 60 | - shared-network 61 | 62 | networks: 63 | shared-network: 64 | driver: bridge 65 | volumes: 66 | prometheus-data: 67 | grafana-data: 68 | loki-data: -------------------------------------------------------------------------------- /docker/fluent-bit/fluent-bit.conf: -------------------------------------------------------------------------------- 1 | [INPUT] 2 | Name forward 3 | Listen 0.0.0.0 4 | Port 24224 5 | [Output] 6 | Name grafana-loki 7 | Match * 8 | Url http://loki:3100/loki/api/v1/push 9 | RemoveKeys source,container_id 10 | Labels {job="fluent-bit"} 11 | LabelKeys container_name 12 | BatchWait 1s 13 | BatchSize 1001024 14 | LineFormat json 15 | LogLevel info -------------------------------------------------------------------------------- /docker/loki/loki-config.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | server: 3 | http_listen_port: 3100 4 | ingester: 5 | lifecycler: 6 | address: 0.0.0.0 7 | ring: 8 | kvstore: 9 | store: inmemory 10 | replication_factor: 1 11 | chunk_idle_period: 5m 12 | wal: 13 | enabled: true 14 | dir: /loki/wal 15 | schema_config: 16 | configs: 17 | - from: 2025-03-01 18 | store: tsdb 19 | object_store: filesystem 20 | schema: v13 21 | index: 22 | prefix: index_ 23 | period: 24h 24 | storage_config: 25 | tsdb_shipper: 26 | active_index_directory: /loki/tsdb-index 27 | cache_location: /loki/tsdb-cache 28 | filesystem: 29 | directory: /loki/chunks 30 | compactor: 31 | working_directory: /loki/compactor -------------------------------------------------------------------------------- /docker/postgresql/init.sql: -------------------------------------------------------------------------------- 1 | DO $$ 2 | BEGIN 3 | IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'course_db') THEN 4 | CREATE DATABASE course_db; 5 | END IF; 6 | END $$; 7 | -------------------------------------------------------------------------------- /docker/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | scrape_configs: 4 | - job_name: 'course-composite-app' 5 | metrics_path: '/actuator/prometheus' 6 | static_configs: 7 | - targets: 8 | - 'course-composite:8080' 9 | - 'course:8080' 10 | - 'review:8080' 11 | - 'gateway-service:9000' -------------------------------------------------------------------------------- /docker/tempo/tempo.yml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_address: 0.0.0.0 3 | http_listen_port: 3200 4 | 5 | distributor: 6 | receivers: 7 | otlp: 8 | protocols: 9 | grpc: 10 | endpoint: 0.0.0.0:4317 11 | http: 12 | endpoint: 0.0.0.0:4318 13 | 14 | storage: 15 | trace: 16 | backend: local 17 | local: 18 | path: /tmp/tempo/blocks 19 | 20 | usage_report: 21 | reporting_enabled: false 22 | -------------------------------------------------------------------------------- /kubernetes/infrastructure/fluent-bit/fluent-bit.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: fluent-bit-config 5 | data: 6 | fluent-bit.conf: | 7 | [INPUT] 8 | Name forward 9 | Listen 0.0.0.0 10 | Port 24224 11 | 12 | [OUTPUT] 13 | Name grafana-loki 14 | Match * 15 | Url http://loki:3100/loki/api/v1/push 16 | RemoveKeys source,container_id 17 | Labels {job="fluent-bit"} 18 | LabelKeys container_name 19 | BatchWait 1s 20 | BatchSize 1001024 21 | LineFormat json 22 | LogLevel info 23 | --- 24 | apiVersion: v1 25 | kind: PersistentVolumeClaim 26 | metadata: 27 | name: fluent-bit-pvc 28 | spec: 29 | accessModes: 30 | - ReadWriteOnce 31 | resources: 32 | requests: 33 | storage: 1Gi 34 | --- 35 | apiVersion: apps/v1 36 | kind: Deployment 37 | metadata: 38 | name: fluent-bit 39 | labels: 40 | app: fluent-bit 41 | spec: 42 | replicas: 1 43 | selector: 44 | matchLabels: 45 | app: fluent-bit 46 | template: 47 | metadata: 48 | labels: 49 | app: fluent-bit 50 | spec: 51 | securityContext: 52 | fsGroup: 1000 # Ensures write permission to storage path 53 | containers: 54 | - name: fluent-bit 55 | image: grafana/fluent-bit-plugin-loki:2.9.8 56 | ports: 57 | - containerPort: 24224 58 | env: 59 | - name: LOKI_URL 60 | value: "http://loki:3100/loki/api/v1/push" 61 | volumeMounts: 62 | - name: fluent-bit-config 63 | mountPath: /fluent-bit/etc/fluent-bit.conf 64 | subPath: fluent-bit.conf 65 | - name: fluent-bit-storage 66 | mountPath: /var/log/fluent-bit 67 | volumes: 68 | - name: fluent-bit-config 69 | configMap: 70 | name: fluent-bit-config 71 | - name: fluent-bit-storage 72 | persistentVolumeClaim: 73 | claimName: fluent-bit-pvc 74 | --- 75 | apiVersion: v1 76 | kind: Service 77 | metadata: 78 | name: fluent-bit 79 | spec: 80 | selector: 81 | app: fluent-bit 82 | ports: 83 | - name: fluent-forward 84 | protocol: TCP 85 | port: 24224 86 | targetPort: 24224 87 | type: ClusterIP 88 | -------------------------------------------------------------------------------- /kubernetes/infrastructure/grafana/grafana.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: grafana 5 | labels: 6 | app: grafana 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: grafana 12 | template: 13 | metadata: 14 | labels: 15 | app: grafana 16 | spec: 17 | containers: 18 | - name: grafana 19 | image: grafana/grafana:latest 20 | ports: 21 | - containerPort: 3000 22 | env: 23 | - name: GF_SECURITY_ADMIN_PASSWORD 24 | value: "admin" 25 | --- 26 | apiVersion: v1 27 | kind: Service 28 | metadata: 29 | name: grafana 30 | labels: 31 | app: grafana 32 | spec: 33 | selector: 34 | app: grafana 35 | ports: 36 | - protocol: TCP 37 | port: 3000 38 | targetPort: 3000 39 | type: ClusterIP 40 | --- 41 | apiVersion: networking.k8s.io/v1 42 | kind: Ingress 43 | metadata: 44 | name: grafana-ingress 45 | spec: 46 | rules: 47 | - host: grafana.local 48 | http: 49 | paths: 50 | - path: / 51 | pathType: Prefix 52 | backend: 53 | service: 54 | name: grafana 55 | port: 56 | number: 3000 57 | -------------------------------------------------------------------------------- /kubernetes/infrastructure/keycloak/keycloak.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: keycloak 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: keycloak 10 | template: 11 | metadata: 12 | labels: 13 | app: keycloak 14 | spec: 15 | containers: 16 | - name: keycloak 17 | image: quay.io/keycloak/keycloak:24.0 18 | args: ["start-dev"] # Runs Keycloak in standalone mode with H2 19 | env: 20 | - name: KEYCLOAK_ADMIN 21 | value: admin 22 | - name: KEYCLOAK_ADMIN_PASSWORD 23 | value: admin 24 | ports: 25 | - containerPort: 8080 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: keycloak 31 | spec: 32 | ports: 33 | - name: http 34 | port: 8080 35 | targetPort: 8080 36 | selector: 37 | app: keycloak 38 | --- 39 | apiVersion: networking.k8s.io/v1 40 | kind: Ingress 41 | metadata: 42 | name: keycloak-ingress 43 | spec: 44 | rules: 45 | - host: keycloak.local 46 | http: 47 | paths: 48 | - path: / 49 | pathType: Prefix 50 | backend: 51 | service: 52 | name: keycloak 53 | port: 54 | number: 8080 55 | -------------------------------------------------------------------------------- /kubernetes/infrastructure/loki/loki.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: loki-config 5 | data: 6 | loki-config.yaml: | 7 | auth_enabled: false 8 | server: 9 | http_listen_port: 3100 10 | ingester: 11 | lifecycler: 12 | address: 0.0.0.0 13 | ring: 14 | kvstore: 15 | store: inmemory 16 | replication_factor: 1 17 | chunk_idle_period: 5m 18 | wal: 19 | enabled: true 20 | dir: /loki/wal 21 | schema_config: 22 | configs: 23 | - from: 2025-03-01 24 | store: tsdb 25 | object_store: filesystem 26 | schema: v13 27 | index: 28 | prefix: index_ 29 | period: 24h 30 | storage_config: 31 | tsdb_shipper: 32 | active_index_directory: /loki/tsdb-index 33 | cache_location: /loki/tsdb-cache 34 | filesystem: 35 | directory: /loki/chunks 36 | compactor: 37 | working_directory: /loki/compactor 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: loki 43 | spec: 44 | selector: 45 | app: loki 46 | ports: 47 | - name: http 48 | protocol: TCP 49 | port: 3100 50 | targetPort: 3100 51 | --- 52 | apiVersion: apps/v1 53 | kind: StatefulSet 54 | metadata: 55 | name: loki 56 | spec: 57 | serviceName: loki 58 | replicas: 1 59 | selector: 60 | matchLabels: 61 | app: loki 62 | template: 63 | metadata: 64 | labels: 65 | app: loki 66 | spec: 67 | containers: 68 | - name: loki 69 | image: grafana/loki:3.0.0 70 | args: 71 | - "-config.file=/etc/loki/loki-config.yaml" 72 | ports: 73 | - containerPort: 3100 74 | volumeMounts: 75 | - name: config 76 | mountPath: /etc/loki 77 | - name: data 78 | mountPath: /loki 79 | volumes: 80 | - name: config 81 | configMap: 82 | name: loki-config 83 | volumeClaimTemplates: 84 | - metadata: 85 | name: data 86 | spec: 87 | accessModes: ["ReadWriteOnce"] 88 | resources: 89 | requests: 90 | storage: 10Gi 91 | -------------------------------------------------------------------------------- /kubernetes/infrastructure/mongodb/mongodb.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: review-mongodb 5 | labels: 6 | app: review-mongodb 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: review-mongodb 12 | template: 13 | metadata: 14 | labels: 15 | app: review-mongodb 16 | spec: 17 | containers: 18 | - name: review-mongodb 19 | image: mongo:6.0.4 # Latest stable MongoDB version 20 | ports: 21 | - containerPort: 27017 22 | volumeMounts: 23 | - name: mongodb-storage 24 | mountPath: /data/db 25 | volumes: 26 | - name: mongodb-storage 27 | emptyDir: {} # Use PersistentVolumeClaim for production 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | name: review-mongodb 33 | spec: 34 | selector: 35 | app: review-mongodb 36 | ports: 37 | - protocol: TCP 38 | port: 27017 39 | targetPort: 27017 40 | type: ClusterIP # Change to LoadBalancer or NodePort if needed 41 | -------------------------------------------------------------------------------- /kubernetes/infrastructure/postgres/postgres.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: course-postgres 5 | labels: 6 | db: course-postgres 7 | spec: 8 | selector: 9 | matchLabels: 10 | db: course-postgres 11 | template: 12 | metadata: 13 | labels: 14 | db: course-postgres 15 | spec: 16 | containers: 17 | - name: course-postgres 18 | image: postgres:17.4 19 | env: 20 | - name: POSTGRES_USER 21 | value: user 22 | - name: POSTGRES_PASSWORD 23 | value: pwd 24 | - name: POSTGRES_DB 25 | value: course_db 26 | resources: 27 | requests: 28 | cpu: 100m 29 | memory: 60Mi 30 | limits: 31 | cpu: 200m 32 | memory: 120Mi 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: course-postgres 38 | labels: 39 | db: course-postgres 40 | spec: 41 | type: ClusterIP 42 | selector: 43 | db: course-postgres 44 | ports: 45 | - protocol: TCP 46 | port: 5432 47 | targetPort: 5432 -------------------------------------------------------------------------------- /kubernetes/infrastructure/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: prometheus-config 5 | data: 6 | prometheus.yml: | 7 | global: 8 | scrape_interval: 15s 9 | scrape_configs: 10 | - job_name: 'spring-boot-app' 11 | metrics_path: '/actuator/prometheus' 12 | static_configs: 13 | - targets: 14 | - 'course-composite-service:80' 15 | - 'course-service:80' 16 | - 'review-service:80' 17 | - 'gateway-service:80' 18 | --- 19 | apiVersion: apps/v1 20 | kind: Deployment 21 | metadata: 22 | name: prometheus 23 | spec: 24 | replicas: 1 25 | selector: 26 | matchLabels: 27 | app: prometheus 28 | template: 29 | metadata: 30 | labels: 31 | app: prometheus 32 | spec: 33 | containers: 34 | - name: prometheus 35 | image: prom/prometheus:latest 36 | args: 37 | - "--config.file=/etc/prometheus/prometheus.yml" 38 | ports: 39 | - containerPort: 9090 40 | volumeMounts: 41 | - name: config-volume 42 | mountPath: "/etc/prometheus/" 43 | volumes: 44 | - name: config-volume 45 | configMap: 46 | name: prometheus-config 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: prometheus 52 | spec: 53 | selector: 54 | app: prometheus 55 | ports: 56 | - port: 9090 57 | targetPort: 9090 58 | protocol: TCP 59 | type: ClusterIP 60 | --- 61 | apiVersion: networking.k8s.io/v1 62 | kind: Ingress 63 | metadata: 64 | name: prometheus-ingress 65 | spec: 66 | rules: 67 | - host: prometheus.local 68 | http: 69 | paths: 70 | - path: / 71 | pathType: Prefix 72 | backend: 73 | service: 74 | name: prometheus 75 | port: 76 | number: 9090 -------------------------------------------------------------------------------- /kubernetes/infrastructure/tempo/tempo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: tempo-config 5 | data: 6 | tempo.yml: | 7 | server: 8 | http_listen_address: 0.0.0.0 9 | http_listen_port: 3200 10 | 11 | distributor: 12 | receivers: 13 | otlp: 14 | protocols: 15 | grpc: 16 | endpoint: 0.0.0.0:4317 17 | http: 18 | endpoint: 0.0.0.0:4318 19 | 20 | storage: 21 | trace: 22 | backend: local 23 | local: 24 | path: /tmp/tempo/blocks 25 | 26 | usage_report: 27 | reporting_enabled: false 28 | --- 29 | apiVersion: v1 30 | kind: PersistentVolumeClaim 31 | metadata: 32 | name: tempo-pvc 33 | spec: 34 | accessModes: 35 | - ReadWriteOnce 36 | resources: 37 | requests: 38 | storage: 5Gi 39 | --- 40 | apiVersion: apps/v1 41 | kind: Deployment 42 | metadata: 43 | name: tempo 44 | labels: 45 | app: tempo 46 | spec: 47 | replicas: 1 48 | selector: 49 | matchLabels: 50 | app: tempo 51 | template: 52 | metadata: 53 | labels: 54 | app: tempo 55 | spec: 56 | securityContext: 57 | fsGroup: 1000 # Ensures write permission to the storage path 58 | containers: 59 | - name: tempo 60 | image: grafana/tempo:latest 61 | args: 62 | - "-config.file=/etc/tempo-config.yml" 63 | ports: 64 | - containerPort: 4317 65 | - containerPort: 4318 66 | - containerPort: 3200 67 | volumeMounts: 68 | - name: tempo-config 69 | mountPath: /etc/tempo-config.yml 70 | subPath: tempo.yml 71 | - name: tempo-storage 72 | mountPath: /tmp/tempo/blocks 73 | volumes: 74 | - name: tempo-config 75 | configMap: 76 | name: tempo-config 77 | - name: tempo-storage 78 | persistentVolumeClaim: 79 | claimName: tempo-pvc 80 | --- 81 | apiVersion: v1 82 | kind: Service 83 | metadata: 84 | name: tempo 85 | spec: 86 | selector: 87 | app: tempo 88 | ports: 89 | - name: grpc 90 | protocol: TCP 91 | port: 4317 92 | targetPort: 4317 93 | - name: http 94 | protocol: TCP 95 | port: 4318 96 | targetPort: 4318 97 | - name: tempo-http 98 | protocol: TCP 99 | port: 3200 100 | targetPort: 3200 101 | type: ClusterIP 102 | -------------------------------------------------------------------------------- /microservices/course-composite-service/.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /microservices/course-composite-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /microservices/course-composite-service/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /microservices/course-composite-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17.0.5_8-jre-focal as builder 2 | WORKDIR extracted 3 | 4 | ADD ./target/*.jar app.jar 5 | 6 | RUN java -Djarmode=layertools -jar app.jar extract 7 | 8 | FROM eclipse-temurin:17.0.5_8-jre-focal 9 | WORKDIR application 10 | 11 | # Copy the dependencies layer from the builder stage 12 | COPY --from=builder extracted/dependencies/ ./ 13 | # Copy the Spring Boot loader layer from the builder stage 14 | COPY --from=builder extracted/spring-boot-loader/ ./ 15 | # Copy the snapshot dependencies layer from the builder stage 16 | COPY --from=builder extracted/snapshot-dependencies/ ./ 17 | # Copy the application layer from the builder stage 18 | COPY --from=builder extracted/application/ ./ 19 | 20 | # Expose port 8080 21 | EXPOSE 8080 22 | 23 | # Set the entry point to launch the application 24 | ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] 25 | 26 | -------------------------------------------------------------------------------- /microservices/course-composite-service/kubernetes/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: course-composite-service 5 | labels: 6 | app: course-composite-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: course-composite-service 12 | template: 13 | metadata: 14 | labels: 15 | app: course-composite-service 16 | spec: 17 | containers: 18 | - name: course-composite-service 19 | image: course-composite-service # Please rename these while building docker image 20 | imagePullPolicy: IfNotPresent 21 | lifecycle: 22 | preStop: 23 | exec: 24 | command: [ "sh", "-c", "sleep 5" ] 25 | ports: 26 | - containerPort: 5000 27 | env: 28 | - name: COURSE_SERVICE_URL 29 | value: http://course-service 30 | - name: REVIEW_SERVICE_URL 31 | value: http://review-service 32 | - name: APP_KEYCLOAK_JWK_SET_URI 33 | value: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 34 | - name: KEYCLOAK_ISSUER_URI 35 | value: http://keycloak:8080/realms/course-management-realm 36 | - name: KEYCLOAK_JWK_SET_URI 37 | value: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs -------------------------------------------------------------------------------- /microservices/course-composite-service/kubernetes/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: course-composite-service 5 | labels: 6 | app: course-composite-service 7 | spec: 8 | type: ClusterIP 9 | selector: 10 | app: course-composite-service 11 | ports: 12 | - protocol: TCP 13 | port: 80 14 | targetPort: 5000 -------------------------------------------------------------------------------- /microservices/course-composite-service/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /microservices/course-composite-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.3 9 | 10 | 11 | io.javatab.microservices.composite.course 12 | course-composite-service 13 | 1.0.0 14 | course-composite-service 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 17 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-actuator 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-webflux 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-security 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-oauth2-resource-server 50 | 51 | 52 | io.javatab.util 53 | util 54 | 1.0.0 55 | compile 56 | 57 | 58 | 59 | org.projectlombok 60 | lombok 61 | provided 62 | 63 | 64 | 65 | io.micrometer 66 | micrometer-registry-prometheus 67 | 68 | 69 | 70 | ch.qos.logback 71 | logback-classic 72 | 73 | 74 | 75 | io.opentelemetry.javaagent 76 | opentelemetry-javaagent 77 | 2.13.3 78 | 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-starter-test 83 | test 84 | 85 | 86 | io.projectreactor 87 | reactor-test 88 | test 89 | 90 | 91 | 92 | 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-maven-plugin 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/CourseCompositeServiceApplication.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | @SpringBootApplication 10 | @ComponentScan({"io.javatab"}) 11 | public class CourseCompositeServiceApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(CourseCompositeServiceApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/MetricsController.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course; 2 | 3 | import io.micrometer.core.instrument.Counter; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import io.micrometer.core.instrument.Timer; 6 | import jakarta.annotation.PostConstruct; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | import org.springframework.web.server.ResponseStatusException; 14 | 15 | 16 | import java.util.Random; 17 | 18 | 19 | /* 20 | * Just for manual test for metrics and errors which doesn't sit under security 21 | * */ 22 | @RestController 23 | @RequestMapping("/api/metrics") 24 | public class MetricsController { 25 | private final Logger logger = LoggerFactory.getLogger(MetricsController.class); 26 | private final MeterRegistry meterRegistry; 27 | private Counter requestCounter; 28 | private Timer requestTimer; 29 | 30 | public MetricsController(MeterRegistry meterRegistry) { 31 | this.meterRegistry = meterRegistry; 32 | } 33 | 34 | @PostConstruct 35 | public void init() { 36 | // Initialize custom metrics 37 | requestCounter = Counter 38 | .builder("api.requests.total") 39 | .description("Total number of API requests") 40 | .tags("endpoint", "/hello") 41 | .register(meterRegistry); 42 | 43 | requestTimer = Timer 44 | .builder("api.request.duration") 45 | .description("Time taken to process requests") 46 | .tags("endpoint", "/hello") 47 | .register(meterRegistry); 48 | } 49 | 50 | @GetMapping("/hello") 51 | public String hello() { 52 | logger.info("Hello endpoint called"); 53 | logger.warn("This is a warning log"); 54 | // Record request count 55 | requestCounter.increment(); 56 | 57 | // Record execution time 58 | return requestTimer.record(() -> { 59 | try { 60 | // Simulate some work 61 | int sleepTime = new Random().nextInt(1000); 62 | Thread.sleep(sleepTime); 63 | return "Hello, World!"; 64 | } catch (InterruptedException e) { 65 | return "Error occurred"; 66 | } 67 | }); 68 | } 69 | 70 | @GetMapping("/runtime-error") 71 | public String error() { 72 | logger.error("An error occurred", new RuntimeException("Test exception")); 73 | return "Error logged"; 74 | } 75 | 76 | @GetMapping("/error") 77 | public String triggerError() { 78 | throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Something went wrong!"); 79 | } 80 | } -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/MetricsConfig.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course.config; 2 | 3 | import io.micrometer.core.instrument.Gauge; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import jakarta.annotation.PostConstruct; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | @Configuration 11 | public class MetricsConfig { 12 | 13 | private final MeterRegistry meterRegistry; 14 | private final AtomicInteger activeUsers = new AtomicInteger(0); 15 | 16 | public MetricsConfig(MeterRegistry meterRegistry) { 17 | this.meterRegistry = meterRegistry; 18 | } 19 | 20 | @PostConstruct 21 | public void init() { 22 | // Register a gauge for active users 23 | Gauge.builder("application.active.users", activeUsers::get) 24 | .description("Number of active users") 25 | .register(meterRegistry); 26 | } 27 | 28 | // Method to update active users (could be called from your service layer) 29 | public void updateActiveUsers(int count) { 30 | activeUsers.set(count); 31 | } 32 | } -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.convert.converter.Converter; 9 | import org.springframework.security.config.Customizer; 10 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 11 | import org.springframework.security.config.web.server.SecurityWebFiltersOrder; 12 | import org.springframework.security.config.web.server.ServerHttpSecurity; 13 | import org.springframework.security.core.GrantedAuthority; 14 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 15 | import org.springframework.security.oauth2.jwt.Jwt; 16 | import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; 17 | import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; 18 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 19 | import org.springframework.security.web.server.SecurityWebFilterChain; 20 | import org.springframework.web.server.ServerWebExchange; 21 | import reactor.core.publisher.Mono; 22 | 23 | import java.util.ArrayList; 24 | import java.util.Collection; 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | @Configuration 29 | @EnableWebFluxSecurity 30 | public class SecurityConfig { 31 | 32 | private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); 33 | 34 | private String jwkSetUri; 35 | 36 | public SecurityConfig(@Value("${app.jwk-set-uri}") String jwkSetUri) { 37 | this.jwkSetUri = jwkSetUri; 38 | } 39 | 40 | @Bean 41 | public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { 42 | http 43 | .authorizeExchange(exchanges -> exchanges 44 | .pathMatchers("/actuator/**", "/api/metrics/**").permitAll() 45 | .pathMatchers("/api/course-aggregate/**").hasAnyRole("COURSE-READ", "REVIEW-READ") 46 | .anyExchange().authenticated() 47 | ) 48 | .oauth2ResourceServer(oauth2 -> oauth2 49 | .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())) 50 | ); 51 | 52 | // Add filter to log roles 53 | http.addFilterAt((exchange, chain) -> logRoles(exchange).then(chain.filter(exchange)), 54 | SecurityWebFiltersOrder.AUTHORIZATION); 55 | 56 | return http.build(); 57 | } 58 | 59 | @Bean 60 | public ReactiveJwtDecoder jwtDecoder() { 61 | return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); 62 | } 63 | 64 | @Bean 65 | public Converter> grantedAuthoritiesExtractor() { 66 | return new Converter>() { 67 | @Override 68 | public Mono convert(Jwt jwt) { 69 | Collection authorities = new ArrayList<>(); 70 | 71 | // Extract realm roles 72 | Map realmAccess = jwt.getClaim("realm_access"); 73 | if (realmAccess != null && realmAccess.containsKey("roles")) { 74 | List roles = (List) realmAccess.get("roles"); 75 | authorities.addAll(roles.stream() 76 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 77 | .toList()); 78 | } 79 | 80 | // Extract client roles (replace "my-resource-server" with your client ID) 81 | /*Map resourceAccess = jwt.getClaim("resource_access"); 82 | if (resourceAccess != null) { 83 | Map clientRoles = (Map) resourceAccess.get("my-resource-server"); 84 | if (clientRoles != null && clientRoles.containsKey("roles")) { 85 | List roles = (List) clientRoles.get("roles"); 86 | authorities.addAll(roles.stream() 87 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 88 | .toList()); 89 | } 90 | }*/ 91 | Map resourceAccess = jwt.getClaim("resource_access"); 92 | if (resourceAccess != null) { 93 | resourceAccess.forEach((resource, access) -> { 94 | if (access instanceof Map) { 95 | Map clientRoles = (Map) access; 96 | if (clientRoles.containsKey("roles")) { 97 | List roles = (List) clientRoles.get("roles"); 98 | authorities.addAll(roles.stream() 99 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 100 | .toList()); 101 | } 102 | } 103 | }); 104 | } 105 | 106 | return Mono.just(new JwtAuthenticationToken(jwt, authorities)); 107 | } 108 | }; 109 | } 110 | 111 | private Mono logRoles(ServerWebExchange exchange) { 112 | return exchange.getPrincipal() 113 | .cast(JwtAuthenticationToken.class) 114 | .doOnNext(jwtAuth -> { 115 | Collection authorities = jwtAuth.getAuthorities(); 116 | logger.info("Roles in Resource Server: {}", authorities); 117 | }) 118 | .then(); 119 | } 120 | } -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/web/Course.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course.web; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Course { 7 | private Long id; 8 | private String title; 9 | private String author; 10 | private Double price; 11 | private String publisher; 12 | } 13 | -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/web/CourseAggregate.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course.web; 2 | 3 | 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @Builder 11 | public class CourseAggregate { 12 | private Course course; 13 | private List reviews; 14 | } 15 | -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/web/CourseAggregateController.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course.web; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 7 | import org.springframework.security.oauth2.jwt.Jwt; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import reactor.core.publisher.Mono; 13 | 14 | import java.util.List; 15 | 16 | @RestController 17 | @RequestMapping("/api/course-aggregate") 18 | public class CourseAggregateController { 19 | 20 | private static final Logger logger = LoggerFactory.getLogger(CourseAggregateController.class); 21 | 22 | 23 | private final CourseCompositeIntegration integration; 24 | //private final NetworkUtility utility; 25 | 26 | public CourseAggregateController(CourseCompositeIntegration integration) { 27 | this.integration = integration; 28 | } 29 | 30 | @GetMapping("/{id}/with-details") 31 | public Mono getCourses(@PathVariable Long id, @AuthenticationPrincipal Jwt jwt) { 32 | logger.info("Fetching course and review details for course id ===> {}", id); 33 | return integration.getCourseDetails(id, jwt); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/web/CourseCompositeIntegration.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course.web; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 7 | import org.springframework.security.oauth2.jwt.Jwt; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.reactive.function.client.WebClient; 10 | import reactor.core.publisher.Mono; 11 | 12 | import java.util.List; 13 | 14 | @Service 15 | public class CourseCompositeIntegration { 16 | 17 | private static final Logger logger = LoggerFactory.getLogger(CourseCompositeIntegration.class); 18 | 19 | private final String courseServiceUrl; 20 | private final String reviewServiceUrl; 21 | private final WebClient webClient; 22 | 23 | public CourseCompositeIntegration( 24 | @Value("${app.course-service.uri}") String courseServiceUrl, 25 | @Value("${app.review-service.uri}") String reviewServiceUrl, 26 | WebClient.Builder webClient 27 | ) { 28 | this.webClient = webClient.build(); 29 | this.courseServiceUrl = courseServiceUrl; 30 | this.reviewServiceUrl = reviewServiceUrl; 31 | } 32 | 33 | public Mono getCourseDetails(Long id, Jwt jwt) { 34 | logger.debug("JWT ===> {}", jwt.getTokenValue()); 35 | String courseUrl = courseServiceUrl + "/api/courses/" + id; 36 | String reviewUrl = reviewServiceUrl + "/api/reviews?course=" + id; 37 | logger.debug("Course URL ===> {}", courseUrl); 38 | logger.debug("Review URL ===> {}", reviewUrl); 39 | Mono courseMono = webClient.get() 40 | .uri(courseUrl) 41 | .header("Authorization", "Bearer " + jwt.getTokenValue()) 42 | .retrieve() 43 | .bodyToMono(Course.class); 44 | 45 | Mono> reviewsMono = webClient.get() 46 | .uri(reviewUrl) 47 | .header("Authorization", "Bearer " + jwt.getTokenValue()) 48 | .retrieve() 49 | .bodyToFlux(Review.class) 50 | .collectList(); 51 | 52 | return Mono.zip(courseMono, reviewsMono) 53 | .map(tuple -> CourseAggregate 54 | .builder() 55 | .course(tuple.getT1()) 56 | .reviews(tuple.getT2()) 57 | .build()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/web/Review.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course.web; 2 | 3 | import lombok.Data; 4 | import lombok.Builder; 5 | 6 | @Data 7 | @Builder 8 | public class Review { 9 | 10 | private String id; 11 | private int courseId; 12 | private String author; 13 | private String content; 14 | private String email; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/web/Vote.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course.web; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | @Data 9 | @Builder 10 | public class Vote { 11 | private AtomicInteger likes; 12 | private AtomicInteger dislikes; 13 | } 14 | -------------------------------------------------------------------------------- /microservices/course-composite-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | jwk-set-uri: ${APP_KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs} 3 | course-service: 4 | uri: ${COURSE_SERVICE_URL:http://localhost:9001} 5 | 6 | review-service: 7 | uri: ${REVIEW_SERVICE_URL:http://localhost:9002} 8 | spring: 9 | application: 10 | name: course-composite-service 11 | server: 12 | port: 5000 13 | logging: 14 | level: 15 | root: INFO 16 | io.javatab.microservices.composite.course: DEBUG 17 | pattern: 18 | console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{trace_id:-N/A}] [%X{span_id:-N/A}] %-5level %logger{36} - %msg%n" 19 | 20 | 21 | # Security related properties 22 | security: 23 | oauth2: 24 | resourceserver: 25 | jwt: 26 | issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:8081/realms/course-management-realm} 27 | jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs} 28 | 29 | management: 30 | endpoints: 31 | web: 32 | exposure: 33 | include: "health,info,metrics,prometheus" 34 | metrics: 35 | export: 36 | prometheus: 37 | enabled: true 38 | tags: 39 | application: ${spring.application.name} 40 | uri: "http_server_requests" 41 | endpoint: 42 | metrics: 43 | enabled: true 44 | prometheus: 45 | enabled: true 46 | health: 47 | show-details: always 48 | distribution: 49 | percentiles-histogram: # enable histogram for percentile 50 | http.server.requests: true 51 | slo: 52 | http.server.requests: 10ms, 50ms, 100ms, 500ms, 1s 53 | 54 | --- 55 | 56 | spring: 57 | config: 58 | activate: 59 | on-profile: docker 60 | security: 61 | oauth2: 62 | resourceserver: 63 | jwt: 64 | issuer-uri: http://keycloak:8080/realms/course-management-realm 65 | jwk-set-uri: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 66 | server: 67 | port: 8080 68 | app: 69 | jwk-set-uri: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 70 | course-service: 71 | uri: http://course:8080 72 | review-service: 73 | uri: http://review:8080 -------------------------------------------------------------------------------- /microservices/course-composite-service/src/test/java/io/javatab/microservices/composite/course/CourseCompositeServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.composite.course; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class CourseCompositeServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /microservices/course-service/.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /microservices/course-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /microservices/course-service/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /microservices/course-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17.0.5_8-jre-focal as builder 2 | WORKDIR extracted 3 | 4 | ADD ./target/*.jar app.jar 5 | 6 | RUN java -Djarmode=layertools -jar app.jar extract 7 | 8 | FROM eclipse-temurin:17.0.5_8-jre-focal 9 | WORKDIR application 10 | 11 | # Copy the dependencies layer from the builder stage 12 | COPY --from=builder extracted/dependencies/ ./ 13 | # Copy the Spring Boot loader layer from the builder stage 14 | COPY --from=builder extracted/spring-boot-loader/ ./ 15 | # Copy the snapshot dependencies layer from the builder stage 16 | COPY --from=builder extracted/snapshot-dependencies/ ./ 17 | # Copy the application layer from the builder stage 18 | COPY --from=builder extracted/application/ ./ 19 | 20 | # Expose port 8080 21 | EXPOSE 8080 22 | 23 | # Set the entry point to launch the application 24 | ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] 25 | 26 | -------------------------------------------------------------------------------- /microservices/course-service/kubernetes/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: course-service 5 | labels: 6 | app: course-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: course-service 12 | template: 13 | metadata: 14 | labels: 15 | app: course-service 16 | spec: 17 | containers: 18 | - name: course-service 19 | image: course-service 20 | imagePullPolicy: IfNotPresent 21 | lifecycle: 22 | preStop: 23 | exec: 24 | command: [ "sh", "-c", "sleep 5" ] 25 | ports: 26 | - containerPort: 9001 27 | env: 28 | - name: SPRING_DATASOURCE_URL 29 | value: jdbc:postgresql://course-postgres/course_db 30 | - name: APP_KEYCLOAK_JWK_SET_URI 31 | value: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 32 | - name: KEYCLOAK_ISSUER_URI 33 | value: http://keycloak:8080/realms/course-management-realm 34 | - name: KEYCLOAK_JWK_SET_URI 35 | value: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs -------------------------------------------------------------------------------- /microservices/course-service/kubernetes/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: course-service 5 | labels: 6 | app: course-service 7 | spec: 8 | type: ClusterIP 9 | selector: 10 | app: course-service 11 | ports: 12 | - protocol: TCP 13 | port: 80 14 | targetPort: 9001 -------------------------------------------------------------------------------- /microservices/course-service/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /microservices/course-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.3 9 | 10 | 11 | io.javatab.microservices.core.course 12 | course-service 13 | 1.0.0 14 | course-service 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 17 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-actuator 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-webflux 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-data-jpa 45 | 46 | 47 | 48 | org.postgresql 49 | postgresql 50 | runtime 51 | 52 | 53 | 54 | org.flywaydb 55 | flyway-database-postgresql 56 | 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-validation 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-security 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-starter-oauth2-resource-server 72 | 73 | 74 | 75 | io.javatab.util 76 | util 77 | 1.0.0 78 | compile 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-starter-actuator 84 | 85 | 86 | io.micrometer 87 | micrometer-registry-prometheus 88 | 89 | 90 | io.opentelemetry.javaagent 91 | opentelemetry-javaagent 92 | 2.13.3 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-starter-test 97 | test 98 | 99 | 100 | io.projectreactor 101 | reactor-test 102 | test 103 | 104 | 105 | 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-maven-plugin 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/CourseServiceApplication.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course; 2 | 3 | 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.ConfigurableApplicationContext; 9 | import org.springframework.context.annotation.ComponentScan; 10 | 11 | 12 | @SpringBootApplication 13 | @ComponentScan({"io.javatab"}) 14 | public class CourseServiceApplication { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(CourseServiceApplication.class); 17 | 18 | public static void main(String[] args) { 19 | ConfigurableApplicationContext ctx = SpringApplication.run(CourseServiceApplication.class, args); 20 | 21 | String postgresUri = ctx.getEnvironment().getProperty("spring.datasource.url"); 22 | LOG.info("Connected to Postgres: " + postgresUri); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/config/DatabaseConfig.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @Configuration 7 | @EnableJpaAuditing 8 | public class DatabaseConfig { 9 | } -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.convert.converter.Converter; 9 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 10 | import org.springframework.security.config.web.server.SecurityWebFiltersOrder; 11 | import org.springframework.security.config.web.server.ServerHttpSecurity; 12 | import org.springframework.security.core.GrantedAuthority; 13 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 14 | import org.springframework.security.oauth2.jwt.Jwt; 15 | import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; 16 | import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; 17 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 18 | import org.springframework.security.web.server.SecurityWebFilterChain; 19 | import org.springframework.web.server.ServerWebExchange; 20 | import reactor.core.publisher.Mono; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Collection; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | @Configuration 28 | @EnableWebFluxSecurity 29 | public class SecurityConfig { 30 | 31 | private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); 32 | 33 | private String jwkSetUri; 34 | 35 | public SecurityConfig(@Value("${app.jwk-set-uri}") String jwkSetUri) { 36 | this.jwkSetUri = jwkSetUri; 37 | } 38 | 39 | @Bean 40 | public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { 41 | http 42 | .authorizeExchange(exchanges -> exchanges 43 | .pathMatchers("/actuator/**").permitAll() 44 | .pathMatchers("/api/courses/**").hasAnyRole("COURSE-READ", "COURSE-WRITE") 45 | .anyExchange().authenticated() 46 | ) 47 | .oauth2ResourceServer(oauth2 -> oauth2 48 | .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())) 49 | ); 50 | 51 | // Add filter to log roles 52 | http.addFilterAt((exchange, chain) -> logRoles(exchange).then(chain.filter(exchange)), 53 | SecurityWebFiltersOrder.AUTHORIZATION); 54 | 55 | return http.build(); 56 | } 57 | 58 | @Bean 59 | public ReactiveJwtDecoder jwtDecoder() { 60 | return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); 61 | } 62 | 63 | @Bean 64 | public Converter> grantedAuthoritiesExtractor() { 65 | return new Converter>() { 66 | @Override 67 | public Mono convert(Jwt jwt) { 68 | Collection authorities = new ArrayList<>(); 69 | 70 | // Extract realm roles 71 | Map realmAccess = jwt.getClaim("realm_access"); 72 | if (realmAccess != null && realmAccess.containsKey("roles")) { 73 | List roles = (List) realmAccess.get("roles"); 74 | authorities.addAll(roles.stream() 75 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 76 | .toList()); 77 | } 78 | 79 | Map resourceAccess = jwt.getClaim("resource_access"); 80 | if (resourceAccess != null) { 81 | resourceAccess.forEach((resource, access) -> { 82 | if (access instanceof Map) { 83 | Map clientRoles = (Map) access; 84 | if (clientRoles.containsKey("roles")) { 85 | List roles = (List) clientRoles.get("roles"); 86 | authorities.addAll(roles.stream() 87 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 88 | .toList()); 89 | } 90 | } 91 | }); 92 | } 93 | 94 | return Mono.just(new JwtAuthenticationToken(jwt, authorities)); 95 | } 96 | }; 97 | } 98 | 99 | private Mono logRoles(ServerWebExchange exchange) { 100 | return exchange.getPrincipal() 101 | .cast(JwtAuthenticationToken.class) 102 | .doOnNext(jwtAuth -> { 103 | Collection authorities = jwtAuth.getAuthorities(); 104 | logger.info("Roles in Resource Server: {}", authorities); 105 | }) 106 | .then(); 107 | } 108 | } -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/domain/Course.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.domain; 2 | 3 | 4 | import jakarta.persistence.*; 5 | import jakarta.validation.constraints.*; 6 | import org.springframework.data.annotation.CreatedDate; 7 | import org.springframework.data.annotation.LastModifiedDate; 8 | 9 | import java.time.Instant; 10 | import java.util.Objects; 11 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 12 | 13 | @Entity 14 | @EntityListeners(AuditingEntityListener.class) 15 | public class Course { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long id; 20 | 21 | @NotBlank(message = "The book title must be defined.") 22 | private String title; 23 | 24 | @NotBlank(message = "The book author must be defined.") 25 | private String author; 26 | 27 | @NotNull(message = "The book price must be defined.") 28 | @Positive(message = "The book price must be greater than zero.") 29 | private Double price; 30 | 31 | private String publisher; 32 | 33 | @CreatedDate 34 | private Instant createdDate; 35 | 36 | @LastModifiedDate 37 | private Instant lastModifiedDate; 38 | 39 | @Version 40 | private int version; 41 | 42 | public Course() {} 43 | 44 | public Course(Long id, String title, String author, Double price, String publisher, Instant createdDate, Instant lastModifiedDate, int version) { 45 | this.id = id; 46 | this.title = title; 47 | this.author = author; 48 | this.price = price; 49 | this.publisher = publisher; 50 | this.createdDate = createdDate; 51 | this.lastModifiedDate = lastModifiedDate; 52 | this.version = version; 53 | } 54 | 55 | public static Course of(String title, String author, Double price, String publisher) { 56 | return new Course(null, title, author, price, publisher, null, null, 0); 57 | } 58 | 59 | public Long getId() { 60 | return id; 61 | } 62 | 63 | public void setId(Long id) { 64 | this.id = id; 65 | } 66 | 67 | public String getTitle() { 68 | return title; 69 | } 70 | 71 | public void setTitle(String title) { 72 | this.title = title; 73 | } 74 | 75 | public String getAuthor() { 76 | return author; 77 | } 78 | 79 | public void setAuthor(String author) { 80 | this.author = author; 81 | } 82 | 83 | public Double getPrice() { 84 | return price; 85 | } 86 | 87 | public void setPrice(Double price) { 88 | this.price = price; 89 | } 90 | 91 | public String getPublisher() { 92 | return publisher; 93 | } 94 | 95 | public void setPublisher(String publisher) { 96 | this.publisher = publisher; 97 | } 98 | 99 | public Instant getCreatedDate() { 100 | return createdDate; 101 | } 102 | 103 | public void setCreatedDate(Instant createdDate) { 104 | this.createdDate = createdDate; 105 | } 106 | 107 | public Instant getLastModifiedDate() { 108 | return lastModifiedDate; 109 | } 110 | 111 | public void setLastModifiedDate(Instant lastModifiedDate) { 112 | this.lastModifiedDate = lastModifiedDate; 113 | } 114 | 115 | public int getVersion() { 116 | return version; 117 | } 118 | 119 | public void setVersion(int version) { 120 | this.version = version; 121 | } 122 | 123 | @Override 124 | public boolean equals(Object o) { 125 | if (o == null || getClass() != o.getClass()) return false; 126 | Course course = (Course) o; 127 | return getVersion() == course.getVersion() && Objects.equals(getId(), course.getId()) && Objects.equals(getTitle(), course.getTitle()) && Objects.equals(getAuthor(), course.getAuthor()) && Objects.equals(getPrice(), course.getPrice()) && Objects.equals(getPublisher(), course.getPublisher()) && Objects.equals(getCreatedDate(), course.getCreatedDate()) && Objects.equals(getLastModifiedDate(), course.getLastModifiedDate()); 128 | } 129 | 130 | @Override 131 | public int hashCode() { 132 | return Objects.hash(getId(), getTitle(), getAuthor(), getPrice(), getPublisher(), getCreatedDate(), getLastModifiedDate(), getVersion()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/domain/CourseAlreadyExitsException.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.domain; 2 | 3 | public class CourseAlreadyExitsException extends RuntimeException { 4 | public CourseAlreadyExitsException(String title) { 5 | super("A course with title " + title + " already exists."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/domain/CourseNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.domain; 2 | 3 | public class CourseNotFoundException extends RuntimeException { 4 | public CourseNotFoundException(String title) { 5 | super("The course with title " + title + " was not found."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/domain/CourseRepository.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.domain; 2 | 3 | import jakarta.transaction.Transactional; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface CourseRepository extends CrudRepository { 9 | 10 | Optional findByTitle(String title); 11 | Optional findById(Long id); 12 | boolean existsByTitle(String title); 13 | 14 | 15 | @Transactional 16 | void deleteById(Long id); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/domain/CourseService.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.domain; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public class CourseService { 9 | 10 | private final Logger logger = LoggerFactory.getLogger(CourseService.class); 11 | 12 | private final CourseRepository courseRepository; 13 | 14 | public CourseService(CourseRepository courseRepository) { 15 | this.courseRepository = courseRepository; 16 | } 17 | 18 | public Iterable viewCourses() { 19 | return courseRepository.findAll(); 20 | } 21 | 22 | public Course viewCourseDetails(String title) { 23 | return courseRepository.findByTitle(title) 24 | .orElseThrow(() -> new CourseNotFoundException(title)); 25 | } 26 | 27 | public Course viewCourseDetailsById(Long id) { 28 | return courseRepository.findById(id) 29 | .orElseThrow(() -> new CourseNotFoundException(String.valueOf(id))); 30 | } 31 | 32 | public Course addCourse(Course course) { 33 | 34 | logger.info("Checking if course '{}' already exists...", course.getTitle()); 35 | 36 | if (courseRepository.existsByTitle(course.getTitle())) { 37 | logger.warn("Course '{}' already exists! Throwing exception.", course.getTitle()); 38 | throw new CourseAlreadyExitsException(course.getTitle()); 39 | } 40 | 41 | Course savedCourse = courseRepository.save(course); 42 | logger.info("Course '{}' saved successfully with ID: {}", savedCourse.getTitle(), savedCourse.getId()); 43 | 44 | return savedCourse; 45 | } 46 | 47 | public void removeCourse(Long id) { 48 | courseRepository.deleteById(id); 49 | } 50 | 51 | public Course editCourseDetails(Long id, Course course) { 52 | return courseRepository.findById(id) 53 | .map(existingCourse -> { 54 | existingCourse.setTitle(course.getTitle()); 55 | existingCourse.setAuthor(course.getAuthor()); 56 | existingCourse.setPrice(course.getPrice()); 57 | existingCourse.setPublisher(course.getPublisher()); 58 | return courseRepository.save(existingCourse); 59 | }).orElseGet(() -> addCourse(course)); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/web/CourseController.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.web; 2 | 3 | import io.javatab.microservices.core.course.domain.Course; 4 | import io.javatab.microservices.core.course.domain.CourseService; 5 | import io.javatab.util.http.NetworkUtility; 6 | import jakarta.validation.Valid; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | @RestController() 13 | @RequestMapping("/api/courses") 14 | public class CourseController { 15 | 16 | private final Logger logger = LoggerFactory.getLogger(CourseController.class); 17 | 18 | private final NetworkUtility utility; 19 | private CourseService courseService; 20 | 21 | public CourseController(NetworkUtility utility, CourseService courseService) { 22 | this.utility = utility; 23 | this.courseService = courseService; 24 | } 25 | 26 | @GetMapping 27 | public Iterable get() { 28 | logger.info("Fetching courses"); 29 | return courseService.viewCourses(); 30 | } 31 | 32 | /* 33 | * Make sure application is running in localhost mode to test and not in docker 34 | * http GET ':9001/api/courses/Microservices with Spring Boot' 35 | * */ 36 | @GetMapping("/title/{title}") 37 | public Course getByTitle(@PathVariable String title) { 38 | return courseService.viewCourseDetails(title); 39 | } 40 | 41 | @GetMapping("/{id}") 42 | public Course getById(@PathVariable Long id) { 43 | return courseService.viewCourseDetailsById(id); 44 | } 45 | 46 | /* 47 | * http POST :9001/api/courses title="Microservices with Spring Boot" author="John Doe" price:=29.79 publisher="GitHub" 48 | * http POST :9001/api/courses title="Spring Boot in Action" author="John Doe" price:=69.45 publisher="GitHub" 49 | * */ 50 | @PostMapping 51 | @ResponseStatus(HttpStatus.CREATED) 52 | public Course post(@Valid @RequestBody Course course) { 53 | logger.info("Received request to create course: {}", course.getTitle()); 54 | Course savedCourse = courseService.addCourse(course); 55 | if (savedCourse.getId() == null) { 56 | logger.error("Course was not saved correctly! ID is null."); 57 | throw new IllegalStateException("Failed to save course, ID is null!"); 58 | } 59 | logger.info("Course created successfully with ID: {}", savedCourse.getId()); 60 | return savedCourse; 61 | } 62 | 63 | @DeleteMapping("/{id}") 64 | @ResponseStatus(HttpStatus.NO_CONTENT) 65 | public void delete(@PathVariable Long id) { 66 | courseService.removeCourse(id); 67 | } 68 | 69 | @PutMapping("/{id}") 70 | public Course put(@PathVariable Long id, @Valid @RequestBody Course course) { 71 | return courseService.editCourseDetails(id, course); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /microservices/course-service/src/main/java/io/javatab/microservices/core/course/web/CourseControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course.web; 2 | 3 | import io.javatab.microservices.core.course.domain.CourseAlreadyExitsException; 4 | import io.javatab.microservices.core.course.domain.CourseNotFoundException; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.validation.FieldError; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RestControllerAdvice; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | @RestControllerAdvice 16 | public class CourseControllerAdvice { 17 | 18 | @ExceptionHandler(CourseNotFoundException.class) 19 | @ResponseStatus(HttpStatus.NOT_FOUND) 20 | String courseNotFoundHandler(CourseNotFoundException ex) { 21 | return ex.getMessage(); 22 | } 23 | 24 | @ExceptionHandler(CourseAlreadyExitsException.class) 25 | @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) 26 | String courseAlreadyExistsHandler(CourseAlreadyExitsException ex) { 27 | return ex.getMessage(); 28 | } 29 | 30 | @ExceptionHandler(MethodArgumentNotValidException.class) 31 | @ResponseStatus(HttpStatus.BAD_REQUEST) 32 | public Map handleValidationExceptions(MethodArgumentNotValidException ex) { 33 | Map errorsMap = new HashMap<>(); 34 | ex.getBindingResult().getAllErrors().forEach(error -> { 35 | String fieldName = ((FieldError) error).getField(); 36 | String errorMessage = error.getDefaultMessage(); 37 | errorsMap.put(fieldName, errorMessage); 38 | }); 39 | return errorsMap; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /microservices/course-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | jwk-set-uri: ${APP_KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs} 3 | server: 4 | port: 9001 5 | spring: 6 | application: 7 | name: course-service 8 | datasource: 9 | driver-class-name: org.postgresql.Driver 10 | username: user 11 | password: pwd 12 | url: jdbc:postgresql://localhost:5432/course_db 13 | hikari: 14 | connection-timeout: 2000 15 | maximum-pool-size: 5 #https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing 16 | jpa: 17 | database-platform: org.hibernate.dialect.PostgreSQLDialect 18 | sql: 19 | init: 20 | mode: always 21 | # flyway: # Only in dev env. Never have them in prod 22 | # validate-on-migrate: false 23 | # outOfOrder: true 24 | security: 25 | oauth2: 26 | resourceserver: 27 | jwt: 28 | issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:8081/realms/course-management-realm} 29 | jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs} 30 | logging: 31 | level: 32 | root: INFO 33 | io.javatab.microservices.core.course: DEBUG 34 | pattern: 35 | console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{trace_id:-N/A}] [%X{span_id:-N/A}] %-5level %logger{36} - %msg%n" 36 | 37 | management: 38 | endpoints: 39 | web: 40 | exposure: 41 | include: "health,info,metrics,prometheus" 42 | metrics: 43 | export: 44 | prometheus: 45 | enabled: true 46 | tags: 47 | application: ${spring.application.name} 48 | uri: "http_server_requests" 49 | endpoint: 50 | metrics: 51 | enabled: true 52 | prometheus: 53 | enabled: true 54 | health: 55 | show-details: always 56 | distribution: 57 | percentiles-histogram: # enable histogram for percentile 58 | http.server.requests: true 59 | slo: 60 | http.server.requests: 10ms, 50ms, 100ms, 500ms, 1s 61 | --- 62 | app: 63 | jwk-set-uri: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 64 | spring: 65 | config: 66 | activate: 67 | on-profile: docker 68 | datasource: 69 | url: jdbc:postgresql://postgres:5432/course_db 70 | security: 71 | oauth2: 72 | resourceserver: 73 | jwt: 74 | issuer-uri: http://keycloak:8080/realms/course-management-realm 75 | jwk-set-uri: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 76 | server: 77 | port: 8080 -------------------------------------------------------------------------------- /microservices/course-service/src/main/resources/db/migration/V1__Initial_schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS course; 2 | CREATE TABLE course ( 3 | id BIGSERIAL PRIMARY KEY NOT NULL, 4 | author varchar(255) NOT NULL, 5 | price float8 NOT NULL, 6 | title varchar(255) NOT NULL, 7 | publisher varchar(255) NOT NULL, 8 | created_date timestamp NOT NULL, 9 | last_modified_date timestamp NOT NULL, 10 | version integer NOT NULL 11 | ); -------------------------------------------------------------------------------- /microservices/course-service/src/test/java/io/javatab/microservices/core/course/CourseServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.course; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class CourseServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /microservices/review-service/.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /microservices/review-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /microservices/review-service/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /microservices/review-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17.0.5_8-jre-focal as builder 2 | WORKDIR extracted 3 | 4 | ADD ./target/*.jar app.jar 5 | 6 | RUN java -Djarmode=layertools -jar app.jar extract 7 | 8 | FROM eclipse-temurin:17.0.5_8-jre-focal 9 | WORKDIR application 10 | 11 | # Copy the dependencies layer from the builder stage 12 | COPY --from=builder extracted/dependencies/ ./ 13 | # Copy the Spring Boot loader layer from the builder stage 14 | COPY --from=builder extracted/spring-boot-loader/ ./ 15 | # Copy the snapshot dependencies layer from the builder stage 16 | COPY --from=builder extracted/snapshot-dependencies/ ./ 17 | # Copy the application layer from the builder stage 18 | COPY --from=builder extracted/application/ ./ 19 | 20 | # Expose port 8080 21 | EXPOSE 8080 22 | 23 | # Set the entry point to launch the application 24 | ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] 25 | 26 | -------------------------------------------------------------------------------- /microservices/review-service/Tiltfile: -------------------------------------------------------------------------------- 1 | # Build the container image using Docker 2 | docker_build( 3 | ref = "review-service", 4 | context = ".", 5 | dockerfile = "Dockerfile" 6 | ) 7 | 8 | # Deploy Kubernetes resources 9 | k8s_yaml(["kubernetes/deployment.yml", "kubernetes/service.yml"]) 10 | 11 | # Manage and enable port forwarding 12 | k8s_resource("review-service", port_forwards=["9002"]) 13 | -------------------------------------------------------------------------------- /microservices/review-service/kubernetes/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: review-service 5 | labels: 6 | app: review-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: review-service 12 | template: 13 | metadata: 14 | labels: 15 | app: review-service 16 | spec: 17 | containers: 18 | - name: review-service 19 | image: review-service 20 | imagePullPolicy: IfNotPresent 21 | lifecycle: 22 | preStop: 23 | exec: 24 | command: [ "sh", "-c", "sleep 5" ] 25 | ports: 26 | - containerPort: 9002 27 | env: 28 | - name: SPRING_DATA_MONGODB_URI 29 | value: mongodb://review-mongodb:27017/review-db 30 | - name: APP_KEYCLOAK_JWK_SET_URI 31 | value: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 32 | - name: KEYCLOAK_ISSUER_URI 33 | value: http://keycloak:8080/realms/course-management-realm 34 | - name: KEYCLOAK_JWK_SET_URI 35 | value: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs -------------------------------------------------------------------------------- /microservices/review-service/kubernetes/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: review-service 5 | labels: 6 | app: review-service 7 | spec: 8 | type: ClusterIP 9 | selector: 10 | app: review-service 11 | ports: 12 | - protocol: TCP 13 | port: 80 14 | targetPort: 9002 -------------------------------------------------------------------------------- /microservices/review-service/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /microservices/review-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.3 9 | 10 | 11 | io.javatab.microservices.core.review 12 | review-service 13 | 1.0.0 14 | review-service 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 17 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-actuator 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-webflux 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-validation 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-security 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-oauth2-resource-server 54 | 55 | 56 | org.projectlombok 57 | lombok 58 | provided 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-data-mongodb 64 | 65 | 66 | 67 | io.javatab.util 68 | util 69 | 1.0.0 70 | compile 71 | 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-starter-actuator 76 | 77 | 78 | io.micrometer 79 | micrometer-registry-prometheus 80 | 81 | 82 | io.opentelemetry.javaagent 83 | opentelemetry-javaagent 84 | 2.13.3 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-test 89 | test 90 | 91 | 92 | io.projectreactor 93 | reactor-test 94 | test 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-maven-plugin 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/ReviewServiceApplication.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.ConfigurableApplicationContext; 8 | import org.springframework.context.annotation.ComponentScan; 9 | 10 | @SpringBootApplication 11 | @ComponentScan({"io.javatab"}) 12 | public class ReviewServiceApplication { 13 | 14 | private static final Logger logger = LoggerFactory.getLogger(ReviewServiceApplication.class); 15 | 16 | public static void main(String[] args) { 17 | ConfigurableApplicationContext ctx = SpringApplication.run(ReviewServiceApplication.class, args); 18 | 19 | String mongoDbUri = ctx.getEnvironment().getProperty("spring.data.mongodb.uri"); 20 | logger.info("Connected to MongoDb ===> {}", mongoDbUri); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.convert.converter.Converter; 9 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 10 | import org.springframework.security.config.web.server.SecurityWebFiltersOrder; 11 | import org.springframework.security.config.web.server.ServerHttpSecurity; 12 | import org.springframework.security.core.GrantedAuthority; 13 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 14 | import org.springframework.security.oauth2.jwt.Jwt; 15 | import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; 16 | import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; 17 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 18 | import org.springframework.security.web.server.SecurityWebFilterChain; 19 | import org.springframework.web.server.ServerWebExchange; 20 | import reactor.core.publisher.Mono; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Collection; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | @Configuration 28 | @EnableWebFluxSecurity 29 | public class SecurityConfig { 30 | 31 | private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); 32 | 33 | private String jwkSetUri; 34 | 35 | public SecurityConfig(@Value("${app.jwk-set-uri}") String jwkSetUri) { 36 | this.jwkSetUri = jwkSetUri; 37 | } 38 | 39 | @Bean 40 | public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { 41 | http 42 | .authorizeExchange(exchanges -> exchanges 43 | .pathMatchers("/actuator/**").permitAll() 44 | .pathMatchers("/api/reviews/**").hasAnyRole("REVIEW-READ", "REVIEW-WRITE") 45 | .anyExchange().authenticated() 46 | ) 47 | .oauth2ResourceServer(oauth2 -> oauth2 48 | .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())) 49 | ); 50 | 51 | // Add filter to log roles 52 | http.addFilterAt((exchange, chain) -> logRoles(exchange).then(chain.filter(exchange)), 53 | SecurityWebFiltersOrder.AUTHORIZATION); 54 | 55 | return http.build(); 56 | } 57 | 58 | @Bean 59 | public ReactiveJwtDecoder jwtDecoder() { 60 | return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); 61 | } 62 | 63 | @Bean 64 | public Converter> grantedAuthoritiesExtractor() { 65 | return new Converter>() { 66 | @Override 67 | public Mono convert(Jwt jwt) { 68 | Collection authorities = new ArrayList<>(); 69 | 70 | // Extract realm roles 71 | Map realmAccess = jwt.getClaim("realm_access"); 72 | if (realmAccess != null && realmAccess.containsKey("roles")) { 73 | List roles = (List) realmAccess.get("roles"); 74 | authorities.addAll(roles.stream() 75 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 76 | .toList()); 77 | } 78 | 79 | Map resourceAccess = jwt.getClaim("resource_access"); 80 | if (resourceAccess != null) { 81 | resourceAccess.forEach((resource, access) -> { 82 | if (access instanceof Map) { 83 | Map clientRoles = (Map) access; 84 | if (clientRoles.containsKey("roles")) { 85 | List roles = (List) clientRoles.get("roles"); 86 | authorities.addAll(roles.stream() 87 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 88 | .toList()); 89 | } 90 | } 91 | }); 92 | } 93 | 94 | return Mono.just(new JwtAuthenticationToken(jwt, authorities)); 95 | } 96 | }; 97 | } 98 | 99 | private Mono logRoles(ServerWebExchange exchange) { 100 | return exchange.getPrincipal() 101 | .cast(JwtAuthenticationToken.class) 102 | .doOnNext(jwtAuth -> { 103 | Collection authorities = jwtAuth.getAuthorities(); 104 | logger.info("Roles in Resource Server: {}", authorities); 105 | }) 106 | .then(); 107 | } 108 | } -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/domain/Review.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review.domain; 2 | 3 | import jakarta.validation.constraints.*; 4 | import lombok.*; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.annotation.Version; 7 | import org.springframework.data.mongodb.core.index.Indexed; 8 | import org.springframework.data.mongodb.core.mapping.Document; 9 | 10 | @Document(collection = "reviews") 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @Builder 16 | public class Review { 17 | 18 | @Id 19 | private String id; 20 | 21 | @Version 22 | private int version; 23 | private int courseId; 24 | private String author; 25 | private String content; 26 | @Indexed() 27 | private String email; 28 | 29 | } -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/domain/ReviewNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review.domain; 2 | 3 | public class ReviewNotFoundException extends RuntimeException { 4 | public ReviewNotFoundException(String id) { 5 | super("The review with id " + id + " was not found."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/domain/ReviewRepository.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review.domain; 2 | 3 | import org.springframework.data.mongodb.repository.MongoRepository; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public interface ReviewRepository extends MongoRepository { 9 | List findByCourseIdAndEmail(Long courseId, String email); 10 | List findByEmail(String email); 11 | List findByCourseId(Long courseId); 12 | Optional findById(String id); 13 | } 14 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/domain/ReviewService.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review.domain; 2 | 3 | import io.javatab.microservices.core.review.web.ReviewDTO; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import java.util.List; 10 | 11 | @Service 12 | public class ReviewService { 13 | 14 | private static final Logger logger = LoggerFactory.getLogger(ReviewService.class); 15 | private final ReviewRepository reviewRepository; 16 | 17 | public ReviewService(ReviewRepository reviewRepository) { 18 | this.reviewRepository = reviewRepository; 19 | } 20 | 21 | @Transactional 22 | public Review addReview(ReviewDTO reviewDto) { 23 | logger.info("Adding new review with email: {}", reviewDto.getEmail()); 24 | Review aReview = Review.builder() 25 | .courseId(reviewDto.getCourseId()) 26 | .author(reviewDto.getAuthor()) 27 | .content(reviewDto.getContent()) 28 | .email(reviewDto.getEmail()) 29 | .build(); 30 | return reviewRepository.save(aReview); 31 | } 32 | 33 | @Transactional(readOnly = true) 34 | public List getAllReviews() { 35 | logger.info("Fetching all reviews"); 36 | return reviewRepository.findAll(); 37 | } 38 | 39 | @Transactional(readOnly = true) 40 | public List getReviewsByEmail(String email) { 41 | logger.info("Fetching review with email: {}", email); 42 | return reviewRepository.findByEmail(email); 43 | } 44 | 45 | @Transactional(readOnly = true) 46 | public List getReviewsByCourseIdAndEmail(Long courseId, String email) { 47 | logger.info("Fetching review with course Id: {} and by email {}", courseId, email); 48 | return reviewRepository.findByCourseIdAndEmail(courseId, email); 49 | } 50 | 51 | @Transactional(readOnly = true) 52 | public List getReviewsByCourseId(Long courseId) { 53 | logger.info("Fetching review with course Id : {}", courseId); 54 | return reviewRepository.findByCourseId(courseId); 55 | } 56 | 57 | @Transactional 58 | public void deleteReview(String id) { 59 | logger.info("Deleting review with id: {}", id); 60 | if (!reviewRepository.existsById(id)) { 61 | throw new ReviewNotFoundException("Review not found with id: " + id); 62 | } 63 | reviewRepository.deleteById(id); 64 | } 65 | 66 | @Transactional 67 | public Review getReviewId(String id) { 68 | logger.info("Fetching review with id: {}", id); 69 | return reviewRepository.findById(id) 70 | .orElseThrow(() -> new ReviewNotFoundException(id)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/web/ReviewController.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review.web; 2 | 3 | import io.javatab.microservices.core.review.domain.Review; 4 | import io.javatab.microservices.core.review.domain.ReviewService; 5 | 6 | import jakarta.validation.Valid; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.List; 14 | 15 | @RestController() 16 | @RequestMapping("/api/reviews") 17 | public class ReviewController { 18 | 19 | private static final Logger logger = LoggerFactory.getLogger(ReviewController.class); 20 | private final ReviewService reviewService; 21 | 22 | public ReviewController(ReviewService reviewService) { 23 | this.reviewService = reviewService; 24 | } 25 | 26 | 27 | /* 28 | * http POST :9002/api/reviews courseId:=1 author="John Doe" content="Amazing book" email="abc@xyz.com" 29 | * */ 30 | @PostMapping 31 | public ResponseEntity addReview(@Valid @RequestBody ReviewDTO review) { 32 | logger.info("Received request to add review to course id {} by email: {} and ", review.getCourseId(), review.getEmail()); 33 | Review addedReview = reviewService.addReview(review); 34 | return new ResponseEntity<>(addedReview, HttpStatus.CREATED); 35 | } 36 | 37 | @GetMapping 38 | public ResponseEntity> getAllReviews() { 39 | logger.info("Received request to fetch all reviews"); 40 | return ResponseEntity.ok(reviewService.getAllReviews()); 41 | } 42 | 43 | @GetMapping("/{id}") 44 | public ResponseEntity getReviewById(@PathVariable String id) { 45 | logger.info("Received request to fetch review with id: {}", id); 46 | return ResponseEntity.ok(reviewService.getReviewId(id)); 47 | } 48 | 49 | @GetMapping(params = {"course"}) 50 | public ResponseEntity> getReviewByCourseId(@RequestParam("course") Long courseId) { 51 | logger.info("Received request to fetch review with course id: {}", courseId); 52 | return ResponseEntity.ok(reviewService.getReviewsByCourseId(courseId)); 53 | } 54 | 55 | /* 56 | * http :9002/api/reviews courseId==1 email==abc@xyz.com 57 | * or 58 | * http GET "http://localhost:9002/api/reviews?courseId=1&email=abc@xyz.com" 59 | * */ 60 | @GetMapping(params = {"courseId", "email"}) 61 | public ResponseEntity> getReviewByCourseIdAndEmail(@RequestParam("courseId") Long courseId, @RequestParam("email") String email) { 62 | logger.info("Received request to fetch review with course id: {} and email : {}", courseId, email); 63 | return ResponseEntity.ok(reviewService.getReviewsByCourseIdAndEmail(courseId, email)); 64 | } 65 | 66 | @DeleteMapping("/{id}") 67 | public ResponseEntity deleteReview(@PathVariable String id) { 68 | logger.info("Received request to delete review with id: {}", id); 69 | reviewService.deleteReview(id); 70 | return ResponseEntity.noContent().build(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/web/ReviewControllerAdvice.java: -------------------------------------------------------------------------------- 1 | /* 2 | package io.javatab.microservices.core.review.web; 3 | 4 | import io.javatab.microservices.core.review.domain.ReviewNotFoundException; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | 11 | import java.util.stream.Collectors; 12 | 13 | @ControllerAdvice 14 | public class ReviewControllerAdvice { 15 | @ExceptionHandler(ReviewNotFoundException.class) 16 | public ResponseEntity handleNotFound(ReviewNotFoundException ex) { 17 | return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); 18 | } 19 | 20 | @ExceptionHandler(MethodArgumentNotValidException.class) 21 | public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { 22 | String errorMessage = ex.getBindingResult() 23 | .getFieldErrors() 24 | .stream() 25 | .map(error -> error.getField() + ": " + error.getDefaultMessage()) 26 | .collect(Collectors.joining(", ")); 27 | return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); 28 | } 29 | 30 | @ExceptionHandler(Exception.class) 31 | public ResponseEntity handleGenericException(Exception ex) { 32 | return new ResponseEntity<>("An unexpected error occurred", HttpStatus.INTERNAL_SERVER_ERROR); 33 | } 34 | } 35 | */ 36 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/web/ReviewDTO.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review.web; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | 10 | @Data 11 | @Builder 12 | public class ReviewDTO { 13 | 14 | @NotNull(message = "The course id must be defined.") 15 | private int courseId; 16 | @NotBlank(message = "Author is required") 17 | @Size(min = 4, max = 40, message = "Author must be between 4 and 40 characters") 18 | private String author; 19 | @NotBlank(message = "Content is required") 20 | @Size(min = 5, max = 500, message = "Content must be between 50 and 500 characters") 21 | private String content; 22 | @NotBlank(message = "Email is required") 23 | @Email(message = "Email should be valid") 24 | private String email; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/java/io/javatab/microservices/core/review/web/ReviewResponse.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review.web; 2 | 3 | 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | @Data 8 | @Builder 9 | public class ReviewResponse { 10 | private String id; 11 | // Add other response related object data 12 | } 13 | -------------------------------------------------------------------------------- /microservices/review-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | jwk-set-uri: ${APP_KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs} 3 | server: 4 | port: 9002 5 | error: 6 | include-message: always 7 | include-binding-errors: always 8 | spring: 9 | application: 10 | name: review-service 11 | data: 12 | mongodb: 13 | uri: mongodb://localhost:27017/review-db 14 | #host: localhost 15 | #port: 27017 16 | #database: review-db 17 | security: 18 | oauth2: 19 | resourceserver: 20 | jwt: 21 | issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:8081/realms/course-management-realm} 22 | jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs} 23 | logging: 24 | level: 25 | root: INFO 26 | io.javatab.microservices.composite.review: DEBUG 27 | pattern: 28 | console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{trace_id:-N/A}] [%X{span_id:-N/A}] %-5level %logger{36} - %msg%n" 29 | management: 30 | endpoints: 31 | web: 32 | exposure: 33 | include: "health,info,metrics,prometheus" 34 | metrics: 35 | export: 36 | prometheus: 37 | enabled: true 38 | tags: 39 | application: ${spring.application.name} 40 | uri: "http_server_requests" 41 | endpoint: 42 | metrics: 43 | enabled: true 44 | prometheus: 45 | enabled: true 46 | health: 47 | show-details: always 48 | distribution: 49 | percentiles-histogram: # enable histogram for percentile 50 | http.server.requests: true 51 | slo: 52 | http.server.requests: 10ms, 50ms, 100ms, 500ms, 1s 53 | 54 | --- 55 | app: 56 | jwk-set-uri: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 57 | spring: 58 | config: 59 | activate: 60 | on-profile: docker 61 | data: 62 | mongodb: 63 | uri: mongodb://mongodb:27017/review-db 64 | security: 65 | oauth2: 66 | resourceserver: 67 | jwt: 68 | issuer-uri: http://keycloak:8080/realms/course-management-realm 69 | jwk-set-uri: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 70 | server: 71 | port: 8080 -------------------------------------------------------------------------------- /microservices/review-service/src/test/java/io/javatab/microservices/core/review/ReviewServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.javatab.microservices.core.review; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ReviewServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /notes/archtecture.md: -------------------------------------------------------------------------------- 1 | # Course Management System 2 | 3 | 4 | This section describes the C4 model for the Course Management System, a Spring Boot microservices application with Course, Review, and CourseComposite services, deployed using Docker Compose and Kubernetes. 5 | 6 | ### Level 1: System Context Diagram 7 | **Description**: High-level view of the system and its external actors. 8 | 9 | - **System**: Course Management System 10 | - A microservices-based application for managing courses and reviews. 11 | - **Actors**: 12 | - **User**: Browses courses and submits reviews (via browser/API). 13 | - **Administrator**: Manages content and monitors system health. 14 | - **External Systems**: 15 | - **Keycloak**: Authentication and authorization (OIDC/OAuth2). 16 | - **Grafana Observability Stack**: Grafana, Loki, Tempo, Fluent-bit, and OpenTelemetry (Otel) for monitoring and logging. 17 | - **Interactions**: 18 | - User → System: HTTP requests via Gateway. 19 | - System → Keycloak: Authenticates users. 20 | - System → Grafana Stack: Sends observability data. 21 | 22 | ### Level 2: Container Diagram 23 | **Description**: Breaks the system into deployable units. 24 | 25 | - **Containers**: 26 | 1. **API Gateway (Spring Cloud Gateway)** 27 | - Tech: Spring Boot + Spring Cloud Gateway 28 | - Role: Routes requests, enforces security. 29 | - Interactions: User → Gateway → CourseComposite. 30 | 2. **CourseComposite (Aggregate Microservice)** 31 | - Tech: Spring Boot 32 | - Role: Aggregates Course and Review data. 33 | - Interactions: Gateway → CourseComposite → Course/Review. 34 | 3. **Course (Core Microservice)** 35 | - Tech: Spring Boot 36 | - Role: Manages course data. 37 | - Database: PostgreSQL 38 | - Interactions: CourseComposite → Course → PostgreSQL. 39 | 4. **Review (Core Microservice)** 40 | - Tech: Spring Boot 41 | - Role: Manages review data. 42 | - Database: MongoDB 43 | - Interactions: CourseComposite → Review → MongoDB. 44 | 5. **Keycloak** 45 | - Role: External auth provider. 46 | - Interactions: Gateway → Keycloak. 47 | 6. **Observability Stack**: 48 | - **Fluent-bit**: Log collection. 49 | - **OpenTelemetry (OTel)**: Trace. 50 | - **Loki**: Log storage. 51 | - **Tempo**: Trace storage. 52 | - **Grafana**: Visualization. 53 | - Interactions: Microservices → Fluent-bit/OTel → Loki/Tempo → Grafana. 54 | 55 | ### Level 3: Component Diagram 56 | **Description**: Key components within containers. 57 | 58 | - **API Gateway**: 59 | - Routing Component (Spring Cloud Gateway). 60 | - Security Component (Keycloak integration). 61 | - Observability Agent (OTel + Fluent-bit). 62 | - **CourseComposite**: 63 | - Course Client (REST). 64 | - Review Client (REST). 65 | - Aggregation Logic. 66 | - Observability Agent. 67 | - **Course**: 68 | - Course Controller (REST). 69 | - Course Service (Logic). 70 | - Course Repository (JPA/PostgreSQL). 71 | - Observability Agent. 72 | - **Review**: 73 | - Review Controller (REST). 74 | - Review Service (Logic). 75 | - Review Repository (MongoDB). 76 | - Observability Agent. 77 | 78 | ### Level 4: Deployment Notes 79 | **Description**: Two deployment setups. 80 | 81 | 1. **Docker Compose Setup**: 82 | - Containers: Gateway, CourseComposite, Course + PostgreSQL, Review + MongoDB, Keycloak, Observability Stack. 83 | - Networking: Single Docker network. 84 | - Config: `docker-compose.yml`. 85 | 86 | 2. **Kubernetes Setup with Tilt**: 87 | - Pods: Gateway, CourseComposite, Course + PostgreSQL, Review + MongoDB, Keycloak, Observability Pods. 88 | - Resources: Ingress, Services, ConfigMaps/Secrets, PVCs. 89 | - Tilt: Automates dev with live updates. 90 | - Observability: Fluent-bit DaemonSet 91 | 92 | ### Additional Notes 93 | - **Tech**: Spring Boot, Spring Cloud, JPA, MongoDB driver, OTel, Fluent-bit. 94 | - **Interactions**: REST/HTTP, JDBC, MongoDB protocol. 95 | - **Scalability**: Kubernetes supports replicas; Docker Compose for local dev. -------------------------------------------------------------------------------- /notes/images/component-level3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/component-level3.png -------------------------------------------------------------------------------- /notes/images/container-level2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/container-level2.png -------------------------------------------------------------------------------- /notes/images/context-level1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/context-level1.png -------------------------------------------------------------------------------- /notes/images/deployment-level4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/deployment-level4.png -------------------------------------------------------------------------------- /notes/images/jwt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/jwt.png -------------------------------------------------------------------------------- /notes/images/keycloak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/keycloak.png -------------------------------------------------------------------------------- /notes/images/loki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/loki.png -------------------------------------------------------------------------------- /notes/images/metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/metrics.png -------------------------------------------------------------------------------- /notes/images/observability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/observability.png -------------------------------------------------------------------------------- /notes/images/prometheus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/prometheus.png -------------------------------------------------------------------------------- /notes/images/statistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/statistic.png -------------------------------------------------------------------------------- /notes/images/tempo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/tempo1.png -------------------------------------------------------------------------------- /notes/images/tempo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/tempo2.png -------------------------------------------------------------------------------- /notes/images/tilt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nasruddin/spring-boot-based-microservices/ef86571249f2fca3f80f74e2b24db67629f64d1c/notes/images/tilt.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.javatab.microservices 8 | course-management-system 9 | 1.0.0 10 | pom 11 | 12 | course-management-system 13 | Parent Pom for the course management system project 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-parent 18 | 3.4.3 19 | 20 | 21 | 22 | util 23 | microservices/course-composite-service 24 | microservices/course-service 25 | microservices/review-service 26 | spring-cloud/gateway-service 27 | 28 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Kill services running on ports 5000, 9001, and 9002 4 | kill_service_on_port() { 5 | local port=$1 6 | local pid=$(lsof -ti :$port) 7 | if [[ -n "$pid" ]]; then 8 | echo "Killing process on port $port (PID: $pid)" 9 | kill -9 $pid 10 | fi 11 | } 12 | 13 | # Build all services before running them 14 | mvn clean package -DskipTests 15 | 16 | # Function to find and run the latest JAR file in a given service directory 17 | run_service() { 18 | kill_service_on_port 5000 19 | kill_service_on_port 9000 20 | kill_service_on_port 9001 21 | kill_service_on_port 9002 22 | local service_dir=$1 23 | local jar_file=$(find "microservices/$service_dir/target" -type f -name "*.jar" | head -n 1) 24 | 25 | if [[ -z "$jar_file" ]]; then 26 | echo "No JAR file found for $service_dir!" 27 | return 1 28 | fi 29 | 30 | echo "Starting $service_dir using $jar_file..." 31 | java -jar "$jar_file" & 32 | } 33 | 34 | # Check if the first argument is 'docker' 35 | if [[ "$1" == "docker" ]]; then 36 | echo "CD to docker dir..." 37 | cd docker 38 | echo "Stopping services using Docker Compose..." 39 | docker compose -f docker-compose-base.yml down -v 40 | echo "Starting services using Docker Compose..." 41 | docker compose -f docker-compose-base.yml up --build 42 | else 43 | echo "Starting services using local JAR files..." 44 | # Run each service in the background 45 | run_service "course-composite-service" 46 | run_service "course-service" 47 | run_service "review-service" 48 | 49 | # Wait for all background processes to finish 50 | wait 51 | 52 | echo "All services started successfully." 53 | fi 54 | -------------------------------------------------------------------------------- /spring-cloud/gateway-service/.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /spring-cloud/gateway-service/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /spring-cloud/gateway-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17.0.5_8-jre-focal as builder 2 | WORKDIR extracted 3 | 4 | ADD ./target/*.jar app.jar 5 | 6 | RUN java -Djarmode=layertools -jar app.jar extract 7 | 8 | FROM eclipse-temurin:17.0.5_8-jre-focal 9 | WORKDIR application 10 | 11 | # Copy the dependencies layer from the builder stage 12 | COPY --from=builder extracted/dependencies/ ./ 13 | # Copy the Spring Boot loader layer from the builder stage 14 | COPY --from=builder extracted/spring-boot-loader/ ./ 15 | # Copy the snapshot dependencies layer from the builder stage 16 | COPY --from=builder extracted/snapshot-dependencies/ ./ 17 | # Copy the application layer from the builder stage 18 | COPY --from=builder extracted/application/ ./ 19 | 20 | # Expose port 9000 21 | EXPOSE 9000 22 | 23 | # Set the entry point to launch the application 24 | ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] 25 | 26 | -------------------------------------------------------------------------------- /spring-cloud/gateway-service/kubernetes/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: gateway-service 5 | labels: 6 | app: gateway-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: gateway-service 12 | template: 13 | metadata: 14 | labels: 15 | app: gateway-service 16 | spec: 17 | containers: 18 | - name: gateway-service 19 | image: gateway-service 20 | imagePullPolicy: IfNotPresent 21 | lifecycle: 22 | preStop: 23 | exec: 24 | command: [ "sh", "-c", "sleep 5" ] 25 | ports: 26 | - containerPort: 9000 27 | env: 28 | - name: COURSE_SERVICE_URL 29 | value: http://course-service 30 | - name: REVIEW_SERVICE_URL 31 | value: http://review-service 32 | - name: COURSE_AGGREGATE_SERVICE_URL 33 | value: http://course-composite-service #there are service names in kubectl get svc 34 | - name: APP_KEYCLOAK_JWK_SET_URI 35 | value: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 36 | - name: KEYCLOAK_ISSUER_URI 37 | value: http://keycloak:8080/realms/course-management-realm 38 | - name: KEYCLOAK_JWK_SET_URI 39 | value: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs -------------------------------------------------------------------------------- /spring-cloud/gateway-service/kubernetes/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: gateway-ingress 5 | spec: 6 | ingressClassName: nginx 7 | rules: 8 | - http: 9 | paths: 10 | - path: / 11 | pathType: Prefix 12 | backend: 13 | service: 14 | name: gateway-service 15 | port: 16 | number: 80 -------------------------------------------------------------------------------- /spring-cloud/gateway-service/kubernetes/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: gateway-service 5 | labels: 6 | app: gateway-service 7 | spec: 8 | type: ClusterIP 9 | selector: 10 | app: gateway-service 11 | ports: 12 | - protocol: TCP 13 | port: 80 14 | targetPort: 9000 -------------------------------------------------------------------------------- /spring-cloud/gateway-service/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /spring-cloud/gateway-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.3 9 | 10 | 11 | com.javatab.springcloud 12 | gateway-service 13 | 1.0.0-SNAPSHOT 14 | gateway-service 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 17 31 | 2024.0.0 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-webflux 38 | 39 | 40 | 41 | 42 | org.springframework.cloud 43 | spring-cloud-starter-gateway 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-oauth2-resource-server 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-oauth2-client 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-actuator 60 | 61 | 62 | io.micrometer 63 | micrometer-registry-prometheus 64 | 65 | 66 | io.opentelemetry.javaagent 67 | opentelemetry-javaagent 68 | 2.13.3 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-starter-test 73 | test 74 | 75 | 76 | 77 | 78 | 79 | org.springframework.cloud 80 | spring-cloud-dependencies 81 | ${spring-cloud.version} 82 | pom 83 | import 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-maven-plugin 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /spring-cloud/gateway-service/src/main/java/com/example/springcloud/gateway/GatewayServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.springcloud.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class GatewayServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(GatewayServiceApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /spring-cloud/gateway-service/src/main/java/com/example/springcloud/gateway/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.springcloud.gateway.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.convert.converter.Converter; 7 | import org.springframework.security.authentication.AbstractAuthenticationToken; 8 | import org.springframework.security.config.Customizer; 9 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 10 | import org.springframework.security.config.web.server.ServerHttpSecurity; 11 | import org.springframework.security.core.GrantedAuthority; 12 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 13 | import org.springframework.security.oauth2.jwt.Jwt; 14 | import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; 15 | import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; 16 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 17 | import org.springframework.security.web.server.SecurityWebFilterChain; 18 | import reactor.core.publisher.Mono; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Collection; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | @Configuration 26 | @EnableWebFluxSecurity 27 | public class SecurityConfig { 28 | 29 | private String jwkSetUri; 30 | public SecurityConfig(@Value("${app.jwk-set-uri}") String jwkSetUri) { 31 | this.jwkSetUri = jwkSetUri; 32 | } 33 | 34 | @Bean 35 | public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { 36 | http 37 | .authorizeExchange(exchanges -> exchanges 38 | .pathMatchers("/api/public", "/actuator/**").permitAll() 39 | .anyExchange().authenticated() 40 | ) 41 | .oauth2ResourceServer(oauth2 -> oauth2 42 | .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())) 43 | ); 44 | 45 | return http.build(); 46 | } 47 | 48 | @Bean 49 | public ReactiveJwtDecoder jwtDecoder() { 50 | System.out.println("======== " + jwkSetUri); 51 | return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); 52 | } 53 | 54 | @Bean 55 | public Converter> grantedAuthoritiesExtractor() { 56 | return new Converter>() { 57 | @Override 58 | public Mono convert(Jwt jwt) { 59 | Collection authorities = new ArrayList<>(); 60 | 61 | // Extract realm roles 62 | Map realmAccess = jwt.getClaim("realm_access"); 63 | if (realmAccess != null && realmAccess.containsKey("roles")) { 64 | List roles = (List) realmAccess.get("roles"); 65 | authorities.addAll(roles.stream() 66 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 67 | .toList()); 68 | } 69 | 70 | Map resourceAccess = jwt.getClaim("resource_access"); 71 | if (resourceAccess != null) { 72 | resourceAccess.forEach((resource, access) -> { 73 | if (access instanceof Map) { 74 | Map clientRoles = (Map) access; 75 | if (clientRoles.containsKey("roles")) { 76 | List roles = (List) clientRoles.get("roles"); 77 | authorities.addAll(roles.stream() 78 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) 79 | .toList()); 80 | } 81 | } 82 | }); 83 | } 84 | return Mono.just(new JwtAuthenticationToken(jwt, authorities)); 85 | } 86 | }; 87 | } 88 | } -------------------------------------------------------------------------------- /spring-cloud/gateway-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | jwk-set-uri: ${APP_KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs} 3 | server: 4 | port: 9000 5 | netty: 6 | connection-timeouts: 2s 7 | idle-timeouts: 15s 8 | shutdown: graceful 9 | spring: 10 | application: 11 | name: gateway-service 12 | lifecycle: 13 | timeout-per-shutdown-phase: 15s 14 | 15 | cloud: 16 | gateway: 17 | routes: 18 | - id: course-service-route 19 | uri: ${COURSE_SERVICE_URL:http://localhost:9001} 20 | predicates: 21 | - Path=/courses/** 22 | filters: 23 | - RewritePath=/courses(?/.*|), /api/courses\${segment} 24 | 25 | - id: review-service-route 26 | uri: ${REVIEW_SERVICE_URL:http://localhost:9002} 27 | predicates: 28 | - Path=/reviews/** 29 | filters: 30 | - RewritePath=/reviews(?/.*|), /api/reviews\${segment} 31 | 32 | - id: course-aggregate-route 33 | uri: ${COURSE_AGGREGATE_SERVICE_URL:http://localhost:5000} 34 | predicates: 35 | - Path=/course-aggregate/** 36 | filters: 37 | - RewritePath=/course-aggregate(?/.*|), /api/course-aggregate\${segment} 38 | 39 | management: 40 | endpoints: 41 | web: 42 | exposure: 43 | include: "health,info,metrics,prometheus" 44 | metrics: 45 | export: 46 | prometheus: 47 | enabled: true 48 | tags: 49 | application: ${spring.application.name} 50 | uri: "http_server_requests" 51 | endpoint: 52 | gateway: 53 | enabled: true 54 | metrics: 55 | enabled: true 56 | prometheus: 57 | enabled: true 58 | health: 59 | show-details: always 60 | distribution: 61 | percentiles-histogram: # enable histogram for percentile 62 | http.server.requests: true 63 | slo: 64 | http.server.requests: 10ms, 50ms, 100ms, 500ms, 1s 65 | 66 | 67 | # Security 68 | security: 69 | oauth2: 70 | resourceserver: 71 | jwt: 72 | issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:8081/realms/course-management-realm} 73 | jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs} 74 | logging: 75 | level: 76 | root: INFO 77 | io.example.springcloud.gateway: DEBUG 78 | pattern: 79 | console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{trace_id:-N/A}] [%X{span_id:-N/A}] %-5level %logger{36} - %msg%n" 80 | --- 81 | app: 82 | jwk-set-uri: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 83 | spring: 84 | config: 85 | activate: 86 | on-profile: docker 87 | cloud: 88 | gateway: 89 | routes: 90 | - id: course-service-route 91 | uri: http://course:8080 92 | predicates: 93 | - Path=/courses/** 94 | filters: 95 | - RewritePath=/courses(?/.*|), /api/courses\${segment} 96 | - id: review-service-route 97 | uri: http://review:8080 98 | predicates: 99 | - Path=/reviews/** 100 | filters: 101 | - RewritePath=/reviews(?/.*|), /api/reviews\${segment} 102 | - id: course-aggregate-route 103 | uri: http://course-composite:8080 104 | predicates: 105 | - Path=/course-aggregate/** 106 | filters: 107 | - RewritePath=/course-aggregate(?/.*|), /api/course-aggregate\${segment} 108 | security: 109 | oauth2: 110 | resourceserver: 111 | jwt: 112 | issuer-uri: http://keycloak:8080/realms/course-management-realm 113 | jwk-set-uri: http://keycloak:8080/realms/course-management-realm/protocol/openid-connect/certs 114 | -------------------------------------------------------------------------------- /spring-cloud/gateway-service/src/test/java/com/example/springcloud/gateway/GatewayServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.springcloud.gateway; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class GatewayServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /util/.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /util/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /util/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /util/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /util/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.3 9 | 10 | 11 | io.javatab.util 12 | util 13 | 1.0.0 14 | util 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 17 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-webflux 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-test 41 | test 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-maven-plugin 50 | 3.4.3 51 | 52 | 53 | repackage 54 | none 55 | 56 | repackage 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /util/src/main/java/io/javatab/util/http/NetworkUtility.java: -------------------------------------------------------------------------------- 1 | package io.javatab.util.http; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.net.InetAddress; 7 | import java.net.UnknownHostException; 8 | 9 | @Component 10 | public class NetworkUtility { 11 | 12 | private final String port; 13 | 14 | private String serviceAddress = null; 15 | 16 | public NetworkUtility(@Value("${server.port}") String port) { 17 | this.port = port; 18 | } 19 | 20 | public String getServiceAddress() { 21 | if (serviceAddress == null) { 22 | serviceAddress = findMyHostname() + "/" + findMyIpAddress() + ":" + port; 23 | } 24 | return serviceAddress; 25 | } 26 | 27 | private String findMyHostname() { 28 | try { 29 | return InetAddress.getLocalHost().getHostName(); 30 | } catch (UnknownHostException e) { 31 | return "unknown host name"; 32 | } 33 | } 34 | 35 | private String findMyIpAddress() { 36 | try { 37 | return InetAddress.getLocalHost().getHostAddress(); 38 | } catch (UnknownHostException e) { 39 | return "unknown IP address"; 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /util/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: util 4 | --------------------------------------------------------------------------------