diff --git a/cloudbank-v5/CBV5.md b/cloudbank-v5/CBV5.md new file mode 100644 index 000000000..7d33c297f --- /dev/null +++ b/cloudbank-v5/CBV5.md @@ -0,0 +1,70 @@ +# Installing Cloudbank v5 + +This repo contains a almost ready deployment of cloudbank v5. There are minimal changes to make this work in your environment. **NOTE:** this example assumes that OBaaS is installed in a namespace called `obaas-dev` and that CBv5 will be installed in the same namespace called `obaas-dev`. + +Cluudbank v5 has only been tested on Java 21. + +You must set the DOCKER_HOST variable to be able to build CLoudbank v5, for example if you're using Rancher Desktop on a Mac `export DOCKER_HOST=unix:///Users/atael/.rd/docker.sock`. + +**NOTE** the repositories must by public. Needs to investigate why private repos aren't working (authentication error). You can use the script `create-oci-repos.sh` to create the necessary repositories. For example `source create-oci-repos.sh andytael sjc.ocir.io/maacloud/cloudbank`. + +## Dependencies for Cloudbank + +Build dependencies and install -- `mvn clean install -pl common,buildtools` + +## Build cloudbank + +A script called `update-jkube-image.sh` is provided to update the `pom.xml` file for JKube to point to the repository that will be used (update-jkube-image.sh (sjc.ocir.io/maacloud/cloudbank-v5 for example)). When that change is made you can execute the following command to build and push the images: + +```bash +mvn clean package k8s:build k8s:push -pl account,customer,transfer,checks,creditscore,testrunner +``` + +## Create account and customer DB users + +Update the `sqljob.yaml` file to reflect your environment. Run the job and **VERIFY** that the job finished successfully before proceeding. + +```bash +k create -f sqljob.yaml +``` + +## Create secrets for account and customer microservices + +A script called `acc_cust_secrets.sh` is provided that could be used create the necessary secrets. This script *MUST* be updated to reflect your environment. + +## Change values.yaml + +A script called `update_image.sh` is provided that could be used to change the repository and tag to your environment (`./update-image.sh `) in the `values.yaml` file. For example `update-image.sh sjc.ocir.io/maacloud/cloudbank-v5 0.0.1-SNAPSHOT`. + +Verify and change credentialSecret and walletSecret values in the `values.yaml` if needed. Names can be found be looking at the secrets in the `obaas-dev` namespace. + +## Install CBv5 + +A script call `deploy-all-services.sh` is provided that can be used to deploy all the Cloudbank services (account,customer,transfer,checks,creditscore,testrunner). For example `./deploy-all-services.sh obaas-dev`. + +## Create APISIX routes + +1. Retrieve the API KEY (requires `yq`) for APISIX by running this command: + +```bash +kubectl -n obaas-dev get configmap apisix -o yaml | yq '.data."config.yaml"' | yq '.deployment.admin.admin_key[] | select(.name == "admin") | .key' +``` + +If the command doesn't work you can get the API key by looking into the ConfigMap +...... + +1. Create tunnel to APISIX + +```shell +kubectl port-forward -n obaas-dev svc/apisix-admin 9180 +``` + +1. Create the routes by running this command: + +```bash +(cd apisix-routes; source ./create-all-routes.sh ) +``` + +## Test the services + +Follow the [README](README.md) section `Test CloudBank Services`. diff --git a/cloudbank-v5/CLAUDE.md b/cloudbank-v5/CLAUDE.md new file mode 100644 index 000000000..34f72cad7 --- /dev/null +++ b/cloudbank-v5/CLAUDE.md @@ -0,0 +1,131 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +CloudBank v5 is a microservices-based banking application built with Spring Boot 3.4.6 and Java 21. It demonstrates Oracle Backend for Microservices and AI (OBaaS) capabilities, including distributed transactions using Long Running Actions (LRA), observability, and service mesh integration. + +## Build and Development Commands + +### Build the entire project +```bash +mvn clean package +``` + +### Run specific tests for a module +```bash +mvn test -pl +# Example: mvn test -pl account +``` + +### Code quality checks +```bash +# Run checkstyle validation +mvn checkstyle:check + +# Run dependency vulnerability scan +mvn org.owasp:dependency-check-maven:check +``` + +### Docker image build and push +```bash +mvn k8s:build +``` + +## Architecture + +### Microservices Structure +- **account**: Account management service with journal/transaction tracking +- **customer**: Customer management and profile operations +- **transfer**: Inter-account transfer service with LRA support +- **checks**: Check deposit/clearance processing +- **creditscore**: Credit scoring service +- **chatbot**: AI-powered chatbot service (optional) +- **testrunner**: Test automation and system testing utilities +- **common**: Shared configuration and utilities +- **buildtools**: Development tooling and quality checks + +### Key Architectural Patterns +- **Event-driven Architecture**: Services communicate via REST APIs with distributed transaction support +- **Saga Pattern**: Implemented using Oracle MicroTx LRA for distributed transactions +- **Service Discovery**: Eureka-based service registration and discovery +- **Circuit Breaker**: OpenFeign with resilience patterns +- **Observability**: OpenTelemetry tracing, Micrometer metrics, Prometheus monitoring + +### Database Design +- Oracle Database with UCP connection pooling +- Liquibase for database migrations +- JPA/Hibernate for ORM with optimistic locking +- Each service has its own schema (account, customer, etc.) + +### Configuration Management +- Spring Cloud Config for centralized configuration +- Service-specific `application.yaml` files +- Common configuration shared via `common.yaml` +- Environment-specific overrides through externalized config + +## Technology Stack + +### Core Framework +- Spring Boot 3.4.6 with Virtual Threads enabled +- Spring Cloud 2024.0.1 for microservices infrastructure +- Oracle MicroTx for distributed transactions + +### Database & Persistence +- Oracle Database with oracle-spring-boot-starter-ucp +- Liquibase for schema migrations +- JPA/Hibernate with Oracle dialect + +### Observability & Monitoring +- OpenTelemetry for distributed tracing +- Micrometer with Prometheus for metrics +- SigNoz for APM and observability dashboards +- Spring Boot Actuator for health checks + +### Service Communication +- OpenFeign for service-to-service communication +- Eureka for service discovery +- APISIX for API gateway and routing + +## Development Guidelines + +### LRA Transaction Patterns +- Use `@LRA(value = LRA.Type.REQUIRES_NEW)` for new transaction boundaries +- Implement `@Complete` and `@Compensate` methods for saga compensation +- Pass LRA context headers between services for transaction correlation + +### REST API Conventions +- All APIs follow `/api/v1/` versioning pattern +- Use standard HTTP status codes (201 for creation, 204 for deletion) +- Return `Location` headers for created resources +- Implement proper error handling with meaningful HTTP status codes + +### Database Patterns +- Use repository pattern with Spring Data JPA +- Implement proper transaction boundaries at service layer +- Follow naming conventions: `findBy*`, `existsById`, etc. +- Use `saveAndFlush()` for immediate persistence when needed + +### Testing Approach +- Unit tests with Spring Boot Test slice annotations +- Integration tests using `@SpringBootTest` +- Use testrunner service for end-to-end system testing +- Mock external service dependencies with WireMock or similar + +## Common Issues & Solutions + +### LRA Transaction Timeouts +- Check MicroTx coordinator connectivity via `${MP_LRA_COORDINATOR_URL}` +- Verify LRA headers are properly propagated between services +- Monitor transaction logs for compensation trigger patterns + +### Service Discovery Issues +- Verify Eureka client configuration in `common.yaml` +- Check service registration with Eureka dashboard +- Ensure `spring.application.name` matches service discovery expectations + +### Database Connection Problems +- Validate Oracle UCP configuration in service-specific `application.yaml` +- Check connection pool sizing (`initial-pool-size`, `max-pool-size`) +- Verify Liquibase migrations complete successfully during startup \ No newline at end of file diff --git a/cloudbank-v5/README.md b/cloudbank-v5/README.md new file mode 100644 index 000000000..87677250c --- /dev/null +++ b/cloudbank-v5/README.md @@ -0,0 +1,465 @@ +# CloudBank Version 5 - 9/17/25 + +**NOTE:** This document and application is WIP. + +To run Cloud Bank you need OBaaS version 2.0.0 [Oracle Backend for Microservices and AI](https://cloudmarketplace.oracle.com/marketplace/en_US/listing/138899911) and Java 21 installed. + +1. Create or obtain Image Repo details +2. Clone the repo from here http:// +3. Got to the cloudbank-v5 directory +4. Build depndencies and install -- `mvn clean install -pl common,buildtools` +5. Build and push -- `mvn clean package k8s:build k8s:push -pl account,customer,transfer,checks,creditscore,chatbot,testrunner` +6. Edit the sqljob.yaml file, change the metadata.namespace to reflect your OBaaS installation (namespace, username, passwords for example) +7. Create DB secrets for account and customer (account-db-secret and customer-sb-secret) +8. For each application modify the pom.xml + a. Account (image name to use the image repository from step 1) + b. Checks (image name to use the image repository from step 1) + c. Customer (image name to use the image repository from step 1) + d. Creditscore (image name to use the image repository from step 1) + e. Testrunner (image name to use the image repository from step 1) + f. Transfer (image name to use the image repository from step 1) +9. For each application modify the chart.yaml + a. Account (name: account) + b. Checks (name: checks) + c. Customer (name: customer) + d. Creditscore (name: creditscore) + e. Testrunner (name: testrunner) + f. Transfer (name: transfer) +10. For each application modify the values.yaml + a. Account (database credentials account, all services enabled) + b. Checks (database credentuals account, all service enabled) + c. Customer (database credentials customer, all services enabled) + d. Creditscore (no db credentials, no mp_lra) + e. Testrunner (db creds account, all servcies enabled) + f. Transfer (no db creds, all services enabled) +11. For each application apply the helm chart. +12. For each application create APISIX +13. Test each application using curl + +TBD below ----- + +## Create APISIX Routes + +1. Get APISIX Gateway Admin Key + + ```shell + kubectl -n apisix get configmap apisix -o yaml + ``` + +1. Create tunnel to APISIX + + ```shell + kubectl port-forward -n apisix svc/apisix-admin 9180 + ``` + +1. Create routes + + In the CloudBank directory run the following command. *NOTE*, you must add the API-KEY to the command + + ```shell + (cd apisix-routes; source ./create-all-routes.sh ) + ``` + +## Optional - autoscaling + +Create autoscalers for CloudBank. + +```text +oractl:>script --file deploy-cmds/autoscale-cmd.txt +``` + +The following commands are executed: + +```script +create-autoscaler --service-name account --min-replicas 1 --max-replicas 4 --cpu-request 100m --cpu-percent 80 +create-autoscaler --service-name checks --min-replicas 1 --max-replicas 4 --cpu-request 100m --cpu-percent 80 +create-autoscaler --service-name customer --min-replicas 1 --max-replicas 4 --cpu-request 100m --cpu-percent 80 +create-autoscaler --service-name creditscore --min-replicas 1 --max-replicas 4 --cpu-request 100m --cpu-percent 80 +create-autoscaler --service-name testrunner --min-replicas 1 --max-replicas 4 --cpu-request 100m --cpu-percent 80 +create-autoscaler --service-name transfer --min-replicas 1 --max-replicas 4 --cpu-request 100m --cpu-percent 80 +``` + +## OpenAPI + +All services have OpenAPI documentation and can be reached via the Swagger UI. For example after starting a port forward to anyone of the services you can the URL ![OPenAPI](http://localhost:*port*/)swagger-ui/index.html to see the documentation. Replace *port* with the port used in the port forward command. For example, to see the API documentation for the `customer32` application do the following: + +```shell +kubectl port-forward -n application svc/customer32 8080 +``` + +And open a browser window and go to [Swagger UI](http://localhost:8080) + +This is an example of the `customer32` application: + +![Swagger UI](images/swagger-example.png " ") + +## Test CloudBank Services + +1. Get the external IP address + + ```shell + kubectl -n ingress-nginx get service ingress-nginx-controller + ``` + + Result. Create a variable called IP with the value of the **EXTERNAL-IP** it will be used in the tests. + + ```text + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE + ingress-nginx-controller LoadBalancer 10.96.172.148 146.235.207.230 80:31393/TCP,443:30506/TCP 158m + ``` + +1. Test `account` service + + 1. Rest endpoint + + ```shell + curl -s http://$IP/api/v1/accounts | jq + ``` + + Should return: + + ```json + [ + { + "accountBalance": -20, + "accountCustomerId": "qwertysdwr", + "accountId": 1, + "accountName": "Andy's checking", + "accountOpenedDate": "2023-06-26T17:39:37.000+00:00", + "accountOtherDetails": "Account Info", + "accountType": "CH" + }, + {...} + ] + ``` + +1. Test `customer` service + + 1. GET REST endpoint. + + ```shell + curl -s http://$IP/api/v1/customer | jq + ``` + + Should return: + + ```json + [ + { + "customerEmail": "andy@andy.com", + "customerId": "qwertysdwr", + "customerName": "Andy", + "customerOtherDetails": "Somekind of Info", + "customerPassword": "SuperSecret", + "dateBecameCustomer": "2023-11-02T17:30:12.000+00:00" + }, + {...} + ] + ``` + + 1. POST endpoint to create a customer. + + ```shell + curl -i -X POST -H 'Content-Type: application/json' -d '{"customerId": "bobsmith", "customerName": "Bob Smith", "customerEmail": "bob@smith.com"}' http://$IP/api/v1/customer + ``` + + Should return the URI of the created object: + + ```text + HTTP/1.1 201 + Location: http://localhost:8080/api/v1/customer/bobsmith + Content-Length: 0 + Date: Tue, 03 Sep 2024 21:01:25 GMT + ``` + +1. Test `creditscore` service + + 1. REST endpoint + + ```shell + curl -s http://$IP/api/v1/creditscore | jq + ``` + + Should return: + + ```json + { + "Date": "2023-12-26", + "Credit Score": "574" + } + ``` + +1. Test `check` service + + 1. REST endpoint - deposit check. *NOTE*: Make sure you use an existing account number + + ```shell + curl -i -X POST -H 'Content-Type: application/json' -d '{"accountId": 1, "amount": 256}' http://$IP/api/v1/testrunner/deposit + ``` + + Should return: + + ```text + HTTP/1.1 201 + Content-Type: application/json + Transfer-Encoding: chunked + Date: Thu, 02 Nov 2023 18:02:06 GMT + + {"accountId":1,"amount":256} + ``` + + 1. Check application log + + ```shell + kubectl logs -n application svc/checks + ``` + + Should contain: + + ```log + Received deposit + ``` + + 1. Check journal entries. Replace '1' with the account number you used. + + ```shell + curl -i http://$IP/api/v1/account/1/journal + ``` + + output should be similar to: + + ```log + HTTP/1.1 200 + Content-Type: application/json + Transfer-Encoding: chunked + Date: Thu, 02 Nov 2023 18:06:45 GMT + + [{"journalId":1,"journalType":"PENDING","accountId":1,"lraId":"0","lraState":null,"journalAmount":256}] + ``` + + 1. Clearance of check - Note the JournalID from earlier step + + ```shell + curl -i -X POST -H 'Content-Type: application/json' -d '{"journalId": 1}' http://$IP/api/v1/testrunner/clear + ``` + + output should be similar to: + + ```text + HTTP/1.1 201 + Content-Type: application/json + Transfer-Encoding: chunked + Date: Thu, 02 Nov 2023 18:09:17 GMT + + {"journalId":1} + ``` + + 1. Check application log + + ```shell + kubectl logs -n application svc/checks + ``` + + Output should be similar to: + + ```log + ... + Received clearance + ... + ``` + + 1. Check journal -- DEPOSIT. Replace '1' with the account number you used. + + ```shell + curl -i http://$IP/api/v1/account/1/journal + ``` + + Output should look like this -- DEPOSIT + + ```text + HTTP/1.1 200 + Content-Type: application/json + Transfer-Encoding: chunked + Date: Thu, 02 Nov 2023 18:36:31 GMT + + [{"journalId":1,"journalType":"DEPOSIT","accountId":1,"lraId":"0","lraState":null,"journalAmount":256}]` + ``` + +1. Run LRA Test Cases + + 1. Check account balances. Note that the account numbers 1 and 2 can be different in your environment + + ```shell + curl -s http://$IP/api/v1/account/1 | jq ; curl -s http://$IP/api/v1/account/2 | jq + ``` + + Output should be similar to this, make a note of the account balance: + + ```json + { + "accountId": 1, + "accountName": "Andy's checking", + "accountType": "CH", + "accountCustomerId": "qwertysdwr", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Account Info", + "accountBalance": -20 + }, + { + "accountId": 2, + "accountName": "Mark's CCard", + "accountType": "CC", + "accountCustomerId": "bkzLp8cozi", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Mastercard account", + "accountBalance": 1000 + } + ``` + + 1. Perform transfer between two accounts. Note account numbers + + ```shell + curl -X POST "http://$IP/transfer?fromAccount=2&toAccount=1&amount=100" + ``` + + Output should look like this: + + ```text + transfer status:withdraw succeeded deposit succeeded + ``` + + 1. Check accounts to see that the transfer have occurred + + ```shell + curl -s http://$IP/api/v1/account/1 | jq ; curl -s http://$IP/api/v1/account/2 | jq + ``` + + Output should be similar to this: + + ```json + { + "accountId": 1, + "accountName": "Andy's checking", + "accountType": "CH", + "accountCustomerId": "qwertysdwr", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Account Info", + "accountBalance": 80 + }, + { + "accountId": 2, + "accountName": "Mark's CCard", + "accountType": "CC", + "accountCustomerId": "bkzLp8cozi", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Mastercard account", + "accountBalance": 900 + } + ``` + + 1. Check the application log to confirm + + ```shell + kubectl logs -n application svc/transfer + ``` + + Output should look similar to this: + + ```text + 2023-12-26T16:50:45.138Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : Started new LRA/transfer Id: http://otmm-tcs.otmm.svc.cluster.local:9000/api/v1/lra-coordinator/ea98ebae-2358-4dd1-9d7c-09f4550d7567 + 2023-12-26T16:50:45.139Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : withdraw accountId = 2, amount = 100 + 2023-12-26T16:50:45.139Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : withdraw lraId = http://otmm-tcs.otmm.svc.cluster.local:9000/api/v1/lra-coordinator/ea98ebae-2358-4dd1-9d7c-09f4550d7567 + 2023-12-26T16:50:45.183Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : withdraw succeeded + 2023-12-26T16:50:45.183Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : deposit accountId = 1, amount = 100 + 2023-12-26T16:50:45.183Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : deposit lraId = http://otmm-tcs.otmm.svc.cluster.local:9000/api/v1/lra-coordinator/ea98ebae-2358-4dd1-9d7c-09f4550d7567 + 2023-12-26T16:50:45.216Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : withdraw succeeded deposit succeeded + 2023-12-26T16:50:45.216Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : LRA/transfer action will be confirm + 2023-12-26T16:50:45.226Z INFO 1 --- [transfer] [nio-8080-exec-1] [] com.example.transfer.TransferService : Received confirm for transfer : http://otmm-tcs.otmm.svc.cluster.local:9000/api/v1/lra-coordinator/ea98ebae-2358-4dd1-9d7c-09f4550d7567 + 2023-12-26T16:50:45.233Z INFO 1 --- [transfer] [io-8080-exec-10] [] com.example.transfer.TransferService : Process confirm for transfer : http://otmm-tcs.otmm.svc.cluster.local:9000/api/v1/lra-coordinator/ea98ebae-2358-4dd1-9d7c-09f4550d7567 + ``` + +## Observability and Tracing + +1. Check the ServiceOPS Center + + 1. Get the external IP + + ```shell + kubectl -n ingress-nginx get service ingress-nginx-controller + ``` + + 1. Get the *obaas-admin* user password + + ```shell + kubectl get secret -n azn-server oractl-passwords -o jsonpath='{.data.admin}' | base64 -d + ``` + + 1. Login into [ServiceOPS Dashboard](https://EXTERNAL-IP/soc) + + ![ServiceOPS Login](images/serviceops_login.png " ") + + 1. Explore the dashboard + + ![ServiceOPS Dashboard](images/serviceops_dashboard.png " ") + +1. Check Eureka dashboard + + 1. Port forward + + ```shell + kubectl -n eureka port-forward svc/eureka 8761 + ``` + + 1. Open [Eureka Dashboard](http://localhost:8761) in a browser and verify that all services are registered + + ![Eureka Dashboard](images/eureka.png " ") + +1. Check Admin Server dashboard + + 1. Port forward + + ```shell + kubectl port-forward -n admin-server svc/admin-server 8989 + ``` + + 1. Open [Admin Server Dashboard](http://localhost:8989) in a browser and verify that all services are registered + + ![Admin Server Dashboard](images/admin_server.png " ") + +2. Check the SigNoz Dashboard + + 1. Get the *admin* email and password for SigNoz + + ```shell + kubectl -n observability get secret signoz-authn -o jsonpath='{.data.email}' | base64 -d + kubectl -n observability get secret signoz-authn -o jsonpath='{.data.password}' | base64 -d + ``` + + 1. Port forward + + ```shell + kubectl -n observability port-forward svc/obaas-signoz-frontend 3301:3301 + ``` + + 2. Open [SigNoz Login](http://localhost:3301/login) in a browser and login with the *admin* email and the password you have retrieved. + + ![SigNoz Login](images/signoz_login.png " ") + + 3. Explore the pre-installed dashboards. + + ![SigNoz](images/signoz-dashboard.png " ") + + 4. Explore the traces view. Choose `customer` Service to view traces related to Customer microservice. + + ![Trace Explorer](images/traces.png " ") + + Click any of the traces to expand its details. + + ![Customer](images/traces-expand.png " ") + 5. Explore the logs view. Choose `customer` Service to view logs related to Customer microservice. + + ![Logs Explorer](images/logs.png " ") + + Click any of the log lines to expand its details. + + ![Customer](images/logs-expand.png " ") \ No newline at end of file diff --git a/cloudbank-v5/acc_cust_secrets.sh b/cloudbank-v5/acc_cust_secrets.sh new file mode 100755 index 000000000..0b3c2b5e2 --- /dev/null +++ b/cloudbank-v5/acc_cust_secrets.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Creates secrets for account and customer microservices in the obaas-dev namespace +# +# Adjust the namespace and secret values as needed + +kubectl -n obaas-dev create secret generic account-db-secrets \ + --from-literal=db.name=helmdb \ + --from-literal=db.username=account \ + --from-literal=db.password=YOUR_PASSWORD \ + --from-literal=db.service=helmdb_tp \ + --from-literal=db.lb_username=admin \ + --from-literal=db.lb_password=YOUR_PASSWORD +kubectl -n obaas-dev create secret generic customer-db-secrets \ + --from-literal=db.name=helmdb \ + --from-literal=db.username=customer \ + --from-literal=db.password=YOUR_PASSWORD \ + --from-literal=db.service=helmdb_tp \ + --from-literal=db.lb_username=admin \ + --from-literal=db.lb_password=YOUR_PASSWORD diff --git a/cloudbank-v5/account/helm/Chart.yaml b/cloudbank-v5/account/helm/Chart.yaml new file mode 100644 index 000000000..e7fd9be9a --- /dev/null +++ b/cloudbank-v5/account/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: account # Replace with your application name +description: A Helm chart for the OBaaS Platform Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.2.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.0.1" diff --git a/cloudbank-v5/account/helm/README.md b/cloudbank-v5/account/helm/README.md new file mode 100644 index 000000000..9403cc5e0 --- /dev/null +++ b/cloudbank-v5/account/helm/README.md @@ -0,0 +1,16 @@ +# OBaas Sample App Chart + +This chart provides an extensible sample for applications running on [OBaaS](https://oracle.github.io/microservices-datadriven/obaas/). + +To use this chart for a given application, download the chart and update the [Chart.name](./Chart.yaml) value to your application's name. + +## Customizing the chart + +The OBaaS sample app chart is meant to serve as a developer template, and is fully customizable. + +Standard parameters for Kubernetes options like node affinity, HPAs, ingress and more are provided in the [values.yaml file](./values.yaml). + +## OBaaS options + +Within the [values.yaml file](./values.yaml), the `obaas` key allows chart developers to enable or disable OBaaS integrations like database connectivity, OpenTelemetry, MicroProfile LRA, SpringBoot, and Eureka. +enabled: true diff --git a/cloudbank-v5/account/helm/templates/NOTES.txt b/cloudbank-v5/account/helm/templates/NOTES.txt new file mode 100644 index 000000000..8d9597d97 --- /dev/null +++ b/cloudbank-v5/account/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "obaas-app.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "obaas-app.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "obaas-app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "obaas-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cloudbank-v5/account/helm/templates/_helpers.tpl b/cloudbank-v5/account/helm/templates/_helpers.tpl new file mode 100644 index 000000000..b8e6d5b79 --- /dev/null +++ b/cloudbank-v5/account/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "obaas-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "obaas-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "obaas-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "obaas-app.labels" -}} +helm.sh/chart: {{ include "obaas-app.chart" . }} +{{ include "obaas-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "obaas-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "obaas-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "obaas-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "obaas-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cloudbank-v5/account/helm/templates/deployment.yaml b/cloudbank-v5/account/helm/templates/deployment.yaml new file mode 100644 index 000000000..d4902a2f2 --- /dev/null +++ b/cloudbank-v5/account/helm/templates/deployment.yaml @@ -0,0 +1,147 @@ +# Load obaas configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + obaas.framework: SPRING_BOOT +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "obaas-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "obaas-app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "obaas-app.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CONNECT_STRING + value: jdbc:oracle:thin:@$(SPRING_DB_SERVICE)?TNS_ADMIN=/oracle/tnsadmin + # Lookup ObaaS configuration + {{- $obaas := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-config") }} + {{- $obaasObs := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-observability-config") }} + {{- if $.Values.obaas.mp_lra.enabled }} + - name: MP_LRA_COORDINATOR_URL + value: {{ $obaas.data.otmm | quote }} + {{- end }} + {{- if $.Values.obaas.eureka.enabled }} + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_CLIENT_REGISTER_WITH_EUREKA + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: {{ $obaas.data.eureka | quote }} + - name: EUREKA_INSTANCE_HOSTNAME + value: {{ include "obaas-app.fullname" . }}-{{ $.Release.Namespace }} + {{- end }} + {{- if $.Values.obaas.otel.enabled }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ (index $obaasObs.data "signoz-otel-collector") | quote }} + {{- end }} + {{- if $.Values.obaas.springboot.enabled }} + - name: SPRING_PROFILES_ACTIVE + value: {{ $obaas.data.SPRING_PROFILES_ACTIVE | quote }} + {{- if $.Values.obaas.database.enabled }} + - name: SPRING_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.username + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.password + - name: LIQUIBASE_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_username + - name: LIQUIBASE_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_password + - name: LIQUIBASE_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DB_SERVICE + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.service + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ $.Values.obaas.database.walletSecret }} + mountPath: /oracle/tnsadmin + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: {{ $.Values.obaas.database.walletSecret }} + secret: + secretName: {{ $.Values.obaas.database.walletSecret }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cloudbank-v5/account/helm/templates/hpa.yaml b/cloudbank-v5/account/helm/templates/hpa.yaml new file mode 100644 index 000000000..7c8805fb0 --- /dev/null +++ b/cloudbank-v5/account/helm/templates/hpa.yaml @@ -0,0 +1,33 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "obaas-app.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/account/helm/templates/ingress.yaml b/cloudbank-v5/account/helm/templates/ingress.yaml new file mode 100644 index 000000000..50aa98cd8 --- /dev/null +++ b/cloudbank-v5/account/helm/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "obaas-app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/account/helm/templates/service.yaml b/cloudbank-v5/account/helm/templates/service.yaml new file mode 100644 index 000000000..5876cf66d --- /dev/null +++ b/cloudbank-v5/account/helm/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "obaas-app.selectorLabels" . | nindent 4 }} diff --git a/cloudbank-v5/account/helm/templates/serviceaccount.yaml b/cloudbank-v5/account/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..a22b73ef6 --- /dev/null +++ b/cloudbank-v5/account/helm/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "obaas-app.serviceAccountName" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/cloudbank-v5/account/helm/templates/tests/test-connection.yaml b/cloudbank-v5/account/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..8596a023f --- /dev/null +++ b/cloudbank-v5/account/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "obaas-app.fullname" . }}-test-connection" + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "obaas-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cloudbank-v5/account/helm/values.yaml b/cloudbank-v5/account/helm/values.yaml new file mode 100644 index 000000000..abb7f5f95 --- /dev/null +++ b/cloudbank-v5/account/helm/values.yaml @@ -0,0 +1,155 @@ +# Default values for obaas-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: "my-repository/account" + # This sets the pull policy for images. + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "0.0.1-SNAPSHOT" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: + signoz.io/path: /actuator/prometheus + signoz.io/port: "8080" + signoz.io/scrape: "true" +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 8080 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + requests: + cpu: 100m + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Oracle Backend for Microservices and AI Settings. +obaas: + namespace: obaas-dev # Replace with your namespace + database: + enabled: true # If true variables with DB secret content will be created + credentialsSecret: account-db-secrets # Replace with your secret name + walletSecret: obaas-adb-tns-admin-1 # Replace with your wallet secret name + otel: + enabled: true # Enable OpenTelemetry + # MicroProfile LRA + mp_lra: + enabled: true # Enable OTMM + # Spring Boot applications + springboot: + enabled: true # Enable Spring Boot specific variables + eureka: + enabled: true # Enable Eureka client \ No newline at end of file diff --git a/cloudbank-v5/account/pom.xml b/cloudbank-v5/account/pom.xml new file mode 100644 index 000000000..2af90d24e --- /dev/null +++ b/cloudbank-v5/account/pom.xml @@ -0,0 +1,85 @@ + + + + + 4.0.0 + + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + + account + 0.0.1-SNAPSHOT + account + Account Application + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.oracle.database.spring + oracle-spring-boot-starter-wallet + ${oracle-springboot-starter.version} + pom + + + org.liquibase + liquibase-core + ${liquibase.version} + + + com.oracle.microtx.lra + microtx-lra-spring-boot-starter + ${oracle-microtx-starter.version} + + + com.example + common + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.graalvm.buildtools + native-maven-plugin + + + org.liquibase + liquibase-maven-plugin + ${liquibase.version} + + + org.eclipse.jkube + kubernetes-maven-plugin + ${jkube.version} + + + + my-repository/account:${project.version} + + ghcr.io/oracle/openjdk-image-obaas:21 + + dir + /deployments + + java -jar /deployments/${project.artifactId}-${project.version}.jar + + + + + + + + + diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/AccountsApplication.java b/cloudbank-v5/account/src/main/java/com/example/accounts/AccountsApplication.java new file mode 100644 index 000000000..785327a06 --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/AccountsApplication.java @@ -0,0 +1,21 @@ +// Copyright (c) 2023, 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts; + +import com.example.common.filter.LoggingFilterConfig; +import com.example.common.ucp.UCPTelemetry; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.Import; + +@EnableDiscoveryClient +@SpringBootApplication +@Import({ LoggingFilterConfig.class, UCPTelemetry.class }) +public class AccountsApplication { + + public static void main(String[] args) { + SpringApplication.run(AccountsApplication.class, args); + } +} diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/controller/AccountController.java b/cloudbank-v5/account/src/main/java/com/example/accounts/controller/AccountController.java new file mode 100644 index 000000000..f4cfe820c --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/controller/AccountController.java @@ -0,0 +1,203 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts.controller; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.example.accounts.model.Account; +import com.example.accounts.model.Journal; +import com.example.accounts.repository.AccountRepository; +import com.example.accounts.repository.JournalRepository; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +@RestController +@RequestMapping("/api/v1") +public class AccountController { + + final AccountRepository accountRepository; + final JournalRepository journalRepository; + + public AccountController(AccountRepository accountRepository, JournalRepository journalRepository) { + this.accountRepository = accountRepository; + this.journalRepository = journalRepository; + } + + /** + * Get all Accounts. + * + * @return List off accounts + */ + @GetMapping("/accounts") + public List getAllAccounts() { + return accountRepository.findAll(); + } + + /** + * Create an account. + * + * @param account Account object. + * @return Returns HTTP Status code or the URI of the created object. + */ + @PostMapping("/account") + public ResponseEntity createAccount(@RequestBody Account account) { + boolean exists = accountRepository.existsById(account.getAccountId()); + + if (!exists) { + try { + Account newAccount = accountRepository.saveAndFlush(account); + URI location = ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{id}") + .buildAndExpand(newAccount.getAccountId()) + .toUri(); + return ResponseEntity.created(location).build(); + } catch (Exception e) { + return new ResponseEntity<>(account, HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + return new ResponseEntity<>(account, HttpStatus.CONFLICT); + } + } + + /** + * Find an account by Account Id. + * + * @param accountId Account Id + * @return An account + */ + @GetMapping("/account/{accountId}") + public ResponseEntity getAccountById(@PathVariable("accountId") long accountId) { + Optional accountData = accountRepository.findById(accountId); + try { + return accountData.map(account -> new ResponseEntity<>(account, HttpStatus.OK)) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Find an account by customer Id. + * + * @param customerId Customer Id + * @return A list opf Account(s) + */ + @GetMapping("/account/getAccounts/{customerId}") + public ResponseEntity> getAccountsByCustomerId(@PathVariable("customerId") String customerId) { + try { + List accountData = new ArrayList(); + accountData.addAll(accountRepository.findByAccountCustomerId(customerId)); + if (accountData.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return new ResponseEntity<>(accountData, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Delete an Account with specific Id. + * + * @param accountId Account ID + * @return HTTP Status Code + */ + @DeleteMapping("/account/{accountId}") + public ResponseEntity deleteAccount(@PathVariable("accountId") long accountId) { + try { + accountRepository.deleteById(accountId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Get transactions (Journal) for an Account Id. + * + * @param accountId Account Id + * @return List of Journal object(s) + */ + @GetMapping("/account/{accountId}/transactions") + public ResponseEntity> getTransactions(@PathVariable("accountId") long accountId) { + try { + List transactions = new ArrayList(); + transactions.addAll(journalRepository.findByAccountId(accountId)); + if (transactions.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return new ResponseEntity<>(transactions, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Create a Journal entry. + * + * @param journalEntry Journal object + * @return HTTP Status Code + */ + @PostMapping("/account/journal") + public ResponseEntity postSimpleJournalEntry(@RequestBody Journal journalEntry) { + boolean exists = journalRepository.existsById(journalEntry.getJournalId()); + if (!exists) { + try { + Journal newJournalEntry = journalRepository.saveAndFlush(journalEntry); + return new ResponseEntity<>(newJournalEntry, HttpStatus.CREATED); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + return new ResponseEntity<>(journalEntry, HttpStatus.CONFLICT); + } + } + + /** + * Find Journal entries by Account Id. + * + * @param accountId Account Id + * @return Journal object(s) + */ + @GetMapping("/account/{accountId}/journal") + public List getJournalEntriesForAccount(@PathVariable("accountId") long accountId) { + return journalRepository.findJournalByAccountId(accountId); + } + + /** + * Clears the Journal Entry. + * + * @param journalId Journal Id + * @return HTTP Status Code + */ + @PostMapping("/account/journal/{journalId}/clear") + public ResponseEntity clearJournalEntry(@PathVariable long journalId) { + try { + Optional data = journalRepository.findById(journalId); + if (data.isPresent()) { + Journal newJournalEntry = data.get(); + newJournalEntry.setJournalType("DEPOSIT"); + journalRepository.saveAndFlush(newJournalEntry); + return new ResponseEntity(newJournalEntry, HttpStatus.OK); + } else { + return new ResponseEntity(new Journal(), HttpStatus.ACCEPTED); + } + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + +} \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/model/Account.java b/cloudbank-v5/account/src/main/java/com/example/accounts/model/Account.java new file mode 100644 index 000000000..0fb536c4d --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/model/Account.java @@ -0,0 +1,54 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@Entity +@Table(name = "ACCOUNTS") +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ACCOUNT_ID") + private long accountId; + + @Column(name = "ACCOUNT_NAME") + private String accountName; + + @Column(name = "ACCOUNT_TYPE") + private String accountType; + + @Column(name = "CUSTOMER_ID") + private String accountCustomerId; + + @Column(name = "ACCOUNT_OTHER_DETAILS") + private String accountOtherDetails; + + @Column(name = "ACCOUNT_BALANCE") + private long accountBalance; + + /** + * Create Account object. + * @param accountName Account name + * @param accountType Account Type + * @param accountOtherDetails Other details about account + * @param accountCustomerId Account Customer ID + */ + public Account(String accountName, String accountType, String accountOtherDetails, String accountCustomerId) { + this.accountName = accountName; + this.accountType = accountType; + this.accountOtherDetails = accountOtherDetails; + this.accountCustomerId = accountCustomerId; + } +} \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/model/Journal.java b/cloudbank-v5/account/src/main/java/com/example/accounts/model/Journal.java new file mode 100644 index 000000000..a08eade51 --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/model/Journal.java @@ -0,0 +1,69 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "JOURNAL") +@Data +@NoArgsConstructor +public class Journal { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "JOURNAL_ID") + private long journalId; + + // type is withdraw or deposit + @Column(name = "JOURNAL_TYPE") + private String journalType; + + @Column(name = "ACCOUNT_ID") + private long accountId; + + @Column(name = "LRA_ID") + private String lraId; + + @Column(name = "LRA_STATE") + private String lraState; + + @Column(name = "JOURNAL_AMOUNT") + private long journalAmount; + + /** + * Create Journal Object. + * @param journalType Journal Type + * @param accountId Account Id + * @param journalAmount Amount + * @param lraId LRA Id + * @param lraState State + */ + public Journal(String journalType, long accountId, long journalAmount, String lraId, String lraState) { + this.journalType = journalType; + this.accountId = accountId; + this.lraId = lraId; + this.lraState = lraState; + this.journalAmount = journalAmount; + } + + /** + * Create Journal object. + * @param journalType Journal Type + * @param accountId Account Id + * @param journalAmount Amount + */ + public Journal(String journalType, long accountId, long journalAmount) { + this.journalType = journalType; + this.accountId = accountId; + this.journalAmount = journalAmount; + } +} \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/repository/AccountRepository.java b/cloudbank-v5/account/src/main/java/com/example/accounts/repository/AccountRepository.java new file mode 100644 index 000000000..fe412b673 --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/repository/AccountRepository.java @@ -0,0 +1,18 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts.repository; + +import java.util.List; + +import com.example.accounts.model.Account; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccountRepository extends JpaRepository { + + List findByAccountCustomerId(String customerId); + + List findAccountsByAccountNameContains(String accountName); + + Account findByAccountId(long accountId); +} \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/repository/JournalRepository.java b/cloudbank-v5/account/src/main/java/com/example/accounts/repository/JournalRepository.java new file mode 100644 index 000000000..fdf2fdf4d --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/repository/JournalRepository.java @@ -0,0 +1,18 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts.repository; + +import java.util.List; + +import com.example.accounts.model.Journal; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JournalRepository extends JpaRepository { + + Journal findJournalByLraIdAndJournalType(String lraId, String journalType); + + List findByAccountId(long accountId); + + List findJournalByAccountId(long accountId); +} \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/services/AccountTransferDAO.java b/cloudbank-v5/account/src/main/java/com/example/accounts/services/AccountTransferDAO.java new file mode 100644 index 000000000..ea75a64e9 --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/services/AccountTransferDAO.java @@ -0,0 +1,140 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts.services; + +import com.example.accounts.model.Account; +import com.example.accounts.model.Journal; +import com.example.accounts.repository.AccountRepository; +import com.example.accounts.repository.JournalRepository; +import com.oracle.microtx.springboot.lra.annotation.ParticipantStatus; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class AccountTransferDAO { + + private static AccountTransferDAO singleton; + final AccountRepository accountRepository; + final JournalRepository journalRepository; + + /** + * Initialize account and journal repository. + * @param accountRepository Account Repository + * @param journalRepository Journal Repository + */ + public AccountTransferDAO(AccountRepository accountRepository, JournalRepository journalRepository) { + this.accountRepository = accountRepository; + this.journalRepository = journalRepository; + singleton = this; + log.info("AccountTransferDAO accountsRepository = " + accountRepository + + ", journalRepository = " + journalRepository); + } + + public static AccountTransferDAO instance() { + return singleton; + } + + /** + * Get status od LRA participant. + * @param status Status code + * @return Returns status code + */ + public static String getStatusString(ParticipantStatus status) { + return switch (status) { + case Compensated -> "Compensated"; + case Completed -> "Completed"; + case FailedToCompensate -> "Failed to Compensate"; + case FailedToComplete -> "Failed to Complete"; + case Active -> "Active"; + case Compensating -> "Compensating"; + case Completing -> "Completing"; + default -> "Unknown"; + }; + } + + /** + * Get LRA Status from a string. + * @param statusString Status + * @return Participant Status + */ + public static ParticipantStatus getStatusFromString(String statusString) { + return switch (statusString) { + case "Compensated" -> ParticipantStatus.Compensated; + case "Completed" -> ParticipantStatus.Completed; + case "Failed to Compensate" -> ParticipantStatus.FailedToCompensate; + case "Failed to Complete" -> ParticipantStatus.FailedToComplete; + case "Active" -> ParticipantStatus.Active; + case "Compensating" -> ParticipantStatus.Compensating; + case "Completing" -> ParticipantStatus.Completing; + default -> null; + }; + } + + public void saveAccount(Account account) { + log.info("saveAccount account" + account.getAccountId() + " account" + account.getAccountBalance()); + accountRepository.save(account); + } + + /** + * TO-DO. + * @param lraId LRA Id + * @param journalType Journal Type + * @return Participant Status + * @throws Exception Exception + */ + public ResponseEntity status(String lraId, String journalType) throws Exception { + Journal journal = getJournalForLRAid(lraId, journalType); + if (AccountTransferDAO.getStatusFromString(journal.getLraState()).equals(ParticipantStatus.Compensated)) { + return ResponseEntity.ok(ParticipantStatus.Compensated); + } else { + return ResponseEntity.ok(ParticipantStatus.Completed); + } + } + + /** + * Update the LRA status in the journal table during the "after LRA" phase. + * @param lraId LRA Id + * @param status Status + * @param journalType Journal Type + * @throws Exception Exception + */ + public void afterLRA(String lraId, String status, String journalType) throws Exception { + Journal journal = getJournalForLRAid(lraId, journalType); + journal.setLraState(status); + journalRepository.save(journal); + } + + Account getAccountForJournal(Journal journal) throws Exception { + Account account = accountRepository.findByAccountId(journal.getAccountId()); + if (account == null) { + throw new Exception("Invalid accountName:" + journal.getAccountId()); + } + return account; + } + + Account getAccountForAccountId(long accountId) { + Account account = accountRepository.findByAccountId(accountId); + if (account == null) { + return null; + } + return account; + } + + Journal getJournalForLRAid(String lraId, String journalType) throws Exception { + Journal journal = journalRepository.findJournalByLraIdAndJournalType(lraId, journalType); + if (journal == null) { + journalRepository.save(new Journal("unknown", -1, 0, lraId, + AccountTransferDAO.getStatusString(ParticipantStatus.FailedToComplete))); + throw new Exception("Journal entry does not exist for lraId:" + lraId); + } + return journal; + } + + public void saveJournal(Journal journal) { + journalRepository.save(journal); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/services/DepositService.java b/cloudbank-v5/account/src/main/java/com/example/accounts/services/DepositService.java new file mode 100644 index 000000000..7f599af27 --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/services/DepositService.java @@ -0,0 +1,137 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts.services; + +import com.example.accounts.model.Account; +import com.example.accounts.model.Journal; +import com.oracle.microtx.springboot.lra.annotation.AfterLRA; +import com.oracle.microtx.springboot.lra.annotation.Compensate; +import com.oracle.microtx.springboot.lra.annotation.Complete; +import com.oracle.microtx.springboot.lra.annotation.LRA; +import com.oracle.microtx.springboot.lra.annotation.ParticipantStatus; +import com.oracle.microtx.springboot.lra.annotation.Status; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static com.oracle.microtx.springboot.lra.annotation.LRA.LRA_HTTP_CONTEXT_HEADER; +import static com.oracle.microtx.springboot.lra.annotation.LRA.LRA_HTTP_ENDED_CONTEXT_HEADER; +import static com.oracle.microtx.springboot.lra.annotation.LRA.LRA_HTTP_PARENT_CONTEXT_HEADER; + +@RestController +@RequestMapping("/deposit") +@Slf4j +public class DepositService { + + private static String DEPOSIT = "DEPOSIT"; + + /** + * Write journal entry re deposit amount. + * Do not increase actual bank account amount + */ + @PostMapping + @LRA(value = LRA.Type.MANDATORY, end = false) + public ResponseEntity deposit(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId, + @RequestParam("accountId") long accountId, + @RequestParam("amount") long depositAmount) { + log.info("...deposit " + depositAmount + " in account:" + accountId + + " (lraId:" + lraId + ") finished (in pending state)"); + Account account = AccountTransferDAO.instance().getAccountForAccountId(accountId); + if (account == null) { + log.info("deposit failed: account does not exist"); + AccountTransferDAO.instance().saveJournal( + new Journal( + DEPOSIT, + accountId, + 0, + lraId, + AccountTransferDAO.getStatusString(ParticipantStatus.Active) + ) + ); + return ResponseEntity.ok("deposit failed: account does not exist"); + } + AccountTransferDAO.instance().saveJournal( + new Journal( + DEPOSIT, + accountId, + depositAmount, + lraId, + AccountTransferDAO.getStatusString(ParticipantStatus.Active) + ) + ); + return ResponseEntity.ok("deposit succeeded"); + } + + /** + * Increase balance amount as recorded in journal during deposit call. + * Update LRA state to ParticipantStatus.Completed. + */ + @PutMapping("/complete") + @Complete + public ResponseEntity completeWork(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) throws Exception { + log.info("deposit complete called for LRA : " + lraId); + + // get the journal and account... + Journal journal = AccountTransferDAO.instance().getJournalForLRAid(lraId, DEPOSIT); + Account account = AccountTransferDAO.instance().getAccountForJournal(journal); + + // set this LRA participant's status to completing... + journal.setLraState(AccountTransferDAO.getStatusString(ParticipantStatus.Completing)); + + // update the account balance and journal entry... + account.setAccountBalance(account.getAccountBalance() + journal.getJournalAmount()); + AccountTransferDAO.instance().saveAccount(account); + journal.setLraState(AccountTransferDAO.getStatusString(ParticipantStatus.Completed)); + AccountTransferDAO.instance().saveJournal(journal); + + // set this LRA participant's status to complete... + return ResponseEntity.ok(ParticipantStatus.Completed.name()); + } + + /** + * Update LRA state to ParticipantStatus.Compensated. + */ + @PutMapping("/compensate") + @Compensate + public ResponseEntity compensateWork(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) + throws Exception { + log.info("deposit compensate called for LRA : " + lraId); + + Journal journal = AccountTransferDAO.instance().getJournalForLRAid(lraId, DEPOSIT); + journal.setLraState(AccountTransferDAO.getStatusString(ParticipantStatus.Compensated)); + AccountTransferDAO.instance().saveJournal(journal); + return ResponseEntity.ok(ParticipantStatus.Compensated.name()); + } + + /** + * Return status. + */ + @GetMapping(value = "/status", produces = "text/plain") + @Status + public ResponseEntity status(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId, + @RequestHeader(LRA_HTTP_PARENT_CONTEXT_HEADER) String parentLRA) throws Exception { + log.info("status called for LRA : " + lraId); + + return AccountTransferDAO.instance().status(lraId, DEPOSIT); + } + + /** + * Delete journal entry for LRA. + */ + @PutMapping(value = "/after", consumes = "text/plain") + @AfterLRA + public ResponseEntity afterLRA(@RequestHeader(LRA_HTTP_ENDED_CONTEXT_HEADER) String lraId, + String status) throws Exception { + log.info("After LRA Called : " + lraId); + AccountTransferDAO.instance().afterLRA(lraId, status, DEPOSIT); + return ResponseEntity.ok(""); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/java/com/example/accounts/services/WithdrawService.java b/cloudbank-v5/account/src/main/java/com/example/accounts/services/WithdrawService.java new file mode 100644 index 000000000..8d5fd7b83 --- /dev/null +++ b/cloudbank-v5/account/src/main/java/com/example/accounts/services/WithdrawService.java @@ -0,0 +1,134 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts.services; + +import com.example.accounts.model.Account; +import com.example.accounts.model.Journal; +import com.oracle.microtx.springboot.lra.annotation.AfterLRA; +import com.oracle.microtx.springboot.lra.annotation.Compensate; +import com.oracle.microtx.springboot.lra.annotation.Complete; +import com.oracle.microtx.springboot.lra.annotation.LRA; +import com.oracle.microtx.springboot.lra.annotation.ParticipantStatus; +import com.oracle.microtx.springboot.lra.annotation.Status; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static com.oracle.microtx.springboot.lra.annotation.LRA.LRA_HTTP_CONTEXT_HEADER; +import static com.oracle.microtx.springboot.lra.annotation.LRA.LRA_HTTP_ENDED_CONTEXT_HEADER; +import static com.oracle.microtx.springboot.lra.annotation.LRA.LRA_HTTP_PARENT_CONTEXT_HEADER; + + +@RestController +@RequestMapping("/withdraw") +@Slf4j +public class WithdrawService { + + public static final String WITHDRAW = "WITHDRAW"; + + /** + * Reduce account balance by given amount and write journal entry re the same. + * Both actions in same local tx + */ + @PostMapping + @LRA(value = LRA.Type.MANDATORY, end = false) + public ResponseEntity withdraw(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId, + @RequestParam("accountId") long accountId, + @RequestParam("amount") long withdrawAmount) { + log.info("withdraw " + withdrawAmount + " in account:" + accountId + " (lraId:" + lraId + ")..."); + Account account = AccountTransferDAO.instance().getAccountForAccountId(accountId); + if (account == null) { + log.info("withdraw failed: account does not exist"); + AccountTransferDAO.instance().saveJournal( + new Journal( + WITHDRAW, + accountId, + 0, + lraId, + AccountTransferDAO.getStatusString(ParticipantStatus.Active))); + return ResponseEntity.ok("withdraw failed: account does not exist"); + } + if (account.getAccountBalance() < withdrawAmount) { + log.info("withdraw failed: insufficient funds"); + AccountTransferDAO.instance().saveJournal( + new Journal( + WITHDRAW, + accountId, + 0, + lraId, + AccountTransferDAO.getStatusString(ParticipantStatus.Active))); + return ResponseEntity.ok("withdraw failed: insufficient funds"); + } + log.info("withdraw current balance:" + account.getAccountBalance() + + " new balance:" + (account.getAccountBalance() - withdrawAmount)); + account.setAccountBalance(account.getAccountBalance() - withdrawAmount); + AccountTransferDAO.instance().saveAccount(account); + AccountTransferDAO.instance().saveJournal( + new Journal( + WITHDRAW, + accountId, + withdrawAmount, + lraId, + AccountTransferDAO.getStatusString(ParticipantStatus.Active))); + return ResponseEntity.ok("withdraw succeeded"); + } + + /** + * Update LRA state. Do nothing else. + */ + @PutMapping("/complete") + @Complete + public ResponseEntity completeWork(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) throws Exception { + log.info("withdraw complete called for LRA : " + lraId); + Journal journal = AccountTransferDAO.instance().getJournalForLRAid(lraId, WITHDRAW); + journal.setLraState(AccountTransferDAO.getStatusString(ParticipantStatus.Completed)); + AccountTransferDAO.instance().saveJournal(journal); + return ResponseEntity.ok(ParticipantStatus.Completed.name()); + } + + /** + * Read the journal and increase the balance by the previous withdraw amount. + * before the LRA + */ + @PutMapping("/compensate") + @Compensate + public ResponseEntity compensateWork(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) + throws Exception { + log.info("Account withdraw compensate() called for LRA : " + lraId); + Journal journal = AccountTransferDAO.instance().getJournalForLRAid(lraId, WITHDRAW); + journal.setLraState(AccountTransferDAO.getStatusString(ParticipantStatus.Compensating)); + Account account = AccountTransferDAO.instance().getAccountForAccountId(journal.getAccountId()); + if (account != null) { + account.setAccountBalance(account.getAccountBalance() + journal.getJournalAmount()); + AccountTransferDAO.instance().saveAccount(account); + } + journal.setLraState(AccountTransferDAO.getStatusString(ParticipantStatus.Compensated)); + AccountTransferDAO.instance().saveJournal(journal); + return ResponseEntity.ok(ParticipantStatus.Compensated.name()); + } + + @Status + public ResponseEntity status(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId, + @RequestHeader(LRA_HTTP_PARENT_CONTEXT_HEADER) String parentLRA) throws Exception { + return AccountTransferDAO.instance().status(lraId, WITHDRAW); + } + + /** + * Delete journal entry for LRA. + */ + @PutMapping(value = "/after", consumes = "text/plain") + @AfterLRA + public ResponseEntity afterLRA(@RequestHeader(LRA_HTTP_ENDED_CONTEXT_HEADER) String lraId, + String status) throws Exception { + log.info("After LRA Called : " + lraId); + AccountTransferDAO.instance().afterLRA(lraId, status, WITHDRAW); + return ResponseEntity.ok(""); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/resources/application.yaml b/cloudbank-v5/account/src/main/resources/application.yaml new file mode 100644 index 000000000..43a9ff9a6 --- /dev/null +++ b/cloudbank-v5/account/src/main/resources/application.yaml @@ -0,0 +1,37 @@ +# Copyright (c) 2023, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +spring: + application: + name: account + microtx: + lra: + coordinator-url: ${MP_LRA_COORDINATOR_URL} + propagation-active: true + headers-propagation-prefix: "{x-b3-, oracle-tmm-, authorization, refresh-}" + cloud: + config: + import-check: + enabled: false + config: + import: classpath:common.yaml + + liquibase: + change-log: classpath:db/changelog/controller.yaml + url: ${LIQUIBASE_DATASOURCE_URL} + user: ${LIQUIBASE_DATASOURCE_USERNAME} + password: ${LIQUIBASE_DATASOURCE_PASSWORD:} + enabled: ${LIQUIBASE_ENABLED:true} + + datasource: + url: ${SPRING_DATASOURCE_URL} + user: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driver-class-name: oracle.jdbc.OracleDriver + type: oracle.ucp.jdbc.PoolDataSource + oracleucp: + connection-factory-class-name: oracle.jdbc.pool.OracleDataSource + connection-pool-name: AccountConnectionPool + initial-pool-size: 15 + min-pool-size: 10 + max-pool-size: 30 diff --git a/cloudbank-v5/account/src/main/resources/banner.txt b/cloudbank-v5/account/src/main/resources/banner.txt new file mode 100644 index 000000000..d82649082 --- /dev/null +++ b/cloudbank-v5/account/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _ _ + / \ ___ ___ ___ _ _ _ __ | |_ + / _ \ / __/ __/ _ \| | | | '_ \| __| + / ___ \ (_| (_| (_) | |_| | | | | |_ + /_/ \_\___\___\___/ \__,_|_| |_|\__| + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} diff --git a/cloudbank-v5/account/src/main/resources/db/changelog/controller.yaml b/cloudbank-v5/account/src/main/resources/db/changelog/controller.yaml new file mode 100644 index 000000000..955a443e4 --- /dev/null +++ b/cloudbank-v5/account/src/main/resources/db/changelog/controller.yaml @@ -0,0 +1,8 @@ +--- +databaseChangeLog: + - include: + file: classpath:db/changelog/table.sql + - include: + file: classpath:db/changelog/data.sql + - include: + file: classpath:db/changelog/txeventq.sql diff --git a/cloudbank-v5/account/src/main/resources/db/changelog/data.sql b/cloudbank-v5/account/src/main/resources/db/changelog/data.sql new file mode 100644 index 000000000..d5946816e --- /dev/null +++ b/cloudbank-v5/account/src/main/resources/db/changelog/data.sql @@ -0,0 +1,20 @@ +-- liquibase formatted sql + +-- changeset account:1 runAlways:true +DELETE FROM ACCOUNT.JOURNAL; +TRUNCATE TABLE ACCOUNT.ACCOUNTS; + +INSERT INTO ACCOUNT.ACCOUNTS (ACCOUNT_NAME,ACCOUNT_TYPE,CUSTOMER_ID,ACCOUNT_OTHER_DETAILS,ACCOUNT_BALANCE) +VALUES ('Andy''s checking','CH','qwertysdwr','Account Info',-20); +INSERT INTO ACCOUNT.ACCOUNTS (ACCOUNT_NAME,ACCOUNT_TYPE,CUSTOMER_ID,ACCOUNT_OTHER_DETAILS,ACCOUNT_BALANCE) +VALUES ('Mark''s CCard','CC','bkzLp8cozi','Mastercard account',1000); +INSERT INTO ACCOUNT.ACCOUNTS (ACCOUNT_NAME,ACCOUNT_TYPE,CUSTOMER_ID,ACCOUNT_OTHER_DETAILS,ACCOUNT_BALANCE) +VALUES ('Andy''s Saving','SA','qwertysdwr','Savings account',1035); +INSERT INTO ACCOUNT.ACCOUNTS (ACCOUNT_NAME,ACCOUNT_TYPE,CUSTOMER_ID,ACCOUNT_OTHER_DETAILS,ACCOUNT_BALANCE) +VALUES ('Sanjay''s Savings','SA','bkzLp8cozi','Savings Account',1040); +INSERT INTO ACCOUNT.ACCOUNTS (ACCOUNT_NAME,ACCOUNT_TYPE,CUSTOMER_ID,ACCOUNT_OTHER_DETAILS,ACCOUNT_BALANCE) +VALUES ('Sanjays Loan','LO','bkzLp8cozi','Car Loan',-80); +INSERT INTO ACCOUNT.ACCOUNTS (ACCOUNT_NAME,ACCOUNT_TYPE,CUSTOMER_ID,ACCOUNT_OTHER_DETAILS,ACCOUNT_BALANCE) +VALUES ('Mark''s Checking','CH','bkzLp8cozi','Checking Account',-1100); + +--rollback DELETE FROM ACCOUNT.ACCOUNTS; \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/resources/db/changelog/table.sql b/cloudbank-v5/account/src/main/resources/db/changelog/table.sql new file mode 100644 index 000000000..6f7eee597 --- /dev/null +++ b/cloudbank-v5/account/src/main/resources/db/changelog/table.sql @@ -0,0 +1,40 @@ +-- liquibase formatted sql + +--changeset account:1 +--preconditions onFail:MARK_RAN onerror:MARK_RAN +--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM ACCOUNT.ACCOUNTS WHERE 1=2 +DROP TABLE ACCOUNT.ACCOUNTS CASCADE CONSTRAINTS; + +--changeset account:2 +--preconditions onFail:MARK_RAN onerror:MARK_RAN +--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM ACCOUNT.JOURNAL WHERE 1=2 +DROP TABLE ACCOUNT.JOURNAL CASCADE CONSTRAINTS; + +--changeset account:3 +CREATE TABLE ACCOUNT.ACCOUNTS ( + ACCOUNT_ID NUMBER GENERATED ALWAYS AS IDENTITY (START WITH 1 CACHE 20), + ACCOUNT_NAME VARCHAR2(40) NOT NULL, + ACCOUNT_TYPE VARCHAR2(2) CHECK (ACCOUNT_TYPE IN ('CH', 'SA', 'CC', 'LO')), + CUSTOMER_ID VARCHAR2 (20), + ACCOUNT_OPENED_DATE DATE DEFAULT SYSDATE NOT NULL, + ACCOUNT_OTHER_DETAILS VARCHAR2(256), + ACCOUNT_BALANCE NUMBER +) LOGGING; +ALTER TABLE ACCOUNT.ACCOUNTS ADD CONSTRAINT ACCOUNTS_PK PRIMARY KEY (ACCOUNT_ID) USING INDEX LOGGING; +COMMENT ON TABLE ACCOUNT.ACCOUNTS is 'CLOUDBANK ACCOUNTS TABLE'; + +CREATE TABLE ACCOUNT.JOURNAL ( + JOURNAL_ID NUMBER GENERATED ALWAYS AS IDENTITY (START WITH 1 CACHE 20), + JOURNAL_TYPE VARCHAR2(20) NOT NULL, + LRA_ID VARCHAR2 (1024), + LRA_STATE VARCHAR2 (40), + JOURNAL_AMOUNT NUMBER, + ACCOUNT_ID NUMBER NOT NULL +) LOGGING; +ALTER TABLE ACCOUNT.JOURNAL ADD CONSTRAINT JOURNAL_PK PRIMARY KEY (JOURNAL_ID) USING INDEX LOGGING; +ALTER TABLE ACCOUNT.JOURNAL ADD CONSTRAINT JOURNAL_ACCOUNT_FK FOREIGN KEY ( ACCOUNT_ID ) + REFERENCES ACCOUNT.ACCOUNTS ( ACCOUNT_ID ); +COMMENT ON TABLE ACCOUNT.JOURNAL is 'CLOUDBANK JOURNAL TABLE'; + +--rollback DROP TABLE ACCOUNT.ACCOUNTS; +--rollback DROP TABLE ACCOUNT.JOURNAL; \ No newline at end of file diff --git a/cloudbank-v5/account/src/main/resources/db/changelog/txeventq.sql b/cloudbank-v5/account/src/main/resources/db/changelog/txeventq.sql new file mode 100644 index 000000000..5359c3556 --- /dev/null +++ b/cloudbank-v5/account/src/main/resources/db/changelog/txeventq.sql @@ -0,0 +1,51 @@ +-- liquibase formatted sql + +--changeset account:1 +grant execute on dbms_aq to account; +grant execute on dbms_aqadm to account; +grant execute on dbms_aqin to account; +grant execute on dbms_aqjms TO account; +grant execute on dbms_aqjms_internal to account; + +--rollback revoke dbms_aq from ACCOUNT; +--rollback revoke dbms_aqadm from ACCOUNT; +--rollback revoke dbms_aqin from ACCOUNT; +--rollback revoke dbms_aqjms from ACCOUNT; +--rollback revoke dbms_aqjms_internal from ACCOUNT; + +--changeset account:2 endDelimiter:/ +begin + -- deposits + begin + dbms_aqadm.create_queue_table( + queue_table => 'ACCOUNT.deposits_qt', + queue_payload_type => 'SYS.AQ$_JMS_TEXT_MESSAGE', + multiple_consumers => false); + dbms_aqadm.create_queue( + queue_name => 'ACCOUNT.deposits', + queue_table => 'ACCOUNT.deposits_qt'); + dbms_aqadm.start_queue( + queue_name => 'ACCOUNT.deposits'); + exception when others then + dbms_output.put_line(SQLCODE); + end; + + -- clearances + begin + dbms_aqadm.create_queue_table( + queue_table => 'ACCOUNT.clearances_qt', + queue_payload_type => 'SYS.AQ$_JMS_TEXT_MESSAGE', + multiple_consumers => false); + dbms_aqadm.create_queue( + queue_name => 'ACCOUNT.clearances', + queue_table => 'ACCOUNT.clearances_qt'); + dbms_aqadm.start_queue( + queue_name => 'ACCOUNT.clearances'); + exception when others then + dbms_output.put_line(SQLCODE); + end; +end; +/ + +--rollback exec DBMS_AQADM.DROP_QUEUE_TABLE(queue_table => 'ACCOUNT.clearances_qt', force => TRUE); +--rollback exec DBMS_AQADM.DROP_QUEUE_TABLE(queue_table => 'ACCOUNT.deposits_qt', force => TRUE); \ No newline at end of file diff --git a/cloudbank-v5/account/src/test/java/com/example/accounts/AccountsApplicationTests.java b/cloudbank-v5/account/src/test/java/com/example/accounts/AccountsApplicationTests.java new file mode 100644 index 000000000..79de15d3d --- /dev/null +++ b/cloudbank-v5/account/src/test/java/com/example/accounts/AccountsApplicationTests.java @@ -0,0 +1,18 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.accounts; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@Disabled +@SpringBootTest +class AccountsApplicationTests { + + @Test + void contextLoads() { + + } +} diff --git a/cloudbank-v5/apisix-routes/create-accounts-route.sh b/cloudbank-v5/apisix-routes/create-accounts-route.sh new file mode 100644 index 000000000..b74e41c82 --- /dev/null +++ b/cloudbank-v5/apisix-routes/create-accounts-route.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +if [ $# -eq 0 ]; then + echo "You must supply the API key as an argument" + exit 1 +fi + +curl http://localhost:9180/apisix/admin/routes/1000 \ + -H "X-API-KEY: $1" \ + -X PUT \ + -i \ + --data-binary @- << EOF +{ + "name": "accounts", + "labels": { + "version": "1.0" + }, + "desc": "ACCOUNT Service", + "uri": "/api/v1/account*", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "HEAD" + ], + "upstream": { + "service_name": "ACCOUNT", + "type": "roundrobin", + "discovery_type": "eureka" + }, + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + } + }, + "prometheus": { + "prefer_name": true + } + } +} +EOF diff --git a/cloudbank-v5/apisix-routes/create-all-routes.sh b/cloudbank-v5/apisix-routes/create-all-routes.sh new file mode 100644 index 000000000..07c578455 --- /dev/null +++ b/cloudbank-v5/apisix-routes/create-all-routes.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Copyright (c) 2024, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +if [ $# -eq 0 ]; then + echo "You must supply the API key as an argument" + exit 1 +fi + +source ./create-accounts-route.sh $1 +source ./create-creditscore-route.sh $1 +source ./create-customer-route.sh $1 +source ./create-testrunner-route.sh $1 +source ./create-transfer-route.sh $1 \ No newline at end of file diff --git a/cloudbank-v5/apisix-routes/create-creditscore-route.sh b/cloudbank-v5/apisix-routes/create-creditscore-route.sh new file mode 100644 index 000000000..9a382dc2d --- /dev/null +++ b/cloudbank-v5/apisix-routes/create-creditscore-route.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +if [ $# -eq 0 ]; then + echo "You must supply the API key as an argument" + exit 1 +fi + +curl http://localhost:9180/apisix/admin/routes/1001 \ + -H "X-API-KEY: $1" \ + -X PUT \ + -i \ + --data-binary @- << EOF +{ + "name": "creditscore", + "labels": { + "version": "1.0" + }, + "desc": "CREDITSCORE Service", + "uri": "/api/v1/creditscore*", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "HEAD" + ], + "upstream": { + "service_name": "CREDITSCORE", + "type": "roundrobin", + "discovery_type": "eureka" + }, + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + } + }, + "prometheus": { + "prefer_name": true + } + } +} +EOF \ No newline at end of file diff --git a/cloudbank-v5/apisix-routes/create-customer-route.sh b/cloudbank-v5/apisix-routes/create-customer-route.sh new file mode 100644 index 000000000..a2fae0836 --- /dev/null +++ b/cloudbank-v5/apisix-routes/create-customer-route.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +if [ $# -eq 0 ]; then + echo "You must supply the API key as an argument" + exit 1 +fi + +curl http://localhost:9180/apisix/admin/routes/1003 \ + -H "X-API-KEY: $1" \ + -X PUT \ + -i \ + --data-binary @- << EOF +{ + "name": "customer", + "labels": { + "version": "1.0" + }, + "desc": "CUSTOMER Service", + "uri": "/api/v1/customer*", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "HEAD" + ], + "upstream": { + "service_name": "CUSTOMER", + "type": "roundrobin", + "discovery_type": "eureka" + }, + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + } + }, + "prometheus": { + "prefer_name": true + } + } +} +EOF diff --git a/cloudbank-v5/apisix-routes/create-customer32-route.sh b/cloudbank-v5/apisix-routes/create-customer32-route.sh new file mode 100644 index 000000000..c835b7611 --- /dev/null +++ b/cloudbank-v5/apisix-routes/create-customer32-route.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) 2024, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +if [ $# -eq 0 ]; then + echo "You must supply the API key as an argument" + exit 1 +fi + +curl http://localhost:9180/apisix/admin/routes/1002 \ + -H "X-API-KEY: $1" \ + -X PUT \ + -i \ + --data-binary @- << EOF +{ + "name": "customer32", + "labels": { + "version": "1.0" + }, + "desc": "CUSTOMER32 Service", + "uri": "/api/v2/customer*", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "HEAD" + ], + "upstream": { + "service_name": "CUSTOMER32", + "type": "roundrobin", + "discovery_type": "eureka" + }, + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + } + }, + "prometheus": { + "prefer_name": true + } + } +} +EOF \ No newline at end of file diff --git a/cloudbank-v5/apisix-routes/create-testrunner-route.sh b/cloudbank-v5/apisix-routes/create-testrunner-route.sh new file mode 100644 index 000000000..483e155ae --- /dev/null +++ b/cloudbank-v5/apisix-routes/create-testrunner-route.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) 2024, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +if [ $# -eq 0 ]; then + echo "You must supply the API key as an argument" + exit 1 +fi + +curl http://localhost:9180/apisix/admin/routes/1004 \ + -H "X-API-KEY: $1" \ + -X PUT \ + -i \ + --data-binary @- << EOF +{ + "name": "testrunner", + "labels": { + "version": "1.0" + }, + "desc": "TESTRUNNER Service", + "uri": "/api/v1/testrunner*", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "HEAD" + ], + "upstream": { + "service_name": "TESTRUNNER", + "type": "roundrobin", + "discovery_type": "eureka" + }, + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + } + }, + "prometheus": { + "prefer_name": true + } + } +} +EOF \ No newline at end of file diff --git a/cloudbank-v5/apisix-routes/create-transfer-route.sh b/cloudbank-v5/apisix-routes/create-transfer-route.sh new file mode 100644 index 000000000..1723ebf98 --- /dev/null +++ b/cloudbank-v5/apisix-routes/create-transfer-route.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) 2024, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +if [ $# -eq 0 ]; then + echo "You must supply the API key as an argument" + exit 1 +fi + +curl http://localhost:9180/apisix/admin/routes/1005 \ + -H "X-API-KEY: $1" \ + -X PUT \ + -i \ + --data-binary @- << EOF +{ + "name": "transfer", + "labels": { + "version": "1.0" + }, + "desc": "TRANSFER Service", + "uri": "/transfer*", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "HEAD" + ], + "upstream": { + "service_name": "TRANSFER", + "type": "roundrobin", + "discovery_type": "eureka" + }, + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + } + }, + "prometheus": { + "prefer_name": true + } + } +} +EOF \ No newline at end of file diff --git a/cloudbank-v5/apisix-routes/list-routes.sh b/cloudbank-v5/apisix-routes/list-routes.sh new file mode 100644 index 000000000..0dd9ae5aa --- /dev/null +++ b/cloudbank-v5/apisix-routes/list-routes.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copyright (c) 2024, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +if [ $# -eq 0 ]; then + echo "You must supply the API key as an argument" + exit 1 +fi + +curl http://localhost:9180/apisix/admin/routes \ + -H "X-API-KEY: $1" \ No newline at end of file diff --git a/cloudbank-v5/buildtools/pom.xml b/cloudbank-v5/buildtools/pom.xml new file mode 100644 index 000000000..260abaf8b --- /dev/null +++ b/cloudbank-v5/buildtools/pom.xml @@ -0,0 +1,13 @@ + + + + + 4.0.0 + com.example + buildtools + 0.0.1-SNAPSHOT + Build Tools + \ No newline at end of file diff --git a/cloudbank-v5/buildtools/src/main/resources/cloudbank/checkstyle/checkstyle.xml b/cloudbank-v5/buildtools/src/main/resources/cloudbank/checkstyle/checkstyle.xml new file mode 100644 index 000000000..9ad277976 --- /dev/null +++ b/cloudbank-v5/buildtools/src/main/resources/cloudbank/checkstyle/checkstyle.xml @@ -0,0 +1,331 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/buildtools/src/main/resources/cloudbank/checkstyle/suppressions.xml b/cloudbank-v5/buildtools/src/main/resources/cloudbank/checkstyle/suppressions.xml new file mode 100644 index 000000000..6bc2697c3 --- /dev/null +++ b/cloudbank-v5/buildtools/src/main/resources/cloudbank/checkstyle/suppressions.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/buildtools/src/main/resources/cloudbank/dependency-check/suppressions.xml b/cloudbank-v5/buildtools/src/main/resources/cloudbank/dependency-check/suppressions.xml new file mode 100644 index 000000000..cbc052c40 --- /dev/null +++ b/cloudbank-v5/buildtools/src/main/resources/cloudbank/dependency-check/suppressions.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/cloudbank-v5/chatbot/helm/Chart.yaml b/cloudbank-v5/chatbot/helm/Chart.yaml new file mode 100644 index 000000000..a64d7527d --- /dev/null +++ b/cloudbank-v5/chatbot/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: chatbot # Replace with your application name +description: A Helm chart for the OBaaS Platform Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/cloudbank-v5/chatbot/helm/templates/NOTES.txt b/cloudbank-v5/chatbot/helm/templates/NOTES.txt new file mode 100644 index 000000000..8d9597d97 --- /dev/null +++ b/cloudbank-v5/chatbot/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "obaas-app.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "obaas-app.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "obaas-app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "obaas-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cloudbank-v5/chatbot/helm/templates/_helpers.tpl b/cloudbank-v5/chatbot/helm/templates/_helpers.tpl new file mode 100644 index 000000000..b8e6d5b79 --- /dev/null +++ b/cloudbank-v5/chatbot/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "obaas-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "obaas-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "obaas-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "obaas-app.labels" -}} +helm.sh/chart: {{ include "obaas-app.chart" . }} +{{ include "obaas-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "obaas-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "obaas-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "obaas-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "obaas-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cloudbank-v5/chatbot/helm/templates/deployment.yaml b/cloudbank-v5/chatbot/helm/templates/deployment.yaml new file mode 100644 index 000000000..16e8ba515 --- /dev/null +++ b/cloudbank-v5/chatbot/helm/templates/deployment.yaml @@ -0,0 +1,135 @@ +# Load obaas configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "obaas-app.fullname" . }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + obaas.framework: SPRING_BOOT +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "obaas-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "obaas-app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "obaas-app.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: ku{{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CONNECT_STRING + value: jdbc:oracle:thin:@$(DB_SERVICE)?TNS_ADMIN=/oracle/tnsadmin + # Lookup ObaaS configuration + {{- $obaas := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-config") }} + {{- if $.Values.obaas.mp_lra.enabled }} + - name: MP_LRA_COORDINATOR_URL + value: {{ $obaas.data.MP_LRA_COORDINATOR_URL | quote }} + - name: MP_LRA_PARTICIPANT_URL + value: {{ $obaas.data.MP_LRA_PARTICIPANT_URL | quote }} + {{- end }} + {{- if $.Values.obaas.eureka.enabled }} + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_CLIENT_REGISTER_WITH_EUREKA + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: {{ $obaas.data.eureka | quote }} + - name: EUREKA_INSTANCE_HOSTNAME + value: {{ include "obaas-app.fullname" . }}-{{ $.Release.Namespace }} + {{- end }} + {{- if $.Values.obaas.otel.enabled }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ $obaas.data.otel | quote }} + {{- end }} + {{- if $.Values.obaas.springboot.enabled }} + - name: SPRING_PROFILES_ACTIVE + value: {{ $obaas.data.SPRING_PROFILES_ACTIVE | quote }} + - name: SPRING_CONFIG_LABEL + value: {{ $obaas.data.SPRING_CONFIG_LABEL | quote }} + - name: SPRING_DATASOURCE_URL + value: "jdbc:oracle:thin:@${DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.username + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.password + - name: DB_SERVICE + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.service + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ $.Values.obaas.database.walletSecret }} + mountPath: /oracle/tnsadmin + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: {{ $.Values.obaas.database.walletSecret }} + secret: + secretName: {{ $.Values.obaas.database.walletSecret }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cloudbank-v5/chatbot/helm/templates/hpa.yaml b/cloudbank-v5/chatbot/helm/templates/hpa.yaml new file mode 100644 index 000000000..928c1e120 --- /dev/null +++ b/cloudbank-v5/chatbot/helm/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "obaas-app.fullname" . }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "obaas-app.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/chatbot/helm/templates/ingress.yaml b/cloudbank-v5/chatbot/helm/templates/ingress.yaml new file mode 100644 index 000000000..796c3fc3c --- /dev/null +++ b/cloudbank-v5/chatbot/helm/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "obaas-app.fullname" . }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "obaas-app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/chatbot/helm/templates/service.yaml b/cloudbank-v5/chatbot/helm/templates/service.yaml new file mode 100644 index 000000000..3f34a0d96 --- /dev/null +++ b/cloudbank-v5/chatbot/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "obaas-app.fullname" . }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "obaas-app.selectorLabels" . | nindent 4 }} diff --git a/cloudbank-v5/chatbot/helm/templates/serviceaccount.yaml b/cloudbank-v5/chatbot/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..f4a38f1fb --- /dev/null +++ b/cloudbank-v5/chatbot/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "obaas-app.serviceAccountName" . }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/cloudbank-v5/chatbot/helm/templates/tests/test-connection.yaml b/cloudbank-v5/chatbot/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..8596a023f --- /dev/null +++ b/cloudbank-v5/chatbot/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "obaas-app.fullname" . }}-test-connection" + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "obaas-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cloudbank-v5/chatbot/helm/values.yaml b/cloudbank-v5/chatbot/helm/values.yaml new file mode 100644 index 000000000..5948e72c2 --- /dev/null +++ b/cloudbank-v5/chatbot/helm/values.yaml @@ -0,0 +1,157 @@ +# Default values for obaas-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: "" + # This sets the pull policy for images. + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: + signoz.io/path: /actuator/prometheus + signoz.io/port: "8080" + signoz.io/scrape: "true" +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 8080 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + requests: + cpu: 100m + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +obaas: + namespace: application + database: + credentialsSecret: obaas-app-db-secrets + walletSecret: obaas-adb-tns-admin-1 + runsqljob: false + + otel: + enabled: true + # MicroProfile LRA + mp_lra: + enabled: false + # Spring Boot applications + springboot: + enabled: true + eureka: + enabled: true + configServer: + enabled: false \ No newline at end of file diff --git a/cloudbank-v5/chatbot/pom.xml b/cloudbank-v5/chatbot/pom.xml new file mode 100644 index 000000000..fb7b898ff --- /dev/null +++ b/cloudbank-v5/chatbot/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + + chatbot + 0.0.1-SNAPSHOT + chatbot + A Simple ChatBot Application + + + 21 + 1.0.0-M6 + + + + + org.springframework.ai + spring-ai-ollama-spring-boot-starter + + + com.example + common + ${project.version} + + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.eclipse.jkube + kubernetes-maven-plugin + ${jkube.version} + + + + my-repository/chatbot:${project.version} + + ghcr.io/oracle/openjdk-image-obaas:21 + + dir + /deployments + + java -jar /deployments/${project.artifactId}-${project.version}.jar + + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/chatbot/src/main/java/com/example/chatbot/ChatbotApplication.java b/cloudbank-v5/chatbot/src/main/java/com/example/chatbot/ChatbotApplication.java new file mode 100644 index 000000000..2064d9306 --- /dev/null +++ b/cloudbank-v5/chatbot/src/main/java/com/example/chatbot/ChatbotApplication.java @@ -0,0 +1,19 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.chatbot; + +import com.example.common.filter.LoggingFilterConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +@SpringBootApplication +@Import(LoggingFilterConfig.class) +public class ChatbotApplication { + + public static void main(String[] args) { + SpringApplication.run(ChatbotApplication.class, args); + } + +} diff --git a/cloudbank-v5/chatbot/src/main/java/com/example/chatbot/controller/ChatController.java b/cloudbank-v5/chatbot/src/main/java/com/example/chatbot/controller/ChatController.java new file mode 100644 index 000000000..4c26d86a1 --- /dev/null +++ b/cloudbank-v5/chatbot/src/main/java/com/example/chatbot/controller/ChatController.java @@ -0,0 +1,47 @@ +// Copyright (c) 2024, 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.chatbot.controller; + + +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.ollama.api.OllamaModel; +import org.springframework.ai.ollama.api.OllamaOptions; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/chat") +public class ChatController { + + final ChatModel chatModel; + + public ChatController(ChatModel chatModel) { + this.chatModel = chatModel; + } + + /** + * Returns a chatresponse on a provided question. + * @param question Question asked. + * @return Chatresponse content. + */ + @PostMapping + public String chat(@RequestBody String question) { + + ChatResponse response = chatModel.call( + new Prompt(question, + OllamaOptions.builder() + .model(OllamaModel.LLAMA3) + .temperature(0.4d) + .build() + )); + + return response.getResult().getOutput().getText(); + + } + +} \ No newline at end of file diff --git a/cloudbank-v5/chatbot/src/main/resources/application.yaml b/cloudbank-v5/chatbot/src/main/resources/application.yaml new file mode 100644 index 000000000..1eeff4e5c --- /dev/null +++ b/cloudbank-v5/chatbot/src/main/resources/application.yaml @@ -0,0 +1,19 @@ +# Copyright (c) 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +spring: + application: + name: chatbot + ai: + ollama: + base-url: http://ollama.ollama.svc.cluster.local:11434 + chat: + enabled: true + options: + model: llama3 + cloud: + config: + import-check: + enabled: false + config: + import: classpath:common.yaml \ No newline at end of file diff --git a/cloudbank-v5/chatbot/src/main/resources/banner.txt b/cloudbank-v5/chatbot/src/main/resources/banner.txt new file mode 100644 index 000000000..f0d86c0fc --- /dev/null +++ b/cloudbank-v5/chatbot/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + ____ _ _ _ _ + / ___| |__ __ _| |_| |__ ___ | |_ + | | | '_ \ / _` | __| '_ \ / _ \| __| + | |___| | | | (_| | |_| |_) | (_) | |_ + \____|_| |_|\__,_|\__|_.__/ \___/ \__| + + ${application.title} ${application.version} + Powered by Spring Boot ${spring-boot.version} diff --git a/cloudbank-v5/checks/.gitignore b/cloudbank-v5/checks/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/cloudbank-v5/checks/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/cloudbank-v5/checks/helm/Chart.yaml b/cloudbank-v5/checks/helm/Chart.yaml new file mode 100644 index 000000000..df46a4df1 --- /dev/null +++ b/cloudbank-v5/checks/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: checks # Replace with your application name +description: A Helm chart for the OBaaS Platform Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.2.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.0.1" diff --git a/cloudbank-v5/checks/helm/README.md b/cloudbank-v5/checks/helm/README.md new file mode 100644 index 000000000..9403cc5e0 --- /dev/null +++ b/cloudbank-v5/checks/helm/README.md @@ -0,0 +1,16 @@ +# OBaas Sample App Chart + +This chart provides an extensible sample for applications running on [OBaaS](https://oracle.github.io/microservices-datadriven/obaas/). + +To use this chart for a given application, download the chart and update the [Chart.name](./Chart.yaml) value to your application's name. + +## Customizing the chart + +The OBaaS sample app chart is meant to serve as a developer template, and is fully customizable. + +Standard parameters for Kubernetes options like node affinity, HPAs, ingress and more are provided in the [values.yaml file](./values.yaml). + +## OBaaS options + +Within the [values.yaml file](./values.yaml), the `obaas` key allows chart developers to enable or disable OBaaS integrations like database connectivity, OpenTelemetry, MicroProfile LRA, SpringBoot, and Eureka. +enabled: true diff --git a/cloudbank-v5/checks/helm/templates/NOTES.txt b/cloudbank-v5/checks/helm/templates/NOTES.txt new file mode 100644 index 000000000..8d9597d97 --- /dev/null +++ b/cloudbank-v5/checks/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "obaas-app.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "obaas-app.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "obaas-app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "obaas-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cloudbank-v5/checks/helm/templates/_helpers.tpl b/cloudbank-v5/checks/helm/templates/_helpers.tpl new file mode 100644 index 000000000..b8e6d5b79 --- /dev/null +++ b/cloudbank-v5/checks/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "obaas-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "obaas-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "obaas-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "obaas-app.labels" -}} +helm.sh/chart: {{ include "obaas-app.chart" . }} +{{ include "obaas-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "obaas-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "obaas-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "obaas-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "obaas-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cloudbank-v5/checks/helm/templates/deployment.yaml b/cloudbank-v5/checks/helm/templates/deployment.yaml new file mode 100644 index 000000000..d4902a2f2 --- /dev/null +++ b/cloudbank-v5/checks/helm/templates/deployment.yaml @@ -0,0 +1,147 @@ +# Load obaas configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + obaas.framework: SPRING_BOOT +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "obaas-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "obaas-app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "obaas-app.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CONNECT_STRING + value: jdbc:oracle:thin:@$(SPRING_DB_SERVICE)?TNS_ADMIN=/oracle/tnsadmin + # Lookup ObaaS configuration + {{- $obaas := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-config") }} + {{- $obaasObs := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-observability-config") }} + {{- if $.Values.obaas.mp_lra.enabled }} + - name: MP_LRA_COORDINATOR_URL + value: {{ $obaas.data.otmm | quote }} + {{- end }} + {{- if $.Values.obaas.eureka.enabled }} + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_CLIENT_REGISTER_WITH_EUREKA + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: {{ $obaas.data.eureka | quote }} + - name: EUREKA_INSTANCE_HOSTNAME + value: {{ include "obaas-app.fullname" . }}-{{ $.Release.Namespace }} + {{- end }} + {{- if $.Values.obaas.otel.enabled }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ (index $obaasObs.data "signoz-otel-collector") | quote }} + {{- end }} + {{- if $.Values.obaas.springboot.enabled }} + - name: SPRING_PROFILES_ACTIVE + value: {{ $obaas.data.SPRING_PROFILES_ACTIVE | quote }} + {{- if $.Values.obaas.database.enabled }} + - name: SPRING_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.username + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.password + - name: LIQUIBASE_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_username + - name: LIQUIBASE_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_password + - name: LIQUIBASE_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DB_SERVICE + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.service + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ $.Values.obaas.database.walletSecret }} + mountPath: /oracle/tnsadmin + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: {{ $.Values.obaas.database.walletSecret }} + secret: + secretName: {{ $.Values.obaas.database.walletSecret }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cloudbank-v5/checks/helm/templates/hpa.yaml b/cloudbank-v5/checks/helm/templates/hpa.yaml new file mode 100644 index 000000000..7c8805fb0 --- /dev/null +++ b/cloudbank-v5/checks/helm/templates/hpa.yaml @@ -0,0 +1,33 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "obaas-app.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/checks/helm/templates/ingress.yaml b/cloudbank-v5/checks/helm/templates/ingress.yaml new file mode 100644 index 000000000..50aa98cd8 --- /dev/null +++ b/cloudbank-v5/checks/helm/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "obaas-app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/checks/helm/templates/service.yaml b/cloudbank-v5/checks/helm/templates/service.yaml new file mode 100644 index 000000000..5876cf66d --- /dev/null +++ b/cloudbank-v5/checks/helm/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "obaas-app.selectorLabels" . | nindent 4 }} diff --git a/cloudbank-v5/checks/helm/templates/serviceaccount.yaml b/cloudbank-v5/checks/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..a22b73ef6 --- /dev/null +++ b/cloudbank-v5/checks/helm/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "obaas-app.serviceAccountName" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/cloudbank-v5/checks/helm/templates/tests/test-connection.yaml b/cloudbank-v5/checks/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..8596a023f --- /dev/null +++ b/cloudbank-v5/checks/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "obaas-app.fullname" . }}-test-connection" + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "obaas-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cloudbank-v5/checks/helm/values.yaml b/cloudbank-v5/checks/helm/values.yaml new file mode 100644 index 000000000..6c159858a --- /dev/null +++ b/cloudbank-v5/checks/helm/values.yaml @@ -0,0 +1,155 @@ +# Default values for obaas-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: "my-repository/checks" + # This sets the pull policy for images. + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "0.0.1-SNAPSHOT" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: + signoz.io/path: /actuator/prometheus + signoz.io/port: "8080" + signoz.io/scrape: "true" +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 8080 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + requests: + cpu: 100m + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Oracle Backend for Microservices and AI Settings. +obaas: + namespace: obaas-dev # Replace with your namespace + database: + enabled: true # If true variables with DB secret content will be created + credentialsSecret: account-db-secrets # Replace with your secret name + walletSecret: obaas-adb-tns-admin-1 # Replace with your wallet secret name + otel: + enabled: true # Enable OpenTelemetry + # MicroProfile LRA + mp_lra: + enabled: true # Enable OTMM + # Spring Boot applications + springboot: + enabled: true # Enable Spring Boot specific variables + eureka: + enabled: true # Enable Eureka client diff --git a/cloudbank-v5/checks/pom.xml b/cloudbank-v5/checks/pom.xml new file mode 100644 index 000000000..7e4a06898 --- /dev/null +++ b/cloudbank-v5/checks/pom.xml @@ -0,0 +1,73 @@ + + + + + 4.0.0 + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + checks + 0.0.1-SNAPSHOT + checks + Check Processing Application + + + com.oracle.database.spring + oracle-spring-boot-starter-aqjms + ${oracle-springboot-starter.version} + + + com.oracle.database.spring + oracle-spring-boot-starter-wallet + ${oracle-springboot-starter.version} + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + com.example + common + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.graalvm.buildtools + native-maven-plugin + + + org.eclipse.jkube + kubernetes-maven-plugin + ${jkube.version} + + + + my-repository/checks:${project.version} + + ghcr.io/oracle/openjdk-image-obaas:21 + + dir + /deployments + + java -jar /deployments/${project.artifactId}-${project.version}.jar + + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/java/com/example/checks/ChecksApplication.java b/cloudbank-v5/checks/src/main/java/com/example/checks/ChecksApplication.java new file mode 100644 index 000000000..4d123d1ab --- /dev/null +++ b/cloudbank-v5/checks/src/main/java/com/example/checks/ChecksApplication.java @@ -0,0 +1,107 @@ +// Copyright (c) 2023, 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.checks; + +import java.sql.SQLException; +import javax.sql.DataSource; + +import com.example.common.filter.LoggingFilterConfig; +import com.example.common.ucp.UCPTelemetry; +import jakarta.jms.ConnectionFactory; +import jakarta.jms.JMSException; +import lombok.extern.slf4j.Slf4j; +import oracle.jakarta.jms.AQjmsFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.support.converter.MappingJackson2MessageConverter; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.converter.MessageType; + +@SpringBootApplication +@EnableFeignClients +@EnableJms +@EnableDiscoveryClient +@Import({ LoggingFilterConfig.class, UCPTelemetry.class }) +@Slf4j +public class ChecksApplication { + + /** + * Application main class. + * + * @param args - main arguments + */ + public static void main(String[] args) { + SpringApplication.run(ChecksApplication.class, args); + } + + /** + * Serialize message content to json using TextMessage. + * + * @return TO-DO + */ + @Bean + public MessageConverter jacksonJmsMessageConverter() { + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + converter.setTargetType(MessageType.TEXT); + converter.setTypeIdPropertyName("_type"); + return converter; + } + + /** + * TO-DO. + * + * @param connectionFactory TO-DO + * @return TO-DO + */ + @Bean + public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { + JmsTemplate jmsTemplate = new JmsTemplate(); + jmsTemplate.setConnectionFactory(connectionFactory); + jmsTemplate.setMessageConverter(jacksonJmsMessageConverter()); + return jmsTemplate; + } + + /** + * TO-DO. + * + * @param connectionFactory TO-DO + * @param configurer TO-DO + * @return TO-DO + */ + @Bean + public JmsListenerContainerFactory factory(ConnectionFactory connectionFactory, + DefaultJmsListenerContainerFactoryConfigurer configurer) { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + // This provides all boot's default to this factory, including the message + // converter + configurer.configure(factory, connectionFactory); + // You could still override some of Boot's default if necessary. + return factory; + } + + /** + * Initialize AQ JMS ConnectionFactory. + * + * @param ds Oracle Datasource. + * @return ConnectionFactory The AQ JMS ConnectionFactory. + * @throws JMSException when an error is encountered while creating a JMS + * ConnectionFactory. + * @throws SQLException when an error is encountered while accessing backing + * Oracle DataSource. + */ + @Bean + public ConnectionFactory aqJmsConnectionFactory(DataSource ds) throws JMSException, SQLException { + return AQjmsFactory.getConnectionFactory(ds.unwrap(DataSource.class)); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/java/com/example/checks/clients/AccountClient.java b/cloudbank-v5/checks/src/main/java/com/example/checks/clients/AccountClient.java new file mode 100644 index 000000000..93d223369 --- /dev/null +++ b/cloudbank-v5/checks/src/main/java/com/example/checks/clients/AccountClient.java @@ -0,0 +1,20 @@ +// Copyright (c) 2023, 2024, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.checks.clients; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient("account") +public interface AccountClient { + + @PostMapping("/api/v1/account/journal") + void journal(@RequestBody Journal journal); + + @PostMapping("/api/v1/account/journal/{journalId}/clear") + void clear(@PathVariable long journalId); + +} \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/java/com/example/checks/clients/Journal.java b/cloudbank-v5/checks/src/main/java/com/example/checks/clients/Journal.java new file mode 100644 index 000000000..33f79d015 --- /dev/null +++ b/cloudbank-v5/checks/src/main/java/com/example/checks/clients/Journal.java @@ -0,0 +1,34 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.checks.clients; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Journal { + private long journalId; + private String journalType; + private long accountId; + private String lraId; + private String lraState; + private long journalAmount; + + /** + * Create Journal object. + * @param journalType Journal Type + * @param accountId Account Id + * @param journalAmount Amount + */ + public Journal(String journalType, long accountId, long journalAmount) { + this.journalType = journalType; + this.accountId = accountId; + this.journalAmount = journalAmount; + this.lraId = "0"; + this.lraState = ""; + } +} \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/java/com/example/checks/controller/CheckReceiver.java b/cloudbank-v5/checks/src/main/java/com/example/checks/controller/CheckReceiver.java new file mode 100644 index 000000000..aac721083 --- /dev/null +++ b/cloudbank-v5/checks/src/main/java/com/example/checks/controller/CheckReceiver.java @@ -0,0 +1,27 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.checks.controller; + +import com.example.checks.clients.Journal; +import com.example.checks.service.AccountService; +import com.example.testrunner.model.CheckDeposit; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class CheckReceiver { + + private AccountService accountService; + + public CheckReceiver(AccountService accountService) { + this.accountService = accountService; + } + + @JmsListener(destination = "deposits", containerFactory = "factory") + public void receiveMessage(CheckDeposit deposit) { + System.out.println("Received deposit <" + deposit + ">"); + accountService.journal(new Journal("PENDING", deposit.getAccountId(), deposit.getAmount())); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/java/com/example/checks/controller/ClearanceReceiver.java b/cloudbank-v5/checks/src/main/java/com/example/checks/controller/ClearanceReceiver.java new file mode 100644 index 000000000..bb33d448c --- /dev/null +++ b/cloudbank-v5/checks/src/main/java/com/example/checks/controller/ClearanceReceiver.java @@ -0,0 +1,26 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.checks.controller; + +import com.example.checks.service.AccountService; +import com.example.testrunner.model.Clearance; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class ClearanceReceiver { + + private AccountService accountService; + + public ClearanceReceiver(AccountService accountService) { + this.accountService = accountService; + } + + @JmsListener(destination = "clearances", containerFactory = "factory") + public void receiveMessage(Clearance clearance) { + System.out.println("Received clearance <" + clearance + ">"); + accountService.clear(clearance); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/java/com/example/checks/service/AccountService.java b/cloudbank-v5/checks/src/main/java/com/example/checks/service/AccountService.java new file mode 100644 index 000000000..9209401b3 --- /dev/null +++ b/cloudbank-v5/checks/src/main/java/com/example/checks/service/AccountService.java @@ -0,0 +1,26 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.checks.service; + +import com.example.checks.clients.AccountClient; +import com.example.checks.clients.Journal; +import com.example.testrunner.model.Clearance; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AccountService { + + private final AccountClient accountClient; + + public void journal(Journal journal) { + accountClient.journal(journal); + } + + public void clear(Clearance clearance) { + accountClient.clear(clearance.getJournalId()); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/java/com/example/testrunner/model/CheckDeposit.java b/cloudbank-v5/checks/src/main/java/com/example/testrunner/model/CheckDeposit.java new file mode 100644 index 000000000..cc600c2d7 --- /dev/null +++ b/cloudbank-v5/checks/src/main/java/com/example/testrunner/model/CheckDeposit.java @@ -0,0 +1,16 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.testrunner.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CheckDeposit { + private long accountId; + private long amount; +} \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/java/com/example/testrunner/model/Clearance.java b/cloudbank-v5/checks/src/main/java/com/example/testrunner/model/Clearance.java new file mode 100644 index 000000000..2ae467220 --- /dev/null +++ b/cloudbank-v5/checks/src/main/java/com/example/testrunner/model/Clearance.java @@ -0,0 +1,15 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.testrunner.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Clearance { + private long journalId; +} \ No newline at end of file diff --git a/cloudbank-v5/checks/src/main/resources/application.yaml b/cloudbank-v5/checks/src/main/resources/application.yaml new file mode 100644 index 000000000..80a967db5 --- /dev/null +++ b/cloudbank-v5/checks/src/main/resources/application.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2023, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +spring: + application: + name: checks + datasource: + url: ${SPRING_DATASOURCE_URL} + user: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + type: oracle.ucp.jdbc.PoolDataSource + oracleucp: + connection-factory-class-name: oracle.jdbc.pool.OracleDataSource + connection-pool-name: ChecksConnectionPool + initial-pool-size: 15 + min-pool-size: 10 + max-pool-size: 30 + cloud: + config: + import-check: + enabled: false + config: + import: classpath:common.yaml diff --git a/cloudbank-v5/checks/src/main/resources/banner.txt b/cloudbank-v5/checks/src/main/resources/banner.txt new file mode 100644 index 000000000..798b41869 --- /dev/null +++ b/cloudbank-v5/checks/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + ____ _ _ + / ___| |__ ___ ___| | _____ + | | | '_ \ / _ \/ __| |/ / __| + | |___| | | | __/ (__| <\__ \ + \____|_| |_|\___|\___|_|\_\___/ + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} diff --git a/cloudbank-v5/checks/src/test/java/com/example/checks/ChecksApplicationTests.java b/cloudbank-v5/checks/src/test/java/com/example/checks/ChecksApplicationTests.java new file mode 100644 index 000000000..60002c652 --- /dev/null +++ b/cloudbank-v5/checks/src/test/java/com/example/checks/ChecksApplicationTests.java @@ -0,0 +1,18 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.checks; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@Disabled +@SpringBootTest +class ChecksApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/cloudbank-v5/cloudbank-test-doc.md b/cloudbank-v5/cloudbank-test-doc.md new file mode 100644 index 000000000..981fb5079 --- /dev/null +++ b/cloudbank-v5/cloudbank-test-doc.md @@ -0,0 +1,554 @@ +# CloudBank Services Testing Guide + +## Overview + +This guide walks you through testing your CloudBank microservices deployment in an Oracle Backend as a Service (OBaaS) environment. You'll verify that all services are functioning correctly, test the distributed transaction capabilities using Long Running Actions (LRA), and explore the observability features built into the platform. + +## Prerequisites + +Before beginning these tests, ensure you have: + +- A running OBaaS environment with CloudBank services deployed +- `kubectl` command-line tool installed and configured +- `curl` command-line tool for making HTTP requests +- `jq` for parsing JSON responses (optional but recommended) +- Access to the Kubernetes cluster where CloudBank is deployed + +## Step 1: Getting Started + +### 1.1 Retrieving the External IP Address + +First, you need to obtain the external IP address of your ingress controller. This IP address will be used as the base URL for all API requests. + +**1.1.1 Get the ingress controller information:** + +Run the following command: + + ```shell + kubectl -n ingress-nginx get service ingress-nginx-controller + ``` + +**1.1.2 Review the output:** + +You should see output similar to this: + + ```text + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE + ingress-nginx-controller LoadBalancer 10.96.172.148 146.235.207.230 80:31393/TCP,443:30506/TCP 158m + ``` + +**1.1.3 Create an environment variable:** + +Use the following command to automatically extract and set the external IP address: + + ```shell + export IP=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + ``` + +You can verify the IP was set correctly by running: + + ```shell + echo $IP + ``` + +This should display the external IP address (e.g., `146.235.207.230`). + +--- + +## Step 2: Testing Core Services + +### 2.1 Account Service + +The Account service manages customer bank accounts including checking and credit card accounts. + +**2.1.1 Test the REST Endpoint:** + +Execute the following command to retrieve all accounts: + +```shell +curl -s http://$IP/api/v1/accounts | jq +``` + +**2.1.2 Verify the Response:** + +You should receive a JSON array containing account objects. Each account includes details such as account ID, customer ID, account type, balance, and opening date: + +```json +[ + { + "accountBalance": -20, + "accountCustomerId": "qwertysdwr", + "accountId": 1, + "accountName": "Andy's checking", + "accountOpenedDate": "2023-06-26T17:39:37.000+00:00", + "accountOtherDetails": "Account Info", + "accountType": "CH" + }, + { + "accountBalance": 1000, + "accountCustomerId": "bkzLp8cozi", + "accountId": 2, + "accountName": "Mark's CCard", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Mastercard account", + "accountType": "CC" + } +] +``` + +**Note:** Account types include "CH" for checking accounts and "CC" for credit card accounts. + +--- + +### 2.2 Customer Service + +The Customer service handles customer information and profile management. + +**2.2.1 Retrieve All Customers:** + +To view all customers in the system: + +```shell +curl -s http://$IP/api/v1/customer | jq +``` + +**Expected Response:** + +```json +[ + { + "customerEmail": "andy@andy.com", + "customerId": "qwertysdwr", + "customerName": "Andy", + "customerOtherDetails": "Somekind of Info", + "customerPassword": "SuperSecret", + "dateBecameCustomer": "2023-11-02T17:30:12.000+00:00" + } +] +``` + +**2.2.2 Create a New Customer:** + +To create a new customer, send a POST request with customer details: + +```shell +curl -i -X POST -H 'Content-Type: application/json' \ + -d '{"customerId": "bobsmith", "customerName": "Bob Smith", "customerEmail": "bob@smith.com"}' \ + http://$IP/api/v1/customer +``` + +**Expected Response:** + +A successful creation returns HTTP status 201 with a Location header pointing to the newly created resource: + +```text +HTTP/1.1 201 +Location: http://localhost:8080/api/v1/customer/bobsmith +Content-Length: 0 +Date: Tue, 03 Sep 2024 21:01:25 GMT +``` + +--- + +### 2.3 Credit Score Service + +The Credit Score service provides credit scoring information for customers. + +**2.3.1 Check Credit Score:** + +Retrieve the current credit score data: + +```shell +curl -s http://$IP/api/v1/creditscore | jq +``` + +**Expected Response:** + +```json +{ + "Date": "2023-12-26", + "Credit Score": "574" +} +``` + +--- + +### 2.4 Check Processing Service + +The Check service handles check deposits and clearances through a multi-step workflow involving journal entries. + +**2.4.1 Deposit a Check:** + +Initiate a check deposit for an existing account: + +```shell +curl -i -X POST -H 'Content-Type: application/json' \ + -d '{"accountId": 1, "amount": 256}' \ + http://$IP/api/v1/testrunner/deposit +``` + +**Important:** Replace `1` with an actual account ID from your environment. + +**Expected Response:** + +```text +HTTP/1.1 201 +Content-Type: application/json +Transfer-Encoding: chunked +Date: Thu, 02 Nov 2023 18:02:06 GMT + +{"accountId":1,"amount":256} +``` + +**2.4.2 Verify Check Service Logs:** + +Confirm the deposit was received by checking the service logs: + +```shell +kubectl logs -n application svc/checks +``` + +**Expected Log Entry:** + +```log +Received deposit +``` + +**2.4.3 Check Journal Entries:** + +View the journal entries for the account to see the pending deposit: + +```shell +curl -i http://$IP/api/v1/account/1/journal +``` + +Replace `1` with your account number. + +**Expected Response:** + +```text +HTTP/1.1 200 +Content-Type: application/json +Transfer-Encoding: chunked +Date: Thu, 02 Nov 2023 18:06:45 GMT + +[{"journalId":1,"journalType":"PENDING","accountId":1,"lraId":"0","lraState":null,"journalAmount":256}] +``` + +**Note:** The journal type is "PENDING" at this stage, indicating the check has not yet cleared. + +**2.4.4 Clear the Check:** + +Process the check clearance using the journal ID from the previous step: + +```shell +curl -i -X POST -H 'Content-Type: application/json' \ + -d '{"journalId": 1}' \ + http://$IP/api/v1/testrunner/clear +``` + +Replace `1` with the actual journal ID. + +**Expected Response:** + +```text +HTTP/1.1 201 +Content-Type: application/json +Transfer-Encoding: chunked +Date: Thu, 02 Nov 2023 18:09:17 GMT + +{"journalId":1} +``` + +**2.4.5 Verify Clearance in Logs:** + +Check the service logs again to confirm clearance was processed: + +```shell +kubectl logs -n application svc/checks +``` + +**Expected Log Entry:** + +```log +Received clearance +``` + +**2.4.6 Verify Final Journal Entry:** + +Check the journal entries again to confirm the deposit has been completed: + +```shell +curl -i http://$IP/api/v1/account/1/journal +``` + +**Expected Response:** + +```text +HTTP/1.1 200 +Content-Type: application/json +Transfer-Encoding: chunked +Date: Thu, 02 Nov 2023 18:36:31 GMT + +[{"journalId":1,"journalType":"DEPOSIT","accountId":1,"lraId":"0","lraState":null,"journalAmount":256}] +``` + +**Note:** The journal type has changed from "PENDING" to "DEPOSIT", indicating successful clearance. + +--- + +## Step 3: Testing Long Running Actions (LRA) + +Long Running Actions provide distributed transaction coordination across microservices. This test demonstrates a fund transfer between two accounts using the LRA pattern. + +### 3.1 Check Initial Account Balances + +Before performing the transfer, record the initial balances of both accounts: + +```shell +curl -s http://$IP/api/v1/account/1 | jq +curl -s http://$IP/api/v1/account/2 | jq +``` + +**Note:** Account numbers may differ in your environment. Adjust the account IDs as needed. + +**Example Output:** + +```json +{ + "accountId": 1, + "accountName": "Andy's checking", + "accountType": "CH", + "accountCustomerId": "qwertysdwr", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Account Info", + "accountBalance": -20 +} +``` + +```json +{ + "accountId": 2, + "accountName": "Mark's CCard", + "accountType": "CC", + "accountCustomerId": "bkzLp8cozi", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Mastercard account", + "accountBalance": 1000 +} +``` + +### 3.2 Perform the Transfer + +Execute a transfer of funds from one account to another: + +```shell +curl -X POST "http://$IP/transfer?fromAccount=2&toAccount=1&amount=100" +``` + +Adjust the account numbers and amount as needed. + +**Expected Response:** + +```text +transfer status:withdraw succeeded deposit succeeded +``` + +This indicates that both the withdrawal from the source account and the deposit to the destination account completed successfully. + +### 3.3 Verify Account Balances + +Check both accounts again to confirm the transfer was applied correctly: + +```shell +curl -s http://$IP/api/v1/account/1 | jq +curl -s http://$IP/api/v1/account/2 | jq +``` + +**Expected Output:** + +Account 1 balance should have increased by 100: + +```json +{ + "accountId": 1, + "accountName": "Andy's checking", + "accountType": "CH", + "accountCustomerId": "qwertysdwr", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Account Info", + "accountBalance": 80 +} +``` + +Account 2 balance should have decreased by 100: + +```json +{ + "accountId": 2, + "accountName": "Mark's CCard", + "accountType": "CC", + "accountCustomerId": "bkzLp8cozi", + "accountOpenedDate": "2023-11-02T17:23:53.000+00:00", + "accountOtherDetails": "Mastercard account", + "accountBalance": 900 +} +``` + +### 3.4 Review Transfer Service Logs + +Examine the detailed LRA coordination logs: + +```shell +kubectl logs -n application svc/transfer +``` + +**Expected Log Output:** + +```text +2023-12-26T16:50:45.138Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : Started new LRA/transfer Id: http://otmm-tcs.otmm.svc.cluster.local:9000/api/v1/lra-coordinator/ea98ebae-2358-4dd1-9d7c-09f4550d7567 +2023-12-26T16:50:45.139Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : withdraw accountId = 2, amount = 100 +2023-12-26T16:50:45.183Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : withdraw succeeded +2023-12-26T16:50:45.183Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : deposit accountId = 1, amount = 100 +2023-12-26T16:50:45.216Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : withdraw succeeded deposit succeeded +2023-12-26T16:50:45.216Z INFO 1 --- [transfer] [nio-8080-exec-9] [] com.example.transfer.TransferService : LRA/transfer action will be confirm +2023-12-26T16:50:45.226Z INFO 1 --- [transfer] [nio-8080-exec-1] [] com.example.transfer.TransferService : Received confirm for transfer +2023-12-26T16:50:45.233Z INFO 1 --- [transfer] [io-8080-exec-10] [] com.example.transfer.TransferService : Process confirm for transfer +``` + +These logs show the complete LRA lifecycle: initiation, withdrawal, deposit, and confirmation. + +--- + +## Step 4: Observability and Monitoring + +OBaaS includes several built-in observability tools to help you monitor and troubleshoot your microservices. + +### 4.1 Eureka Service Registry + +Eureka provides service discovery, showing all registered microservices. + +**4.1.1 Create a Port Forward:** + +Create a port forward to the Eureka service: + +```shell +kubectl -n eureka port-forward svc/eureka 8761 +``` + +**4.1.2 Access the Dashboard:** + +Open your browser and navigate to: + +``` +http://localhost:8761 +``` + +**4.1.3 Verify Service Registration:** + +The dashboard displays all registered services, their health status, and instance information. Verify that all CloudBank services (account, customer, check, transfer, etc.) appear in the registry. + +--- + +### 4.2 Spring Boot Admin Server + +The Admin Server provides detailed management and monitoring capabilities for Spring Boot applications. + +**4.2.1 Create a Port Forward:** + +Create a port forward to the Admin Server: + +```shell +kubectl port-forward -n admin-server svc/admin-server 8989 +``` + +**4.2.2 Access the Dashboard:** + +Open your browser and navigate to: + +``` +http://localhost:8989 +``` + +**4.2.3 Explore Service Metrics:** + +The dashboard shows detailed metrics, health checks, environment properties, and logging information for each registered service. + +--- + +### 4.3 SigNoz - Distributed Tracing and Observability + +SigNoz provides comprehensive observability including distributed tracing, metrics, and log aggregation. + +**4.3.1 Retrieve Admin Credentials:** + +Get the admin email and password: + +```shell +kubectl -n observability get secret signoz-authn -o jsonpath='{.data.email}' | base64 -d +kubectl -n observability get secret signoz-authn -o jsonpath='{.data.password}' | base64 -d +``` + +**4.3.2 Create a Port Forward:** + +Create a port forward to the SigNoz frontend: + +```shell +kubectl -n observability port-forward svc/obaas-signoz-frontend 3301:3301 +``` + +**4.3.3 Access the Login Page:** + +Open your browser and navigate to: + +``` +http://localhost:3301/login +``` + +Log in using the email and password retrieved in step 4.3.1. + +**4.3.4 Explore Pre-installed Dashboards:** + +Navigate through the pre-configured dashboards to view system metrics, service performance, and infrastructure health. + +**4.3.5 View Distributed Traces:** + +To explore distributed traces: + +1. Click on the "Traces" section in the navigation menu +2. Select the `customer` service from the service dropdown to view traces related to customer operations +3. Click on any trace to expand and view detailed timing information across all involved services +4. Use the trace visualization to identify performance bottlenecks and understand request flow + +**4.3.6 Browse Application Logs:** + +To view application logs: + +1. Click on the "Logs" section in the navigation menu +2. Select the `customer` service from the service dropdown +3. Browse through application logs with full context and filtering capabilities +4. Click on any log line to expand and view complete details including trace correlation + +--- + +## Troubleshooting Tips + +- **Service Not Responding:** Check that all pods are running using `kubectl get pods -n application` +- **Connection Refused:** Verify the external IP is correct and accessible +- **401/403 Errors:** Ensure you're using the correct authentication credentials +- **Transaction Failures:** Review service logs for detailed error messages +- **Port Forward Issues:** Ensure no other process is using the specified port + +--- + +## Next Steps + +After successfully completing these tests, you can: + +- Deploy additional microservices to your CloudBank application +- Configure custom observability dashboards +- Implement additional business logic and workflows +- Explore advanced LRA patterns for complex transactions +- Set up alerts and notifications for critical events + +For more information about OBaaS and CloudBank, consult the official Oracle documentation. \ No newline at end of file diff --git a/cloudbank-v5/cloudbank-v5-install.md b/cloudbank-v5/cloudbank-v5-install.md new file mode 100644 index 000000000..0d9234ee4 --- /dev/null +++ b/cloudbank-v5/cloudbank-v5-install.md @@ -0,0 +1,313 @@ +# CloudBank v5 Installation Guide + +## Overview + +This guide walks you through installing CloudBank v5, a microservices-based banking application, into an existing Oracle Backend as a Service (OBaaS) environment. CloudBank v5 consists of six microservices: account, customer, transfer, checks, creditscore, and testrunner. + +**Estimated time:** 30-45 minutes + +--- + +## Prerequisites + +Before you begin, ensure you have: + +- Oracle Backend as a Service (OBaaS) installed in namespace `obaas-dev` +- Java 21 installed +- Maven installed +- kubectl configured and connected to your cluster +- Docker or compatible container runtime (Rancher Desktop, Docker Desktop, etc.) +- `yq` command-line tool installed. On ubuntu it must be the `go` version and not the Python based. +- Access to a container registry (OCI Registry or Docker Hub) +- **Note:** Container registry repositories must be public (private repositories are currently not supported due to authentication issues) + +--- + +## Step 1: Configure Your Environment + +### 1.1 Set Docker Host + +Configure the DOCKER_HOST variable for your container runtime. + +**For Rancher Desktop on macOS:** + +```bash +export DOCKER_HOST=unix:///Users//.rd/docker.sock +``` + +**For Docker Desktop:** + +```bash +export DOCKER_HOST=unix:///var/run/docker.sock +``` + +### 1.2 Create Container Registry Repositories + +Run the provided script to create necessary repositories in OCI Registry: + +```bash +source create-oci-repos.sh +``` + +**Example:** + +```bash +source create-oci-repos.sh andytael sjc.ocir.io/maacloud/cloudbank +``` + +**Expected outcome:** Six repositories created for each microservice + +--- + +## Step 2: Build Dependencies + +Build and install the common dependencies: + +```bash +mvn clean install -pl common,buildtools +``` + +**Expected outcome:** Build completes with `BUILD SUCCESS` message + +--- + +## Step 3: Build CloudBank Microservices + +### 3.1 Update Image Repository Configuration + +Update the JKube configuration to point to your container registry: + +```bash +./update-jkube-image.sh +``` + +**Example:** + +```bash +./update-jkube-image.sh sjc.ocir.io/maacloud/cloudbank-v5 +``` + +### 3.2 Build and Push Images + +Build all microservices and push to your container registry: + +```bash +mvn clean package k8s:build k8s:push -pl account,customer,transfer,checks,creditscore,testrunner +``` + +**Expected outcome:** All six microservice images built and pushed successfully + +--- + +## Step 4: Configure Database + +### 4.1 Update SQL Job Configuration + +Edit the `sqljob.yaml` file and update these values for your environment: + +- Database connection details +- Admin credentials +- Namespace (if different from `obaas-dev`) +- SQL Statement (the default is for an Oracle Database 23ai and will not work with 19c) + +### 4.2 Create Database Users + +Run the SQL job to create the account and customer database users: + +```bash +kubectl create -f sqljob.yaml +``` + +### 4.3 Verify Job Completion + +**Important:** Verify the job completed successfully before proceeding: + +```bash +kubectl get jobs -n obaas-dev +kubectl logs job/ -n obaas-dev +``` + +**Expected outcome:** Job status shows "Completed" and logs show successful user creation + +--- + +## Step 5: Create Kubernetes Secrets + +### 5.1 Update Secret Configuration + +Edit the `acc_cust_secrets.sh` script and update these values: + +- Database passwords +- Connection strings +- Wallet paths + +### 5.2 Create Secrets + +Run the script to create Kubernetes secrets: + +```bash +source acc_cust_secrets.sh +``` + +**Expected outcome:** Secrets created in `obaas-dev` namespace + +--- + +## Step 6: Configure Helm Values + +### 6.1 Update Image References + +Update the `values.yaml` file with your registry and tag: + +```bash +./update-image.sh +``` + +**Example:** + +```bash +./update-image.sh sjc.ocir.io/maacloud/cloudbank-v5 0.0.1-SNAPSHOT +``` + +### 6.2 Verify Secret Names + +Open `values.yaml` and verify these values match your environment: + +- `credentialSecret`: Database credential secret name +- `walletSecret`: Database wallet secret name + +To find the correct secret names: + +```bash +kubectl get secrets -n obaas-dev +``` + +--- + +## Step 7: Deploy CloudBank Services + +Deploy all CloudBank microservices: + +```bash +./deploy-all-services.sh obaas-dev +``` + +**Verify deployment:** + +```bash +kubectl get pods -n obaas-dev | grep cloudbank +``` + +**Expected outcome:** All six microservices show "Running" status + +--- + +## Step 8: Configure APISIX Routes + +### 8.1 Retrieve APISIX Admin API Key + +**Option 1 - Using yq:** + +```bash +kubectl -n obaas-dev get configmap apisix -o yaml | yq '.data."config.yaml"' | yq '.deployment.admin.admin_key[] | select(.name == "admin") | .key' +``` + +**Option 2 - Manual retrieval:** + +If the command above doesn't work: + +1. Run: `kubectl get configmap apisix -n obaas-dev -o yaml` +2. Look for the `config.yaml` section +3. Find `deployment.admin.admin_key` and copy the key value + +### 8.2 Create Port Forward to APISIX + +Open a new terminal and create a tunnel to APISIX: + +```bash +kubectl port-forward -n obaas-dev svc/apisix-admin 9180 +``` + +**Keep this terminal open during route creation.** + +### 8.3 Create Routes + +In your original terminal, create the APISIX routes: + +```bash +cd apisix-routes +source ./create-all-routes.sh +cd .. +``` + +**Expected outcome:** Routes created successfully for all microservices + +--- + +## Step 9: Test the Installation + +Follow the testing instructions in the [README](README.md) under the "Test CloudBank Services" section. + +**Basic health check:** + +```bash +# Check all pods are running +kubectl get pods -n obaas-dev + +# Check service endpoints +kubectl get svc -n obaas-dev | grep cloudbank +``` + +--- + +## Troubleshooting + +### Build Failures + +**Problem:** Maven build fails with dependency errors + +**Solution:** Ensure you've run `mvn clean install -pl common,buildtools` first + +### Docker Connection Issues + +**Problem:** Cannot connect to Docker daemon + +**Solution:** Verify DOCKER_HOST is set correctly and Docker/Rancher is running + +### Job Not Completing + +**Problem:** SQL job stays in "Running" state + +**Solution:** Check logs with `kubectl logs job/ -n obaas-dev` to identify database connection issues + +### Pods Not Starting + +**Problem:** Pods show "ImagePullBackOff" error + +**Solution:** + +- Verify images were pushed successfully to registry +- Ensure registry is public or credentials are configured +- Check image names in `values.yaml` match pushed images + +### APISIX Route Creation Fails + +**Problem:** Route creation returns authentication error + +**Solution:** Verify you're using the correct admin API key from the configmap + +--- + +## Additional Resources + +- [CloudBank README](README.md) - Application overview and testing guide +- [Oracle Backend as a Service documentation](https://oracle.github.io/microservices-datadriven/obaas/) +- [Report Issues](https://github.com/oracle/microservices-datadriven/issues) - GitHub Issues for this repository + +--- + +## Notes + +- This installation assumes both OBaaS and CloudBank v5 are deployed in the `obaas-dev` namespace +- CloudBank v5 has only been tested with Java 21 +- Private container registries are currently not supported due to authentication issues \ No newline at end of file diff --git a/cloudbank-v5/common/pom.xml b/cloudbank-v5/common/pom.xml new file mode 100644 index 000000000..c7c5a12d3 --- /dev/null +++ b/cloudbank-v5/common/pom.xml @@ -0,0 +1,32 @@ + + + + + 4.0.0 + + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + + common + 0.0.1-SNAPSHOT + common + common + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/common/src/main/java/com/example/common/filter/LoggingFilterConfig.java b/cloudbank-v5/common/src/main/java/com/example/common/filter/LoggingFilterConfig.java new file mode 100644 index 000000000..a4eb2f698 --- /dev/null +++ b/cloudbank-v5/common/src/main/java/com/example/common/filter/LoggingFilterConfig.java @@ -0,0 +1,93 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.common.filter; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; + +@Slf4j +@Configuration +public class LoggingFilterConfig { + + /** + * Create CommonsRequestLoggingFilter bean. + * + * @return CommonsRequestLoggingFilter bean + */ + @Bean + public CommonsRequestLoggingFilter logFilter() { + log.info("Log filter initialized"); + CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter() { + @Value("${request.logging.shouldLog}") + private boolean shouldLog; + + @Override + protected boolean shouldLog(HttpServletRequest request) { + if (request.getRequestURI().contains("/actuator")) { + return false; + } + + return shouldLog; + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + logger.info(message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + logger.info(message); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + long startTime = System.currentTimeMillis(); + + try { + super.doFilterInternal(request, response, filterChain); + } finally { + long duration = System.currentTimeMillis() - startTime; + if (shouldLog(request)) { + logger.info(String.format("Response: Status={%s}, URI={%s}, Duration={%d}ms", + response.getStatus(), + request.getRequestURI(), + duration)); + + if (request.getAttribute("javax.servlet.error.exception") != null) { + logger.error("Exception during request processing", + (Exception) request.getAttribute("javax.servlet.error.exception")); + } + + if (request.getAttribute("jakarta.servlet.error.exception") != null) { + logger.error("Exception during request processing", + (Exception) request.getAttribute("jakarta.servlet.error.exception")); + } + } + + } + } + + }; + filter.setIncludeQueryString(true); + filter.setIncludePayload(true); + filter.setMaxPayloadLength(10000); + filter.setIncludeHeaders(true); + filter.setIncludeClientInfo(true); + filter.setBeforeMessagePrefix("Request started => "); + filter.setAfterMessagePrefix("Request ended => "); + return filter; + } +} \ No newline at end of file diff --git a/cloudbank-v5/common/src/main/java/com/example/common/ucp/UCPTelemetry.java b/cloudbank-v5/common/src/main/java/com/example/common/ucp/UCPTelemetry.java new file mode 100644 index 000000000..eeb4ce2e4 --- /dev/null +++ b/cloudbank-v5/common/src/main/java/com/example/common/ucp/UCPTelemetry.java @@ -0,0 +1,90 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.common.ucp; + +import javax.sql.DataSource; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BatchCallback; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; +import io.opentelemetry.instrumentation.oracleucp.v11_2.OracleUcpTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import oracle.ucp.UniversalConnectionPool; +import oracle.ucp.admin.UniversalConnectionPoolManagerImpl; +import oracle.ucp.jdbc.PoolDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@AutoConfigureAfter(OpenTelemetryAutoConfiguration.class) +@AutoConfiguration +public class UCPTelemetry { + + @Autowired + private DataSource connectionPool; + + private OracleUcpTelemetry ucpTelemetry; + private OpenTelemetry openTelemetry; + + public UCPTelemetry(OpenTelemetry openTelemetry) { + this.ucpTelemetry = OracleUcpTelemetry.create(openTelemetry); + this.openTelemetry = openTelemetry; + } + + private BatchCallback additionMetrics; + + @PostConstruct + protected void configure() throws Exception { + UniversalConnectionPool universalConnectionPool = UniversalConnectionPoolManagerImpl + .getUniversalConnectionPoolManager() + .getConnectionPool(connectionPool.unwrap(PoolDataSource.class).getConnectionPoolName()); + this.ucpTelemetry.registerMetrics(universalConnectionPool); + DbConnectionPoolMetrics metrics = DbConnectionPoolMetrics.create( + openTelemetry, "obaas-ucp", universalConnectionPool.getName()); + + DoubleHistogram connectionCreateTime = metrics.connectionCreateTime(); + DoubleHistogram connectionUseTime = metrics.connectionUseTime(); + LongCounter connectionTimeOuts = metrics.connectionTimeouts(); + DoubleHistogram connectionWaitTime = metrics.connectionWaitTime(); + ObservableLongMeasurement connections = metrics.connections(); + + Attributes attributes = metrics.getAttributes(); + + additionMetrics = metrics.batchCallback( + () -> { + connectionUseTime.record( + universalConnectionPool.getStatistics().getCumulativeConnectionUseTime(), attributes); + connectionWaitTime.record( + universalConnectionPool.getStatistics().getCumulativeConnectionWaitTime(), attributes); + connectionCreateTime.record(universalConnectionPool.getStatistics().getAverageConnectionWaitTime(), + attributes); + connectionTimeOuts.add( + universalConnectionPool.getStatistics().getCumulativeFailedConnectionWaitCount(), + attributes); + + }, connections); + } + + @PreDestroy + protected void shutdown() throws Exception { + UniversalConnectionPool universalConnectionPool = UniversalConnectionPoolManagerImpl + .getUniversalConnectionPoolManager() + .getConnectionPool(this.connectionPool.unwrap(PoolDataSource.class).getConnectionPoolName()); + this.ucpTelemetry.unregisterMetrics(universalConnectionPool); + if (this.additionMetrics != null) { + this.additionMetrics.close(); + } + } + +} diff --git a/cloudbank-v5/common/src/main/resources/common.yaml b/cloudbank-v5/common/src/main/resources/common.yaml new file mode 100644 index 000000000..e3c9b76b1 --- /dev/null +++ b/cloudbank-v5/common/src/main/resources/common.yaml @@ -0,0 +1,79 @@ +# Copyright (c) 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +spring: + threads: + virtual: + enabled: true + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.OracleDialect + format_sql: true + show-sql: true + +eureka: + instance: + hostname: ${spring.application.name} + preferIpAddress: true + client: + service-url: + defaultZone: ${eureka.service-url} + fetch-registry: true + register-with-eureka: true + enabled: true + +management: + endpoint: + health: + show-details: always + show-components: always + endpoints: + web: + exposure: + include: "*" + metrics: + tags: + application: ${spring.application.name} + distribution: + percentiles[http.server.requests]: 0.5, 0.90, 0.95, 0.99 + percentiles-histogram[http.server.requests]: true + slo[http.server.requests]: 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 30s + percentiles[http.client.requests]: 0.5, 0.90, 0.95, 0.99 + percentiles-histogram[http.client.requests]: true + slo[http.client.requests]: 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 30s + health: + probes: + enabled: true + tracing: + sampling: + probability: 1.0 + info: + os: + enabled: true + env: + enabled: true + java: + enabled: true + observations: + key-values: + app: ${spring.application.name} + +logging: + level: + root: INFO + com.example: INFO + org.springframework.web.filter.AbstractRequestLoggingFilter: INFO + +jdbc: + datasource-proxy: + query: + enable-logging: true + log-level: INFO + include-parameter-values: true + +request: + logging: + shouldLog: true \ No newline at end of file diff --git a/cloudbank-v5/create-oci-repos.sh b/cloudbank-v5/create-oci-repos.sh new file mode 100755 index 000000000..f49b6a2d1 --- /dev/null +++ b/cloudbank-v5/create-oci-repos.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Script to create OCI container repositories for CloudBank v5 services +# Usage: ./create-oci-repos.sh + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 my-compartment sjc.ocir.io/maacloud/cloudbank-v5" + return 1 2>/dev/null || exit 1 +fi + +COMPARTMENT_NAME="$1" +REPO_PREFIX="$2" + +# Look up compartment OCID from name +echo "Looking up compartment: $COMPARTMENT_NAME" +COMPARTMENT_OCID=$(oci iam compartment list --all --compartment-id-in-subtree true \ + --query "data[?name=='$COMPARTMENT_NAME'].id | [0]" --raw-output 2>&1) + +if [ -z "$COMPARTMENT_OCID" ] || [ "$COMPARTMENT_OCID" = "null" ]; then + echo "✗ Error: Compartment '$COMPARTMENT_NAME' not found" + return 1 2>/dev/null || exit 1 +fi + +echo "Found compartment OCID: $COMPARTMENT_OCID" +echo "" + +# Remove trailing slash if present +REPO_PREFIX="${REPO_PREFIX%/}" + +# Array of services that need repositories +SERVICES=( + "account" + "customer" + "transfer" + "checks" + "creditscore" + "testrunner" +) + +echo "Creating OCI container repositories" +echo "====================================" +echo "Compartment: $COMPARTMENT_OCID" +echo "Repository prefix: $REPO_PREFIX" +echo "" + +# Create repository for each service +for service in "${SERVICES[@]}"; do + REPO_NAME="${REPO_PREFIX}/${service}" + echo "Creating repository: $REPO_NAME" + + oci artifacts container repository create \ + --compartment-id "$COMPARTMENT_OCID" \ + --display-name "$REPO_NAME" \ + --is-public true \ + 2>&1 + + if [ $? -eq 0 ]; then + echo "✓ Repository $REPO_NAME created successfully" + else + echo "⚠ Warning: Failed to create repository $REPO_NAME (may already exist)" + fi + echo "" +done + +echo "====================================" +echo "Repository creation complete!" +echo "" +echo "To list all repositories, run:" +echo "oci artifacts container repository list --compartment-id $COMPARTMENT_OCID" diff --git a/cloudbank-v5/creditscore/helm/Chart.yaml b/cloudbank-v5/creditscore/helm/Chart.yaml new file mode 100644 index 000000000..3e91be754 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: creditscore # Replace with your application name +description: A Helm chart for the OBaaS Platform Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.2.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.0.1" diff --git a/cloudbank-v5/creditscore/helm/README.md b/cloudbank-v5/creditscore/helm/README.md new file mode 100644 index 000000000..9403cc5e0 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/README.md @@ -0,0 +1,16 @@ +# OBaas Sample App Chart + +This chart provides an extensible sample for applications running on [OBaaS](https://oracle.github.io/microservices-datadriven/obaas/). + +To use this chart for a given application, download the chart and update the [Chart.name](./Chart.yaml) value to your application's name. + +## Customizing the chart + +The OBaaS sample app chart is meant to serve as a developer template, and is fully customizable. + +Standard parameters for Kubernetes options like node affinity, HPAs, ingress and more are provided in the [values.yaml file](./values.yaml). + +## OBaaS options + +Within the [values.yaml file](./values.yaml), the `obaas` key allows chart developers to enable or disable OBaaS integrations like database connectivity, OpenTelemetry, MicroProfile LRA, SpringBoot, and Eureka. +enabled: true diff --git a/cloudbank-v5/creditscore/helm/templates/NOTES.txt b/cloudbank-v5/creditscore/helm/templates/NOTES.txt new file mode 100644 index 000000000..8d9597d97 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "obaas-app.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "obaas-app.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "obaas-app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "obaas-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cloudbank-v5/creditscore/helm/templates/_helpers.tpl b/cloudbank-v5/creditscore/helm/templates/_helpers.tpl new file mode 100644 index 000000000..b8e6d5b79 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "obaas-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "obaas-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "obaas-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "obaas-app.labels" -}} +helm.sh/chart: {{ include "obaas-app.chart" . }} +{{ include "obaas-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "obaas-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "obaas-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "obaas-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "obaas-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cloudbank-v5/creditscore/helm/templates/deployment.yaml b/cloudbank-v5/creditscore/helm/templates/deployment.yaml new file mode 100644 index 000000000..d4902a2f2 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/templates/deployment.yaml @@ -0,0 +1,147 @@ +# Load obaas configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + obaas.framework: SPRING_BOOT +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "obaas-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "obaas-app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "obaas-app.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CONNECT_STRING + value: jdbc:oracle:thin:@$(SPRING_DB_SERVICE)?TNS_ADMIN=/oracle/tnsadmin + # Lookup ObaaS configuration + {{- $obaas := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-config") }} + {{- $obaasObs := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-observability-config") }} + {{- if $.Values.obaas.mp_lra.enabled }} + - name: MP_LRA_COORDINATOR_URL + value: {{ $obaas.data.otmm | quote }} + {{- end }} + {{- if $.Values.obaas.eureka.enabled }} + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_CLIENT_REGISTER_WITH_EUREKA + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: {{ $obaas.data.eureka | quote }} + - name: EUREKA_INSTANCE_HOSTNAME + value: {{ include "obaas-app.fullname" . }}-{{ $.Release.Namespace }} + {{- end }} + {{- if $.Values.obaas.otel.enabled }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ (index $obaasObs.data "signoz-otel-collector") | quote }} + {{- end }} + {{- if $.Values.obaas.springboot.enabled }} + - name: SPRING_PROFILES_ACTIVE + value: {{ $obaas.data.SPRING_PROFILES_ACTIVE | quote }} + {{- if $.Values.obaas.database.enabled }} + - name: SPRING_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.username + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.password + - name: LIQUIBASE_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_username + - name: LIQUIBASE_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_password + - name: LIQUIBASE_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DB_SERVICE + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.service + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ $.Values.obaas.database.walletSecret }} + mountPath: /oracle/tnsadmin + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: {{ $.Values.obaas.database.walletSecret }} + secret: + secretName: {{ $.Values.obaas.database.walletSecret }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cloudbank-v5/creditscore/helm/templates/hpa.yaml b/cloudbank-v5/creditscore/helm/templates/hpa.yaml new file mode 100644 index 000000000..7c8805fb0 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/templates/hpa.yaml @@ -0,0 +1,33 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "obaas-app.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/creditscore/helm/templates/ingress.yaml b/cloudbank-v5/creditscore/helm/templates/ingress.yaml new file mode 100644 index 000000000..50aa98cd8 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "obaas-app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/creditscore/helm/templates/service.yaml b/cloudbank-v5/creditscore/helm/templates/service.yaml new file mode 100644 index 000000000..5876cf66d --- /dev/null +++ b/cloudbank-v5/creditscore/helm/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "obaas-app.selectorLabels" . | nindent 4 }} diff --git a/cloudbank-v5/creditscore/helm/templates/serviceaccount.yaml b/cloudbank-v5/creditscore/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..a22b73ef6 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "obaas-app.serviceAccountName" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/cloudbank-v5/creditscore/helm/templates/tests/test-connection.yaml b/cloudbank-v5/creditscore/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..8596a023f --- /dev/null +++ b/cloudbank-v5/creditscore/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "obaas-app.fullname" . }}-test-connection" + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "obaas-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cloudbank-v5/creditscore/helm/values.yaml b/cloudbank-v5/creditscore/helm/values.yaml new file mode 100644 index 000000000..562daf1b2 --- /dev/null +++ b/cloudbank-v5/creditscore/helm/values.yaml @@ -0,0 +1,155 @@ +# Default values for obaas-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: "my-repository/creditscore" + # This sets the pull policy for images. + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "0.0.1-SNAPSHOT" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: + signoz.io/path: /actuator/prometheus + signoz.io/port: "8080" + signoz.io/scrape: "true" +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 8080 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + requests: + cpu: 100m + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Oracle Backend for Microservices and AI Settings. +obaas: + namespace: obaas-dev # Replace with your namespace + database: + enabled: false # If true variables with DB secret content will be created + credentialsSecret: obaas-app-db-secrets # Replace with your secret name + walletSecret: obaas-adb-tns-admin-1 # Replace with your wallet secret name + otel: + enabled: true # Enable OpenTelemetry + # MicroProfile LRA + mp_lra: + enabled: false # Enable OTMM + # Spring Boot applications + springboot: + enabled: true # Enable Spring Boot specific variables + eureka: + enabled: true # Enable Eureka client diff --git a/cloudbank-v5/creditscore/pom.xml b/cloudbank-v5/creditscore/pom.xml new file mode 100644 index 000000000..4500d7c23 --- /dev/null +++ b/cloudbank-v5/creditscore/pom.xml @@ -0,0 +1,60 @@ + + + + + 4.0.0 + + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + + creditscore + 0.0.1-SNAPSHOT + creditscore + Creditscore Application + + + com.example + common + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.graalvm.buildtools + native-maven-plugin + + + org.eclipse.jkube + kubernetes-maven-plugin + ${jkube.version} + + + + my-repository/creditscore:${project.version} + + ghcr.io/oracle/openjdk-image-obaas:21 + + dir + /deployments + + java -jar /deployments/${project.artifactId}-${project.version}.jar + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/creditscore/src/main/java/com/example/creditscore/CreditscoreApplication.java b/cloudbank-v5/creditscore/src/main/java/com/example/creditscore/CreditscoreApplication.java new file mode 100644 index 000000000..5ab537497 --- /dev/null +++ b/cloudbank-v5/creditscore/src/main/java/com/example/creditscore/CreditscoreApplication.java @@ -0,0 +1,21 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.creditscore; + +import com.example.common.filter.LoggingFilterConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.Import; + +@EnableDiscoveryClient +@SpringBootApplication +@Import(LoggingFilterConfig.class) +public class CreditscoreApplication { + + public static void main(String[] args) { + SpringApplication.run(CreditscoreApplication.class, args); + } + +} diff --git a/cloudbank-v5/creditscore/src/main/java/com/example/creditscore/controller/CreditScoreController.java b/cloudbank-v5/creditscore/src/main/java/com/example/creditscore/controller/CreditScoreController.java new file mode 100644 index 000000000..baf8a05c2 --- /dev/null +++ b/cloudbank-v5/creditscore/src/main/java/com/example/creditscore/controller/CreditScoreController.java @@ -0,0 +1,37 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.creditscore.controller; + +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1") +@Slf4j +public class CreditScoreController { + + /** + * Get a random creditscore at current date. + * @return Returns creditscore at current date + */ + @GetMapping("/creditscore") + @Operation(summary = "Get a random creditscore at current date") + public Map getCreditScore() { + log.debug("CREDITSCORE: getCreditScore"); + int max = 900; + int min = 500; + SecureRandom secureRandom = new SecureRandom(); + HashMap map = new HashMap<>(); + map.put("Credit Score", String.valueOf(secureRandom.nextInt(max - min) + min)); + map.put("Date", String.valueOf(java.time.LocalDate.now())); + return map; + } +} \ No newline at end of file diff --git a/cloudbank-v5/creditscore/src/main/resources/application.yaml b/cloudbank-v5/creditscore/src/main/resources/application.yaml new file mode 100644 index 000000000..00b5eee09 --- /dev/null +++ b/cloudbank-v5/creditscore/src/main/resources/application.yaml @@ -0,0 +1,15 @@ +# Copyright (c) 2023, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +spring: + application: + name: creditscore +# Disables autoconfiguration for the refresh scope and associated features. + cloud: + refresh: + enabled: false + config: + import-check: + enabled: false + config: + import: classpath:common.yaml \ No newline at end of file diff --git a/cloudbank-v5/creditscore/src/main/resources/banner.txt b/cloudbank-v5/creditscore/src/main/resources/banner.txt new file mode 100644 index 000000000..09a36d31e --- /dev/null +++ b/cloudbank-v5/creditscore/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + ____ _ _ _ ____ + / ___|_ __ ___ __| (_) |_/ ___| ___ ___ _ __ ___ + | | | '__/ _ \/ _` | | __\___ \ / __/ _ \| '__/ _ \ + | |___| | | __/ (_| | | |_ ___) | (_| (_) | | | __/ + \____|_| \___|\__,_|_|\__|____/ \___\___/|_| \___| + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} diff --git a/cloudbank-v5/creditscore/src/test/java/com/example/creditscore/CreditscoreApplicationTests.java b/cloudbank-v5/creditscore/src/test/java/com/example/creditscore/CreditscoreApplicationTests.java new file mode 100644 index 000000000..25347196f --- /dev/null +++ b/cloudbank-v5/creditscore/src/test/java/com/example/creditscore/CreditscoreApplicationTests.java @@ -0,0 +1,18 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.creditscore; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@Disabled +@SpringBootTest +class CreditscoreApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/cloudbank-v5/customer-helidon/.dockerignore b/cloudbank-v5/customer-helidon/.dockerignore new file mode 100644 index 000000000..c8b241f22 --- /dev/null +++ b/cloudbank-v5/customer-helidon/.dockerignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/.gitignore b/cloudbank-v5/customer-helidon/.gitignore new file mode 100644 index 000000000..900f5b4f0 --- /dev/null +++ b/cloudbank-v5/customer-helidon/.gitignore @@ -0,0 +1,38 @@ +# Compiled class file +*.class + +# Maven +target/ +.m2/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# IntelliJ Idea +.idea/* +!.idea/runConfigurations +*.iws +*.ipr +*.iml +*.releaseBackup +atlassian-ide-plugin.xml + +# Netbeans +nbactions.xml +nb-configuration.xml + +# Eclipse +.settings +.settings/ +.project +.classpath +.factorypath + +# OCA +.oca/* diff --git a/cloudbank-v5/customer-helidon/.helidon b/cloudbank-v5/customer-helidon/.helidon new file mode 100644 index 000000000..10bc0857a --- /dev/null +++ b/cloudbank-v5/customer-helidon/.helidon @@ -0,0 +1,6 @@ +#Helidon Project Configuration +#Wed Jun 18 10:11:56 EDT 2025 +schema.version=1.1.0 +helidon.version=4.2.3 +project.flavor=mp +project.archetype=quickstart diff --git a/cloudbank-v5/customer-helidon/Dockerfile.manual b/cloudbank-v5/customer-helidon/Dockerfile.manual new file mode 100644 index 000000000..15b11afef --- /dev/null +++ b/cloudbank-v5/customer-helidon/Dockerfile.manual @@ -0,0 +1,35 @@ +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 as build + +# Install maven +WORKDIR /usr/share +RUN set -x && \ + curl -O https://archive.apache.org/dist/maven/maven-3/3.8.4/binaries/apache-maven-3.8.4-bin.tar.gz && \ + tar -xvf apache-maven-*-bin.tar.gz && \ + rm apache-maven-*-bin.tar.gz && \ + mv apache-maven-* maven && \ + ln -s /usr/share/maven/bin/mvn /bin/ + +WORKDIR /helidon + +# Create a first layer to cache the "Maven World" in the local repository. +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip -DskipOpenApiGenerate + +# Do the Maven build with fat JAR! +ADD src src +RUN mvn package -DskipTests + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 + +WORKDIR /helidon + +# Copy ONLY the fat JAR (not libs directory) +COPY --from=build /helidon/target/*.jar app.jar + +# Simple fat JAR execution +CMD ["java", "-jar", "app.jar"] + +EXPOSE 8080 + diff --git a/cloudbank-v5/customer-helidon/README.md b/cloudbank-v5/customer-helidon/README.md new file mode 100644 index 000000000..ddc66ac80 --- /dev/null +++ b/cloudbank-v5/customer-helidon/README.md @@ -0,0 +1,254 @@ +# customer-helidon + +Helidon MP version of the "customer" microservice built using the **Helidon MP profile** for enterprise Java applications with CDI, JPA, and microservices capabilities. + +## Build and run + +### Prerequisites +- JDK 21 +- Maven 3.8+ + +### Building the Application + +The build process creates a **thin JAR deployment package** as a ZIP file containing the application JAR and all dependencies: + +```bash +mvn clean package +``` + +This creates: +- `target/customer-helidon.jar` - The thin application JAR +- `target/customer-helidon-deployment.zip` - Complete deployment package with structure: + ``` + customer-helidon.jar (main application) + app/ + libs/ (all dependency JARs) + ``` + +### Building and Pushing Container Image + +#### Environment Setup (macOS with Rancher Desktop) + +```bash +# Set Docker host for JKube compatibility +export DOCKER_HOST=unix:///Users/$USER/.rd/docker.sock +``` + +#### Commands + +```bash +# Build thin JAR and libs +mvn clean package + +# Build container image +mvn k8s:build + +# Push to Oracle Cloud Registry +docker push us-ashburn-1.ocir.io/tenancy/customer-helidon:5.0-SNAPSHOT +``` + +#### Output +- **JAR**: `target/customer-helidon.jar` (thin JAR) +- **Dependencies**: `target/libs/` (all dependencies) +- **Deployment**: `target/customer-helidon-deployment.zip` +- **Image**: Uses JKube Java base image with automatic Helidon configuration + +### Running the Application + +**Option 1: Using the thin JAR (requires dependencies in classpath):** +```bash +# Extract the deployment ZIP first +cd target +unzip customer-helidon-deployment.zip +java -jar customer-helidon.jar +``` + +**Option 2: Using Maven to run directly:** +```bash +mvn exec:java +``` + +## Quick Start with Local Oracle Database + +To run against a local Oracle Docker container, simply: + +1. **Start Oracle Database container:** + ```bash + docker run -d --name oracle-db -p 1521:1521 \ + -e ORACLE_PWD=Welcome12345 \ + container-registry.oracle.com/database/free:latest + ``` + +2. **Uncomment database configuration** in `src/main/resources/application.yaml`: + ```yaml + javax.sql.DataSource.customer.URL = jdbc:oracle:thin:@//localhost:1521/freepdb1 + javax.sql.DataSource.customer.user = customer + javax.sql.DataSource.customer.password = Welcome12345 + ``` + +3. **Rebuild and run:** + ```bash + mvn clean package + cd target && unzip customer-helidon-deployment.zip + java -jar customer-helidon.jar + ``` + +The application will automatically create the necessary database tables on startup using Hibernate's DDL auto-generation. + +### Basic: +```bash +curl -X GET http://localhost:8080/simple-greet +Hello World! +``` + +### JSON: +```bash +curl -X GET http://localhost:8080/greet +{"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +{"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +{"message":"Hola Jose!"} +``` + +### Try health +```bash +curl -s -X GET http://localhost:8080/health +{"outcome":"UP",... +``` + +### Try metrics +```bash +# Prometheus Format +curl -s -X GET http://localhost:8080/metrics +# TYPE base:gc_g1_young_generation_count gauge +. . . + +# JSON Format +curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics +{"base":... +. . . +``` + +## Building a Native Image + +The generation of native binaries requires an installation of GraalVM 22.1.0+. +You can build a native binary using Maven as follows: + +```bash +mvn -Pnative-image install -DskipTests +``` + +The generation of the executable binary may take a few minutes to complete depending on your hardware and operating system. When completed, the executable file will be available under the `target` directory and be named after the artifact ID you have chosen during the project generation phase. + +## Docker Support + +### Building the Docker Image Locally + +**Note:** The `Dockerfile.manual` must be renamed to `Dockerfile` before building locally, as JKube uses the Dockerfile when present. + +```bash +# Rename Dockerfile for local build +git mv Dockerfile.manual Dockerfile + +# Build the Docker image +docker build -t customer-helidon . + +# Rename back to avoid conflicts with JKube builds +git mv Dockerfile Dockerfile.manual +``` + +### Running the Docker Image +```bash +docker run --rm -p 8080:8080 customer-helidon:latest +``` + +Exercise the application as described above. + +## Configuration + +### Application Properties (`application.yaml`) +```yaml +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 +# Change the following to true to enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=false + +# Application properties. This is the default greeting +app.greeting=Hello + +# Database connection factory - specifies Oracle UCP driver for connection pooling +javax.sql.DataSource.customer.connectionFactoryClassName = oracle.jdbc.pool.OracleDataSource + +# Local Oracle Database Configuration +# Uncomment the following lines to connect to a local Oracle Docker container out-of-the-box: +# javax.sql.DataSource.customer.URL = jdbc:oracle:thin:@//localhost:1521/freepdb1 +# javax.sql.DataSource.customer.user = customer +# javax.sql.DataSource.customer.password = Welcome12345 + +# Hibernate/JPA Configuration +hibernate.hbm2ddl.auto=create +hibernate.show_sql=true +hibernate.format_sql=true +# Fix JTA transaction coordinator issue +hibernate.transaction.coordinator_class=jta + +# Eureka service discovery configuration +server.features.eureka.client.base-uri=http://eureka.eureka:8761/eureka +server.features.eureka.instance.name=helidon-customer-service +server.features.eureka.instance.hostName=helidon.helidon +``` + +## Build Architecture + +This project uses: +- **Helidon MP (MicroProfile)** - Enterprise Java microservices profile +- **Thin JAR deployment** - Application JAR + separate dependencies for optimal Docker layering +- **Maven Assembly Plugin** - Creates deployment ZIP with proper structure for containerization +- **Hibernate + JTA** - Database persistence with transaction management +- **Oracle UCP** - Connection pooling for Oracle Database +- **Eureka integration** - Service discovery support + +## Dockerfile Structure + +The included Dockerfile uses a **multi-stage build**: + +```dockerfile +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 as build + +# Install maven +WORKDIR /usr/share +RUN set -x && \ + curl -O https://archive.apache.org/dist/maven/maven-3/3.8.4/binaries/apache-maven-3.8.4-bin.tar.gz && \ + tar -xvf apache-maven-*-bin.tar.gz && \ + rm apache-maven-*-bin.tar.gz && \ + mv apache-maven-* maven && \ + ln -s /usr/share/maven/bin/mvn /bin/ + +WORKDIR /helidon + +# Create a first layer to cache the "Maven World" in the local repository. +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip -DskipOpenApiGenerate + +# Do the Maven build with fat JAR! +ADD src src +RUN mvn package -DskipTests + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy ONLY the fat JAR (not libs directory) +COPY --from=build /helidon/target/*.jar app.jar + +# Simple fat JAR execution +CMD ["java", "-jar", "app.jar"] +EXPOSE 8080 +``` diff --git a/cloudbank-v5/customer-helidon/pom.xml b/cloudbank-v5/customer-helidon/pom.xml new file mode 100644 index 000000000..ddf2f5076 --- /dev/null +++ b/cloudbank-v5/customer-helidon/pom.xml @@ -0,0 +1,269 @@ + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.2.3 + + + com.example + customer-helidon + 5.0-SNAPSHOT + + + 4.0.14 + 1.16.2 + us-ashburn-1.ocir.io/tenancy/${project.artifactId}:${project.version} + + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-ucp + runtime + + + io.helidon.integrations.db + ojdbc + runtime + + + jakarta.transaction + jakarta.transaction-api + + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + runtime + + + jakarta.persistence + jakarta.persistence-api + + + + io.helidon.integrations.eureka + helidon-integrations-eureka + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-hibernate + runtime + + + org.hibernate + hibernate-core + ${version.lib.hibernate} + + + + io.helidon.microprofile.openapi + helidon-microprofile-openapi + + + io.helidon.microprofile.health + helidon-microprofile-health + + + jakarta.json.bind + jakarta.json.bind-api + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.smallrye + jandex + runtime + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + + + io.helidon.microprofile.metrics + helidon-microprofile-metrics + + + io.helidon.microprofile.telemetry + helidon-microprofile-telemetry + + + io.opentelemetry + opentelemetry-exporter-otlp + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + 5.5.0 + test + + + org.mockito + mockito-junit-jupiter + 5.5.0 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.4.2 + + + src/assembly/jib-ready.xml + + + + + package + + single + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + libs/ + io.helidon.microprofile.cdi.Main + + + + + + org.hibernate.orm.tooling + hibernate-enhance-maven-plugin + + + Statically enhance JPA entities for Hibernate + compile + + enhance + + + true + true + true + + + + + + + io.helidon.build-tools + helidon-maven-plugin + ${helidon-maven-plugin.version} + + + + org.eclipse.jkube + kubernetes-maven-plugin + ${jkube.version} + + + + + + + + + + maven-compiler-plugin + + + default-compile + + + + org.hibernate.orm + hibernate-jpamodelgen + ${version.lib.hibernate} + + + + + + + + + + + + + + + diff --git a/cloudbank-v5/customer-helidon/src/assembly/jib-ready.xml b/cloudbank-v5/customer-helidon/src/assembly/jib-ready.xml new file mode 100644 index 000000000..f6ac2485c --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/assembly/jib-ready.xml @@ -0,0 +1,24 @@ + + deployment + + zip + + false + + + + ${project.build.directory} + + ${project.build.finalName}.jar + + . + 755 + + + + ${project.build.directory}/libs + app/libs + 644 + + + \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/src/main/java/com/example/Customer.java b/cloudbank-v5/customer-helidon/src/main/java/com/example/Customer.java new file mode 100644 index 000000000..35093cf4e --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/java/com/example/Customer.java @@ -0,0 +1,149 @@ +package com.example; + +import java.util.Date; +import org.hibernate.annotations.CreationTimestamp; +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; + +@Entity +@Table(name = "CUSTOMER") +@Access(AccessType.PROPERTY) +@NamedQueries({ + @NamedQuery(name = "getCustomers", + query = "SELECT c FROM Customer c"), + @NamedQuery(name = "getCustomerById", + query = "SELECT c FROM Customer c WHERE c.customerId = :id"), + @NamedQuery(name = "getCustomerByCustomerNameContaining", + query = "SELECT c FROM Customer c WHERE c.customerName LIKE :customerName"), + @NamedQuery(name = "getCustomerByCustomerEmailContaining", + query = "SELECT c FROM Customer c WHERE c.customerEmail LIKE :customerEmail") +}) +public class Customer { + + private String customerId; + private String customerName; + private String customerEmail; + private Date dateBecameCustomer; + private String customerOtherDetails; + private String customerPassword; + + // Default constructor required by JPA + public Customer() {} + + /** + * Creates a Customer object. + * @param customerId The Customer ID + * @param customerName The Customer Name + * @param customerEmail The Customer Email + * @param customerOtherDetails Other details about the customer + */ + public Customer(String customerId, String customerName, String customerEmail, String customerOtherDetails) { + this.customerId = customerId; + this.customerName = customerName; + this.customerEmail = customerEmail; + this.customerOtherDetails = customerOtherDetails; + } + + /** + * Full constructor including password + * @param customerId The Customer ID + * @param customerName The Customer Name + * @param customerEmail The Customer Email + * @param customerOtherDetails Other details about the customer + * @param customerPassword The Customer Password + */ + public Customer(String customerId, String customerName, String customerEmail, + String customerOtherDetails, String customerPassword) { + this.customerId = customerId; + this.customerName = customerName; + this.customerEmail = customerEmail; + this.customerOtherDetails = customerOtherDetails; + this.customerPassword = customerPassword; + } + + @Id + @Column(name = "CUSTOMER_ID", nullable = false, updatable = false) + public String getCustomerId() { + return customerId; + } + + public void setCustomerId(String customerId) { + this.customerId = customerId; + } + + @Column(name = "CUSTOMER_NAME") + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + @Column(name = "CUSTOMER_EMAIL") + public String getCustomerEmail() { + return customerEmail; + } + + public void setCustomerEmail(String customerEmail) { + this.customerEmail = customerEmail; + } + + @CreationTimestamp + @Column(name = "DATE_BECAME_CUSTOMER", updatable = false, insertable = false) + public Date getDateBecameCustomer() { + return dateBecameCustomer; + } + + public void setDateBecameCustomer(Date dateBecameCustomer) { + this.dateBecameCustomer = dateBecameCustomer; + } + + @Column(name = "CUSTOMER_OTHER_DETAILS") + public String getCustomerOtherDetails() { + return customerOtherDetails; + } + + public void setCustomerOtherDetails(String customerOtherDetails) { + this.customerOtherDetails = customerOtherDetails; + } + + @Column(name = "PASSWORD") + public String getCustomerPassword() { + return customerPassword; + } + + public void setCustomerPassword(String customerPassword) { + this.customerPassword = customerPassword; + } + + @Override + public String toString() { + return "Customer{" + + "customerId='" + customerId + '\'' + + ", customerName='" + customerName + '\'' + + ", customerEmail='" + customerEmail + '\'' + + ", dateBecameCustomer=" + dateBecameCustomer + + ", customerOtherDetails='" + customerOtherDetails + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Customer customer = (Customer) o; + return customerId != null ? customerId.equals(customer.customerId) : customer.customerId == null; + } + + @Override + public int hashCode() { + return customerId != null ? customerId.hashCode() : 0; + } +} \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/src/main/java/com/example/CustomerResource.java b/cloudbank-v5/customer-helidon/src/main/java/com/example/CustomerResource.java new file mode 100644 index 000000000..cf7f96a29 --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/java/com/example/CustomerResource.java @@ -0,0 +1,236 @@ +package com.example; + +import java.util.List; +import java.net.URI; +import jakarta.enterprise.context.RequestScoped; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; +import java.util.logging.Logger; +import java.util.logging.Level; + +/** + * Helidon MP Customer REST Resource with all features from original Spring API + */ +@RequestScoped +@Path("/api/v1/customer") +public class CustomerResource { + + private static final Logger LOGGER = Logger.getLogger(CustomerResource.class.getName()); + + @PersistenceContext(unitName = "customer") + private EntityManager entityManager; + + @Context + private UriInfo uriInfo; + + + /** + * Get all customers + * + * @return List of all customers + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getCustomers() { + try { + List customers = entityManager.createNamedQuery("getCustomers", Customer.class).getResultList(); + return Response.ok(customers).build(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error finding all customers", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Find customers by name (containing) + * + * @param customerName The customer name to search for + * @return List of customers with matching names + */ + @GET + @Path("name/{customerName}") + @Produces(MediaType.APPLICATION_JSON) + public Response getCustomerByName(@PathParam("customerName") String customerName) { + try { + TypedQuery query = entityManager.createNamedQuery("getCustomerByCustomerNameContaining", Customer.class); + query.setParameter("customerName", "%" + customerName + "%"); + List customers = query.getResultList(); + return Response.ok(customers).build(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error finding customers by name: " + customerName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Get Customer with specific ID. + * + * @param id The CustomerId + * @return If the customer is found, a customer and HTTP Status code. + */ + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + public Response getCustomerById(@PathParam("id") String id) { + try { + Customer customer = entityManager.find(Customer.class, id); + if (customer != null) { + return Response.ok(customer).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error getting customer by ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Get customer that contains an email. + * + * @param email of the customer + * @return Returns a list of customers if found + */ + @GET + @Path("byemail/{email}") + @Produces(MediaType.APPLICATION_JSON) + public Response getCustomerByEmail(@PathParam("email") String email) { + try { + TypedQuery query = entityManager.createNamedQuery("getCustomerByCustomerEmailContaining", Customer.class); + query.setParameter("customerEmail", "%" + email + "%"); + List customers = query.getResultList(); + return Response.ok(customers).build(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error finding customers by email: " + email, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Create a customer. + * + * @param customer Customer object with the customer details. + * @return Returns HTTP Status code or the URI of the created object. + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Transactional(Transactional.TxType.REQUIRED) + public Response createCustomer(Customer customer) { + try { + // Check if customer already exists + Customer existingCustomer = entityManager.find(Customer.class, customer.getCustomerId()); + + if (existingCustomer == null) { + entityManager.persist(customer); + entityManager.flush(); // Ensure the entity is persisted + + // Build the location URI for the created resource + URI location = UriBuilder.fromResource(CustomerResource.class) + .path("{id}") + .build(customer.getCustomerId()); + + return Response.created(location).build(); + } else { + return Response.status(Response.Status.CONFLICT).entity(customer).build(); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error creating customer", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Update a specific Customer (ID). + * + * @param id The id of the customer + * @param customer A customer object + * @return A Http Status code and updated customer + */ + @PUT + @Path("{id}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Transactional(Transactional.TxType.REQUIRED) + public Response updateCustomer(@PathParam("id") String id, Customer customer) { + try { + Customer existingCustomer = entityManager.find(Customer.class, id); + if (existingCustomer != null) { + // Update the existing customer with new values + existingCustomer.setCustomerName(customer.getCustomerName()); + existingCustomer.setCustomerEmail(customer.getCustomerEmail()); + existingCustomer.setCustomerOtherDetails(customer.getCustomerOtherDetails()); + + Customer updatedCustomer = entityManager.merge(existingCustomer); + return Response.ok(updatedCustomer).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error updating customer with ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Delete a specific customer (ID). + * + * @param customerId the Id of the customer to be deleted + * @return A Http Status code + */ + @DELETE + @Path("{customerId}") + @Transactional(Transactional.TxType.REQUIRED) + public Response deleteCustomer(@PathParam("customerId") String customerId) { + try { + Customer customer = entityManager.find(Customer.class, customerId); + if (customer != null) { + entityManager.remove(customer); + return Response.status(Response.Status.NO_CONTENT).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error deleting customer with ID: " + customerId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Apply for loan - Method isn't fully implemented. + * + * @param amount Loan amount + * @return A Http Status + */ + @POST + @Path("applyLoan/{amount}") + @Produces(MediaType.APPLICATION_JSON) + public Response applyForLoan(@PathParam("amount") long amount) { + try { + // Check Credit Rating + // Amount vs Rating approval? + // Create Account + // Update Account Balance + // Notify + return Response.status(418).build(); // I_AM_A_TEAPOT equivalent + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error processing loan application for amount: " + amount, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } +} \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/src/main/resources/META-INF/beans.xml b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..b0993408c --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/beans.xml @@ -0,0 +1,8 @@ + + + diff --git a/cloudbank-v5/customer-helidon/src/main/resources/META-INF/init-script.sql b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/init-script.sql new file mode 100644 index 000000000..4c0f1f8cf --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/init-script.sql @@ -0,0 +1,11 @@ +create table customer if not exists ( + CUSTOMER_ID varchar2(256), + CUSTOMER_NAME varchar2(256), + CUSTOMER_EMAIL varchar2(256), + DATE_BECAME_CUSTOMER date, + CUSTOMER_OTHER_DETAILS varchar2(256), + PASSWORD varchar2(256) +); +insert into customer (customer_id, customer_name, customer_email, customer_other_details, password) +values ('abc123', 'Bob Drake', 'bob@drake.com', '', 'Welcome-12345'); +commit; \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/src/main/resources/META-INF/microprofile-config.properties b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..87160b545 --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,31 @@ +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +# OpenTelemetry configuration for SigNoz metrics integration +metrics.rest-request.enabled=true +otel.sdk.disabled=false +otel.service.name=customer + +# Application properties. This is the default greeting +app.greeting=Hello + +# Database connection factory - specifies Oracle UCP driver for connection pooling +javax.sql.DataSource.customer.connectionFactoryClassName = oracle.jdbc.pool.OracleDataSource + +# Enable when connecting to local Oracle container +# javax.sql.DataSource.customer.URL = jdbc:oracle:thin:@//localhost:1521/freepdb1 +# javax.sql.DataSource.customer.user = customer +# javax.sql.DataSource.customer.password = Welcome12345 + +# Enable Table Creation +hibernate.hbm2ddl.auto=create +hibernate.show_sql=true +hibernate.transaction.coordinator_class=jta + +# Liquibase configuration - Helidon style +liquibase.change-log=classpath:db/changelog/controller.yaml +liquibase.url=${javax.sql.DataSource.customer.URL} +liquibase.user=${liquibase.datasource.username} +liquibase.password=${liquibase.datasource.password} +liquibase.enabled=${LIQUIBASE_ENABLED:true} diff --git a/cloudbank-v5/customer-helidon/src/main/resources/META-INF/native-image/com/example/customer-helidon/native-image.properties b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/native-image/com/example/customer-helidon/native-image.properties new file mode 100644 index 000000000..b450206b5 --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/native-image/com/example/customer-helidon/native-image.properties @@ -0,0 +1 @@ +Args=--initialize-at-build-time=com.example \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/src/main/resources/META-INF/persistence.xml b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/persistence.xml new file mode 100644 index 000000000..df1145500 --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,18 @@ + + + + + customer + com.example.Customer + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/src/main/resources/application.yaml b/cloudbank-v5/customer-helidon/src/main/resources/application.yaml new file mode 100644 index 000000000..9a7fce1c9 --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/resources/application.yaml @@ -0,0 +1,12 @@ + server: + features: + eureka: + enabled: true + client: + base-uri: ${eureka.client.service-url.defaultZone} + connect-timeout: PT10S + read-timeout: PT30S + instance: + name: "customer-helidon" + hostname: ${eureka.instance.hostname} + prefer-ip-address: ${eureka.instance.preferIpAddress:true} \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/src/main/resources/logging.properties b/cloudbank-v5/customer-helidon/src/main/resources/logging.properties new file mode 100644 index 000000000..669559735 --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/main/resources/logging.properties @@ -0,0 +1,21 @@ +# Example Logging Configuration File +# Use standard Java logging instead of Helidon custom handlers +handlers=java.util.logging.ConsoleHandler + +# Console handler configuration +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter + +# Use a similar format to your original but without Helidon-specific tokens +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s Thread[%2$s]: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Quiet Weld +org.jboss.level=WARNING + +# Component specific log levels +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/cloudbank-v5/customer-helidon/src/test/java/com/example/CustomerResourceUnitTest.java b/cloudbank-v5/customer-helidon/src/test/java/com/example/CustomerResourceUnitTest.java new file mode 100644 index 000000000..1631150d7 --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/test/java/com/example/CustomerResourceUnitTest.java @@ -0,0 +1,428 @@ +package com.example; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import java.util.Arrays; +import java.util.List; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for CustomerResource using Mockito + * These tests run without any containers or databases + */ +@ExtendWith(MockitoExtension.class) +public class CustomerResourceUnitTest { + + @Mock + private EntityManager entityManager; + + @Mock + private UriInfo uriInfo; + + @Mock + private TypedQuery typedQuery; + + @InjectMocks + private CustomerResource customerResource; + + private Customer testCustomer; + private List testCustomers; + + @BeforeEach + void setUp() { + // Create test data + testCustomer = new Customer("CUST001", "John Doe", "john.doe@example.com", "Premium customer"); + + Customer customer2 = new Customer("CUST002", "Jane Smith", "jane.smith@gmail.com", "Standard customer"); + Customer customer3 = new Customer("CUST003", "Bob Johnson", "bob.johnson@yahoo.com", "VIP customer"); + + testCustomers = Arrays.asList(testCustomer, customer2, customer3); + } + + // ============================================== + // GET ALL CUSTOMERS TESTS + // ============================================== + + @Test + @DisplayName("Should get all customers successfully") + void testGetAllCustomers_Success() { + // Arrange + when(entityManager.createNamedQuery("getCustomers", Customer.class)).thenReturn(typedQuery); + when(typedQuery.getResultList()).thenReturn(testCustomers); + + // Act + Response response = customerResource.getCustomers(); + + // Assert + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(testCustomers, response.getEntity()); + + verify(entityManager).createNamedQuery("getCustomers", Customer.class); + verify(typedQuery).getResultList(); + } + + @Test + @DisplayName("Should handle exception when getting all customers") + void testGetAllCustomers_Exception() { + // Arrange + when(entityManager.createNamedQuery("getCustomers", Customer.class)) + .thenThrow(new RuntimeException("Database error")); + + // Act + Response response = customerResource.getCustomers(); + + // Assert + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + } + + @Test + @DisplayName("Should return empty list when no customers exist") + void testGetAllCustomers_EmptyList() { + // Arrange + when(entityManager.createNamedQuery("getCustomers", Customer.class)).thenReturn(typedQuery); + when(typedQuery.getResultList()).thenReturn(Collections.emptyList()); + + // Act + Response response = customerResource.getCustomers(); + + // Assert + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(Collections.emptyList(), response.getEntity()); + } + + // ============================================== + // GET CUSTOMER BY ID TESTS + // ============================================== + + @Test + @DisplayName("Should get customer by ID successfully") + void testGetCustomerById_Success() { + // Arrange + String customerId = "CUST001"; + when(entityManager.find(Customer.class, customerId)).thenReturn(testCustomer); + + // Act + Response response = customerResource.getCustomerById(customerId); + + // Assert + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(testCustomer, response.getEntity()); + + verify(entityManager).find(Customer.class, customerId); + } + + @Test + @DisplayName("Should return 404 when customer not found by ID") + void testGetCustomerById_NotFound() { + // Arrange + String customerId = "NONEXISTENT"; + when(entityManager.find(Customer.class, customerId)).thenReturn(null); + + // Act + Response response = customerResource.getCustomerById(customerId); + + // Assert + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + assertNull(response.getEntity()); + } + + @Test + @DisplayName("Should handle exception when getting customer by ID") + void testGetCustomerById_Exception() { + // Arrange + String customerId = "CUST001"; + when(entityManager.find(Customer.class, customerId)) + .thenThrow(new RuntimeException("Database error")); + + // Act + Response response = customerResource.getCustomerById(customerId); + + // Assert + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + } + + // ============================================== + // SEARCH BY NAME TESTS + // ============================================== + + @Test + @DisplayName("Should search customers by name successfully") + void testGetCustomerByName_Success() { + // Arrange + String searchName = "John"; + when(entityManager.createNamedQuery("getCustomerByCustomerNameContaining", Customer.class)) + .thenReturn(typedQuery); + when(typedQuery.getResultList()).thenReturn(Arrays.asList(testCustomer)); + + // Act + Response response = customerResource.getCustomerByName(searchName); + + // Assert + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(Arrays.asList(testCustomer), response.getEntity()); + + verify(typedQuery).setParameter("customerName", "%" + searchName + "%"); + } + + @Test + @DisplayName("Should return empty list when no customers match name") + void testGetCustomerByName_NoMatches() { + // Arrange + String searchName = "NonExistent"; + when(entityManager.createNamedQuery("getCustomerByCustomerNameContaining", Customer.class)) + .thenReturn(typedQuery); + when(typedQuery.getResultList()).thenReturn(Collections.emptyList()); + + // Act + Response response = customerResource.getCustomerByName(searchName); + + // Assert + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(Collections.emptyList(), response.getEntity()); + } + + // ============================================== + // SEARCH BY EMAIL TESTS + // ============================================== + + @Test + @DisplayName("Should search customers by email successfully") + void testGetCustomerByEmail_Success() { + // Arrange + String searchEmail = "gmail"; + Customer gmailCustomer = new Customer("CUST002", "Jane Smith", "jane.smith@gmail.com", "Standard customer"); + + when(entityManager.createNamedQuery("getCustomerByCustomerEmailContaining", Customer.class)) + .thenReturn(typedQuery); + when(typedQuery.getResultList()).thenReturn(Arrays.asList(gmailCustomer)); + + // Act + Response response = customerResource.getCustomerByEmail(searchEmail); + + // Assert + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(Arrays.asList(gmailCustomer), response.getEntity()); + + verify(typedQuery).setParameter("customerEmail", "%" + searchEmail + "%"); + } + + // ============================================== + // CREATE CUSTOMER TESTS + // ============================================== + + @Test + @DisplayName("Should create customer successfully") + void testCreateCustomer_Success() { + // Arrange + Customer newCustomer = new Customer("CUST004", "New Customer", "new@example.com", "Details"); + when(entityManager.find(Customer.class, newCustomer.getCustomerId())).thenReturn(null); + + // Act + Response response = customerResource.createCustomer(newCustomer); + + // Assert + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + verify(entityManager).persist(newCustomer); + verify(entityManager).flush(); + } + + @Test + @DisplayName("Should return conflict when customer already exists") + void testCreateCustomer_Conflict() { + // Arrange + when(entityManager.find(Customer.class, testCustomer.getCustomerId())).thenReturn(testCustomer); + + // Act + Response response = customerResource.createCustomer(testCustomer); + + // Assert + assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus()); + assertEquals(testCustomer, response.getEntity()); + + verify(entityManager, never()).persist(any()); + } + + @Test + @DisplayName("Should handle exception when creating customer") + void testCreateCustomer_Exception() { + // Arrange + Customer newCustomer = new Customer("CUST004", "New Customer", "new@example.com", "Details"); + when(entityManager.find(Customer.class, newCustomer.getCustomerId())).thenReturn(null); + doThrow(new RuntimeException("Database error")).when(entityManager).persist(newCustomer); + + // Act + Response response = customerResource.createCustomer(newCustomer); + + // Assert + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + } + + // ============================================== + // UPDATE CUSTOMER TESTS + // ============================================== + + @Test + @DisplayName("Should update customer successfully") + void testUpdateCustomer_Success() { + // Arrange + String customerId = "CUST001"; + Customer updatedData = new Customer(customerId, "John Doe Updated", "john.updated@example.com", "Updated details"); + Customer existingCustomer = new Customer(customerId, "John Doe", "john.doe@example.com", "Original details"); + + when(entityManager.find(Customer.class, customerId)).thenReturn(existingCustomer); + when(entityManager.merge(existingCustomer)).thenReturn(existingCustomer); + + // Act + Response response = customerResource.updateCustomer(customerId, updatedData); + + // Assert + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(existingCustomer, response.getEntity()); + + // Verify the customer was updated with new values + assertEquals("John Doe Updated", existingCustomer.getCustomerName()); + assertEquals("john.updated@example.com", existingCustomer.getCustomerEmail()); + assertEquals("Updated details", existingCustomer.getCustomerOtherDetails()); + + verify(entityManager).merge(existingCustomer); + } + + @Test + @DisplayName("Should return 404 when updating non-existent customer") + void testUpdateCustomer_NotFound() { + // Arrange + String customerId = "NONEXISTENT"; + Customer updatedData = new Customer(customerId, "Updated Name", "updated@example.com", "Updated details"); + + when(entityManager.find(Customer.class, customerId)).thenReturn(null); + + // Act + Response response = customerResource.updateCustomer(customerId, updatedData); + + // Assert + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + + verify(entityManager, never()).merge(any()); + } + + // ============================================== + // DELETE CUSTOMER TESTS + // ============================================== + + @Test + @DisplayName("Should delete customer successfully") + void testDeleteCustomer_Success() { + // Arrange + String customerId = "CUST001"; + when(entityManager.find(Customer.class, customerId)).thenReturn(testCustomer); + + // Act + Response response = customerResource.deleteCustomer(customerId); + + // Assert + assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); + + verify(entityManager).remove(testCustomer); + } + + @Test + @DisplayName("Should return 404 when deleting non-existent customer") + void testDeleteCustomer_NotFound() { + // Arrange + String customerId = "NONEXISTENT"; + when(entityManager.find(Customer.class, customerId)).thenReturn(null); + + // Act + Response response = customerResource.deleteCustomer(customerId); + + // Assert + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + + verify(entityManager, never()).remove(any()); + } + + // ============================================== + // LOAN APPLICATION TESTS + // ============================================== + + @Test + @DisplayName("Should return 418 for loan application") + void testApplyForLoan_TeapotResponse() { + // Arrange + long loanAmount = 10000L; + + // Act + Response response = customerResource.applyForLoan(loanAmount); + + // Assert + assertEquals(418, response.getStatus()); // I'm a Teapot + } + + @Test + @DisplayName("Should handle different loan amounts") + void testApplyForLoan_DifferentAmounts() { + // Test multiple loan amounts + long[] amounts = {1000L, 50000L, 100000L}; + + for (long amount : amounts) { + Response response = customerResource.applyForLoan(amount); + assertEquals(418, response.getStatus()); + } + } + + // ============================================== + // CUSTOMER ENTITY TESTS + // ============================================== + + @Test + @DisplayName("Should create customer with all constructors") + void testCustomerConstructors() { + // Test default constructor + Customer customer1 = new Customer(); + assertNull(customer1.getCustomerId()); + + // Test 4-parameter constructor + Customer customer2 = new Customer("ID", "Name", "email@test.com", "Details"); + assertEquals("ID", customer2.getCustomerId()); + assertEquals("Name", customer2.getCustomerName()); + assertEquals("email@test.com", customer2.getCustomerEmail()); + assertEquals("Details", customer2.getCustomerOtherDetails()); + + // Test 5-parameter constructor + Customer customer3 = new Customer("ID", "Name", "email@test.com", "Details", "password"); + assertEquals("password", customer3.getCustomerPassword()); + } + + @Test + @DisplayName("Should test customer equals and hashCode") + void testCustomerEqualsAndHashCode() { + Customer customer1 = new Customer("CUST001", "John", "john@test.com", "Details"); + Customer customer2 = new Customer("CUST001", "Jane", "jane@test.com", "Other details"); + Customer customer3 = new Customer("CUST002", "John", "john@test.com", "Details"); + + // Same ID should be equal + assertEquals(customer1, customer2); + assertEquals(customer1.hashCode(), customer2.hashCode()); + + // Different ID should not be equal + assertNotEquals(customer1, customer3); + + // toString should not contain password + assertFalse(customer1.toString().contains("password")); + } +} \ No newline at end of file diff --git a/cloudbank-v5/customer-helidon/src/test/resources/META-INF/microprofile-config.properties b/cloudbank-v5/customer-helidon/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..e69de29bb diff --git a/cloudbank-v5/customer-helidon/src/test/resources/application-test.yaml b/cloudbank-v5/customer-helidon/src/test/resources/application-test.yaml new file mode 100644 index 000000000..94f345144 --- /dev/null +++ b/cloudbank-v5/customer-helidon/src/test/resources/application-test.yaml @@ -0,0 +1,2 @@ +security: + enabled: false \ No newline at end of file diff --git a/cloudbank-v5/customer/helm/Chart.yaml b/cloudbank-v5/customer/helm/Chart.yaml new file mode 100644 index 000000000..2728b2f1d --- /dev/null +++ b/cloudbank-v5/customer/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: customer # Replace with your application name +description: A Helm chart for the OBaaS Platform Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.2.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.0.1" diff --git a/cloudbank-v5/customer/helm/README.md b/cloudbank-v5/customer/helm/README.md new file mode 100644 index 000000000..9403cc5e0 --- /dev/null +++ b/cloudbank-v5/customer/helm/README.md @@ -0,0 +1,16 @@ +# OBaas Sample App Chart + +This chart provides an extensible sample for applications running on [OBaaS](https://oracle.github.io/microservices-datadriven/obaas/). + +To use this chart for a given application, download the chart and update the [Chart.name](./Chart.yaml) value to your application's name. + +## Customizing the chart + +The OBaaS sample app chart is meant to serve as a developer template, and is fully customizable. + +Standard parameters for Kubernetes options like node affinity, HPAs, ingress and more are provided in the [values.yaml file](./values.yaml). + +## OBaaS options + +Within the [values.yaml file](./values.yaml), the `obaas` key allows chart developers to enable or disable OBaaS integrations like database connectivity, OpenTelemetry, MicroProfile LRA, SpringBoot, and Eureka. +enabled: true diff --git a/cloudbank-v5/customer/helm/templates/NOTES.txt b/cloudbank-v5/customer/helm/templates/NOTES.txt new file mode 100644 index 000000000..8d9597d97 --- /dev/null +++ b/cloudbank-v5/customer/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "obaas-app.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "obaas-app.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "obaas-app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "obaas-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cloudbank-v5/customer/helm/templates/_helpers.tpl b/cloudbank-v5/customer/helm/templates/_helpers.tpl new file mode 100644 index 000000000..b8e6d5b79 --- /dev/null +++ b/cloudbank-v5/customer/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "obaas-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "obaas-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "obaas-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "obaas-app.labels" -}} +helm.sh/chart: {{ include "obaas-app.chart" . }} +{{ include "obaas-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "obaas-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "obaas-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "obaas-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "obaas-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cloudbank-v5/customer/helm/templates/deployment.yaml b/cloudbank-v5/customer/helm/templates/deployment.yaml new file mode 100644 index 000000000..d4902a2f2 --- /dev/null +++ b/cloudbank-v5/customer/helm/templates/deployment.yaml @@ -0,0 +1,147 @@ +# Load obaas configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + obaas.framework: SPRING_BOOT +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "obaas-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "obaas-app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "obaas-app.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CONNECT_STRING + value: jdbc:oracle:thin:@$(SPRING_DB_SERVICE)?TNS_ADMIN=/oracle/tnsadmin + # Lookup ObaaS configuration + {{- $obaas := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-config") }} + {{- $obaasObs := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-observability-config") }} + {{- if $.Values.obaas.mp_lra.enabled }} + - name: MP_LRA_COORDINATOR_URL + value: {{ $obaas.data.otmm | quote }} + {{- end }} + {{- if $.Values.obaas.eureka.enabled }} + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_CLIENT_REGISTER_WITH_EUREKA + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: {{ $obaas.data.eureka | quote }} + - name: EUREKA_INSTANCE_HOSTNAME + value: {{ include "obaas-app.fullname" . }}-{{ $.Release.Namespace }} + {{- end }} + {{- if $.Values.obaas.otel.enabled }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ (index $obaasObs.data "signoz-otel-collector") | quote }} + {{- end }} + {{- if $.Values.obaas.springboot.enabled }} + - name: SPRING_PROFILES_ACTIVE + value: {{ $obaas.data.SPRING_PROFILES_ACTIVE | quote }} + {{- if $.Values.obaas.database.enabled }} + - name: SPRING_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.username + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.password + - name: LIQUIBASE_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_username + - name: LIQUIBASE_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_password + - name: LIQUIBASE_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DB_SERVICE + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.service + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ $.Values.obaas.database.walletSecret }} + mountPath: /oracle/tnsadmin + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: {{ $.Values.obaas.database.walletSecret }} + secret: + secretName: {{ $.Values.obaas.database.walletSecret }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cloudbank-v5/customer/helm/templates/hpa.yaml b/cloudbank-v5/customer/helm/templates/hpa.yaml new file mode 100644 index 000000000..7c8805fb0 --- /dev/null +++ b/cloudbank-v5/customer/helm/templates/hpa.yaml @@ -0,0 +1,33 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "obaas-app.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/customer/helm/templates/ingress.yaml b/cloudbank-v5/customer/helm/templates/ingress.yaml new file mode 100644 index 000000000..50aa98cd8 --- /dev/null +++ b/cloudbank-v5/customer/helm/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "obaas-app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/customer/helm/templates/service.yaml b/cloudbank-v5/customer/helm/templates/service.yaml new file mode 100644 index 000000000..5876cf66d --- /dev/null +++ b/cloudbank-v5/customer/helm/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "obaas-app.selectorLabels" . | nindent 4 }} diff --git a/cloudbank-v5/customer/helm/templates/serviceaccount.yaml b/cloudbank-v5/customer/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..a22b73ef6 --- /dev/null +++ b/cloudbank-v5/customer/helm/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "obaas-app.serviceAccountName" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/cloudbank-v5/customer/helm/templates/tests/test-connection.yaml b/cloudbank-v5/customer/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..8596a023f --- /dev/null +++ b/cloudbank-v5/customer/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "obaas-app.fullname" . }}-test-connection" + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "obaas-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cloudbank-v5/customer/helm/values.yaml b/cloudbank-v5/customer/helm/values.yaml new file mode 100644 index 000000000..8697172c0 --- /dev/null +++ b/cloudbank-v5/customer/helm/values.yaml @@ -0,0 +1,155 @@ +# Default values for obaas-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: "my-repository/customer" + # This sets the pull policy for images. + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "0.0.1-SNAPSHOT" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: + signoz.io/path: /actuator/prometheus + signoz.io/port: "8080" + signoz.io/scrape: "true" +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 8080 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + requests: + cpu: 100m + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Oracle Backend for Microservices and AI Settings. +obaas: + namespace: obaas-dev # Replace with your namespace + database: + enabled: true # If true variables with DB secret content will be created + credentialsSecret: customer-db-secrets # Replace with your secret name + walletSecret: obaas-adb-tns-admin-1 # Replace with your wallet secret name + otel: + enabled: true # Enable OpenTelemetry + # MicroProfile LRA + mp_lra: + enabled: false # Enable OTMM + # Spring Boot applications + springboot: + enabled: true # Enable Spring Boot specific variables + eureka: + enabled: true # Enable Eureka client diff --git a/cloudbank-v5/customer/pom.xml b/cloudbank-v5/customer/pom.xml new file mode 100644 index 000000000..2a136f2a9 --- /dev/null +++ b/cloudbank-v5/customer/pom.xml @@ -0,0 +1,88 @@ + + + + + 4.0.0 + + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + + customer + 0.0.1-SNAPSHOT + customer + Customer Application + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.oracle.database.spring + oracle-spring-boot-starter-wallet + ${oracle-springboot-starter.version} + pom + + + org.liquibase + liquibase-core + ${liquibase.version} + + + net.ttddyy.observation + datasource-micrometer-spring-boot + ${datasource-micrometer-spring-boot.version} + + + com.example + common + ${project.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.graalvm.buildtools + native-maven-plugin + + + org.liquibase + liquibase-maven-plugin + ${liquibase.version} + + + org.eclipse.jkube + kubernetes-maven-plugin + ${jkube.version} + + + + my-repository/customer:${project.version} + + ghcr.io/oracle/openjdk-image-obaas:21 + + dir + /deployments + + java -jar /deployments/${project.artifactId}-${project.version}.jar + + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/customer/src/main/java/com/example/customer/CustomerApplication.java b/cloudbank-v5/customer/src/main/java/com/example/customer/CustomerApplication.java new file mode 100644 index 000000000..823b7ed3b --- /dev/null +++ b/cloudbank-v5/customer/src/main/java/com/example/customer/CustomerApplication.java @@ -0,0 +1,22 @@ +// Copyright (c) 2023, 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.customer; + +import com.example.common.filter.LoggingFilterConfig; +import com.example.common.ucp.UCPTelemetry; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.Import; + +@SpringBootApplication +@EnableDiscoveryClient +@Import({ LoggingFilterConfig.class, UCPTelemetry.class }) +public class CustomerApplication { + + public static void main(String[] args) { + SpringApplication.run(CustomerApplication.class, args); + } + +} diff --git a/cloudbank-v5/customer/src/main/java/com/example/customer/controller/CustomerController.java b/cloudbank-v5/customer/src/main/java/com/example/customer/controller/CustomerController.java new file mode 100644 index 000000000..29e697b7b --- /dev/null +++ b/cloudbank-v5/customer/src/main/java/com/example/customer/controller/CustomerController.java @@ -0,0 +1,169 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.customer.controller; + +import java.net.URI; +import java.util.List; +import java.util.Optional; + +import com.example.customer.model.Customers; +import com.example.customer.repository.CustomersRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +@RestController +@RequestMapping("/api/v1") +@Slf4j +public class CustomerController { + final CustomersRepository customersRepository; + + public CustomerController(CustomersRepository customersRepository) { + this.customersRepository = customersRepository; + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/customer") + public List findAll() { + return customersRepository.findAll(); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/customer/name/{customerName}") + public List findByCustomerByName(@PathVariable String customerName) { + return customersRepository.findByCustomerNameIsContaining(customerName); + } + + + /** + * Get Customer with specific ID. + * + * @param id The CustomerId + * @return If the customers is found, a customer and HTTP Status code. + */ + @GetMapping("/customer/{id}") + public ResponseEntity getCustomerById(@PathVariable("id") String id) { + Optional customerData = customersRepository.findById(id); + try { + return customerData.map(customers -> new ResponseEntity<>(customers, HttpStatus.OK)) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Get customer that contains an email. + * + * @param email of the customer + * @return Returns a customer if found + */ + @GetMapping("/customer/byemail/{email}") + public List getCustomerByEmail(@PathVariable("email") String email) { + return customersRepository.findByCustomerEmailIsContaining(email); + } + + /** + * Create a customer. + * + * @param customer Customer object with the customer details. + * @return Returns HTTP Status code or the URI of the created object. + */ + @PostMapping("/customer") + public ResponseEntity createCustomer(@RequestBody Customers customer) { + boolean exists = customersRepository.existsById(customer.getCustomerId()); + + if (!exists) { + try { + Customers newCustomer = customersRepository.saveAndFlush(new Customers( + customer.getCustomerId(), + customer.getCustomerName(), + customer.getCustomerEmail(), + customer.getCustomerOtherDetails())); + + URI location = ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{id}") + .buildAndExpand(newCustomer.getCustomerId()) + .toUri(); + return ResponseEntity.created(location).build(); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + return new ResponseEntity<>(customer, HttpStatus.CONFLICT); + } + } + + /** + * Update a specific Customer (ID). + * + * @param id The id of the customer + * @param customer A customer object + * @return A Http Status code + */ + @PutMapping("/customer/{id}") + public ResponseEntity updateCustomer(@PathVariable("id") String id, @RequestBody Customers customer) { + Optional customerData = customersRepository.findById(id); + try { + if (customerData.isPresent()) { + Customers updCustomer = customerData.get(); + updCustomer.setCustomerName(customer.getCustomerName()); + updCustomer.setCustomerEmail(customer.getCustomerEmail()); + updCustomer.setCustomerOtherDetails(customer.getCustomerOtherDetails()); + return new ResponseEntity<>(customersRepository.save(updCustomer), HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Delete a specific customer (ID). + * + * @param customerId the Id of the customer to be deleted + * @return A Http Status code + */ + @DeleteMapping("/customer/{customerId}") + public ResponseEntity deleteCustomer(@PathVariable("customerId") String customerId) { + try { + customersRepository.deleteById(customerId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Method isn't implemented. + * + * @param amount Loan amount + * @return A Http Status + */ + @PostMapping("/customer/applyLoan/{amount}") + public ResponseEntity applyForLoan(@PathVariable("amount") long amount) { + try { + // Check Credit Rating + // Amount vs Rating approval? + // Create Account + // Update Account Balance + // Notify + return new ResponseEntity<>(HttpStatus.I_AM_A_TEAPOT); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/cloudbank-v5/customer/src/main/java/com/example/customer/model/Customers.java b/cloudbank-v5/customer/src/main/java/com/example/customer/model/Customers.java new file mode 100644 index 000000000..8325b75d7 --- /dev/null +++ b/cloudbank-v5/customer/src/main/java/com/example/customer/model/Customers.java @@ -0,0 +1,58 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.customer.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Generated; +import org.hibernate.annotations.GenerationTime; + +@SuppressWarnings("deprecation") +@Entity +@Table(name = "CUSTOMERS") +@Data +@NoArgsConstructor +public class Customers { + + @Id + @Column(name = "CUSTOMER_ID") + private String customerId; + + @Column(name = "CUSTOMER_NAME") + private String customerName; + + @Column(name = "CUSTOMER_EMAIL") + private String customerEmail; + + @Generated(GenerationTime.INSERT) + @Column(name = "DATE_BECAME_CUSTOMER", updatable = false, insertable = false) + private Date dateBecameCustomer; + + @Column(name = "CUSTOMER_OTHER_DETAILS") + private String customerOtherDetails; + + @Column(name = "PASSWORD") + private String customerPassword; + + + /** + * Creates a Customers object. + * @param customerId The Customer ID + * @param customerName The Customer Name + * @param customerEmail The Customer Email + * @param customerOtherDetails Other details about the customer + */ + public Customers(String customerId, String customerName, String customerEmail, String customerOtherDetails) { + this.customerId = customerId; + this.customerName = customerName; + this.customerEmail = customerEmail; + this.customerOtherDetails = customerOtherDetails; + } +} diff --git a/cloudbank-v5/customer/src/main/java/com/example/customer/repository/CustomersRepository.java b/cloudbank-v5/customer/src/main/java/com/example/customer/repository/CustomersRepository.java new file mode 100644 index 000000000..74835628f --- /dev/null +++ b/cloudbank-v5/customer/src/main/java/com/example/customer/repository/CustomersRepository.java @@ -0,0 +1,17 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.customer.repository; + +import java.util.List; + +import com.example.customer.model.Customers; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CustomersRepository extends JpaRepository { + + List findByCustomerNameIsContaining(String customerName); + + List findByCustomerEmailIsContaining(String customerEmail); + +} diff --git a/cloudbank-v5/customer/src/main/resources/application.yaml b/cloudbank-v5/customer/src/main/resources/application.yaml new file mode 100644 index 000000000..e23ceadda --- /dev/null +++ b/cloudbank-v5/customer/src/main/resources/application.yaml @@ -0,0 +1,33 @@ +# Copyright (c) 2023, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ +spring: + application: + name: customer + cloud: + config: + import-check: + enabled: false + + liquibase: + change-log: classpath:db/changelog/controller.yaml + url: ${LIQUIBASE_DATASOURCE_URL} + user: ${LIQUIBASE_DATASOURCE_USERNAME} + password: ${LIQUIBASE_DATASOURCE_PASSWORD:} + enabled: ${LIQUIBASE_ENABLED:true} + + datasource: + url: ${SPRING_DATASOURCE_URL} + user: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driver-class-name: oracle.jdbc.OracleDriver + type: oracle.ucp.jdbc.PoolDataSource + oracleucp: + connection-factory-class-name: oracle.jdbc.pool.OracleDataSource + connection-pool-name: CustomerConnectionPool + initial-pool-size: 15 + min-pool-size: 10 + max-pool-size: 30 + shared: true + + config: + import: classpath:common.yaml \ No newline at end of file diff --git a/cloudbank-v5/customer/src/main/resources/banner.txt b/cloudbank-v5/customer/src/main/resources/banner.txt new file mode 100644 index 000000000..b09aa88d0 --- /dev/null +++ b/cloudbank-v5/customer/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + ____ _ + / ___| _ ___| |_ ___ _ __ ___ ___ _ __ + | | | | | / __| __/ _ \| '_ ` _ \ / _ \ '__| + | |__| |_| \__ \ || (_) | | | | | | __/ | + \____\__,_|___/\__\___/|_| |_| |_|\___|_| + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} diff --git a/cloudbank-v5/customer/src/main/resources/db/changelog/controller.yaml b/cloudbank-v5/customer/src/main/resources/db/changelog/controller.yaml new file mode 100644 index 000000000..cec0ecb3b --- /dev/null +++ b/cloudbank-v5/customer/src/main/resources/db/changelog/controller.yaml @@ -0,0 +1,6 @@ +--- +databaseChangeLog: + - include: + file: classpath:db/changelog/table.sql + - include: + file: classpath:db/changelog/data.sql diff --git a/cloudbank-v5/customer/src/main/resources/db/changelog/data.sql b/cloudbank-v5/customer/src/main/resources/db/changelog/data.sql new file mode 100644 index 000000000..5f9e3d918 --- /dev/null +++ b/cloudbank-v5/customer/src/main/resources/db/changelog/data.sql @@ -0,0 +1,14 @@ +-- liquibase formatted sql + +-- changeset customer:1 runAlways:true +TRUNCATE TABLE CUSTOMER.CUSTOMERS; + +INSERT INTO CUSTOMER.CUSTOMERS (CUSTOMER_ID,CUSTOMER_NAME,CUSTOMER_EMAIL,CUSTOMER_OTHER_DETAILS,PASSWORD,ROLE) +VALUES ('qwertysdwr','Andy','andy@andy.com','Somekind of Info','SuperSecret','USER_ROLE'); +INSERT INTO CUSTOMER.CUSTOMERS (CUSTOMER_ID,CUSTOMER_NAME,CUSTOMER_EMAIL,CUSTOMER_OTHER_DETAILS,PASSWORD,ROLE) +VALUES ('aerg45sffd','Sanjay','sanjay@sanjay.com','Information','Welcome','USER_ROLE'); +INSERT INTO CUSTOMER.CUSTOMERS (CUSTOMER_ID,CUSTOMER_NAME,CUSTOMER_EMAIL,CUSTOMER_OTHER_DETAILS,PASSWORD,ROLE) +VALUES ('bkzLp8cozi','Mark','mark@mark.com','Important Info','Secret','USER_ROLE'); +COMMIT; + +--rollback DELETE FROM CUSTOMER.CUSTOMERS; \ No newline at end of file diff --git a/cloudbank-v5/customer/src/main/resources/db/changelog/table.sql b/cloudbank-v5/customer/src/main/resources/db/changelog/table.sql new file mode 100644 index 000000000..673ac5c60 --- /dev/null +++ b/cloudbank-v5/customer/src/main/resources/db/changelog/table.sql @@ -0,0 +1,22 @@ +-- liquibase formatted sql + +--changeset customer:1 +--preconditions onFail:MARK_RAN onerror:MARK_RAN +--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM CUSTOMER.CUSTOMERS WHERE 1=2 +DROP TABLE CUSTOMER.CUSTOMERS; + +--changeset customer:2 +CREATE TABLE CUSTOMER.CUSTOMERS ( + CUSTOMER_ID VARCHAR2 (20), + CUSTOMER_NAME VARCHAR2 (40), + CUSTOMER_EMAIL VARCHAR2 (40), + DATE_BECAME_CUSTOMER DATE DEFAULT SYSDATE NOT NULL, + CUSTOMER_OTHER_DETAILS VARCHAR2 (4000), + PASSWORD VARCHAR2(40), + ROLE VARCHAR2(40) +) LOGGING; + +ALTER TABLE CUSTOMER.CUSTOMERS ADD CONSTRAINT CUSTOMERS_PK PRIMARY KEY (CUSTOMER_ID) USING INDEX LOGGING; +COMMENT ON TABLE CUSTOMER.CUSTOMERS IS 'CLOUDBANK CUSTOMERS TABLE'; + +--rollback DROP TABLE CUSTOMER.CUSTOMERS; diff --git a/cloudbank-v5/customer/src/test/java/com/example/customer/CustomerApplicationTests.java b/cloudbank-v5/customer/src/test/java/com/example/customer/CustomerApplicationTests.java new file mode 100644 index 000000000..68605aa7d --- /dev/null +++ b/cloudbank-v5/customer/src/test/java/com/example/customer/CustomerApplicationTests.java @@ -0,0 +1,18 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.customer; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@Disabled +@SpringBootTest +class CustomerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/cloudbank-v5/deploy-all-services.sh b/cloudbank-v5/deploy-all-services.sh new file mode 100755 index 000000000..7051ec128 --- /dev/null +++ b/cloudbank-v5/deploy-all-services.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Script to deploy all CloudBank v5 services using Helm +# Usage: ./deploy-all-services.sh + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + echo "Example: $0 obaas-dev" + return 1 2>/dev/null || exit 1 +fi + +NAMESPACE="$1" + +# Array of services to deploy +SERVICES=( + "account" + "customer" + "transfer" + "checks" + "creditscore" + "testrunner" +) + +echo "Deploying CloudBank v5 services" +echo "================================" + +# Deploy each service +for service in "${SERVICES[@]}"; do + echo "" + echo "Deploying $service..." + + if [ -d "$service/helm" ]; then + helm upgrade --install "$service" "./$service/helm" --namespace "$NAMESPACE" --wait --debug + + if [ $? -eq 0 ]; then + echo "✓ $service deployed successfully" + else + echo "✗ Failed to deploy $service" + return 1 2>/dev/null || exit 1 + fi + else + echo "⚠ Warning: Helm chart not found for $service at $service/helm" + fi +done + +echo "" +echo "================================" +echo "All services deployed successfully!" diff --git a/cloudbank-v5/pom.xml b/cloudbank-v5/pom.xml new file mode 100644 index 000000000..10d95e8eb --- /dev/null +++ b/cloudbank-v5/pom.xml @@ -0,0 +1,280 @@ + + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.6 + + + + pom + + CloudBank + Sample application for Oracle Backend for Microservices and AI + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + + buildtools + common + account + + checks + customer + creditscore + transfer + testrunner + + + + UTF-8 + 21 + 2024.0.1 + 2.4.0 + 1.4.4 + 1.18.38 + 25.2.0 + 24.2.2 + 4.31.1 + 1.1.0 + + + 1.18.1 + + + 3.3.1 + 10.14.1 + ${project.basedir}/checkstyle/checkstyle.xml + 9.0.9 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.micrometer + micrometer-core + + + + io.micrometer + micrometer-registry-prometheus + + + + io.micrometer + micrometer-tracing-bridge-otel + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api-incubator + + + + + + io.opentelemetry + opentelemetry-exporter-otlp + + + + io.micrometer + micrometer-tracing + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + io.opentelemetry.instrumentation + opentelemetry-spring-boot-starter + + + net.ttddyy.observation + datasource-micrometer-spring-boot + ${datasource-micrometer-spring-boot.version} + + + org.projectlombok + lombok + true + ${lombok.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${org.springdoc-version} + + + org.springframework.cloud + spring-cloud-starter-config + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + io.github.openfeign + feign-okhttp + + + io.github.openfeign + feign-micrometer + + + com.oracle.database.spring + oracle-spring-boot-starter-ucp + ${oracle-springboot-starter.version} + pom + + + io.opentelemetry.instrumentation + opentelemetry-oracle-ucp-11.2 + 2.13.1-alpha + + + + + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-bom + 2.13.1 + pom + import + + + io.micrometer + micrometer-tracing-bom + ${micrometer-tracing.version} + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.eclipse.jkube + kubernetes-maven-plugin + 1.18.1 + + + + my-repository/${project.artifactId}:${project.version} + + + + + + + build + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + true + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin-version} + + + com.example + buildtools + 0.0.1-SNAPSHOT + + + com.puppycrawl.tools + checkstyle + ${checkstyle-version} + + + + + checkstyle + test + + check + + + true + cloudbank/checkstyle/checkstyle.xml + cloudbank/checkstyle/suppressions.xml + true + warning + true + true + + + + + + org.owasp + dependency-check-maven + ${dependency-check-version} + + + com.example + buildtools + 0.0.1-SNAPSHOT + + + + true + 0 + false + + HTML + CSV + + + + cloudbank/dependency-check/suppressions.xml + + + + + + diff --git a/cloudbank-v5/sqljob.yaml b/cloudbank-v5/sqljob.yaml new file mode 100644 index 000000000..51a1bb0dd --- /dev/null +++ b/cloudbank-v5/sqljob.yaml @@ -0,0 +1,70 @@ +# The Job to run the SQLcl container with embedded SQL +# Make sure to replace the namespace and secrets as needed below +apiVersion: batch/v1 +kind: Job +metadata: + generateName: sqlcl-runner-job- + namespace: obaas-dev +spec: + backoffLimit: 2 + template: + spec: + restartPolicy: Never + containers: + - name: sqlcl-container + image: container-registry.oracle.com/database/sqlcl:25.2.2 + command: ["/bin/sh", "-c"] + args: + - | + export TNS_ADMIN=/etc/oracle/wallet + + # Create the SQL script inline + cat > /tmp/run.sql << 'EOF' + SET SERVEROUTPUT ON; + WHENEVER SQLERROR EXIT SQL.SQLCODE; + + create user customer identified by "Welcome-12345"; + grant db_developer_role to customer; + grant unlimited tablespace to customer; + + + create user account identified by "Welcome-12345"; + grant db_developer_role to account; + grant unlimited tablespace to account; + grant execute on dbms_aq to account; + grant execute on dbms_aqadm to account; + grant execute on dbms_aqin to account; + grant execute on dbms_aqjms to account; + grant execute on dbms_aqjms_internal to account; + commit; + + / + EXIT; + EOF + + # Execute the SQL script + sql $(DB_USER)/$(DB_PASSWORD)@$(TNS_ALIAS) @/tmp/run.sql + env: + - name: DB_USER + valueFrom: + secretKeyRef: + name: obaas-db-secret + key: db.username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: obaas-db-secret + key: db.password + - name: TNS_ALIAS + valueFrom: + secretKeyRef: + name: obaas-db-secret + key: db.service + volumeMounts: + - name: db-wallet-volume + mountPath: /etc/oracle/wallet + readOnly: true + volumes: + - name: db-wallet-volume + secret: + secretName: obaas-adb-tns-admin-1 \ No newline at end of file diff --git a/cloudbank-v5/testrunner/.gitignore b/cloudbank-v5/testrunner/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/cloudbank-v5/testrunner/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/cloudbank-v5/testrunner/helm/Chart.yaml b/cloudbank-v5/testrunner/helm/Chart.yaml new file mode 100644 index 000000000..0c127d981 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: testrunner # Replace with your application name +description: A Helm chart for the OBaaS Platform Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.2.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.0.1" diff --git a/cloudbank-v5/testrunner/helm/README.md b/cloudbank-v5/testrunner/helm/README.md new file mode 100644 index 000000000..9403cc5e0 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/README.md @@ -0,0 +1,16 @@ +# OBaas Sample App Chart + +This chart provides an extensible sample for applications running on [OBaaS](https://oracle.github.io/microservices-datadriven/obaas/). + +To use this chart for a given application, download the chart and update the [Chart.name](./Chart.yaml) value to your application's name. + +## Customizing the chart + +The OBaaS sample app chart is meant to serve as a developer template, and is fully customizable. + +Standard parameters for Kubernetes options like node affinity, HPAs, ingress and more are provided in the [values.yaml file](./values.yaml). + +## OBaaS options + +Within the [values.yaml file](./values.yaml), the `obaas` key allows chart developers to enable or disable OBaaS integrations like database connectivity, OpenTelemetry, MicroProfile LRA, SpringBoot, and Eureka. +enabled: true diff --git a/cloudbank-v5/testrunner/helm/templates/NOTES.txt b/cloudbank-v5/testrunner/helm/templates/NOTES.txt new file mode 100644 index 000000000..8d9597d97 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "obaas-app.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "obaas-app.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "obaas-app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "obaas-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cloudbank-v5/testrunner/helm/templates/_helpers.tpl b/cloudbank-v5/testrunner/helm/templates/_helpers.tpl new file mode 100644 index 000000000..b8e6d5b79 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "obaas-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "obaas-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "obaas-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "obaas-app.labels" -}} +helm.sh/chart: {{ include "obaas-app.chart" . }} +{{ include "obaas-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "obaas-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "obaas-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "obaas-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "obaas-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cloudbank-v5/testrunner/helm/templates/deployment.yaml b/cloudbank-v5/testrunner/helm/templates/deployment.yaml new file mode 100644 index 000000000..d4902a2f2 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/templates/deployment.yaml @@ -0,0 +1,147 @@ +# Load obaas configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + obaas.framework: SPRING_BOOT +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "obaas-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "obaas-app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "obaas-app.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CONNECT_STRING + value: jdbc:oracle:thin:@$(SPRING_DB_SERVICE)?TNS_ADMIN=/oracle/tnsadmin + # Lookup ObaaS configuration + {{- $obaas := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-config") }} + {{- $obaasObs := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-observability-config") }} + {{- if $.Values.obaas.mp_lra.enabled }} + - name: MP_LRA_COORDINATOR_URL + value: {{ $obaas.data.otmm | quote }} + {{- end }} + {{- if $.Values.obaas.eureka.enabled }} + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_CLIENT_REGISTER_WITH_EUREKA + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: {{ $obaas.data.eureka | quote }} + - name: EUREKA_INSTANCE_HOSTNAME + value: {{ include "obaas-app.fullname" . }}-{{ $.Release.Namespace }} + {{- end }} + {{- if $.Values.obaas.otel.enabled }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ (index $obaasObs.data "signoz-otel-collector") | quote }} + {{- end }} + {{- if $.Values.obaas.springboot.enabled }} + - name: SPRING_PROFILES_ACTIVE + value: {{ $obaas.data.SPRING_PROFILES_ACTIVE | quote }} + {{- if $.Values.obaas.database.enabled }} + - name: SPRING_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.username + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.password + - name: LIQUIBASE_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_username + - name: LIQUIBASE_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_password + - name: LIQUIBASE_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DB_SERVICE + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.service + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ $.Values.obaas.database.walletSecret }} + mountPath: /oracle/tnsadmin + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: {{ $.Values.obaas.database.walletSecret }} + secret: + secretName: {{ $.Values.obaas.database.walletSecret }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cloudbank-v5/testrunner/helm/templates/hpa.yaml b/cloudbank-v5/testrunner/helm/templates/hpa.yaml new file mode 100644 index 000000000..7c8805fb0 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/templates/hpa.yaml @@ -0,0 +1,33 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "obaas-app.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/testrunner/helm/templates/ingress.yaml b/cloudbank-v5/testrunner/helm/templates/ingress.yaml new file mode 100644 index 000000000..50aa98cd8 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "obaas-app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/testrunner/helm/templates/service.yaml b/cloudbank-v5/testrunner/helm/templates/service.yaml new file mode 100644 index 000000000..5876cf66d --- /dev/null +++ b/cloudbank-v5/testrunner/helm/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "obaas-app.selectorLabels" . | nindent 4 }} diff --git a/cloudbank-v5/testrunner/helm/templates/serviceaccount.yaml b/cloudbank-v5/testrunner/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..a22b73ef6 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "obaas-app.serviceAccountName" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/cloudbank-v5/testrunner/helm/templates/tests/test-connection.yaml b/cloudbank-v5/testrunner/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..8596a023f --- /dev/null +++ b/cloudbank-v5/testrunner/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "obaas-app.fullname" . }}-test-connection" + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "obaas-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cloudbank-v5/testrunner/helm/values.yaml b/cloudbank-v5/testrunner/helm/values.yaml new file mode 100644 index 000000000..2c25e8374 --- /dev/null +++ b/cloudbank-v5/testrunner/helm/values.yaml @@ -0,0 +1,155 @@ +# Default values for obaas-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: "my-repository/testrunner" + # This sets the pull policy for images. + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "0.0.1-SNAPSHOT" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: + signoz.io/path: /actuator/prometheus + signoz.io/port: "8080" + signoz.io/scrape: "true" +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 8080 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + requests: + cpu: 100m + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Oracle Backend for Microservices and AI Settings. +obaas: + namespace: obaas-dev # Replace with your namespace + database: + enabled: true # If true variables with DB secret content will be created + credentialsSecret: account-db-secrets # Replace with your secret name + walletSecret: obaas-adb-tns-admin-1 # Replace with your wallet secret name + otel: + enabled: true # Enable OpenTelemetry + # MicroProfile LRA + mp_lra: + enabled: true # Enable OTMM + # Spring Boot applications + springboot: + enabled: true # Enable Spring Boot specific variables + eureka: + enabled: true # Enable Eureka client diff --git a/cloudbank-v5/testrunner/pom.xml b/cloudbank-v5/testrunner/pom.xml new file mode 100644 index 000000000..2e0341297 --- /dev/null +++ b/cloudbank-v5/testrunner/pom.xml @@ -0,0 +1,75 @@ + + + + + 4.0.0 + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + + testrunner + 0.0.1-SNAPSHOT + testrunner + Test Runner Application + + + + com.oracle.database.spring + oracle-spring-boot-starter-aqjms + ${oracle-springboot-starter.version} + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.oracle.database.spring + oracle-spring-boot-starter-wallet + ${oracle-springboot-starter.version} + + + com.example + common + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.graalvm.buildtools + native-maven-plugin + + + org.eclipse.jkube + kubernetes-maven-plugin + ${jkube.version} + + + + my-repository/testrunner:${project.version} + + ghcr.io/oracle/openjdk-image-obaas:21 + + dir + /deployments + + java -jar /deployments/${project.artifactId}-${project.version}.jar + + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/TestrunnerApplication.java b/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/TestrunnerApplication.java new file mode 100644 index 000000000..129f5a008 --- /dev/null +++ b/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/TestrunnerApplication.java @@ -0,0 +1,78 @@ +// Copyright (c) 2023, 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.testrunner; + +import java.sql.SQLException; +import javax.sql.DataSource; + +import com.example.common.filter.LoggingFilterConfig; +import com.example.common.ucp.UCPTelemetry; +import jakarta.jms.ConnectionFactory; +import jakarta.jms.JMSException; +import oracle.jakarta.jms.AQjmsFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.support.converter.MappingJackson2MessageConverter; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.converter.MessageType; + +// TO-DO Add Eureka +@SpringBootApplication +@EnableJms +@EnableDiscoveryClient +@Import({ LoggingFilterConfig.class, UCPTelemetry.class }) +public class TestrunnerApplication { + + public static void main(String[] args) { + SpringApplication.run(TestrunnerApplication.class, args); + } + + /** + * Serialize message content to json using TextMessage. + * + * @return TO-DO + */ + @Bean + public MessageConverter jacksonJmsMessageConverter() { + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + converter.setTargetType(MessageType.TEXT); + converter.setTypeIdPropertyName("_type"); + return converter; + } + + /** + * TO-DO. + * + * @param connectionFactory TO-DO + * @return TO-DO + */ + @Bean + public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { + JmsTemplate jmsTemplate = new JmsTemplate(); + jmsTemplate.setConnectionFactory(connectionFactory); + jmsTemplate.setMessageConverter(jacksonJmsMessageConverter()); + return jmsTemplate; + } + + /** + * Initialize AQ JMS ConnectionFactory. + * + * @param ds Oracle Datasource. + * @return ConnectionFactory The AQ JMS ConnectionFactory. + * @throws JMSException when an error is encountered while creating a JMS + * ConnectionFactory. + * @throws SQLException when an error is encountered while accessing backing + * Oracle DataSource. + */ + @Bean + public ConnectionFactory aqJmsConnectionFactory(DataSource ds) throws JMSException, SQLException { + return AQjmsFactory.getConnectionFactory(ds.unwrap(DataSource.class)); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/controller/TestRunnerController.java b/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/controller/TestRunnerController.java new file mode 100644 index 000000000..483b1da48 --- /dev/null +++ b/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/controller/TestRunnerController.java @@ -0,0 +1,35 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.testrunner.controller; + +import com.example.testrunner.model.CheckDeposit; +import com.example.testrunner.model.Clearance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/testrunner") +public class TestRunnerController { + + @Autowired + private JmsTemplate jmsTemplate; + + @PostMapping("/deposit") + public ResponseEntity depositCheck(@RequestBody CheckDeposit deposit) { + jmsTemplate.convertAndSend("deposits", deposit); + return new ResponseEntity(deposit, HttpStatus.CREATED); + } + + @PostMapping("/clear") + public ResponseEntity clearCheck(@RequestBody Clearance clearance) { + jmsTemplate.convertAndSend("clearances", clearance); + return new ResponseEntity(clearance, HttpStatus.CREATED); + } +} \ No newline at end of file diff --git a/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/model/CheckDeposit.java b/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/model/CheckDeposit.java new file mode 100644 index 000000000..cc600c2d7 --- /dev/null +++ b/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/model/CheckDeposit.java @@ -0,0 +1,16 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.testrunner.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CheckDeposit { + private long accountId; + private long amount; +} \ No newline at end of file diff --git a/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/model/Clearance.java b/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/model/Clearance.java new file mode 100644 index 000000000..2ae467220 --- /dev/null +++ b/cloudbank-v5/testrunner/src/main/java/com/example/testrunner/model/Clearance.java @@ -0,0 +1,15 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.testrunner.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Clearance { + private long journalId; +} \ No newline at end of file diff --git a/cloudbank-v5/testrunner/src/main/resources/application.yaml b/cloudbank-v5/testrunner/src/main/resources/application.yaml new file mode 100644 index 000000000..55c12e2c7 --- /dev/null +++ b/cloudbank-v5/testrunner/src/main/resources/application.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2023, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +spring: + application: + name: testrunner + datasource: + url: ${SPRING_DATASOURCE_URL} + user: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + type: oracle.ucp.jdbc.PoolDataSource + oracleucp: + connection-factory-class-name: oracle.jdbc.pool.OracleDataSource + connection-pool-name: TestrunnerConnectionPool + initial-pool-size: 15 + min-pool-size: 10 + max-pool-size: 30 + cloud: + config: + import-check: + enabled: false + config: + import: classpath:common.yaml diff --git a/cloudbank-v5/testrunner/src/main/resources/banner.txt b/cloudbank-v5/testrunner/src/main/resources/banner.txt new file mode 100644 index 000000000..21ed4e259 --- /dev/null +++ b/cloudbank-v5/testrunner/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _____ _ ____ + |_ _|__ ___| |_| _ \ _ _ _ __ _ __ ___ _ __ + | |/ _ \/ __| __| |_) | | | | '_ \| '_ \ / _ \ '__| + | | __/\__ \ |_| _ <| |_| | | | | | | | __/ | + |_|\___||___/\__|_| \_\\__,_|_| |_|_| |_|\___|_| + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} diff --git a/cloudbank-v5/testrunner/src/test/java/com/example/testrunner/TestrunnerApplicationTests.java b/cloudbank-v5/testrunner/src/test/java/com/example/testrunner/TestrunnerApplicationTests.java new file mode 100644 index 000000000..41f17334f --- /dev/null +++ b/cloudbank-v5/testrunner/src/test/java/com/example/testrunner/TestrunnerApplicationTests.java @@ -0,0 +1,18 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.testrunner; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@Disabled +@SpringBootTest +class TestrunnerApplicationTests { + + @Test + void contextLoads() { + + } +} diff --git a/cloudbank-v5/transfer/.gitignore b/cloudbank-v5/transfer/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/cloudbank-v5/transfer/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/cloudbank-v5/transfer/helm/Chart.yaml b/cloudbank-v5/transfer/helm/Chart.yaml new file mode 100644 index 000000000..2aef63544 --- /dev/null +++ b/cloudbank-v5/transfer/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: transfer # Replace with your application name +description: A Helm chart for the OBaaS Platform Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.2.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.0.1" diff --git a/cloudbank-v5/transfer/helm/README.md b/cloudbank-v5/transfer/helm/README.md new file mode 100644 index 000000000..9403cc5e0 --- /dev/null +++ b/cloudbank-v5/transfer/helm/README.md @@ -0,0 +1,16 @@ +# OBaas Sample App Chart + +This chart provides an extensible sample for applications running on [OBaaS](https://oracle.github.io/microservices-datadriven/obaas/). + +To use this chart for a given application, download the chart and update the [Chart.name](./Chart.yaml) value to your application's name. + +## Customizing the chart + +The OBaaS sample app chart is meant to serve as a developer template, and is fully customizable. + +Standard parameters for Kubernetes options like node affinity, HPAs, ingress and more are provided in the [values.yaml file](./values.yaml). + +## OBaaS options + +Within the [values.yaml file](./values.yaml), the `obaas` key allows chart developers to enable or disable OBaaS integrations like database connectivity, OpenTelemetry, MicroProfile LRA, SpringBoot, and Eureka. +enabled: true diff --git a/cloudbank-v5/transfer/helm/templates/NOTES.txt b/cloudbank-v5/transfer/helm/templates/NOTES.txt new file mode 100644 index 000000000..8d9597d97 --- /dev/null +++ b/cloudbank-v5/transfer/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "obaas-app.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "obaas-app.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "obaas-app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "obaas-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cloudbank-v5/transfer/helm/templates/_helpers.tpl b/cloudbank-v5/transfer/helm/templates/_helpers.tpl new file mode 100644 index 000000000..b8e6d5b79 --- /dev/null +++ b/cloudbank-v5/transfer/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "obaas-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "obaas-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "obaas-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "obaas-app.labels" -}} +helm.sh/chart: {{ include "obaas-app.chart" . }} +{{ include "obaas-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "obaas-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "obaas-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "obaas-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "obaas-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cloudbank-v5/transfer/helm/templates/deployment.yaml b/cloudbank-v5/transfer/helm/templates/deployment.yaml new file mode 100644 index 000000000..d4902a2f2 --- /dev/null +++ b/cloudbank-v5/transfer/helm/templates/deployment.yaml @@ -0,0 +1,147 @@ +# Load obaas configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + obaas.framework: SPRING_BOOT +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "obaas-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "obaas-app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "obaas-app.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CONNECT_STRING + value: jdbc:oracle:thin:@$(SPRING_DB_SERVICE)?TNS_ADMIN=/oracle/tnsadmin + # Lookup ObaaS configuration + {{- $obaas := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-config") }} + {{- $obaasObs := (lookup "v1" "ConfigMap" .Values.obaas.namespace "obaas-observability-config") }} + {{- if $.Values.obaas.mp_lra.enabled }} + - name: MP_LRA_COORDINATOR_URL + value: {{ $obaas.data.otmm | quote }} + {{- end }} + {{- if $.Values.obaas.eureka.enabled }} + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_CLIENT_REGISTER_WITH_EUREKA + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: {{ $obaas.data.eureka | quote }} + - name: EUREKA_INSTANCE_HOSTNAME + value: {{ include "obaas-app.fullname" . }}-{{ $.Release.Namespace }} + {{- end }} + {{- if $.Values.obaas.otel.enabled }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ (index $obaasObs.data "signoz-otel-collector") | quote }} + {{- end }} + {{- if $.Values.obaas.springboot.enabled }} + - name: SPRING_PROFILES_ACTIVE + value: {{ $obaas.data.SPRING_PROFILES_ACTIVE | quote }} + {{- if $.Values.obaas.database.enabled }} + - name: SPRING_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.username + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.password + - name: LIQUIBASE_DATASOURCE_USERNAME + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_username + - name: LIQUIBASE_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.lb_password + - name: LIQUIBASE_DATASOURCE_URL + value: "jdbc:oracle:thin:@${SPRING_DB_SERVICE}?TNS_ADMIN=/oracle/tnsadmin" + - name: SPRING_DB_SERVICE + valueFrom: + secretKeyRef: + name: {{ $.Values.obaas.database.credentialsSecret }} + key: db.service + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ $.Values.obaas.database.walletSecret }} + mountPath: /oracle/tnsadmin + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: {{ $.Values.obaas.database.walletSecret }} + secret: + secretName: {{ $.Values.obaas.database.walletSecret }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cloudbank-v5/transfer/helm/templates/hpa.yaml b/cloudbank-v5/transfer/helm/templates/hpa.yaml new file mode 100644 index 000000000..7c8805fb0 --- /dev/null +++ b/cloudbank-v5/transfer/helm/templates/hpa.yaml @@ -0,0 +1,33 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "obaas-app.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/transfer/helm/templates/ingress.yaml b/cloudbank-v5/transfer/helm/templates/ingress.yaml new file mode 100644 index 000000000..50aa98cd8 --- /dev/null +++ b/cloudbank-v5/transfer/helm/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "obaas-app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cloudbank-v5/transfer/helm/templates/service.yaml b/cloudbank-v5/transfer/helm/templates/service.yaml new file mode 100644 index 000000000..5876cf66d --- /dev/null +++ b/cloudbank-v5/transfer/helm/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "obaas-app.fullname" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "obaas-app.selectorLabels" . | nindent 4 }} diff --git a/cloudbank-v5/transfer/helm/templates/serviceaccount.yaml b/cloudbank-v5/transfer/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..a22b73ef6 --- /dev/null +++ b/cloudbank-v5/transfer/helm/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "obaas-app.serviceAccountName" . }} + namespace: {{ .Values.obaas.namespace }} + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/cloudbank-v5/transfer/helm/templates/tests/test-connection.yaml b/cloudbank-v5/transfer/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..8596a023f --- /dev/null +++ b/cloudbank-v5/transfer/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "obaas-app.fullname" . }}-test-connection" + labels: + {{- include "obaas-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "obaas-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cloudbank-v5/transfer/helm/values.yaml b/cloudbank-v5/transfer/helm/values.yaml new file mode 100644 index 000000000..22fefb3ba --- /dev/null +++ b/cloudbank-v5/transfer/helm/values.yaml @@ -0,0 +1,155 @@ +# Default values for obaas-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: "my-repository/transfer" + # This sets the pull policy for images. + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "0.0.1-SNAPSHOT" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: + signoz.io/path: /actuator/prometheus + signoz.io/port: "8080" + signoz.io/scrape: "true" +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 8080 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + requests: + cpu: 100m + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Oracle Backend for Microservices and AI Settings. +obaas: + namespace: obaas-dev # Replace with your namespace + database: + enabled: true # If true variables with DB secret content will be created + credentialsSecret: account-db-secrets # Replace with your secret name + walletSecret: obaas-adb-tns-admin-1 # Replace with your wallet secret name + otel: + enabled: true # Enable OpenTelemetry + # MicroProfile LRA + mp_lra: + enabled: true # Enable OTMM + # Spring Boot applications + springboot: + enabled: true # Enable Spring Boot specific variables + eureka: + enabled: true # Enable Eureka client diff --git a/cloudbank-v5/transfer/pom.xml b/cloudbank-v5/transfer/pom.xml new file mode 100644 index 000000000..ef00389b4 --- /dev/null +++ b/cloudbank-v5/transfer/pom.xml @@ -0,0 +1,66 @@ + + + + + 4.0.0 + + + com.example + cloudbank-apps + 0.0.1-SNAPSHOT + + + transfer + 0.0.1-SNAPSHOT + transfer + Transfer Application + + + + com.oracle.microtx.lra + microtx-lra-spring-boot-starter + ${oracle-microtx-starter.version} + + + com.example + common + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.graalvm.buildtools + native-maven-plugin + + + org.eclipse.jkube + kubernetes-maven-plugin + ${jkube.version} + + + + my-repository/transfer:${project.version} + + ghcr.io/oracle/openjdk-image-obaas:21 + + dir + /deployments + + java -jar /deployments/${project.artifactId}-${project.version}.jar + + + + + + + + \ No newline at end of file diff --git a/cloudbank-v5/transfer/src/main/java/com/example/transfer/TransferApplication.java b/cloudbank-v5/transfer/src/main/java/com/example/transfer/TransferApplication.java new file mode 100644 index 000000000..513c2febb --- /dev/null +++ b/cloudbank-v5/transfer/src/main/java/com/example/transfer/TransferApplication.java @@ -0,0 +1,18 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.transfer; + +import com.example.common.filter.LoggingFilterConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +@SpringBootApplication +@Import(LoggingFilterConfig.class) +public class TransferApplication { + + public static void main(String[] args) { + SpringApplication.run(TransferApplication.class, args); + } +} \ No newline at end of file diff --git a/cloudbank-v5/transfer/src/main/java/com/example/transfer/TransferService.java b/cloudbank-v5/transfer/src/main/java/com/example/transfer/TransferService.java new file mode 100644 index 000000000..c1489e483 --- /dev/null +++ b/cloudbank-v5/transfer/src/main/java/com/example/transfer/TransferService.java @@ -0,0 +1,214 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +package com.example.transfer; + +import java.net.URI; + +import com.oracle.microtx.springboot.lra.annotation.Compensate; +import com.oracle.microtx.springboot.lra.annotation.Complete; +import com.oracle.microtx.springboot.lra.annotation.LRA; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import static com.oracle.microtx.springboot.lra.annotation.LRA.LRA_HTTP_CONTEXT_HEADER; + +@RestController +@Slf4j +public class TransferService { + + public static final String TRANSFER_ID = "TRANSFER_ID"; + + @Value("${account.withdraw.url}") URI withdrawUri; + @Value("${account.deposit.url}") URI depositUri; + @Value("${transfer.cancel.url}") URI transferCancelUri; + @Value("${transfer.cancel.process.url}") URI transferProcessCancelUri; + @Value("${transfer.confirm.url}") URI transferConfirmUri; + @Value("${transfer.confirm.process.url}") URI transferProcessConfirmUri; + + /** + * Ping method. + * @return Http OK. + */ + @GetMapping("/hello") + public ResponseEntity ping() { + log.info("Say Hello!"); + return ResponseEntity.ok(""); + } + + /** + * Transfer amount between two accounts. + * @param fromAccount From an account + * @param toAccount To an account + * @param amount Amount to transfer + * @param lraId LRA Id + * @return TO-DO + */ + @PostMapping("/transfer") + @LRA(value = LRA.Type.REQUIRES_NEW, end = false) + public ResponseEntity transfer(@RequestParam("fromAccount") long fromAccount, + @RequestParam("toAccount") long toAccount, + @RequestParam("amount") long amount, + @RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) { + if (lraId == null) { + return new ResponseEntity<>("Failed to create LRA", HttpStatus.INTERNAL_SERVER_ERROR); + } + log.info("Started new LRA/transfer Id: " + lraId); + + boolean isCompensate = false; + String returnString = ""; + + // perform the withdrawal + returnString += withdraw(lraId, fromAccount, amount); + log.info(returnString); + if (returnString.contains("succeeded")) { + // if it worked, perform the deposit + returnString += " " + deposit(lraId, toAccount, amount); + log.info(returnString); + if (returnString.contains("failed")) { + isCompensate = true; // deposit failed + } + } else { + isCompensate = true; // withdraw failed + } + log.info("LRA/transfer action will be " + (isCompensate ? "cancel" : "confirm")); + + // call complete or cancel based on outcome of previous actions + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + headers.set(TRANSFER_ID, lraId); + HttpEntity request = new HttpEntity("", headers); + + ResponseEntity response = restTemplate.postForEntity( + (isCompensate ? transferCancelUri : transferConfirmUri).toString(), + request, + String.class); + + returnString += response.getBody(); + + // return status + return ResponseEntity.ok("transfer status:" + returnString); + } + + private String withdraw(String lraId, long accountId, long amount) { + log.info("withdraw accountId = " + accountId + ", amount = " + amount); + log.info("withdraw lraId = " + lraId); + + UriComponentsBuilder builder = UriComponentsBuilder.fromUri(withdrawUri) + .queryParam("accountId", accountId) + .queryParam("amount", amount); + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + headers.set(LRA_HTTP_CONTEXT_HEADER, lraId.toString()); + HttpEntity request = new HttpEntity("", headers); + + ResponseEntity response = restTemplate.postForEntity( + builder.buildAndExpand().toUri(), + request, + String.class); + + return response.getBody(); + } + + private String deposit(String lraId, long accountId, long amount) { + log.info("deposit accountId = " + accountId + ", amount = " + amount); + log.info("deposit lraId = " + lraId); + + UriComponentsBuilder builder = UriComponentsBuilder.fromUri(depositUri) + .queryParam("accountId", accountId) + .queryParam("amount", amount); + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + headers.set(LRA_HTTP_CONTEXT_HEADER, lraId.toString()); + HttpEntity request = new HttpEntity("", headers); + + ResponseEntity response = restTemplate.postForEntity( + builder.buildAndExpand().toUri(), + request, + String.class); + + return response.getBody(); + } + + @PostMapping("/processconfirm") + @LRA(value = LRA.Type.MANDATORY) + public ResponseEntity processconfirm(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) { + log.info("Process confirm for transfer : " + lraId); + return ResponseEntity.ok(""); + } + + @PostMapping("/processcancel") + @LRA(value = LRA.Type.MANDATORY, cancelOn = HttpStatus.OK) + public ResponseEntity processcancel(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) { + log.info("Process cancel for transfer : " + lraId); + return ResponseEntity.ok(""); + } + + /** + * Confirm a transfer. + * @param transferId Transfer Id + * @return TO-DO + */ + @PostMapping("/confirm") + @Complete + @LRA(value = LRA.Type.NOT_SUPPORTED) + public ResponseEntity confirm(@RequestHeader(TRANSFER_ID) String transferId) { + log.info("Received confirm for transfer : " + transferId); + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + headers.set(LRA_HTTP_CONTEXT_HEADER, transferId); + HttpEntity request = new HttpEntity("", headers); + + ResponseEntity response = restTemplate.postForEntity( + transferProcessConfirmUri, + request, + String.class); + + return ResponseEntity.ok(response.getBody()); + } + + /** + * Cancel a transfer. + * @param transferId Transfer Id + * @return TO-DO + */ + @PostMapping("/cancel") + @Compensate + @LRA(value = LRA.Type.NOT_SUPPORTED, cancelOn = HttpStatus.OK) + public ResponseEntity cancel(@RequestHeader(TRANSFER_ID) String transferId) { + log.info("Received cancel for transfer : " + transferId); + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + headers.set(LRA_HTTP_CONTEXT_HEADER, transferId); + HttpEntity request = new HttpEntity("", headers); + + ResponseEntity response = restTemplate.postForEntity( + transferProcessCancelUri, + request, + String.class); + + return ResponseEntity.ok(response.getBody()); + } + +} \ No newline at end of file diff --git a/cloudbank-v5/transfer/src/main/resources/application.yaml b/cloudbank-v5/transfer/src/main/resources/application.yaml new file mode 100644 index 000000000..97d1ee6eb --- /dev/null +++ b/cloudbank-v5/transfer/src/main/resources/application.yaml @@ -0,0 +1,40 @@ +# Copyright (c) 2023, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +spring: + application: + name: transfer + mvc: + enforced-prefixes: + - /actuator + - /rest + url-mappings: + - "/rest/*" + - "/actuator/*" + - "/error/*" + microtx: + lra: + coordinator-url: ${MP_LRA_COORDINATOR_URL} + propagation-active: true + headers-propagation-prefix: "{x-b3-, oracle-tmm-, authorization, refresh-}" + cloud: + config: + import-check: + enabled: false + config: + import: classpath:common.yaml + +account: + deposit: + url: http://account.obaas-dev:8080/deposit + withdraw: + url: http://account.obaas-dev:8080/withdraw +transfer: + cancel: + url: http://transfer.obaas-dev:8080/cancel + process: + url: http://transfer.obaas-dev:8080/processcancel + confirm: + url: http://transfer.obaas-dev:8080/confirm + process: + url: http://transfer.obaas-dev:8080/processconfirm \ No newline at end of file diff --git a/cloudbank-v5/transfer/src/main/resources/banner.txt b/cloudbank-v5/transfer/src/main/resources/banner.txt new file mode 100644 index 000000000..9801e0013 --- /dev/null +++ b/cloudbank-v5/transfer/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _____ __ + |_ _| __ __ _ _ __ ___ / _| ___ _ __ + | || '__/ _` | '_ \/ __| |_ / _ \ '__| + | || | | (_| | | | \__ \ _| __/ | + |_||_| \__,_|_| |_|___/_| \___|_| + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} diff --git a/cloudbank-v5/update-image.sh b/cloudbank-v5/update-image.sh new file mode 100755 index 000000000..740e311b3 --- /dev/null +++ b/cloudbank-v5/update-image.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Script to update image repository and tag in values.yaml files +# Usage: ./update-image.sh + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 my-repository 0.0.1-SNAPSHOT" + return 1 2>/dev/null || exit 1 +fi + +REPOSITORY="$1" +TAG="$2" + +# Ensure repository ends with / +if [[ ! "$REPOSITORY" =~ /$ ]]; then + REPOSITORY="${REPOSITORY}/" +fi + +# Find all values.yaml files in helm directories +find . -path "*/helm/values.yaml" -type f | while read -r file; do + # Get the directory containing the values.yaml file + helm_dir=$(dirname "$file") + + # Look for Chart.yaml in the same directory + chart_file="${helm_dir}/Chart.yaml" + + if [ -f "$chart_file" ]; then + # Extract the application name from Chart.yaml + app_name=$(grep "^name:" "$chart_file" | sed 's/name: *//;s/ *#.*//' | tr -d ' ') + + if [ -n "$app_name" ]; then + full_repository="${REPOSITORY}${app_name}" + echo "Updating $file with repository: $full_repository" + + # Update repository line + sed -i.bak "s|repository:.*|repository: \"$full_repository\"|" "$file" + + # Update tag line + sed -i.bak "s|tag:.*|tag: \"$TAG\"|" "$file" + + # Remove backup file + rm "${file}.bak" + else + echo "Warning: Could not extract app name from $chart_file" + fi + else + echo "Warning: No Chart.yaml found for $file" + fi +done + +echo "All values.yaml files updated with repository: $REPOSITORY and tag: $TAG" diff --git a/cloudbank-v5/update-jkube-image.sh b/cloudbank-v5/update-jkube-image.sh new file mode 100755 index 000000000..497e89afb --- /dev/null +++ b/cloudbank-v5/update-jkube-image.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Script to update JKube image name in pom.xml files +# Usage: ./update-jkube-image.sh + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + echo "Example: $0 myregistry.io/myorg/cloudbank-v5" + echo "" + echo "This will update the image name to: /:\${project.version}" + return 1 2>/dev/null || exit 1 +fi + +NEW_IMAGE_PREFIX="$1" + +# Array of services to update +SERVICES=( + "account" + "customer" + "transfer" + "checks" + "creditscore" + "testrunner" +) + +echo "Updating JKube image names with prefix: $NEW_IMAGE_PREFIX" +echo "==========================================================" + +for service in "${SERVICES[@]}"; do + POM_FILE="$service/pom.xml" + + if [ -f "$POM_FILE" ]; then + echo "Updating $POM_FILE..." + + # Update the tag to: /:${project.version} + sed -i.bak "s|.*/${service}:\${project.version}|$NEW_IMAGE_PREFIX/$service:\${project.version}|" "$POM_FILE" + + # Remove backup file + rm "${POM_FILE}.bak" + + echo "✓ $service updated to $NEW_IMAGE_PREFIX/$service:\${project.version}" + else + echo "⚠ Warning: pom.xml not found for $service" + fi +done + +echo "" +echo "==========================================================" +echo "All pom.xml files updated successfully!"