diff --git a/.github/workflows/api-gateway-pipeline-dev-pr.yml b/.github/workflows/api-gateway-pipeline-dev-pr.yml deleted file mode 100644 index 80e0836c5..000000000 --- a/.github/workflows/api-gateway-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Develop, on PR Java CI/CD of api-gateway, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/api-gateway-pipeline-dev-push.yml b/.github/workflows/api-gateway-pipeline-dev-push.yml deleted file mode 100644 index 7650bf445..000000000 --- a/.github/workflows/api-gateway-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Develop, on PUSH Java CI/CD of api-gateway-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for api-gateway-ecommerce-boot Microservice - run: docker build -f api-gateway/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/api-gateway-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push api-gateway-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/api-gateway-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/api-gateway-pipeline-prod-pr.yml b/.github/workflows/api-gateway-pipeline-prod-pr.yml deleted file mode 100644 index 97b059d5f..000000000 --- a/.github/workflows/api-gateway-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Prod, on PR Java CI/CD of api-gateway, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/api-gateway-pipeline-prod-push.yml b/.github/workflows/api-gateway-pipeline-prod-push.yml deleted file mode 100644 index f5a06db5d..000000000 --- a/.github/workflows/api-gateway-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of api-gateway-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for api-gateway-ecommerce-boot Microservice - run: docker build -f api-gateway/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/api-gateway-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push api-gateway-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/api-gateway-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/api-gateway-pipeline-stage-pr.yml b/.github/workflows/api-gateway-pipeline-stage-pr.yml deleted file mode 100644 index ca12270d8..000000000 --- a/.github/workflows/api-gateway-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of api-gateway, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/api-gateway-pipeline-stage-push.yml b/.github/workflows/api-gateway-pipeline-stage-push.yml deleted file mode 100644 index 5fed6b320..000000000 --- a/.github/workflows/api-gateway-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Stage, on PUSH Java CI/CD of api-gateway-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for api-gateway-ecommerce-boot Microservice - run: docker build -f api-gateway/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/api-gateway-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push api-gateway-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/api-gateway-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/api-gateway-pipeline.yml b/.github/workflows/api-gateway-pipeline.yml new file mode 100644 index 000000000..cec5657de --- /dev/null +++ b/.github/workflows/api-gateway-pipeline.yml @@ -0,0 +1,46 @@ +name: CI/CD - API Gateway + +on: + workflow_dispatch: + +jobs: + build_test_deploy: + runs-on: self-hosted + defaults: + run: + shell: pwsh + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + # Usando imagen pública selimhorri/api-gateway-ecommerce-boot:0.1.0, no se requiere build ni push + - name: Apply Namespace + run: kubectl apply -f k8s/namespace.yaml --validate=false + - name: Apply Config Maps + run: kubectl apply -f k8s/config-maps.yaml --validate=false + - name: Wait for Service Discovery + run: kubectl rollout status deployment/service-discovery -n ecommerce --timeout=180s + - name: Wait for Cloud Config + run: kubectl rollout status deployment/cloud-config -n ecommerce --timeout=300s + - name: Deploy API Gateway + run: kubectl apply -f k8s/api-gateway-deployment.yaml --validate=false + - name: Wait for API Gateway rollout + run: kubectl rollout status deployment/api-gateway -n ecommerce --timeout=180s + - name: Deploy API Gateway Service + run: kubectl apply -f k8s/api-gateway-service.yaml --validate=false + - name: Deploy Proxy Client + run: kubectl apply -f k8s/proxy-client-deployment.yaml --validate=false + - name: Wait for Proxy Client rollout + run: kubectl rollout status deployment/proxy-client -n ecommerce --timeout=180s + - name: Generate Release Notes + run: | + git log -1 --pretty=format:"%h - %s (%an, %ad)" > release-notes.txt + - name: Upload Release Notes + uses: actions/upload-artifact@v4 + with: + name: release-notes + path: release-notes.txt \ No newline at end of file diff --git a/.github/workflows/business-services-pipeline.yml b/.github/workflows/business-services-pipeline.yml new file mode 100644 index 000000000..8ef7bc68f --- /dev/null +++ b/.github/workflows/business-services-pipeline.yml @@ -0,0 +1,35 @@ + +name: CI/CD - Business Services + +on: + workflow_dispatch: + + +jobs: + build_deploy_business: + runs-on: self-hosted + defaults: + run: + shell: pwsh + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Set up KUBECONFIG + run: | + $env:KUBECONFIG = "${{ secrets.KUBECONFIG }}" + - name: Apply Namespace + run: kubectl apply -f k8s/namespace.yaml --validate=false + - name: Apply Config Maps + run: kubectl apply -f k8s/config-maps.yaml --validate=false + - name: Deploy Product Service + run: kubectl apply -f k8s/product-service-deployment.yaml --validate=false + - name: Deploy User Service + run: kubectl apply -f k8s/user-service-deployment.yaml --validate=false + - name: Deploy Favourite Service + run: kubectl apply -f k8s/favourite-service-deployment.yaml --validate=false + - name: Deploy Order Service + run: kubectl apply -f k8s/order-service-deployment.yaml --validate=false + - name: Deploy Payment Service + run: kubectl apply -f k8s/payment-service-deployment.yaml --validate=false + - name: Deploy Shipping Service + run: kubectl apply -f k8s/shipping-service-deployment.yaml --validate=false diff --git a/.github/workflows/cloud-config-pipeline-dev-pr.yml b/.github/workflows/cloud-config-pipeline-dev-pr.yml deleted file mode 100644 index ce6e97bc0..000000000 --- a/.github/workflows/cloud-config-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of cloud-gateway, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/cloud-config-pipeline-dev-push.yml b/.github/workflows/cloud-config-pipeline-dev-push.yml deleted file mode 100644 index 242318ae2..000000000 --- a/.github/workflows/cloud-config-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of cloud-config-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for cloud-config-ecommerce-boot Microservice - run: docker build -f cloud-config/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/cloud-config-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push cloud-config-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/cloud-config-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/cloud-config-pipeline-prod-pr.yml b/.github/workflows/cloud-config-pipeline-prod-pr.yml deleted file mode 100644 index db0e6313e..000000000 --- a/.github/workflows/cloud-config-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of cloud-gateway, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/cloud-config-pipeline-prod-push.yml b/.github/workflows/cloud-config-pipeline-prod-push.yml deleted file mode 100644 index 2869ddf96..000000000 --- a/.github/workflows/cloud-config-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of cloud-config-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for cloud-config-ecommerce-boot Microservice - run: docker build -f cloud-config/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/cloud-config-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push cloud-config-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/cloud-config-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/cloud-config-pipeline-stage-pr.yml b/.github/workflows/cloud-config-pipeline-stage-pr.yml deleted file mode 100644 index f448bb16d..000000000 --- a/.github/workflows/cloud-config-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of cloud-gateway, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/cloud-config-pipeline-stage-push.yml b/.github/workflows/cloud-config-pipeline-stage-push.yml deleted file mode 100644 index 962bb4a8a..000000000 --- a/.github/workflows/cloud-config-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of cloud-config-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for cloud-config-ecommerce-boot Microservice - run: docker build -f cloud-config/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/cloud-config-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push cloud-config-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/cloud-config-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/core-services-pipeline.yml b/.github/workflows/core-services-pipeline.yml new file mode 100644 index 000000000..592e7b39f --- /dev/null +++ b/.github/workflows/core-services-pipeline.yml @@ -0,0 +1,30 @@ +name: CI/CD - Core Services + +on: + workflow_dispatch: + +jobs: + build_deploy_core: + runs-on: self-hosted + defaults: + run: + shell: pwsh + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Apply Namespace + run: kubectl apply -f k8s/namespace.yaml --validate=false + - name: Apply Config Maps + run: kubectl apply -f k8s/config-maps.yaml --validate=false + - name: Deploy Zipkin + run: kubectl apply -f k8s/zipkin-deployment.yaml --validate=false + - name: Wait for Zipkin rollout + run: kubectl rollout status deployment/zipkin -n ecommerce --timeout=180s + - name: Deploy Cloud Config + run: kubectl apply -f k8s/cloud-config-deployment.yaml --validate=false + - name: Wait for Cloud Config rollout + run: kubectl rollout status deployment/cloud-config -n ecommerce --timeout=300s + - name: Deploy Service Discovery + run: kubectl apply -f k8s/service-discovery-deployment.yaml --validate=false + - name: Wait for Service Discovery rollout + run: kubectl rollout status deployment/service-discovery -n ecommerce --timeout=180s diff --git a/.github/workflows/favourite-service-pipeline-dev-pr.yml b/.github/workflows/favourite-service-pipeline-dev-pr.yml deleted file mode 100644 index d075c7092..000000000 --- a/.github/workflows/favourite-service-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of favourite-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/favourite-service-pipeline-dev-push.yml b/.github/workflows/favourite-service-pipeline-dev-push.yml deleted file mode 100644 index e40742278..000000000 --- a/.github/workflows/favourite-service-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of favourite-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for favourite-service-ecommerce-boot Microservice - run: docker build -f favourite-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/favourite-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push favourite-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/favourite-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/favourite-service-pipeline-prod-pr.yml b/.github/workflows/favourite-service-pipeline-prod-pr.yml deleted file mode 100644 index d650ed18c..000000000 --- a/.github/workflows/favourite-service-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of favourite-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/favourite-service-pipeline-prod-push.yml b/.github/workflows/favourite-service-pipeline-prod-push.yml deleted file mode 100644 index 9402e25a0..000000000 --- a/.github/workflows/favourite-service-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of favourite-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for favourite-service-ecommerce-boot Microservice - run: docker build -f favourite-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/favourite-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push favourite-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/favourite-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/favourite-service-pipeline-stage-pr.yml b/.github/workflows/favourite-service-pipeline-stage-pr.yml deleted file mode 100644 index cb9a5090d..000000000 --- a/.github/workflows/favourite-service-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of favourite-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/favourite-service-pipeline-stage-push.yml b/.github/workflows/favourite-service-pipeline-stage-push.yml deleted file mode 100644 index 072304cc9..000000000 --- a/.github/workflows/favourite-service-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of favourite-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for favourite-service-ecommerce-boot Microservice - run: docker build -f favourite-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/favourite-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push favourite-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/favourite-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/order-service-pipeline-dev-pr.yml b/.github/workflows/order-service-pipeline-dev-pr.yml deleted file mode 100644 index 82ab25df6..000000000 --- a/.github/workflows/order-service-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of order-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/order-service-pipeline-dev-push.yml b/.github/workflows/order-service-pipeline-dev-push.yml deleted file mode 100644 index d7731fec4..000000000 --- a/.github/workflows/order-service-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of order-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for order-service-ecommerce-boot Microservice - run: docker build -f order-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/order-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push order-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/order-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/order-service-pipeline-prod-pr.yml b/.github/workflows/order-service-pipeline-prod-pr.yml deleted file mode 100644 index a86b3ff57..000000000 --- a/.github/workflows/order-service-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of order-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/order-service-pipeline-prod-push.yml b/.github/workflows/order-service-pipeline-prod-push.yml deleted file mode 100644 index 855ec8ea8..000000000 --- a/.github/workflows/order-service-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of order-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for order-service-ecommerce-boot Microservice - run: docker build -f order-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/order-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push order-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/order-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/order-service-pipeline-stage-pr.yml b/.github/workflows/order-service-pipeline-stage-pr.yml deleted file mode 100644 index 46ef07c6a..000000000 --- a/.github/workflows/order-service-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of order-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/order-service-pipeline-stage-push.yml b/.github/workflows/order-service-pipeline-stage-push.yml deleted file mode 100644 index 34d3ef1a8..000000000 --- a/.github/workflows/order-service-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of order-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for order-service-ecommerce-boot Microservice - run: docker build -f order-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/order-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push order-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/order-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/payment-service-pipeline-dev-pr.yml b/.github/workflows/payment-service-pipeline-dev-pr.yml deleted file mode 100644 index 49242655b..000000000 --- a/.github/workflows/payment-service-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of payment-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/payment-service-pipeline-dev-push.yml b/.github/workflows/payment-service-pipeline-dev-push.yml deleted file mode 100644 index 6800aec86..000000000 --- a/.github/workflows/payment-service-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of payment-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for payment-service-ecommerce-boot Microservice - run: docker build -f payment-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/payment-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push payment-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/payment-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/payment-service-pipeline-prod-pr.yml b/.github/workflows/payment-service-pipeline-prod-pr.yml deleted file mode 100644 index e9ea6f92d..000000000 --- a/.github/workflows/payment-service-pipeline-prod-pr.yml +++ /dev/null @@ -1,22 +0,0 @@ - -name: Stage, on PR Java CI/CD of payment-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - - \ No newline at end of file diff --git a/.github/workflows/payment-service-pipeline-prod-push.yml b/.github/workflows/payment-service-pipeline-prod-push.yml deleted file mode 100644 index 5f6456ffa..000000000 --- a/.github/workflows/payment-service-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of payment-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for payment-service-ecommerce-boot Microservice - run: docker build -f payment-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/payment-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push payment-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/payment-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/payment-service-pipeline-stage-pr.yml b/.github/workflows/payment-service-pipeline-stage-pr.yml deleted file mode 100644 index 3b9e3ade3..000000000 --- a/.github/workflows/payment-service-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of payment-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/payment-service-pipeline-stage-push.yml b/.github/workflows/payment-service-pipeline-stage-push.yml deleted file mode 100644 index b966416e0..000000000 --- a/.github/workflows/payment-service-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of payment-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for payment-service-ecommerce-boot Microservice - run: docker build -f payment-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/payment-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push payment-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/payment-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/product-service-pipeline-dev-pr.yml b/.github/workflows/product-service-pipeline-dev-pr.yml deleted file mode 100644 index 27abf9d42..000000000 --- a/.github/workflows/product-service-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of product-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/product-service-pipeline-dev-push.yml b/.github/workflows/product-service-pipeline-dev-push.yml deleted file mode 100644 index 33b639efa..000000000 --- a/.github/workflows/product-service-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of product-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for product-service-ecommerce-boot Microservice - run: docker build -f product-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/product-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push product-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/product-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/product-service-pipeline-prod-pr.yml b/.github/workflows/product-service-pipeline-prod-pr.yml deleted file mode 100644 index 0715d8c69..000000000 --- a/.github/workflows/product-service-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of product-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/product-service-pipeline-prod-push.yml b/.github/workflows/product-service-pipeline-prod-push.yml deleted file mode 100644 index 2f21434ec..000000000 --- a/.github/workflows/product-service-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of product-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for product-service-ecommerce-boot Microservice - run: docker build -f product-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/product-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push product-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/product-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/product-service-pipeline-stage-pr.yml b/.github/workflows/product-service-pipeline-stage-pr.yml deleted file mode 100644 index 95e6e7036..000000000 --- a/.github/workflows/product-service-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of product-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/product-service-pipeline-stage-push.yml b/.github/workflows/product-service-pipeline-stage-push.yml deleted file mode 100644 index 9d2f31c03..000000000 --- a/.github/workflows/product-service-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of product-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for product-service-ecommerce-boot Microservice - run: docker build -f product-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/product-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push product-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/product-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/product-service-stage.yml b/.github/workflows/product-service-stage.yml new file mode 100644 index 000000000..e7f91cf6e --- /dev/null +++ b/.github/workflows/product-service-stage.yml @@ -0,0 +1,60 @@ +name: Stage Deploy - Product Service + +on: + workflow_dispatch: + push: + branches: [ stage ] + +env: + SERVICE: product-service + IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/product-service + KUBE_NAMESPACE: ecommerce + +jobs: + build-test-deploy: + runs-on: self-hosted + defaults: + run: + shell: pwsh + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '11' + + - name: Run unit tests + run: .\mvnw.cmd -pl $env:SERVICE -am test + + - name: Package service + run: .\mvnw.cmd -pl $env:SERVICE -am -DskipTests=false package + + - name: Log in to Docker Hub + run: | + docker logout 2>$null + echo ${{ secrets.DOCKER_PASSWORD }} | docker login --username ${{ secrets.DOCKER_USERNAME }} --password-stdin + + - name: Build Docker image + run: | + $tag = "${{ env.IMAGE_NAME }}:${{ github.sha }}" + docker build -t $tag -f $env:SERVICE/Dockerfile . + echo "IMAGE_TAG=$tag" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + + - name: Push Docker image + run: docker push $env:IMAGE_TAG + + - name: Configure kubeconfig + run: | + $env:KUBECONFIG = "${{ secrets.KUBECONFIG }}" + echo "KUBECONFIG set" + + - name: Deploy to stage namespace + run: | + $content = Get-Content k8s/${{ env.SERVICE }}-deployment.yaml -Raw + $content = $content -replace 'namespace: ecommerce', "namespace: ${{ env.KUBE_NAMESPACE }}" + $content | kubectl apply -f - --validate=false + kubectl set image deployment/${{ env.SERVICE }} ${{ env.SERVICE }}=$env:IMAGE_TAG -n ${{ env.KUBE_NAMESPACE }} + kubectl rollout status deployment/${{ env.SERVICE }} -n ${{ env.KUBE_NAMESPACE }} --timeout=600s diff --git a/.github/workflows/proxy-client-pipeline-dev-pr.yml b/.github/workflows/proxy-client-pipeline-dev-pr.yml deleted file mode 100644 index 432181a62..000000000 --- a/.github/workflows/proxy-client-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Develop, on PR Java CI/CD of proxy-client, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/proxy-client-pipeline-dev-push.yml b/.github/workflows/proxy-client-pipeline-dev-push.yml deleted file mode 100644 index 3ccf5b788..000000000 --- a/.github/workflows/proxy-client-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Develop, on PUSH Java CI/CD of proxy-client-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for proxy-client-ecommerce-boot Microservice - run: docker build -f proxy-client/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/proxy-client-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push proxy-client-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/proxy-client-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/proxy-client-pipeline-prod-pr.yml b/.github/workflows/proxy-client-pipeline-prod-pr.yml deleted file mode 100644 index a427fc571..000000000 --- a/.github/workflows/proxy-client-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Develop, on PR Java CI/CD of proxy-client, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/proxy-client-pipeline-prod-push.yml b/.github/workflows/proxy-client-pipeline-prod-push.yml deleted file mode 100644 index 21667eb9d..000000000 --- a/.github/workflows/proxy-client-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Develop, on PUSH Java CI/CD of proxy-client-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for proxy-client-ecommerce-boot Microservice - run: docker build -f proxy-client/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/proxy-client-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push proxy-client-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/proxy-client-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/proxy-client-pipeline-stage-pr.yml b/.github/workflows/proxy-client-pipeline-stage-pr.yml deleted file mode 100644 index a942ad7fc..000000000 --- a/.github/workflows/proxy-client-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Develop, on PR Java CI/CD of proxy-client, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/proxy-client-pipeline-stage-push.yml b/.github/workflows/proxy-client-pipeline-stage-push.yml deleted file mode 100644 index 5bd23a72f..000000000 --- a/.github/workflows/proxy-client-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Develop, on PUSH Java CI/CD of proxy-client-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for proxy-client-ecommerce-boot Microservice - run: docker build -f proxy-client/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/proxy-client-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push proxy-client-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/proxy-client-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/service-discovery-pipeline-dev-pr.yml b/.github/workflows/service-discovery-pipeline-dev-pr.yml deleted file mode 100644 index c2ea3c005..000000000 --- a/.github/workflows/service-discovery-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Develop, on PR Java CI/CD of service-discovery, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/service-discovery-pipeline-dev-push.yml b/.github/workflows/service-discovery-pipeline-dev-push.yml deleted file mode 100644 index ff37dad98..000000000 --- a/.github/workflows/service-discovery-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Develop, on PUSH Java CI/CD of service-discovery-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for service-discovery-ecommerce-boot Microservice - run: docker build -f service-discovery/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/service-discovery-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push service-discovery-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/service-discovery-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/service-discovery-pipeline-prod-pr.yml b/.github/workflows/service-discovery-pipeline-prod-pr.yml deleted file mode 100644 index d98d1493f..000000000 --- a/.github/workflows/service-discovery-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Prod, on PR Java CI/CD of service-discovery, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/service-discovery-pipeline-prod-push.yml b/.github/workflows/service-discovery-pipeline-prod-push.yml deleted file mode 100644 index c80f9b275..000000000 --- a/.github/workflows/service-discovery-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Prod, on PUSH Java CI/CD of service-discovery-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for service-discovery-ecommerce-boot Microservice - run: docker build -f service-discovery/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/service-discovery-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push service-discovery-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/service-discovery-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/service-discovery-pipeline-stage-pr.yml b/.github/workflows/service-discovery-pipeline-stage-pr.yml deleted file mode 100644 index 3b2f70c7c..000000000 --- a/.github/workflows/service-discovery-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of service-discovery, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/service-discovery-pipeline-stage-push.yml b/.github/workflows/service-discovery-pipeline-stage-push.yml deleted file mode 100644 index e490365ce..000000000 --- a/.github/workflows/service-discovery-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Stage, on PUSH Java CI/CD of service-discovery-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for service-discovery-ecommerce-boot Microservice - run: docker build -f service-discovery/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/service-discovery-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push service-discovery-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/service-discovery-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/shipping-service-pipeline-dev-pr.yml b/.github/workflows/shipping-service-pipeline-dev-pr.yml deleted file mode 100644 index 9cd3ec44a..000000000 --- a/.github/workflows/shipping-service-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of shipping-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/shipping-service-pipeline-dev-push.yml b/.github/workflows/shipping-service-pipeline-dev-push.yml deleted file mode 100644 index 5dc801ff5..000000000 --- a/.github/workflows/shipping-service-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Stage, on PUSH Java CI/CD of shipping-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for shipping-service-ecommerce-boot Microservice - run: docker build -f shipping-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/shipping-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push shipping-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/shipping-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/shipping-service-pipeline-prod-pr.yml b/.github/workflows/shipping-service-pipeline-prod-pr.yml deleted file mode 100644 index 7c22bae52..000000000 --- a/.github/workflows/shipping-service-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of shipping-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/shipping-service-pipeline-prod-push.yml b/.github/workflows/shipping-service-pipeline-prod-push.yml deleted file mode 100644 index 3e07b4f28..000000000 --- a/.github/workflows/shipping-service-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Stage, on PUSH Java CI/CD of shipping-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for shipping-service-ecommerce-boot Microservice - run: docker build -f shipping-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/shipping-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push shipping-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/shipping-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/shipping-service-pipeline-stage-pr.yml b/.github/workflows/shipping-service-pipeline-stage-pr.yml deleted file mode 100644 index 2447a0b9a..000000000 --- a/.github/workflows/shipping-service-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of shipping-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/shipping-service-pipeline-stage-push.yml b/.github/workflows/shipping-service-pipeline-stage-push.yml deleted file mode 100644 index 6c4ec225c..000000000 --- a/.github/workflows/shipping-service-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Stage, on PUSH Java CI/CD of shipping-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for shipping-service-ecommerce-boot Microservice - run: docker build -f shipping-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/shipping-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push shipping-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/shipping-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/user-service-pipeline-dev-pr.yml b/.github/workflows/user-service-pipeline-dev-pr.yml deleted file mode 100644 index d271f8642..000000000 --- a/.github/workflows/user-service-pipeline-dev-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of user-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ develop ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/user-service-pipeline-dev-push.yml b/.github/workflows/user-service-pipeline-dev-push.yml deleted file mode 100644 index b27c71bd2..000000000 --- a/.github/workflows/user-service-pipeline-dev-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Stage, on PUSH Java CI/CD of user-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ develop ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for user-service-ecommerce-boot Microservice - run: docker build -f user-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/user-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev . - - name: Push user-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/user-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}dev - - diff --git a/.github/workflows/user-service-pipeline-prod-pr.yml b/.github/workflows/user-service-pipeline-prod-pr.yml deleted file mode 100644 index a048fe1ed..000000000 --- a/.github/workflows/user-service-pipeline-prod-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of user-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/user-service-pipeline-prod-push.yml b/.github/workflows/user-service-pipeline-prod-push.yml deleted file mode 100644 index ae58b153c..000000000 --- a/.github/workflows/user-service-pipeline-prod-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Stage, on PUSH Java CI/CD of user-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ master ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for user-service-ecommerce-boot Microservice - run: docker build -f user-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/user-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} . - - name: Push user-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/user-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }} - - diff --git a/.github/workflows/user-service-pipeline-stage-pr.yml b/.github/workflows/user-service-pipeline-stage-pr.yml deleted file mode 100644 index f11ce58df..000000000 --- a/.github/workflows/user-service-pipeline-stage-pr.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: Stage, on PR Java CI/CD of user-service, ecommerce-microservice-backend app - -on: - pull_request: - branches: [ stage ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - diff --git a/.github/workflows/user-service-pipeline-stage-push.yml b/.github/workflows/user-service-pipeline-stage-push.yml deleted file mode 100644 index 132f33fcc..000000000 --- a/.github/workflows/user-service-pipeline-stage-push.yml +++ /dev/null @@ -1,33 +0,0 @@ - -name: Stage, on PUSH Java CI/CD of user-service-ecommerce-boot, ecommerce-microservice-backend app - -on: - push: - branches: [ stage ] - -jobs: - build_push: - #needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Setup env variables for Docker - run: echo Setup env variables for Docker - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Login - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Build Docker image for user-service-ecommerce-boot Microservice - run: docker build -f user-service/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/user-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage . - - name: Push user-service-ecommerce-boot image - run: docker push ${{ secrets.DOCKER_USERNAME }}/user-service-ecommerce-boot:${{ secrets.PROJECT_VERSION }}stage - - diff --git a/.github/workflows/user-service-stage.yml b/.github/workflows/user-service-stage.yml new file mode 100644 index 000000000..527806287 --- /dev/null +++ b/.github/workflows/user-service-stage.yml @@ -0,0 +1,60 @@ +name: Stage Deploy - User Service + +on: + workflow_dispatch: + push: + branches: [ stage ] + +env: + SERVICE: user-service + IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/user-service + KUBE_NAMESPACE: ecommerce + +jobs: + build-test-deploy: + runs-on: self-hosted + defaults: + run: + shell: pwsh + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '11' + + - name: Run unit tests + run: .\mvnw.cmd -pl $env:SERVICE -am test + + - name: Package service + run: .\mvnw.cmd -pl $env:SERVICE -am -DskipTests=false package + + - name: Log in to Docker Hub + run: | + docker logout 2>$null + echo ${{ secrets.DOCKER_PASSWORD }} | docker login --username ${{ secrets.DOCKER_USERNAME }} --password-stdin + + - name: Build Docker image + run: | + $tag = "${{ env.IMAGE_NAME }}:${{ github.sha }}" + docker build -t $tag -f $env:SERVICE/Dockerfile . + echo "IMAGE_TAG=$tag" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + + - name: Push Docker image + run: docker push $env:IMAGE_TAG + + - name: Configure kubeconfig + run: | + $env:KUBECONFIG = "${{ secrets.KUBECONFIG }}" + echo "KUBECONFIG set" + + - name: Deploy to stage namespace + run: | + $content = Get-Content k8s/${{ env.SERVICE }}-deployment.yaml -Raw + $content = $content -replace 'namespace: ecommerce', "namespace: ${{ env.KUBE_NAMESPACE }}" + $content | kubectl apply -f - --validate=false + kubectl set image deployment/${{ env.SERVICE }} ${{ env.SERVICE }}=$env:IMAGE_TAG -n ${{ env.KUBE_NAMESPACE }} + kubectl rollout status deployment/${{ env.SERVICE }} -n ${{ env.KUBE_NAMESPACE }} --timeout=600s diff --git a/.gitignore b/.gitignore index 549e00a2a..7e17fbdf1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# Ignorar archivos y carpetas del runner y logs +actions-runner/ +*.log +# Ignorar carpetas temporales y de trabajo +*_work/ +_diag/ +tmp/cloud-config-server/ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar diff --git a/REPORTE.md b/REPORTE.md new file mode 100644 index 000000000..1a8bfdae8 --- /dev/null +++ b/REPORTE.md @@ -0,0 +1,224 @@ +# Reporte de configuración y despliegue + +## 1. Separación de archivos Compose +- Se dividió el archivo principal en tres grupos: + - `core.yml`: servicios centrales (zipkin, cloud-config, service-discovery). + - `compose-gateway.yml`: api-gateway y proxy-client. + - `compose-business.yml`: microservicios de negocio (product-service, user-service, order-service, payment-service, favourite-service, shipping-service). +- Se recomendó levantar los servicios en el siguiente orden para evitar saturar la PC y asegurar dependencias: + 1. Servicios centrales + 2. Gateway y proxy + 3. Microservicios de negocio + +![1761369714268](image/REPORTE/1761369714268.png) + +![1761872582002](image/REPORTE/1761872582002.png) + +## 2. Docker y Compose +- Se revisaron y adaptaron los Dockerfile de cada microservicio para asegurar consistencia y correcta configuración de variables de entorno. +- Se corrigieron los compose.yml individuales para que todos los servicios se conecten a la red `microservices_network` y tengan las variables necesarias para Eureka, Config Server y Zipkin. + +## 3. GitHub Actions +- Se crearon workflows para los seis microservicios principales: + - product-service + - user-service + - order-service + - payment-service + - proxy-client + - api-gateway +- Cada workflow compila el código, construye la imagen Docker y la publica en Docker Hub. + +## 4. Kubernetes +- Se generaron manifiestos de despliegue y servicio para los seis microservicios principales y los servicios centrales (Eureka, Config Server, Zipkin). +- Se recomendó reemplazar `` por el usuario real de Docker Hub antes de desplegar. +- Se indicaron los comandos para aplicar los manifiestos en el clúster. + +## 5. Recomendaciones de uso +- Mantener solo un archivo core para los servicios centrales. +- Levantar los servicios por grupos para evitar saturación de recursos. +- Verificar la salud de los servicios en Eureka y Zipkin. +- Usar los workflows de GitHub Actions para automatizar el build y despliegue. +- Usar los manifiestos de Kubernetes para el despliegue en clúster. + +## 6. Automatización CI/CD y Despliegue en Kubernetes + +### Workflows de GitHub Actions +- Se crearon dos workflows principales en `.github/workflows/`: + - `core-services-pipeline.yml`: Despliega los servicios centrales (Zipkin, Cloud Config, Service Discovery) en el clúster de Kubernetes usando los manifiestos en `k8s/`. + - `api-gateway-pipeline.yml`: Compila, construye y publica la imagen Docker del API Gateway, y luego la despliega en Kubernetes. +- Ambos workflows quedaron únicamente con el disparador manual (`workflow_dispatch`). Se desactivaron los eventos `push` y `workflow_run` para evitar despliegues automáticos cuando se hace `git push`; a partir de ahora se deben ejecutar desde la pestaña *Actions* siguiendo el orden core → gateway. +- El workflow de core despliega en el orden Zipkin → Cloud Config → Service Discovery y espera a que cada `Deployment` quede listo con `kubectl rollout status` antes de avanzar. De este modo Eureka arranca una vez que el Config Server ya expone la configuración. El job del gateway aplica el propio deployment y proxy-client, verificando su rollout antes de finalizar. +- Ambos workflows usan un runner self-hosted y requieren los secretos `DOCKER_USERNAME`, `DOCKER_PASSWORD` y `KUBECONFIG` para autenticación y acceso al clúster. +- Se añadieron pipelines específicos para entorno *stage* de los microservicios `product-service` y `user-service` (`product-service-stage.yml`, `user-service-stage.yml`). Cada pipeline: + 1. Ejecuta las pruebas unitarias del módulo (`mvnw -pl -am test`). + 2. Empaqueta el servicio sin saltar pruebas. + 3. Construye y publica una imagen con tag `SHA` en Docker Hub. + 4. Aplica los manifiestos de Kubernetes ajustando el namespace a `ecommerce-stage` y fuerza la actualización del deployment con la nueva imagen. + 5. Espera el `rollout` exitoso antes de terminar. +- Estos pipelines se disparan de forma manual o con `push` a la rama `stage`. Requieren los secretos `DOCKER_USERNAME`, `DOCKER_PASSWORD` y `KUBECONFIG` del clúster. +- Actualmente despliegan sobre el namespace `ecommerce` existente, por lo que comparten ConfigMaps/Secrets con el entorno ya configurado. Si más adelante decides aislar un entorno stage, basta con ajustar la variable `KUBE_NAMESPACE` en los workflows. + +![1761673447373](image/REPORTE/1761673447373.png) + +![1761673515739](image/REPORTE/1761673515739.png) + +![1761673532742](image/REPORTE/1761673532742.png) + +![1761673788175](image/REPORTE/1761673788175.png) + +### Manifiestos de Kubernetes +- Los manifiestos para Zipkin, Cloud Config y Service Discovery se encuentran en la carpeta `k8s/`: + - `zipkin-deployment.yaml` + - `cloud-config-deployment.yaml` + - `service-discovery-deployment.yaml` +- Cada manifiesto define un Deployment y un Service (NodePort) para exponer los servicios en el clúster. +- El API Gateway también cuenta con sus propios manifiestos (`api-gateway-deployment.yaml`, `api-gateway-service.yaml`). + +### Orden de despliegue recomendado +1. Ejecutar el workflow `core-services-pipeline.yml` para desplegar los servicios centrales. +2. Verificar que los pods y servicios estén corriendo correctamente (`kubectl get pods`, `kubectl get services`). +3. Ejecutar el workflow `api-gateway-pipeline.yml` para compilar, construir, publicar y desplegar el API Gateway. +4. Validar el acceso al API Gateway y la integración con los servicios centrales. + +![1761673829556](image/REPORTE/1761673829556.png) + +### Notas adicionales +- Si se requiere que el workflow del API Gateway espere a que los servicios centrales estén listos, se puede configurar una dependencia entre workflows en GitHub Actions usando `needs` o ejecutando manualmente en el orden correcto. +- Se recomienda documentar cualquier cambio en los manifiestos y workflows en este reporte para mantener trazabilidad. + +## 7. Ajustes recientes para despliegue en Minikube +- Se corrigieron los valores de los ConfigMaps en `k8s/config-maps.yaml` para que `ZIPKIN_BASE_URL` apunte a `http://zipkin-service:9411` y `CONFIG_SERVER_URL` a `http://cloud-config-service:9296`, alineando los hosts con los nombres reales de los servicios dentro del clúster. +- Se actualizaron los deployments `k8s/order-service-deployment.yaml`, `k8s/payment-service-deployment.yaml`, `k8s/product-service-deployment.yaml`, `k8s/shipping-service-deployment.yaml`, `k8s/user-service-deployment.yaml`, `k8s/cloud-config-deployment.yaml` y `k8s/zipkin-deployment.yaml` para que todos consuman el Config Server mediante `http://cloud-config-service:9296/` y lean la URL de Eureka desde el ConfigMap. +- Para asegurar la detección del Config Server en Eureka, se añadieron variables de entorno alternativas (`EUREKA_CLIENT_SERVICE*_DEFAULTZONE`) y `JAVA_TOOL_OPTIONS=-Deureka.client.service-url.defaultZone=http://service-discovery-service:8761/eureka/` en los deployments de Cloud Config y Zipkin, evitando el fallback a `localhost` que usaba la configuración por defecto del repositorio Git de configuración. +- Se eliminaron temporalmente los deployments de negocio (order, payment, product, shipping, user) para aislar las pruebas de los servicios core. Próximos despliegues deberán recrearlos cuando se continúe con las pruebas integrales. +- Se agregó un controlador simple `RootController` en el proyecto `cloud-config` para responder con 200 en la raíz (`/`) y así evitar trazas de error 404 al monitorear con Zipkin. El contenedor en ejecución sigue usando la imagen publicada originalmente; solo es necesario reconstruirla si se quiere aprovechar este endpoint opcional. + + +--- + +![1761885010813](image/REPORTE/1761885010813.png) + +![1761885057490](image/REPORTE/1761885057490.png) + + +![1762047836686](image/REPORTE/1762047836686.png) + + +## 8. Cobertura de pruebas unitarias en microservicios +- Se crearon suites unitarias con Mockito para los servicios de negocio que aparecen en Zipkin (`favourite-service`, `user-service`, `product-service`, `order-service`, `shipping-service`, `payment-service`). Cada prueba valida el mapeo DTO ↔ entidad, la interacción con los repositorios JPA y la integración con `RestTemplate` cuando aplica. +- Durante la implementación se detectó que las entidades y DTOs no exponen `toBuilder`; por lo tanto, las pruebas construyen instancias actualizadas explícitamente para mantener compatibilidad con Lombok y reflejar estados modificados. +- Se documentó el flujo completo de persistencia y eliminación en los servicios, incluyendo las excepciones personalizadas (`FavouriteNotFoundException`, `UserObjectNotFoundException`, etc.) para cubrir escenarios felices y de error. +- Se ejecutaron los tests con `.\mvnw.cmd -pl favourite-service,user-service,product-service,order-service,shipping-service,payment-service test`, garantizando que todas las suites compilan y pasan en cadena dentro del reactor Maven. +- La evidencia del `BUILD SUCCESS` se adjunta en la captura proporcionada, donde se observa el tiempo individual de ejecución por módulo y la ausencia de fallos. + + +Este reporte documenta todos los pasos y configuraciones realizadas para correr y desplegar el sistema de microservicios de manera óptima y escalable. + +## 9. Validación manual servicio por servicio en Minikube + +Para comprobar rápidamente la salud de cada componente dentro del clúster, se recomienda mantener abiertos los `port-forward` en sesiones independientes y validar con `curl`. Las combinaciones sugeridas evitan conflictos de puertos en la máquina local: + +- **Zipkin** + - `kubectl port-forward deployment/zipkin -n ecommerce 19411:9411` + - `curl http://127.0.0.1:19411/actuator/health` + - `curl http://127.0.0.1:19411/api/v2/services` +- **Service Discovery (Eureka)** + - `kubectl port-forward deployment/service-discovery -n ecommerce 18761:8761` + - `curl http://127.0.0.1:18761/actuator/health` + - `curl http://127.0.0.1:18761/eureka/apps` +- **Cloud Config** + - `kubectl port-forward deployment/cloud-config -n ecommerce 19296:9296` + - `curl http://127.0.0.1:19296/actuator/health` + - `curl http://127.0.0.1:19296/user-service/dev` +- **User Service** + - `kubectl port-forward deployment/user-service -n ecommerce 18700:8700` + - `curl http://127.0.0.1:18700/user-service/actuator/health` + - `curl http://127.0.0.1:18700/user-service/api/users` +- **Product Service** + - `kubectl port-forward deployment/product-service -n ecommerce 18500:8500` + - `curl http://127.0.0.1:18500/product-service/actuator/health` + - `curl http://127.0.0.1:18500/product-service/api/products` +- **Favourite Service** + - `kubectl port-forward deployment/favourite-service-deployment -n ecommerce 18800:8800` + - `curl http://127.0.0.1:18800/favourite-service/actuator/health` + - `curl http://127.0.0.1:18800/favourite-service/api/favourites` +- **Order Service** + - `kubectl port-forward deployment/order-service -n ecommerce 18300:8300` + - `curl http://127.0.0.1:18300/order-service/actuator/health` + - `curl http://127.0.0.1:18300/order-service/api/orders` +- **Shipping Service** + - `kubectl port-forward deployment/shipping-service -n ecommerce 18600:8600` + - `curl http://127.0.0.1:18600/shipping-service/actuator/health` + - `curl http://127.0.0.1:18600/shipping-service/api/shippings` +- **Payment Service** + - `kubectl port-forward deployment/payment-service -n ecommerce 18400:8400` + - `curl http://127.0.0.1:18400/payment-service/actuator/health` + - `curl http://127.0.0.1:18400/payment-service/api/payments` + +Mantener estos túneles activos permite lanzar rápidamente diagnósticos tipo `curl` o ejecutar la prueba de integración global sin sufrir timeouts por dependencias no accesibles. + +## 10. Perfil Maven `integration` para pruebas Minikube + +Se actualizó el `pom.xml` raíz con dos cambios importantes: + +- El plugin `maven-surefire-plugin` excluye por defecto `MinikubeServiceCommunicationTest` de la fase `test`, de modo que `mvn test` o los builds estándar sigan ejecutándose rápido solo con los tests unitarios. +- Se creó el perfil `integration` que activa el plugin `maven-failsafe-plugin` para ejecutar `MinikubeServiceCommunicationTest` en la fase `integration-test/verify`. +- Ese plugin inyecta variables de entorno (`PRODUCT_SERVICE_BASE_URL`, `USER_SERVICE_BASE_URL`, etc.) apuntando a los puertos locales abiertos mediante `kubectl port-forward`, por lo que la prueba consume los túneles locales en lugar del DNS interno del clúster. + - Ese plugin inyecta variables de entorno (`PRODUCT_SERVICE_BASE_URL`, `USER_SERVICE_BASE_URL`, etc.) apuntando a los puertos locales abiertos mediante `kubectl port-forward`, por lo que la prueba consume los túneles locales en lugar del DNS interno del clúster. + +Uso recomendado: + +```powershell +mvn -Pintegration verify +``` + +Antes de lanzar el comando, asegurarse de tener vivos todos los `port-forward` listados en la sección anterior para evitar fallos de conectividad durante la prueba. El resultado se puede revisar en `target/surefire-reports/TEST-com.selimhorri.app.integration.MinikubeServiceCommunicationTest.xml` o directamente en la consola Maven. + +![1762208725906](image/REPORTE/1762208725906.png) + +### Tutorial rápido: ejecutar la prueba de integración + +1. Abre los túneles necesarios (un terminal por puerto) siguiendo la tabla de la sección 9. Revisa que cada `curl` responda 200. +2. Desde la raíz del repo ejecuta una compilación limpia del módulo raíz si vienes de un `clean` previo: + ```powershell + mvn --% -Pintegration -pl :ecommerce-microservice-backend -am compiler:testCompile + ``` + Este paso recompila los tests en `src/test/java`. +3. Lanza la suite completa (unitarias + integración + E2E) con: + ```powershell + mvn -Pintegration verify + ``` +4. Los reportes de Failsafe se generan en `target/failsafe-reports/` y los de Surefire en cada módulo (`/target/surefire-reports/`). +5. Ante un fallo de conectividad, revisa primero los `port-forward` y vuelve a ejecutar el comando. + +## 11. Nuevas pruebas E2E orientadas al flujo de usuario + +- Se añadió el archivo `src/test/java/com/selimhorri/app/e2e/UserJourneyE2ETest.java` con cinco casos que validan escenarios de usuario extremo a extremo a través del API Gateway (catálogo de productos, detalle, favoritos enriquecidos, resumen de envíos y estado de pagos). +- Para ejecutarlas es necesario abrir un túnel adicional hacia el gateway: `kubectl port-forward deployment/api-gateway -n ecommerce 18080:8080`. El plugin Failsafe expone esta URL a los tests mediante `API_GATEWAY_BASE_URL`. +- Las pruebas se ejecutan junto con la suite de integración existente utilizando el mismo comando `mvn -Pintegration verify`. El reporte consolidado queda disponible en `target/failsafe-reports/`. +- Se reforzó el test para entornos inestables: el `RestTemplate` ahora usa timeouts más generosos (30 s de conexión y 45 s de lectura) y reintenta hasta tres veces las llamadas al gateway antes de fallar, minimizando falsos positivos cuando los `port-forward` tardan en responder. + + +![1762301114758](image/REPORTE/1762301114758.png) + +### Tutorial rápido: ejecutar solo las E2E + +1. Asegúrate de tener activos los `port-forward` hacia todos los servicios y, en especial, al API Gateway (`18080:8080`). +2. Recompila las clases de test en caso de haber hecho `clean`: + ```powershell + mvn --% -Pintegration -pl :ecommerce-microservice-backend -am compiler:testCompile + ``` +3. Ejecuta únicamente el E2E filtrando por nombre y excluyendo la suite de Minikube: + ```powershell + mvn --% -Pintegration "-Dfailsafe.includes=**/UserJourneyE2ETest.java" "-Dfailsafe.excludes=**/MinikubeServiceCommunicationTest.java" verify + ``` +4. Observa en consola la traza `Running com.selimhorri.app.e2e.UserJourneyE2ETest` y verifica el archivo `target/failsafe-reports/TEST-com.selimhorri.app.e2e.UserJourneyE2ETest.xml`. +5. Vuelve a `mvn -Pintegration verify` sin filtros para ejecutar ambas suites juntas. + +## 12. Pruebas de rendimiento con Locust + +- Se agregó la carpeta `load-testing/` con `locustfile.py`, que reproduce las mismas jornadas del E2E (catálogo, detalle, favoritos, envíos y pagos) para medir desempeño desde el API Gateway. +- Cada tarea valida que las respuestas incluyan los campos clave (`collection`, `product`, `order`, etc.) y registra fallos si la API responde con estado inesperado o JSON inválido. +- El archivo `load-testing/README.md` documenta los prerequisitos y comandos para ejecutar Locust de forma interactiva y headless, incluyendo ejemplos con exporte CSV/HTML. +- Para evitar conflictos de entorno se recomienda usar `py -3.13 -m locust -f load-testing/locustfile.py` y mantener activos los `port-forward` hacia el gateway y los microservicios. +- Se sugirió una estrategia escalonada para las pruebas: comenzar con 10 usuarios (ramp-up 2/s) y aumentar progresivamente a 25, 50, 75 usuarios monitoreando RPS, latencias P95/P99 y métricas de los pods (`kubectl top pod -n ecommerce`). \ No newline at end of file diff --git a/api-gateway/compose.yml b/api-gateway/compose.yml index 56a28e0a3..a1607c2c9 100644 --- a/api-gateway/compose.yml +++ b/api-gateway/compose.yml @@ -1,12 +1,29 @@ + version: '3' services: api-gateway-container: image: selimhorri/api-gateway-ecommerce-boot:0.1.0 ports: - - 8080:8080 + - "8080:8080" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=api-gateway-container + +networks: + microservices_network: + external: true diff --git a/api-gateway/src/main/resources/application.yml b/api-gateway/src/main/resources/application.yml index dd013a72d..0101de0b8 100644 --- a/api-gateway/src/main/resources/application.yml +++ b/api-gateway/src/main/resources/application.yml @@ -5,9 +5,9 @@ server: spring: zipkin: - base-url: ${SPRING_ZIPKIN_BASE_URL:http://localhost:9411/} + base-url: ${SPRING_ZIPKIN_BASE_URL:http://zipkin-service.ecommerce:9411/} config: - import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://localhost:9296} + import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://cloud-config-service.ecommerce:9296} application: name: API-GATEWAY profiles: @@ -62,6 +62,11 @@ spring: predicates: - Path=/app/** +eureka: + client: + service-url: + defaultZone: ${EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE:http://service-discovery-service.ecommerce:8761/eureka} + resilience4j: circuitbreaker: instances: diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 0b342ca01..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,22 +0,0 @@ - -trigger: -- develop -- stage -- master - -pool: - vmImage: ubuntu-latest - -steps: -- task: Maven@3 - inputs: - mavenPomFile: 'pom.xml' - mavenOptions: '-Xmx3072m' - javaHomeOption: 'JDKVersion' - jdkVersionOption: '11' - jdkArchitectureOption: 'x64' - publishJUnitResults: true - testResultsFiles: '**/surefire-reports/TEST-*.xml' - goals: 'package' - - diff --git a/cloud-config/Dockerfile b/cloud-config/Dockerfile index cd5b044ee..9bef3a0af 100644 --- a/cloud-config/Dockerfile +++ b/cloud-config/Dockerfile @@ -1,11 +1,13 @@ + FROM openjdk:11 ARG PROJECT_VERSION=0.1.0 RUN mkdir -p /home/app WORKDIR /home/app +ENV SPRING_PROFILES_ACTIVE dev COPY cloud-config/ . ADD cloud-config/target/cloud-config-v${PROJECT_VERSION}.jar cloud-config.jar EXPOSE 9296 -ENTRYPOINT ["java", "-jar", "cloud-config.jar"] +ENTRYPOINT ["java", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "-jar", "cloud-config.jar"] diff --git a/cloud-config/compose.yml b/cloud-config/compose.yml index 2ebe9109c..7f9114348 100644 --- a/cloud-config/compose.yml +++ b/cloud-config/compose.yml @@ -1,12 +1,24 @@ + version: '3' services: cloud-config-container: image: selimhorri/cloud-config-ecommerce-boot:0.1.0 ports: - - 9296:9296 + - "9296:9296" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - EUREKA_CLIENT_REGION=default + - EUREKA_CLIENT_AVAILABILITY_ZONES_DEFAULT=myzone + - EUREKA_CLIENT_SERVICEURL_MYZONE=http://service-discovery-container:8761/eureka + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + +networks: + microservices_network: + external: true diff --git a/cloud-config/src/main/java/com/selimhorri/app/web/RootController.java b/cloud-config/src/main/java/com/selimhorri/app/web/RootController.java new file mode 100644 index 000000000..bef38bbf7 --- /dev/null +++ b/cloud-config/src/main/java/com/selimhorri/app/web/RootController.java @@ -0,0 +1,16 @@ +package com.selimhorri.app.web; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +class RootController { + + @GetMapping({ "", "/" }) + ResponseEntity root() { + // Provide a simple OK response so health checks and manual probes do not + // produce 404 traces. + return ResponseEntity.ok("cloud-config-service"); + } +} diff --git a/cloud-config/src/main/resources/application.yml b/cloud-config/src/main/resources/application.yml index 6ac6f81e6..ce1e656b6 100644 --- a/cloud-config/src/main/resources/application.yml +++ b/cloud-config/src/main/resources/application.yml @@ -11,7 +11,7 @@ spring: config: server: git: - uri: https://github.com/SelimHorri/cloud-config-server + uri: https://github.com/Freddyedd21/cloud-config-server clone-on-start: true resilience4j: diff --git a/compose-business.yml b/compose-business.yml new file mode 100644 index 000000000..3568b681a --- /dev/null +++ b/compose-business.yml @@ -0,0 +1,121 @@ +version: '3' +services: + product-service-container: + image: selimhorri/product-service-ecommerce-boot:0.1.0 + ports: + - "8500:8500" + networks: + - microservices_network + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=product-service-container + + user-service-container: + image: selimhorri/user-service-ecommerce-boot:0.1.0 + ports: + - "8700:8700" + networks: + - microservices_network + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=user-service-container + + favourite-service-container: + image: selimhorri/favourite-service-ecommerce-boot:0.1.0 + ports: + - "8800:8800" + networks: + - microservices_network + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=favourite-service-container + + order-service-container: + image: selimhorri/order-service-ecommerce-boot:0.1.0 + ports: + - "8300:8300" + networks: + - microservices_network + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=order-service-container + - EUREKA_INSTANCE_NON_SECURE_PORT=8300 + + payment-service-container: + image: selimhorri/payment-service-ecommerce-boot:0.1.0 + ports: + - "8400:8400" + networks: + - microservices_network + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=payment-service-container + - EUREKA_INSTANCE_NON_SECURE_PORT=8400 + + shipping-service-container: + image: selimhorri/shipping-service-ecommerce-boot:0.1.0 + ports: + - "8600:8600" + networks: + - microservices_network + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=shipping-service-container + +networks: + microservices_network: + external: true diff --git a/compose-gateway.yml b/compose-gateway.yml new file mode 100644 index 000000000..16f851c61 --- /dev/null +++ b/compose-gateway.yml @@ -0,0 +1,49 @@ +version: '3' +services: + api-gateway-container: + image: selimhorri/api-gateway-ecommerce-boot:0.1.0 + ports: + - "8080:8080" + networks: + - microservices_network + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - EUREKA_CLIENT_ENABLED=true + - RIBBON_EUREKA_ENABLED=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=api-gateway-container + + proxy-client-container: + image: selimhorri/proxy-client-ecommerce-boot:0.1.0 + ports: + - "8900:8900" + networks: + - microservices_network + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - EUREKA_CLIENT_ENABLED=true + - RIBBON_EUREKA_ENABLED=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=proxy-client-container + +networks: + microservices_network: + external: true diff --git a/compose.yml b/compose.yml index 2d6442c32..fa8f53241 100644 --- a/compose.yml +++ b/compose.yml @@ -1,70 +1,163 @@ - version: '3' services: - zipkin-container: - image: openzipkin/zipkin - ports: - - 9411:9411 - service-discovery-container: - image: selimhorri/service-discovery-ecommerce-boot:0.1.0 - ports: - - 8761:8761 - environment: - - SPRING_PROFILES_ACTIVE=dev - cloud-config-container: - image: selimhorri/cloud-config-ecommerce-boot:0.1.0 - ports: - - 9296:9296 - environment: - - SPRING_PROFILES_ACTIVE=dev api-gateway-container: image: selimhorri/api-gateway-ecommerce-boot:0.1.0 ports: - - 8080:8080 + - "8080:8080" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=api-gateway-container + proxy-client-container: image: selimhorri/proxy-client-ecommerce-boot:0.1.0 ports: - - 8900:8900 + - "8900:8900" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=proxy-client-container + order-service-container: image: selimhorri/order-service-ecommerce-boot:0.1.0 ports: - - 8300:8300 + - "8300:8300" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=order-service-container + - EUREKA_INSTANCE_NON_SECURE_PORT=8300 + payment-service-container: image: selimhorri/payment-service-ecommerce-boot:0.1.0 ports: - - 8400:8400 + - "8400:8400" + networks: + - microservices_network + depends_on: + - order-service-container environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=payment-service-container + - EUREKA_INSTANCE_NON_SECURE_PORT=8400 + product-service-container: image: selimhorri/product-service-ecommerce-boot:0.1.0 ports: - - 8500:8500 + - "8500:8500" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=product-service-container + - EUREKA_INSTANCE_NON_SECURE_PORT=8500 + shipping-service-container: image: selimhorri/shipping-service-ecommerce-boot:0.1.0 ports: - - 8600:8600 + - "8600:8600" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=shipping-service-container + user-service-container: image: selimhorri/user-service-ecommerce-boot:0.1.0 ports: - - 8700:8700 + - "8700:8700" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=user-service-container + favourite-service-container: image: selimhorri/favourite-service-ecommerce-boot:0.1.0 ports: - - 8800:8800 + - "8800:8800" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=favourite-service-container - - +networks: + microservices_network: + external: true + name: microservices_network \ No newline at end of file diff --git a/core.yml b/core.yml new file mode 100644 index 000000000..119d22090 --- /dev/null +++ b/core.yml @@ -0,0 +1,43 @@ +services: + zipkin: + image: openzipkin/zipkin + ports: + - "9411:9411" + networks: + microservices_network: + aliases: + - zipkin + + service-discovery-container: + image: selimhorri/service-discovery-ecommerce-boot:0.1.0 + ports: + - "8761:8761" + networks: + microservices_network: + aliases: + - service-discovery-container + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + + cloud-config-container: + image: selimhorri/cloud-config-ecommerce-boot:0.1.0 + ports: + - "9296:9296" + networks: + microservices_network: + aliases: + - cloud-config-container + environment: + - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - EUREKA_CLIENT_REGION=default + - EUREKA_CLIENT_AVAILABILITY_ZONES_DEFAULT=myzone + - EUREKA_CLIENT_AVAILABILITYZONES_DEFAULT=myzone + - EUREKA_CLIENT_SERVICEURL_MYZONE=http://service-discovery-container:8761/eureka + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + +networks: + microservices_network: + external: true \ No newline at end of file diff --git a/deploy-all.sh b/deploy-all.sh new file mode 100644 index 000000000..9b809a2e6 --- /dev/null +++ b/deploy-all.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -e + +# 1. Namespace +kubectl apply -f k8s/namespace.yaml + +# 2. ConfigMaps +kubectl apply -f k8s/config-maps.yaml + +# 3. Infra services +kubectl apply -f k8s/service-discovery-deployment.yaml +kubectl apply -f k8s/cloud-config-deployment.yaml +kubectl apply -f k8s/zipkin-deployment.yaml + +# 4. Business microservices +kubectl apply -f k8s/favourite-service-deployment.yaml +kubectl apply -f k8s/order-service-deployment.yaml +kubectl apply -f k8s/payment-service-deployment.yaml +kubectl apply -f k8s/product-service-deployment.yaml +kubectl apply -f k8s/shipping-service-deployment.yaml +kubectl apply -f k8s/user-service-deployment.yaml +kubectl apply -f k8s/api-gateway-deployment.yaml + +echo "Despliegue completo." diff --git a/ecommerce-microservice-backend-app.code-workspace b/ecommerce-microservice-backend-app.code-workspace new file mode 100644 index 000000000..699ba25a3 --- /dev/null +++ b/ecommerce-microservice-backend-app.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../cloud-config-server" + } + ], + "settings": { + "java.compile.nullAnalysis.mode": "automatic" + } +} \ No newline at end of file diff --git a/favourite-service/compose.yml b/favourite-service/compose.yml index 6a5607451..86e51a096 100644 --- a/favourite-service/compose.yml +++ b/favourite-service/compose.yml @@ -1,12 +1,29 @@ + version: '3' services: favourite-service-container: image: selimhorri/favourite-service-ecommerce-boot:0.1.0 ports: - - 8800:8800 + - "8800:8800" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=favourite-service-container + +networks: + microservices_network: + external: true diff --git a/favourite-service/src/main/resources/application.yml b/favourite-service/src/main/resources/application.yml index 6474033d8..81e8097bd 100644 --- a/favourite-service/src/main/resources/application.yml +++ b/favourite-service/src/main/resources/application.yml @@ -5,15 +5,20 @@ server: spring: zipkin: - base-url: ${SPRING_ZIPKIN_BASE_URL:http://localhost:9411/} + base-url: ${SPRING_ZIPKIN_BASE_URL:http://zipkin-service.ecommerce:9411/} config: - import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://localhost:9296} + import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://cloud-config-service.ecommerce:9296} application: name: FAVOURITE-SERVICE profiles: active: - dev +eureka: + client: + service-url: + defaultZone: ${EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE:http://service-discovery-service.ecommerce:8761/eureka} + resilience4j: circuitbreaker: instances: diff --git a/favourite-service/src/test/java/com/selimhorri/app/service/impl/FavouriteServiceImplTest.java b/favourite-service/src/test/java/com/selimhorri/app/service/impl/FavouriteServiceImplTest.java new file mode 100644 index 000000000..4af6f341f --- /dev/null +++ b/favourite-service/src/test/java/com/selimhorri/app/service/impl/FavouriteServiceImplTest.java @@ -0,0 +1,189 @@ +package com.selimhorri.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.client.RestTemplate; + +import com.selimhorri.app.constant.AppConstant; +import com.selimhorri.app.domain.Favourite; +import com.selimhorri.app.domain.id.FavouriteId; +import com.selimhorri.app.dto.FavouriteDto; +import com.selimhorri.app.dto.ProductDto; +import com.selimhorri.app.dto.UserDto; +import com.selimhorri.app.exception.wrapper.FavouriteNotFoundException; +import com.selimhorri.app.repository.FavouriteRepository; + +@ExtendWith(MockitoExtension.class) +class FavouriteServiceImplTest { + + @Mock + private FavouriteRepository favouriteRepository; + + @Mock + private RestTemplate restTemplate; + + private FavouriteServiceImpl favouriteService; + + private Favourite favourite; + private FavouriteId favouriteId; + private UserDto userDto; + private ProductDto productDto; + + @BeforeEach + void setUp() { + this.favouriteService = new FavouriteServiceImpl(this.favouriteRepository, this.restTemplate); + + LocalDateTime likeDate = LocalDateTime.of(2024, 1, 1, 10, 15); + this.favourite = Favourite.builder() + .userId(5) + .productId(7) + .likeDate(likeDate) + .build(); + this.favouriteId = new FavouriteId(this.favourite.getUserId(), this.favourite.getProductId(), likeDate); + + this.userDto = UserDto.builder() + .userId(this.favourite.getUserId()) + .firstName("first") + .lastName("last") + .build(); + this.productDto = ProductDto.builder() + .productId(this.favourite.getProductId()) + .productTitle("keyboard") + .build(); + } + + @Test + void findAllReturnsFavouritesWithRemoteData() { + when(this.favouriteRepository.findAll()).thenReturn(List.of(this.favourite)); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.USER_SERVICE_API_URL + "/" + this.favourite.getUserId(), + UserDto.class)) + .thenReturn(this.userDto); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.PRODUCT_SERVICE_API_URL + "/" + this.favourite.getProductId(), + ProductDto.class)) + .thenReturn(this.productDto); + + List result = this.favouriteService.findAll(); + + assertEquals(1, result.size()); + FavouriteDto dto = result.get(0); + assertEquals(this.favourite.getUserId(), dto.getUserId()); + assertEquals(this.favourite.getProductId(), dto.getProductId()); + assertSame(this.userDto, dto.getUserDto()); + assertSame(this.productDto, dto.getProductDto()); + verify(this.restTemplate).getForObject( + AppConstant.DiscoveredDomainsApi.USER_SERVICE_API_URL + "/" + this.favourite.getUserId(), + UserDto.class); + verify(this.restTemplate).getForObject( + AppConstant.DiscoveredDomainsApi.PRODUCT_SERVICE_API_URL + "/" + this.favourite.getProductId(), + ProductDto.class); + } + + @Test + void findByIdReturnsFavouriteWhenPresent() { + when(this.favouriteRepository.findById(this.favouriteId)).thenReturn(Optional.of(this.favourite)); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.USER_SERVICE_API_URL + "/" + this.favourite.getUserId(), + UserDto.class)) + .thenReturn(this.userDto); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.PRODUCT_SERVICE_API_URL + "/" + this.favourite.getProductId(), + ProductDto.class)) + .thenReturn(this.productDto); + + FavouriteDto result = this.favouriteService.findById(this.favouriteId); + + assertEquals(this.favourite.getUserId(), result.getUserId()); + assertEquals(this.favourite.getProductId(), result.getProductId()); + assertSame(this.userDto, result.getUserDto()); + assertSame(this.productDto, result.getProductDto()); + } + + @Test + void findByIdThrowsWhenFavouriteMissing() { + when(this.favouriteRepository.findById(this.favouriteId)).thenReturn(Optional.empty()); + + assertThrows(FavouriteNotFoundException.class, () -> this.favouriteService.findById(this.favouriteId)); + } + + @Test + void savePersistsMappedFavourite() { + FavouriteDto input = FavouriteDto.builder() + .userId(this.favourite.getUserId()) + .productId(this.favourite.getProductId()) + .likeDate(this.favourite.getLikeDate()) + .userDto(UserDto.builder().userId(this.favourite.getUserId()).build()) + .productDto(ProductDto.builder().productId(this.favourite.getProductId()).build()) + .build(); + + when(this.favouriteRepository.save(any(Favourite.class))).thenReturn(this.favourite); + + FavouriteDto result = this.favouriteService.save(input); + + assertEquals(this.favourite.getUserId(), result.getUserId()); + assertEquals(this.favourite.getProductId(), result.getProductId()); + + ArgumentCaptor favouriteCaptor = ArgumentCaptor.forClass(Favourite.class); + verify(this.favouriteRepository).save(favouriteCaptor.capture()); + Favourite saved = favouriteCaptor.getValue(); + assertEquals(input.getUserId(), saved.getUserId()); + assertEquals(input.getProductId(), saved.getProductId()); + assertEquals(input.getLikeDate(), saved.getLikeDate()); + } + + @Test + void updatePersistsMappedFavourite() { + FavouriteDto input = FavouriteDto.builder() + .userId(this.favourite.getUserId()) + .productId(this.favourite.getProductId()) + .likeDate(this.favourite.getLikeDate()) + .userDto(UserDto.builder().userId(this.favourite.getUserId()).build()) + .productDto(ProductDto.builder().productId(this.favourite.getProductId()).build()) + .build(); + + Favourite updated = Favourite.builder() + .userId(input.getUserId()) + .productId(input.getProductId()) + .likeDate(input.getLikeDate()) + .build(); + + when(this.favouriteRepository.save(any(Favourite.class))).thenReturn(updated); + + FavouriteDto result = this.favouriteService.update(input); + + assertEquals(updated.getUserId(), result.getUserId()); + assertEquals(updated.getProductId(), result.getProductId()); + + ArgumentCaptor favouriteCaptor = ArgumentCaptor.forClass(Favourite.class); + verify(this.favouriteRepository).save(favouriteCaptor.capture()); + Favourite saved = favouriteCaptor.getValue(); + assertEquals(input.getUserId(), saved.getUserId()); + assertEquals(input.getProductId(), saved.getProductId()); + } + + @Test + void deleteByIdDelegatesToRepository() { + doNothing().when(this.favouriteRepository).deleteById(this.favouriteId); + + this.favouriteService.deleteById(this.favouriteId); + + verify(this.favouriteRepository).deleteById(this.favouriteId); + } +} diff --git a/image/REPORTE/1761369714268.png b/image/REPORTE/1761369714268.png new file mode 100644 index 000000000..769d02ea3 Binary files /dev/null and b/image/REPORTE/1761369714268.png differ diff --git a/image/REPORTE/1761673443344.png b/image/REPORTE/1761673443344.png new file mode 100644 index 000000000..f2d2a6f50 Binary files /dev/null and b/image/REPORTE/1761673443344.png differ diff --git a/image/REPORTE/1761673447373.png b/image/REPORTE/1761673447373.png new file mode 100644 index 000000000..f2d2a6f50 Binary files /dev/null and b/image/REPORTE/1761673447373.png differ diff --git a/image/REPORTE/1761673515739.png b/image/REPORTE/1761673515739.png new file mode 100644 index 000000000..6fd8e507e Binary files /dev/null and b/image/REPORTE/1761673515739.png differ diff --git a/image/REPORTE/1761673532742.png b/image/REPORTE/1761673532742.png new file mode 100644 index 000000000..0722a8ea8 Binary files /dev/null and b/image/REPORTE/1761673532742.png differ diff --git a/image/REPORTE/1761673783679.png b/image/REPORTE/1761673783679.png new file mode 100644 index 000000000..afb261d48 Binary files /dev/null and b/image/REPORTE/1761673783679.png differ diff --git a/image/REPORTE/1761673788175.png b/image/REPORTE/1761673788175.png new file mode 100644 index 000000000..afb261d48 Binary files /dev/null and b/image/REPORTE/1761673788175.png differ diff --git a/image/REPORTE/1761673829556.png b/image/REPORTE/1761673829556.png new file mode 100644 index 000000000..a23c4d690 Binary files /dev/null and b/image/REPORTE/1761673829556.png differ diff --git a/image/REPORTE/1761872582002.png b/image/REPORTE/1761872582002.png new file mode 100644 index 000000000..72c3d84d8 Binary files /dev/null and b/image/REPORTE/1761872582002.png differ diff --git a/image/REPORTE/1761885010813.png b/image/REPORTE/1761885010813.png new file mode 100644 index 000000000..458bf4790 Binary files /dev/null and b/image/REPORTE/1761885010813.png differ diff --git a/image/REPORTE/1761885024391.png b/image/REPORTE/1761885024391.png new file mode 100644 index 000000000..458bf4790 Binary files /dev/null and b/image/REPORTE/1761885024391.png differ diff --git a/image/REPORTE/1761885057490.png b/image/REPORTE/1761885057490.png new file mode 100644 index 000000000..2525024dc Binary files /dev/null and b/image/REPORTE/1761885057490.png differ diff --git a/image/REPORTE/1762047836686.png b/image/REPORTE/1762047836686.png new file mode 100644 index 000000000..6ce363ba8 Binary files /dev/null and b/image/REPORTE/1762047836686.png differ diff --git a/image/REPORTE/1762208684550.png b/image/REPORTE/1762208684550.png new file mode 100644 index 000000000..edffa6ef9 Binary files /dev/null and b/image/REPORTE/1762208684550.png differ diff --git a/image/REPORTE/1762208692241.png b/image/REPORTE/1762208692241.png new file mode 100644 index 000000000..edffa6ef9 Binary files /dev/null and b/image/REPORTE/1762208692241.png differ diff --git a/image/REPORTE/1762208725906.png b/image/REPORTE/1762208725906.png new file mode 100644 index 000000000..edffa6ef9 Binary files /dev/null and b/image/REPORTE/1762208725906.png differ diff --git a/image/REPORTE/1762301114758.png b/image/REPORTE/1762301114758.png new file mode 100644 index 000000000..8c874b071 Binary files /dev/null and b/image/REPORTE/1762301114758.png differ diff --git a/k8s/api-gateway-deployment.yaml b/k8s/api-gateway-deployment.yaml new file mode 100644 index 000000000..fd712254f --- /dev/null +++ b/k8s/api-gateway-deployment.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api-gateway + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: api-gateway + template: + metadata: + labels: + app: api-gateway + spec: + containers: + - name: api-gateway + image: selimhorri/api-gateway-ecommerce-boot:0.1.0 + ports: + - containerPort: 8080 + env: + - name: SPRING_CLOUD_CONFIG_NAME + value: "api-gateway" + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_CONFIG_IMPORT + value: "optional:configserver:http://cloud-config-service:9296/" + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296" + - name: SPRING_ZIPKIN_BASE_URL + valueFrom: + configMapKeyRef: + name: zipkin-config + key: ZIPKIN_BASE_URL + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: "api-gateway" + - name: EUREKA_CLIENT_ENABLED + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: RIBBON_EUREKA_ENABLED + value: "true" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" diff --git a/k8s/api-gateway-service.yaml b/k8s/api-gateway-service.yaml new file mode 100644 index 000000000..0253c22f6 --- /dev/null +++ b/k8s/api-gateway-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: api-gateway-service + namespace: ecommerce +spec: + selector: + app: api-gateway + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + type: ClusterIP diff --git a/k8s/cloud-config-deployment.yaml b/k8s/cloud-config-deployment.yaml new file mode 100644 index 000000000..fe9e6b019 --- /dev/null +++ b/k8s/cloud-config-deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cloud-config + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: cloud-config + template: + metadata: + labels: + app: cloud-config + spec: + containers: + - name: cloud-config + image: selimhorri/cloud-config-ecommerce-boot:0.1.0 + ports: + - containerPort: 9296 + env: + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_BASE_URL + value: "http://zipkin-service.ecommerce:9411/" + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: "http://service-discovery-service.ecommerce:8761/eureka" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: SPRING_CLOUD_CONFIG_SERVER_GIT_URI + value: "https://github.com/Freddyedd21/cloud-config-server" + - name: JAVA_TOOL_OPTIONS + value: "-Deureka.client.service-url.defaultZone=http://service-discovery-service:8761/eureka/" + - name: EUREKA_INSTANCE + value: "service-discovery-service.ecommerce" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + + +--- +apiVersion: v1 +kind: Service +metadata: + name: cloud-config-service + namespace: ecommerce +spec: + selector: + app: cloud-config + ports: + - protocol: TCP + port: 9296 + targetPort: 9296 + type: ClusterIP + diff --git a/k8s/config-maps.yaml b/k8s/config-maps.yaml new file mode 100644 index 000000000..a718b905e --- /dev/null +++ b/k8s/config-maps.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: eureka-config + namespace: ecommerce +data: + EUREKA_SERVER_URL: "http://service-discovery-service:8761/eureka/" + EUREKA_INSTANCE_PREFER_IP: "false" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: zipkin-config + namespace: ecommerce +data: + ZIPKIN_BASE_URL: "http://zipkin-service:9411" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-server-config + namespace: ecommerce +data: + CONFIG_SERVER_URL: "http://cloud-config-service:9296" diff --git a/k8s/favourite-service-deployment.yaml b/k8s/favourite-service-deployment.yaml new file mode 100644 index 000000000..b78ff655f --- /dev/null +++ b/k8s/favourite-service-deployment.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: favourite-service-deployment + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: favourite-service + template: + metadata: + labels: + app: favourite-service + spec: + containers: + - name: favourite-service + image: selimhorri/favourite-service-ecommerce-boot:0.1.0 + ports: + - containerPort: 8800 + env: + - name: SPRING_CLOUD_CONFIG_NAME + value: "favourite-service" + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_BASE_URL + valueFrom: + configMapKeyRef: + name: zipkin-config + key: ZIPKIN_BASE_URL + - name: SPRING_CONFIG_IMPORT + value: "optional:configserver:http://cloud-config-service:9296/" + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296" + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_REGISTER_WITH_EUREKA + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: SPRING_JPA_HIBERNATE_DDL_AUTO + value: "update" + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: "favourite-service-container" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + + +--- +apiVersion: v1 +kind: Service +metadata: + name: favourite-service + namespace: ecommerce +spec: + selector: + app: favourite-service + ports: + - protocol: TCP + port: 8800 + targetPort: 8800 + type: ClusterIP diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 000000000..ecfa56b22 --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ecommerce + labels: + name: ecommerce + environment: local diff --git a/k8s/order-service-deployment.yaml b/k8s/order-service-deployment.yaml new file mode 100644 index 000000000..482ca3637 --- /dev/null +++ b/k8s/order-service-deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: order-service + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: order-service + template: + metadata: + labels: + app: order-service + spec: + containers: + - name: order-service + image: selimhorri/order-service-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8300 + name: http + env: + - name: SPRING_CLOUD_CONFIG_NAME + value: "order-service" + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_BASE_URL + valueFrom: + configMapKeyRef: + name: zipkin-config + key: ZIPKIN_BASE_URL + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: SPRING_CONFIG_IMPORT + value: "optional:configserver:http://cloud-config-service:9296/" + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296" + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: "order-service" + - name: EUREKA_CLIENT_ENABLED + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: RIBBON_EUREKA_ENABLED + value: "true" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + +--- +apiVersion: v1 +kind: Service +metadata: + name: order-service + namespace: ecommerce +spec: + type: ClusterIP + ports: + - port: 8300 + targetPort: 8300 + protocol: TCP + name: http + selector: + app: order-service diff --git a/k8s/payment-service-deployment.yaml b/k8s/payment-service-deployment.yaml new file mode 100644 index 000000000..4c6cfcca2 --- /dev/null +++ b/k8s/payment-service-deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: payment-service + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: payment-service + template: + metadata: + labels: + app: payment-service + spec: + containers: + - name: payment-service + image: selimhorri/payment-service-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8400 + name: http + env: + - name: SPRING_CLOUD_CONFIG_NAME + value: "payment-service" + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_BASE_URL + valueFrom: + configMapKeyRef: + name: zipkin-config + key: ZIPKIN_BASE_URL + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: SPRING_CONFIG_IMPORT + value: "optional:configserver:http://cloud-config-service:9296/" + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296" + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: "payment-service" + - name: EUREKA_CLIENT_ENABLED + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: RIBBON_EUREKA_ENABLED + value: "true" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + +--- +apiVersion: v1 +kind: Service +metadata: + name: payment-service + namespace: ecommerce +spec: + type: ClusterIP + ports: + - port: 8400 + targetPort: 8400 + protocol: TCP + name: http + selector: + app: payment-service diff --git a/k8s/product-service-deployment.yaml b/k8s/product-service-deployment.yaml new file mode 100644 index 000000000..28eaa2882 --- /dev/null +++ b/k8s/product-service-deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: product-service + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: product-service + template: + metadata: + labels: + app: product-service + spec: + containers: + - name: product-service + image: selimhorri/product-service-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8500 + name: http + env: + - name: SPRING_CLOUD_CONFIG_NAME + value: "product-service" + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_BASE_URL + valueFrom: + configMapKeyRef: + name: zipkin-config + key: ZIPKIN_BASE_URL + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: SPRING_CONFIG_IMPORT + value: "optional:configserver:http://cloud-config-service:9296/" + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296" + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: "product-service" + - name: EUREKA_CLIENT_ENABLED + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: RIBBON_EUREKA_ENABLED + value: "true" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + +--- +apiVersion: v1 +kind: Service +metadata: + name: product-service + namespace: ecommerce +spec: + type: ClusterIP + ports: + - port: 8500 + targetPort: 8500 + protocol: TCP + name: http + selector: + app: product-service diff --git a/k8s/proxy-client-deployment.yaml b/k8s/proxy-client-deployment.yaml new file mode 100644 index 000000000..d6ff8e5a1 --- /dev/null +++ b/k8s/proxy-client-deployment.yaml @@ -0,0 +1,108 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: proxy-client + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: proxy-client + template: + metadata: + labels: + app: proxy-client + spec: + initContainers: + - name: wait-for-config + image: busybox:1.36 + command: + - sh + - -c + - 'until nc -z cloud-config-service 9296; do echo "waiting for cloud-config"; sleep 3; done' + - name: wait-for-eureka + image: busybox:1.36 + command: + - sh + - -c + - 'until nc -z service-discovery-service 8761; do echo "waiting for service-discovery"; sleep 3; done' + containers: + - name: proxy-client + image: selimhorri/proxy-client-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8900 + name: http + env: + - name: SPRING_CLOUD_CONFIG_NAME + value: "PROXY-CLIENT" + - name: SPRING_APPLICATION_NAME + value: "PROXY-CLIENT" + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_BASE_URL + valueFrom: + configMapKeyRef: + name: zipkin-config + key: ZIPKIN_BASE_URL + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: SPRING_CONFIG_IMPORT + value: "optional:configserver:http://cloud-config-service:9296/" + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296" + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: "proxy-client" + - name: EUREKA_CLIENT_ENABLED + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: RIBBON_EUREKA_ENABLED + value: "true" + - name: SPRING_CLOUD_CONFIG_FAILFAST + value: "true" + - name: SPRING_CLOUD_CONFIG_RETRY_MAX_ATTEMPTS + value: "12" + - name: SPRING_CLOUD_CONFIG_RETRY_INITIAL_INTERVAL + value: "2000" + - name: SPRING_CLOUD_CONFIG_RETRY_MULTIPLIER + value: "1.5" + - name: SPRING_CLOUD_CONFIG_RETRY_MAX_INTERVAL + value: "10000" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: proxy-client + namespace: ecommerce +spec: + type: ClusterIP + ports: + - port: 8900 + targetPort: 8900 + protocol: TCP + name: http + selector: + app: proxy-client diff --git a/k8s/service-discovery-deployment.yaml b/k8s/service-discovery-deployment.yaml new file mode 100644 index 000000000..c0a6a889a --- /dev/null +++ b/k8s/service-discovery-deployment.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: service-discovery + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: service-discovery + template: + metadata: + labels: + app: service-discovery + spec: + containers: + - name: service-discovery + image: selimhorri/service-discovery-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8761 + name: http + env: + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_ENABLED + value: "false" + - name: SPRING_SLEUTH_ENABLED + value: "false" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: service-discovery-service + namespace: ecommerce +spec: + selector: + app: service-discovery + ports: + - protocol: TCP + port: 8761 + targetPort: 8761 + type: ClusterIP diff --git a/k8s/shipping-service-deployment.yaml b/k8s/shipping-service-deployment.yaml new file mode 100644 index 000000000..9de3f1fdb --- /dev/null +++ b/k8s/shipping-service-deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shipping-service + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: shipping-service + template: + metadata: + labels: + app: shipping-service + spec: + containers: + - name: shipping-service + image: selimhorri/shipping-service-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8600 + name: http + env: + - name: SPRING_CLOUD_CONFIG_NAME + value: "shipping-service" + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_BASE_URL + valueFrom: + configMapKeyRef: + name: zipkin-config + key: ZIPKIN_BASE_URL + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: SPRING_CONFIG_IMPORT + value: "optional:configserver:http://cloud-config-service:9296/" + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296" + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: "shipping-service" + - name: EUREKA_CLIENT_ENABLED + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: RIBBON_EUREKA_ENABLED + value: "true" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + +--- +apiVersion: v1 +kind: Service +metadata: + name: shipping-service + namespace: ecommerce +spec: + type: ClusterIP + ports: + - port: 8600 + targetPort: 8600 + protocol: TCP + name: http + selector: + app: shipping-service diff --git a/k8s/user-service-deployment.yaml b/k8s/user-service-deployment.yaml new file mode 100644 index 000000000..1b3d1d490 --- /dev/null +++ b/k8s/user-service-deployment.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-service + namespace: ecommerce +spec: + progressDeadlineSeconds: 900 + replicas: 1 + selector: + matchLabels: + app: user-service + template: + metadata: + labels: + app: user-service + spec: + containers: + - name: user-service + image: selimhorri/user-service-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8700 + name: http + env: + - name: SPRING_CLOUD_CONFIG_NAME + value: "user-service" + - name: SPRING_PROFILES_ACTIVE + value: "dev" + - name: SPRING_ZIPKIN_BASE_URL + value: "http://zipkin-service.ecommerce:9411/" + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: "http://service-discovery-service.ecommerce:8761/eureka" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + name: eureka-config + key: EUREKA_SERVER_URL + - name: SPRING_APPLICATION_JSON + value: '{"eureka":{"client":{"serviceUrl":{"defaultZone":"http://service-discovery-service:8761/eureka/"}}}}' + - name: SPRING_CONFIG_IMPORT + value: "optional:configserver:http://cloud-config-service.ecommerce:9296" + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296" + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: "user-service" + - name: EUREKA_CLIENT_ENABLED + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: RIBBON_EUREKA_ENABLED + value: "true" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + +--- +apiVersion: v1 +kind: Service +metadata: + name: user-service + namespace: ecommerce +spec: + type: ClusterIP + ports: + - port: 8700 + targetPort: 8700 + protocol: TCP + name: http + selector: + app: user-service diff --git a/k8s/zipkin-deployment.yaml b/k8s/zipkin-deployment.yaml new file mode 100644 index 000000000..375754dc6 --- /dev/null +++ b/k8s/zipkin-deployment.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zipkin + namespace: ecommerce +spec: + replicas: 1 + selector: + matchLabels: + app: zipkin + template: + metadata: + labels: + app: zipkin + spec: + containers: + - name: zipkin + image: openzipkin/zipkin:latest + ports: + - containerPort: 9411 + env: + - name: SPRING_CLOUD_CONFIG_URI + value: "http://cloud-config-service:9296/" + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + value: "http://service-discovery-service:8761/eureka/" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: "http://service-discovery-service:8761/eureka/" + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + value: "http://service-discovery-service:8761/eureka/" + - name: JAVA_TOOL_OPTIONS + value: "-Deureka.client.service-url.defaultZone=http://service-discovery-service:8761/eureka/" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: zipkin-service + namespace: ecommerce +spec: + selector: + app: zipkin + ports: + - protocol: TCP + port: 9411 + targetPort: 9411 + type: ClusterIP diff --git a/load-testing/README.md b/load-testing/README.md new file mode 100644 index 000000000..6a485ae27 --- /dev/null +++ b/load-testing/README.md @@ -0,0 +1,47 @@ +# Load Testing with Locust + +This folder contains a Locust scenario that exercises the key API Gateway user journeys end-to-end. Each virtual user executes the same flow validated by the integration and E2E suites: + +1. Browse the product catalogue and capture a product id. +2. Fetch the product details. +3. Review the favourites list with resolved user/product data. +4. Inspect the shipping summary for embedded order/product payloads. +5. Check the payment status entries. + +## Prerequisites + +- Minikube services running and the port-forward for the API Gateway active (`kubectl port-forward svc/api-gateway-service 18080:8080 -n ecommerce`). +- Python 3.9+ with `pip` available. +- Locust installed (`pip install locust`). + +> The Locust file defaults to `http://127.0.0.1:18080` but you can override the host by exporting `API_GATEWAY_BASE_URL`. + +## Running Locust (interactive UI) + +```powershell +cd load-testing +locust -f locustfile.py +``` + +Open the web UI (http://localhost:8089), set the number of users/spawn rate, and start the test. + +## Running Locust headlessly (CI-friendly) + +```powershell +cd load-testing +locust -f locustfile.py --headless -u 50 -r 5 --run-time 10m --stop-timeout 30 +``` + +- `-u 50`: simulate 50 concurrent virtual users. +- `-r 5`: ramp up 5 users per second. +- `--run-time 10m`: run for 10 minutes. +- `--stop-timeout 30`: give users time to finish their current journey before shutdown. + +After the run completes, Locust prints aggregate stats to the console and writes a CSV report when additional flags (`--csv`) are provided. You can also enable HTML reports: + +```powershell +locust -f locustfile.py --headless -u 50 -r 5 --run-time 10m --csv results --html results.html +``` + +The scenario uses 45-second request timeouts and retries internally on gateway calls, matching the resilience added to the E2E test. +![1762298187130](image/README/1762298187130.png) \ No newline at end of file diff --git a/load-testing/__pycache__/locustfile.cpython-313.pyc b/load-testing/__pycache__/locustfile.cpython-313.pyc new file mode 100644 index 000000000..541870d15 Binary files /dev/null and b/load-testing/__pycache__/locustfile.cpython-313.pyc differ diff --git a/load-testing/image/README/1762298187130.png b/load-testing/image/README/1762298187130.png new file mode 100644 index 000000000..276d711f7 Binary files /dev/null and b/load-testing/image/README/1762298187130.png differ diff --git a/load-testing/locustfile.py b/load-testing/locustfile.py new file mode 100644 index 000000000..8afc5fbd8 --- /dev/null +++ b/load-testing/locustfile.py @@ -0,0 +1,159 @@ +"""Locust scenarios that exercise real API Gateway user journeys. + +Each virtual user completes the same flow that the end-to-end tests cover: +1. Browse the product catalogue. +2. Open the details of the first product returned. +3. Review the enriched favourites list. +4. Inspect the shipping summary. +5. Check payment status for orders. + +Point Locust at the API Gateway base URL (for example http://127.0.0.1:18080) +and keep the Minikube port-forwards active while the load test runs. +""" + +import os +from typing import Any, Dict, Optional + +from locust import HttpUser, SequentialTaskSet, between, task + +_TIMEOOUT_SECONDS = 45 + + +def _extract_collection(payload: Dict[str, Any]) -> Optional[list]: + collection = payload.get("collection") if isinstance(payload, dict) else None + return collection if isinstance(collection, list) else None + + +class MarketplaceJourney(SequentialTaskSet): + """Sequential user flow that mirrors the critical UI journey.""" + + product_id: Optional[int] = None + + def on_start(self) -> None: + self.product_id = None + + @task + def browse_product_catalogue(self) -> None: + with self.client.get( + "/product-service/api/products", + name="GET /product-service/api/products", + timeout=_TIMEOOUT_SECONDS, + catch_response=True, + ) as response: + if not response.ok: + response.failure(f"Unexpected status {response.status_code}") + return + try: + payload = response.json() + except ValueError as exc: + response.failure(f"Invalid JSON: {exc}") + return + + products = _extract_collection(payload) + if not products: + response.failure("Product catalogue response did not contain data") + return + + first_product = products[0] + product_id = first_product.get("productId") + if not isinstance(product_id, int): + response.failure("Product collection missing numeric productId") + return + + self.product_id = product_id + response.success() + + @task + def view_product_details(self) -> None: + if self.product_id is None: + # Previous step already recorded the failure. + return + + with self.client.get( + f"/product-service/api/products/{self.product_id}", + name="GET /product-service/api/products/{id}", + timeout=_TIMEOOUT_SECONDS, + catch_response=True, + ) as response: + if not response.ok: + response.failure(f"Unexpected status {response.status_code}") + return + try: + payload = response.json() + except ValueError as exc: + response.failure(f"Invalid JSON: {exc}") + return + + if payload.get("productId") != self.product_id: + response.failure("Product endpoint did not return the expected id") + return + + response.success() + + @task + def review_favourites(self) -> None: + self._validate_collection_endpoint( + path="/favourite-service/api/favourites", + name="GET /favourite-service/api/favourites", + required_fields=("user", "product"), + ) + + @task + def inspect_shipping_summary(self) -> None: + self._validate_collection_endpoint( + path="/shipping-service/api/shippings", + name="GET /shipping-service/api/shippings", + required_fields=("order", "product"), + ) + + @task + def check_payment_status(self) -> None: + self._validate_collection_endpoint( + path="/payment-service/api/payments", + name="GET /payment-service/api/payments", + required_fields=("paymentStatus", "order"), + ) + + def _validate_collection_endpoint( + self, + *, + path: str, + name: str, + required_fields: tuple[str, ...], + ) -> None: + with self.client.get( + path, + name=name, + timeout=_TIMEOOUT_SECONDS, + catch_response=True, + ) as response: + if not response.ok: + response.failure(f"Unexpected status {response.status_code}") + return + + try: + payload = response.json() + except ValueError as exc: + response.failure(f"Invalid JSON: {exc}") + return + + collection = _extract_collection(payload) + if not collection: + response.failure("Collection response did not contain elements") + return + + first_item = collection[0] + missing = [field for field in required_fields if field not in first_item] + if missing: + response.failure(f"Payload missing fields: {', '.join(missing)}") + return + + response.success() + + +class EcommerceUser(HttpUser): + tasks = [MarketplaceJourney] + wait_time = between(1, 3) + + # Allow overriding via API_GATEWAY_BASE_URL to mirror the integration profile. + host = os.getenv("API_GATEWAY_BASE_URL", "http://127.0.0.1:18080") diff --git a/open-all-minikube-services.ps1 b/open-all-minikube-services.ps1 new file mode 100644 index 000000000..7faefd6d0 --- /dev/null +++ b/open-all-minikube-services.ps1 @@ -0,0 +1,15 @@ +# Script para abrir todos los servicios de Minikube en PowerShell +# Guarda este archivo como open-all-minikube-services.ps1 y ejecútalo en tu terminal + +$services = kubectl get svc --no-headers | ForEach-Object { + $_.Split(" ")[0] +} + +foreach ($service in $services) { + Write-Host "Obteniendo URL de servicio: $service" + $url = minikube service $service --url + if ($url -match "http") { + Write-Host "Abriendo en navegador: $url" + Start-Process $url + } +} diff --git a/order-service/compose.yml b/order-service/compose.yml index 34107eacf..5e33b890f 100644 --- a/order-service/compose.yml +++ b/order-service/compose.yml @@ -1,12 +1,30 @@ + version: '3' services: order-service-container: image: selimhorri/order-service-ecommerce-boot:0.1.0 ports: - - 8300:8300 + - "8300:8300" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=order-service-container + - EUREKA_INSTANCE_NON_SECURE_PORT=8300 + +networks: + microservices_network: + external: true diff --git a/order-service/src/main/resources/application.yml b/order-service/src/main/resources/application.yml index 1713e8409..346990ade 100644 --- a/order-service/src/main/resources/application.yml +++ b/order-service/src/main/resources/application.yml @@ -5,15 +5,20 @@ server: spring: zipkin: - base-url: ${SPRING_ZIPKIN_BASE_URL:http://localhost:9411/} + base-url: ${SPRING_ZIPKIN_BASE_URL:http://zipkin-service.ecommerce:9411/} config: - import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://localhost:9296} + import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://cloud-config-service.ecommerce:9296} application: name: ORDER-SERVICE profiles: active: - dev +eureka: + client: + service-url: + defaultZone: ${EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE:http://service-discovery-service.ecommerce:8761/eureka} + resilience4j: circuitbreaker: instances: diff --git a/order-service/src/test/java/com/selimhorri/app/service/impl/OrderServiceImplTest.java b/order-service/src/test/java/com/selimhorri/app/service/impl/OrderServiceImplTest.java new file mode 100644 index 000000000..13c9e39c6 --- /dev/null +++ b/order-service/src/test/java/com/selimhorri/app/service/impl/OrderServiceImplTest.java @@ -0,0 +1,151 @@ +package com.selimhorri.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.selimhorri.app.domain.Cart; +import com.selimhorri.app.domain.Order; +import com.selimhorri.app.dto.CartDto; +import com.selimhorri.app.dto.OrderDto; +import com.selimhorri.app.exception.wrapper.OrderNotFoundException; +import com.selimhorri.app.repository.OrderRepository; + +@ExtendWith(MockitoExtension.class) +class OrderServiceImplTest { + + @Mock + private OrderRepository orderRepository; + + private OrderServiceImpl orderService; + + private Order order; + private OrderDto orderDto; + + @BeforeEach + void setUp() { + this.orderService = new OrderServiceImpl(this.orderRepository); + + Cart cart = Cart.builder() + .cartId(4) + .userId(9) + .build(); + + this.order = Order.builder() + .orderId(15) + .orderDate(LocalDateTime.of(2024, 2, 12, 14, 30)) + .orderDesc("test-order") + .orderFee(99.95) + .cart(cart) + .build(); + + this.orderDto = OrderDto.builder() + .orderId(this.order.getOrderId()) + .orderDate(this.order.getOrderDate()) + .orderDesc(this.order.getOrderDesc()) + .orderFee(this.order.getOrderFee()) + .cartDto(CartDto.builder() + .cartId(cart.getCartId()) + .userId(cart.getUserId()) + .build()) + .build(); + } + + @Test + void findAllReturnsMappedOrders() { + when(this.orderRepository.findAll()).thenReturn(List.of(this.order)); + + List result = this.orderService.findAll(); + + assertEquals(1, result.size()); + OrderDto dto = result.get(0); + assertEquals(this.order.getOrderId(), dto.getOrderId()); + assertEquals(this.order.getCart().getCartId(), dto.getCartDto().getCartId()); + } + + @Test + void findByIdReturnsOrderWhenPresent() { + when(this.orderRepository.findById(this.order.getOrderId())).thenReturn(Optional.of(this.order)); + + OrderDto result = this.orderService.findById(this.order.getOrderId()); + + assertEquals(this.order.getOrderId(), result.getOrderId()); + assertEquals(this.order.getOrderFee(), result.getOrderFee()); + } + + @Test + void findByIdThrowsWhenMissing() { + when(this.orderRepository.findById(this.order.getOrderId())).thenReturn(Optional.empty()); + + assertThrows(OrderNotFoundException.class, () -> this.orderService.findById(this.order.getOrderId())); + } + + @Test + void savePersistsMappedOrder() { + when(this.orderRepository.save(any(Order.class))).thenReturn(this.order); + + OrderDto result = this.orderService.save(this.orderDto); + + assertEquals(this.order.getOrderId(), result.getOrderId()); + assertEquals(this.order.getOrderDesc(), result.getOrderDesc()); + + ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(Order.class); + verify(this.orderRepository).save(orderCaptor.capture()); + Order saved = orderCaptor.getValue(); + assertEquals(this.orderDto.getOrderId(), saved.getOrderId()); + assertEquals(this.orderDto.getCartDto().getCartId(), saved.getCart().getCartId()); + } + + @Test + void updatePersistsMappedOrder() { + Order updated = Order.builder() + .orderId(this.order.getOrderId()) + .orderDate(this.order.getOrderDate()) + .orderDesc(this.order.getOrderDesc()) + .orderFee(120.0) + .cart(this.order.getCart()) + .build(); + + when(this.orderRepository.save(any(Order.class))).thenReturn(updated); + + OrderDto updatedDto = OrderDto.builder() + .orderId(this.orderDto.getOrderId()) + .orderDate(this.orderDto.getOrderDate()) + .orderDesc(this.orderDto.getOrderDesc()) + .orderFee(120.0) + .cartDto(this.orderDto.getCartDto()) + .build(); + + OrderDto result = this.orderService.update(updatedDto); + + assertEquals(updated.getOrderFee(), result.getOrderFee()); + } + + @Test + void deleteByIdRemovesMappedEntity() { + when(this.orderRepository.findById(this.order.getOrderId())).thenReturn(Optional.of(this.order)); + doNothing().when(this.orderRepository).delete(any(Order.class)); + + this.orderService.deleteById(this.order.getOrderId()); + + ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(Order.class); + verify(this.orderRepository).delete(orderCaptor.capture()); + Order deleted = orderCaptor.getValue(); + assertEquals(this.order.getOrderId(), deleted.getOrderId()); + assertEquals(this.order.getCart().getCartId(), deleted.getCart().getCartId()); + } +} diff --git a/payment-service/compose.yml b/payment-service/compose.yml index 4c3539d19..7ae12c828 100644 --- a/payment-service/compose.yml +++ b/payment-service/compose.yml @@ -1,12 +1,30 @@ + version: '3' services: payment-service-container: image: selimhorri/payment-service-ecommerce-boot:0.1.0 ports: - - 8400:8400 + - "8400:8400" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=payment-service-container + - EUREKA_INSTANCE_NON_SECURE_PORT=8400 + +networks: + microservices_network: + external: true diff --git a/payment-service/src/main/resources/application.yml b/payment-service/src/main/resources/application.yml index f595a0bf3..109f027b3 100644 --- a/payment-service/src/main/resources/application.yml +++ b/payment-service/src/main/resources/application.yml @@ -5,15 +5,20 @@ server: spring: zipkin: - base-url: ${SPRING_ZIPKIN_BASE_URL:http://localhost:9411/} + base-url: ${SPRING_ZIPKIN_BASE_URL:http://zipkin-service.ecommerce:9411/} config: - import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://localhost:9296} + import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://cloud-config-service.ecommerce:9296} application: name: PAYMENT-SERVICE profiles: active: - dev +eureka: + client: + service-url: + defaultZone: ${EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE:http://service-discovery-service.ecommerce:8761/eureka} + resilience4j: circuitbreaker: instances: diff --git a/payment-service/src/test/java/com/selimhorri/app/service/impl/PaymentServiceImplTest.java b/payment-service/src/test/java/com/selimhorri/app/service/impl/PaymentServiceImplTest.java new file mode 100644 index 000000000..2eb0a9222 --- /dev/null +++ b/payment-service/src/test/java/com/selimhorri/app/service/impl/PaymentServiceImplTest.java @@ -0,0 +1,169 @@ +package com.selimhorri.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.client.RestTemplate; + +import com.selimhorri.app.constant.AppConstant; +import com.selimhorri.app.domain.Payment; +import com.selimhorri.app.domain.PaymentStatus; +import com.selimhorri.app.dto.OrderDto; +import com.selimhorri.app.dto.PaymentDto; +import com.selimhorri.app.exception.wrapper.PaymentNotFoundException; +import com.selimhorri.app.repository.PaymentRepository; + +@ExtendWith(MockitoExtension.class) +class PaymentServiceImplTest { + + @Mock + private PaymentRepository paymentRepository; + + @Mock + private RestTemplate restTemplate; + + private PaymentServiceImpl paymentService; + + private Payment payment; + private OrderDto orderDto; + + @BeforeEach + void setUp() { + this.paymentService = new PaymentServiceImpl(this.paymentRepository, this.restTemplate); + + this.payment = Payment.builder() + .paymentId(42) + .orderId(1001) + .isPayed(Boolean.TRUE) + .paymentStatus(PaymentStatus.COMPLETED) + .build(); + + this.orderDto = OrderDto.builder() + .orderId(this.payment.getOrderId()) + .orderDesc("sample-order") + .orderFee(19.99) + .build(); + } + + @Test + void findAllReturnsPaymentsWithEnrichedOrder() { + when(this.paymentRepository.findAll()).thenReturn(List.of(this.payment)); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.ORDER_SERVICE_API_URL + "/" + this.payment.getOrderId(), + OrderDto.class)) + .thenReturn(this.orderDto); + + List result = this.paymentService.findAll(); + + assertEquals(1, result.size()); + PaymentDto dto = result.get(0); + assertEquals(this.payment.getPaymentId(), dto.getPaymentId()); + assertEquals(this.payment.getIsPayed(), dto.getIsPayed()); + assertEquals(this.payment.getPaymentStatus(), dto.getPaymentStatus()); + assertSame(this.orderDto, dto.getOrderDto()); + verify(this.restTemplate) + .getForObject(AppConstant.DiscoveredDomainsApi.ORDER_SERVICE_API_URL + "/" + this.payment.getOrderId(), + OrderDto.class); + } + + @Test + void findByIdReturnsPaymentWhenPresent() { + when(this.paymentRepository.findById(this.payment.getPaymentId())).thenReturn(Optional.of(this.payment)); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.ORDER_SERVICE_API_URL + "/" + this.payment.getOrderId(), + OrderDto.class)) + .thenReturn(this.orderDto); + + PaymentDto result = this.paymentService.findById(this.payment.getPaymentId()); + + assertEquals(this.payment.getPaymentId(), result.getPaymentId()); + assertEquals(this.payment.getIsPayed(), result.getIsPayed()); + assertSame(this.orderDto, result.getOrderDto()); + } + + @Test + void findByIdThrowsWhenNotFound() { + when(this.paymentRepository.findById(this.payment.getPaymentId())).thenReturn(Optional.empty()); + + assertThrows(PaymentNotFoundException.class, () -> this.paymentService.findById(this.payment.getPaymentId())); + } + + @Test + void savePersistsMappedPayment() { + PaymentDto input = PaymentDto.builder() + .paymentId(this.payment.getPaymentId()) + .isPayed(this.payment.getIsPayed()) + .paymentStatus(this.payment.getPaymentStatus()) + .orderDto(OrderDto.builder().orderId(this.payment.getOrderId()).build()) + .build(); + + when(this.paymentRepository.save(any(Payment.class))).thenReturn(this.payment); + + PaymentDto result = this.paymentService.save(input); + + assertEquals(this.payment.getPaymentId(), result.getPaymentId()); + assertEquals(this.payment.getPaymentStatus(), result.getPaymentStatus()); + + ArgumentCaptor paymentCaptor = ArgumentCaptor.forClass(Payment.class); + verify(this.paymentRepository).save(paymentCaptor.capture()); + Payment saved = paymentCaptor.getValue(); + assertEquals(input.getPaymentId(), saved.getPaymentId()); + assertEquals(input.getOrderDto().getOrderId(), saved.getOrderId()); + assertEquals(input.getIsPayed(), saved.getIsPayed()); + } + + @Test + void updatePersistsMappedPayment() { + PaymentDto input = PaymentDto.builder() + .paymentId(this.payment.getPaymentId()) + .isPayed(Boolean.FALSE) + .paymentStatus(PaymentStatus.IN_PROGRESS) + .orderDto(OrderDto.builder().orderId(this.payment.getOrderId()).build()) + .build(); + + Payment updated = Payment.builder() + .paymentId(input.getPaymentId()) + .orderId(input.getOrderDto().getOrderId()) + .isPayed(input.getIsPayed()) + .paymentStatus(input.getPaymentStatus()) + .build(); + + when(this.paymentRepository.save(any(Payment.class))).thenReturn(updated); + + PaymentDto result = this.paymentService.update(input); + + assertEquals(updated.getIsPayed(), result.getIsPayed()); + assertEquals(updated.getPaymentStatus(), result.getPaymentStatus()); + + ArgumentCaptor paymentCaptor = ArgumentCaptor.forClass(Payment.class); + verify(this.paymentRepository).save(paymentCaptor.capture()); + Payment saved = paymentCaptor.getValue(); + assertEquals(input.getPaymentId(), saved.getPaymentId()); + assertEquals(input.getOrderDto().getOrderId(), saved.getOrderId()); + assertEquals(input.getIsPayed(), saved.getIsPayed()); + } + + @Test + void deleteByIdDelegatesToRepository() { + doNothing().when(this.paymentRepository).deleteById(eq(this.payment.getPaymentId())); + + this.paymentService.deleteById(this.payment.getPaymentId()); + + verify(this.paymentRepository).deleteById(this.payment.getPaymentId()); + } +} diff --git a/pom.xml b/pom.xml index 832247436..650ac0c61 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,16 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + **/MinikubeServiceCommunicationTest.java + + + org.springframework.boot spring-boot-maven-plugin @@ -121,6 +131,52 @@ ${project.artifactId}-v${project.version} + + + + integration + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.2 + + + + integration-test + verify + + + + **/MinikubeServiceCommunicationTest.java + **/*E2ETest.java + + + http://127.0.0.1:18500/product-service + http://127.0.0.1:18700/user-service + http://127.0.0.1:18300/order-service + http://127.0.0.1:18800/favourite-service + http://127.0.0.1:18600/shipping-service + http://127.0.0.1:18400/payment-service + http://127.0.0.1:18080 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + + + + + + diff --git a/product-service/Dockerfile b/product-service/Dockerfile index c052995cf..af21ff359 100644 --- a/product-service/Dockerfile +++ b/product-service/Dockerfile @@ -1,11 +1,9 @@ -FROM openjdk:11 +FROM eclipse-temurin:11-jre ARG PROJECT_VERSION=0.1.0 -RUN mkdir -p /home/app -WORKDIR /home/app ENV SPRING_PROFILES_ACTIVE dev -COPY product-service/ . -ADD product-service/target/product-service-v${PROJECT_VERSION}.jar product-service.jar +WORKDIR /home/app +COPY ./product-service/target/product-service-v${PROJECT_VERSION}.jar ./product-service.jar EXPOSE 8500 ENTRYPOINT ["java", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "-jar", "product-service.jar"] diff --git a/product-service/compose.yml b/product-service/compose.yml index 618eb819a..2a358ce1b 100644 --- a/product-service/compose.yml +++ b/product-service/compose.yml @@ -1,12 +1,29 @@ + version: '3' services: product-service-container: image: selimhorri/product-service-ecommerce-boot:0.1.0 ports: - - 8500:8500 + - "8500:8500" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=product-service-container + +networks: + microservices_network: + external: true diff --git a/product-service/src/main/resources/application.yml b/product-service/src/main/resources/application.yml index 75c2b0a46..b65119bf9 100644 --- a/product-service/src/main/resources/application.yml +++ b/product-service/src/main/resources/application.yml @@ -5,15 +5,20 @@ server: spring: zipkin: - base-url: ${SPRING_ZIPKIN_BASE_URL:http://localhost:9411/} + base-url: ${SPRING_ZIPKIN_BASE_URL:http://zipkin-service.ecommerce:9411/} config: - import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://localhost:9296} + import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://cloud-config-service.ecommerce:9296} application: name: PRODUCT-SERVICE profiles: active: - dev +eureka: + client: + service-url: + defaultZone: ${EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE:http://service-discovery-service.ecommerce:8761/eureka} + resilience4j: circuitbreaker: instances: diff --git a/product-service/src/test/java/com/selimhorri/app/service/impl/ProductServiceImplTest.java b/product-service/src/test/java/com/selimhorri/app/service/impl/ProductServiceImplTest.java new file mode 100644 index 000000000..990a85e40 --- /dev/null +++ b/product-service/src/test/java/com/selimhorri/app/service/impl/ProductServiceImplTest.java @@ -0,0 +1,163 @@ +package com.selimhorri.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.selimhorri.app.domain.Category; +import com.selimhorri.app.domain.Product; +import com.selimhorri.app.dto.CategoryDto; +import com.selimhorri.app.dto.ProductDto; +import com.selimhorri.app.exception.wrapper.ProductNotFoundException; +import com.selimhorri.app.repository.ProductRepository; + +@ExtendWith(MockitoExtension.class) +class ProductServiceImplTest { + + @Mock + private ProductRepository productRepository; + + private ProductServiceImpl productService; + + private Product product; + private ProductDto productDto; + + @BeforeEach + void setUp() { + this.productService = new ProductServiceImpl(this.productRepository); + + Category category = Category.builder() + .categoryId(2) + .categoryTitle("Peripherals") + .imageUrl("/images/peripherals") + .build(); + + this.product = Product.builder() + .productId(10) + .productTitle("Mouse") + .imageUrl("/images/mouse") + .sku("SKU-001") + .priceUnit(25.5) + .quantity(100) + .category(category) + .build(); + + this.productDto = ProductDto.builder() + .productId(this.product.getProductId()) + .productTitle(this.product.getProductTitle()) + .imageUrl(this.product.getImageUrl()) + .sku(this.product.getSku()) + .priceUnit(this.product.getPriceUnit()) + .quantity(this.product.getQuantity()) + .categoryDto(CategoryDto.builder() + .categoryId(category.getCategoryId()) + .categoryTitle(category.getCategoryTitle()) + .imageUrl(category.getImageUrl()) + .build()) + .build(); + } + + @Test + void findAllReturnsMappedProducts() { + when(this.productRepository.findAll()).thenReturn(List.of(this.product)); + + List result = this.productService.findAll(); + + assertEquals(1, result.size()); + ProductDto dto = result.get(0); + assertEquals(this.product.getProductId(), dto.getProductId()); + assertEquals(this.product.getCategory().getCategoryId(), dto.getCategoryDto().getCategoryId()); + } + + @Test + void findByIdReturnsProductWhenPresent() { + when(this.productRepository.findById(this.product.getProductId())).thenReturn(Optional.of(this.product)); + + ProductDto result = this.productService.findById(this.product.getProductId()); + + assertEquals(this.product.getProductId(), result.getProductId()); + assertEquals(this.product.getSku(), result.getSku()); + } + + @Test + void findByIdThrowsWhenMissing() { + when(this.productRepository.findById(this.product.getProductId())).thenReturn(Optional.empty()); + + assertThrows(ProductNotFoundException.class, () -> this.productService.findById(this.product.getProductId())); + } + + @Test + void savePersistsMappedProduct() { + when(this.productRepository.save(any(Product.class))).thenReturn(this.product); + + ProductDto result = this.productService.save(this.productDto); + + assertEquals(this.product.getProductId(), result.getProductId()); + assertEquals(this.product.getSku(), result.getSku()); + + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(Product.class); + verify(this.productRepository).save(productCaptor.capture()); + Product saved = productCaptor.getValue(); + assertEquals(this.productDto.getProductId(), saved.getProductId()); + assertEquals(this.productDto.getCategoryDto().getCategoryId(), saved.getCategory().getCategoryId()); + } + + @Test + void updatePersistsMappedProduct() { + Product updated = Product.builder() + .productId(this.product.getProductId()) + .productTitle(this.product.getProductTitle()) + .imageUrl(this.product.getImageUrl()) + .sku(this.product.getSku()) + .priceUnit(30.0) + .quantity(this.product.getQuantity()) + .category(this.product.getCategory()) + .build(); + + when(this.productRepository.save(any(Product.class))).thenReturn(updated); + + ProductDto updatedDto = ProductDto.builder() + .productId(this.productDto.getProductId()) + .productTitle(this.productDto.getProductTitle()) + .imageUrl(this.productDto.getImageUrl()) + .sku(this.productDto.getSku()) + .priceUnit(30.0) + .quantity(this.productDto.getQuantity()) + .categoryDto(this.productDto.getCategoryDto()) + .build(); + + ProductDto result = this.productService.update(updatedDto); + + assertEquals(updated.getPriceUnit(), result.getPriceUnit()); + } + + @Test + void deleteByIdRemovesMappedEntity() { + when(this.productRepository.findById(this.product.getProductId())).thenReturn(Optional.of(this.product)); + doNothing().when(this.productRepository).delete(any(Product.class)); + + this.productService.deleteById(this.product.getProductId()); + + verify(this.productRepository, times(1)).findById(this.product.getProductId()); + + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(Product.class); + verify(this.productRepository).delete(productCaptor.capture()); + Product deleted = productCaptor.getValue(); + assertEquals(this.product.getProductId(), deleted.getProductId()); + assertEquals(this.product.getSku(), deleted.getSku()); + } +} diff --git a/proxy-client/compose.yml b/proxy-client/compose.yml index 6255f4c12..3379f23c0 100644 --- a/proxy-client/compose.yml +++ b/proxy-client/compose.yml @@ -1,12 +1,29 @@ + version: '3' services: - auth-service-container: + proxy-client-container: image: selimhorri/proxy-client-ecommerce-boot:0.1.0 ports: - - 8900:8900 + - "8900:8900" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=proxy-client-container + +networks: + microservices_network: + external: true diff --git a/service-discovery/compose.yml b/service-discovery/compose.yml index 21226ed39..de2ea9bdd 100644 --- a/service-discovery/compose.yml +++ b/service-discovery/compose.yml @@ -1,12 +1,21 @@ + version: '3' services: service-discovery-container: image: selimhorri/service-discovery-ecommerce-boot:0.1.0 ports: - - 8761:8761 + - "8761:8761" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + +networks: + microservices_network: + external: true diff --git a/shipping-service/compose.yml b/shipping-service/compose.yml index 81878a33a..a800c42fb 100644 --- a/shipping-service/compose.yml +++ b/shipping-service/compose.yml @@ -1,12 +1,29 @@ + version: '3' services: shipping-service-container: image: selimhorri/shipping-service-ecommerce-boot:0.1.0 ports: - - 8600:8600 + - "8600:8600" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=shipping-service-container + +networks: + microservices_network: + external: true diff --git a/shipping-service/src/main/resources/application.yml b/shipping-service/src/main/resources/application.yml index a7230f2ae..7338fa3b1 100644 --- a/shipping-service/src/main/resources/application.yml +++ b/shipping-service/src/main/resources/application.yml @@ -5,15 +5,20 @@ server: spring: zipkin: - base-url: ${SPRING_ZIPKIN_BASE_URL:http://localhost:9411/} + base-url: ${SPRING_ZIPKIN_BASE_URL:http://zipkin-service.ecommerce:9411/} config: - import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://localhost:9296} + import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://cloud-config-service.ecommerce:9296} application: name: SHIPPING-SERVICE profiles: active: - dev +eureka: + client: + service-url: + defaultZone: ${EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE:http://service-discovery-service.ecommerce:8761/eureka} + resilience4j: circuitbreaker: instances: diff --git a/shipping-service/src/test/java/com/selimhorri/app/service/impl/OrderItemServiceImplTest.java b/shipping-service/src/test/java/com/selimhorri/app/service/impl/OrderItemServiceImplTest.java new file mode 100644 index 000000000..4d418c6a3 --- /dev/null +++ b/shipping-service/src/test/java/com/selimhorri/app/service/impl/OrderItemServiceImplTest.java @@ -0,0 +1,178 @@ +package com.selimhorri.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.client.RestTemplate; + +import com.selimhorri.app.constant.AppConstant; +import com.selimhorri.app.domain.OrderItem; +import com.selimhorri.app.domain.id.OrderItemId; +import com.selimhorri.app.dto.OrderDto; +import com.selimhorri.app.dto.OrderItemDto; +import com.selimhorri.app.dto.ProductDto; +import com.selimhorri.app.exception.wrapper.OrderItemNotFoundException; +import com.selimhorri.app.repository.OrderItemRepository; + +@ExtendWith(MockitoExtension.class) +class OrderItemServiceImplTest { + + @Mock + private OrderItemRepository orderItemRepository; + + @Mock + private RestTemplate restTemplate; + + private OrderItemServiceImpl orderItemService; + + private OrderItem orderItem; + private OrderItemId orderItemId; + private ProductDto productDto; + private OrderDto orderDto; + + @BeforeEach + void setUp() { + this.orderItemService = new OrderItemServiceImpl(this.orderItemRepository, this.restTemplate); + + this.orderItem = OrderItem.builder() + .productId(21) + .orderId(31) + .orderedQuantity(2) + .build(); + this.orderItemId = new OrderItemId(this.orderItem.getProductId(), this.orderItem.getOrderId()); + + this.productDto = ProductDto.builder() + .productId(this.orderItem.getProductId()) + .productTitle("Laptop") + .build(); + this.orderDto = OrderDto.builder() + .orderId(this.orderItem.getOrderId()) + .orderDesc("shipping-order") + .build(); + } + + @Test + void findAllReturnsOrderItemsWithRemoteData() { + when(this.orderItemRepository.findAll()).thenReturn(List.of(this.orderItem)); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.PRODUCT_SERVICE_API_URL + "/" + this.orderItem.getProductId(), + ProductDto.class)) + .thenReturn(this.productDto); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.ORDER_SERVICE_API_URL + "/" + this.orderItem.getOrderId(), + OrderDto.class)) + .thenReturn(this.orderDto); + + List result = this.orderItemService.findAll(); + + assertEquals(1, result.size()); + OrderItemDto dto = result.get(0); + assertEquals(this.orderItem.getProductId(), dto.getProductId()); + assertEquals(this.orderItem.getOrderId(), dto.getOrderId()); + assertSame(this.productDto, dto.getProductDto()); + assertSame(this.orderDto, dto.getOrderDto()); + verify(this.restTemplate).getForObject( + AppConstant.DiscoveredDomainsApi.PRODUCT_SERVICE_API_URL + "/" + this.orderItem.getProductId(), + ProductDto.class); + verify(this.restTemplate).getForObject( + AppConstant.DiscoveredDomainsApi.ORDER_SERVICE_API_URL + "/" + this.orderItem.getOrderId(), + OrderDto.class); + } + + @Test + void findByIdReturnsOrderItemWhenPresent() { + when(this.orderItemRepository.findById(null)).thenReturn(Optional.of(this.orderItem)); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.PRODUCT_SERVICE_API_URL + "/" + this.orderItem.getProductId(), + ProductDto.class)) + .thenReturn(this.productDto); + when(this.restTemplate.getForObject( + AppConstant.DiscoveredDomainsApi.ORDER_SERVICE_API_URL + "/" + this.orderItem.getOrderId(), + OrderDto.class)) + .thenReturn(this.orderDto); + + OrderItemDto result = this.orderItemService.findById(this.orderItemId); + + assertEquals(this.orderItem.getProductId(), result.getProductId()); + assertEquals(this.orderItem.getOrderId(), result.getOrderId()); + assertSame(this.productDto, result.getProductDto()); + assertSame(this.orderDto, result.getOrderDto()); + } + + @Test + void findByIdThrowsWhenMissing() { + when(this.orderItemRepository.findById(null)).thenReturn(Optional.empty()); + + assertThrows(OrderItemNotFoundException.class, () -> this.orderItemService.findById(this.orderItemId)); + } + + @Test + void savePersistsMappedOrderItem() { + OrderItemDto input = OrderItemDto.builder() + .productId(this.orderItem.getProductId()) + .orderId(this.orderItem.getOrderId()) + .orderedQuantity(this.orderItem.getOrderedQuantity()) + .productDto(ProductDto.builder().productId(this.orderItem.getProductId()).build()) + .orderDto(OrderDto.builder().orderId(this.orderItem.getOrderId()).build()) + .build(); + + when(this.orderItemRepository.save(any(OrderItem.class))).thenReturn(this.orderItem); + + OrderItemDto result = this.orderItemService.save(input); + + assertEquals(this.orderItem.getProductId(), result.getProductId()); + assertEquals(this.orderItem.getOrderId(), result.getOrderId()); + + ArgumentCaptor orderItemCaptor = ArgumentCaptor.forClass(OrderItem.class); + verify(this.orderItemRepository).save(orderItemCaptor.capture()); + OrderItem saved = orderItemCaptor.getValue(); + assertEquals(input.getProductId(), saved.getProductId()); + assertEquals(input.getOrderId(), saved.getOrderId()); + assertEquals(input.getOrderedQuantity(), saved.getOrderedQuantity()); + } + + @Test + void updatePersistsMappedOrderItem() { + OrderItem updated = OrderItem.builder() + .productId(this.orderItem.getProductId()) + .orderId(this.orderItem.getOrderId()) + .orderedQuantity(5) + .build(); + + when(this.orderItemRepository.save(any(OrderItem.class))).thenReturn(updated); + + OrderItemDto result = this.orderItemService.update(OrderItemDto.builder() + .productId(updated.getProductId()) + .orderId(updated.getOrderId()) + .orderedQuantity(updated.getOrderedQuantity()) + .productDto(ProductDto.builder().productId(updated.getProductId()).build()) + .orderDto(OrderDto.builder().orderId(updated.getOrderId()).build()) + .build()); + + assertEquals(updated.getOrderedQuantity(), result.getOrderedQuantity()); + } + + @Test + void deleteByIdDelegatesToRepository() { + doNothing().when(this.orderItemRepository).deleteById(eq(this.orderItemId)); + + this.orderItemService.deleteById(this.orderItemId); + + verify(this.orderItemRepository).deleteById(this.orderItemId); + } +} diff --git a/src/test/java/com/selimhorri/app/e2e/UserJourneyE2ETest.java b/src/test/java/com/selimhorri/app/e2e/UserJourneyE2ETest.java new file mode 100644 index 000000000..9def6c215 --- /dev/null +++ b/src/test/java/com/selimhorri/app/e2e/UserJourneyE2ETest.java @@ -0,0 +1,174 @@ +package com.selimhorri.app.e2e; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * Exercises user-facing journeys end-to-end through the API Gateway while the + * microservices run inside Minikube and are exposed locally via port-forwarding. + * + * Each test simulates a complete user flow and asserts on meaningful business + * data rather than infrastructure-only signals. + */ +public class UserJourneyE2ETest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final RestTemplate REST_TEMPLATE = createRestTemplate(); + private static final int MAX_GATEWAY_ATTEMPTS = 3; + private static final Duration GATEWAY_RETRY_DELAY = Duration.ofSeconds(5); + private static final String GATEWAY_BASE_URL = resolveGatewayBaseUrl(); + + @Test + void userCanBrowseProductCatalogue() throws Exception { + ArrayNode products = fetchCollection("/product-service/api/products"); + assertTrue(products.size() > 0, "Expected product catalogue to contain items"); + JsonNode sample = products.get(0); + assertTrue(sample.path("productId").asInt() > 0, "Product must expose an id"); + assertTrue(hasText(sample.path("productTitle").asText(null)), "Product must expose a title"); + } + + @Test + void userCanOpenProductDetailsFromCatalogue() throws Exception { + ArrayNode products = fetchCollection("/product-service/api/products"); + JsonNode first = products.get(0); + int productId = first.path("productId").asInt(); + JsonNode product = fetchObject("/product-service/api/products/" + productId); + assertEquals(productId, product.path("productId").asInt(), "Product endpoint must return requested id"); + assertTrue(hasText(product.path("productTitle").asText(null)), "Product details should include a title"); + } + + @Test + void userCanReviewFavouriteListWithResolvedDetails() throws Exception { + ArrayNode favourites = fetchCollection("/favourite-service/api/favourites"); + assertTrue(favourites.size() > 0, "Expected at least one favourite item"); + for (JsonNode favourite : favourites) { + JsonNode userNode = favourite.get("user"); + assertNotNull(userNode, "Favourite entry must include resolved user data"); + assertTrue(hasText(userNode.path("firstName").asText(null)), "Resolved user should include firstName"); + + JsonNode productNode = favourite.get("product"); + assertNotNull(productNode, "Favourite entry must include resolved product data"); + assertTrue(hasText(productNode.path("productTitle").asText(null)), "Resolved product should include productTitle"); + } + } + + @Test + void userCanInspectShippingSummaryForOrders() throws Exception { + ArrayNode shippings = fetchCollection("/shipping-service/api/shippings"); + assertTrue(shippings.size() > 0, "Expected at least one shipping entry"); + for (JsonNode shipping : shippings) { + JsonNode orderNode = shipping.get("order"); + assertNotNull(orderNode, "Shipping entry must embed order data"); + assertTrue(orderNode.path("orderId").asInt() > 0, "Embedded order should expose orderId"); + assertTrue(hasText(orderNode.path("orderDesc").asText(null)), "Embedded order should expose orderDesc"); + + JsonNode productNode = shipping.get("product"); + assertNotNull(productNode, "Shipping entry must embed product data"); + assertTrue(productNode.path("productId").asInt() > 0, "Embedded product should expose productId"); + assertTrue(hasText(productNode.path("productTitle").asText(null)), "Embedded product should expose productTitle"); + } + } + + @Test + void userCanCheckPaymentStatusForOrders() throws Exception { + ArrayNode payments = fetchCollection("/payment-service/api/payments"); + assertTrue(payments.size() > 0, "Expected at least one payment record"); + for (JsonNode payment : payments) { + assertTrue(hasText(payment.path("paymentStatus").asText(null)), "Payment must expose its status"); + JsonNode orderNode = payment.get("order"); + assertNotNull(orderNode, "Payment should embed the associated order"); + assertTrue(orderNode.path("orderId").asInt() > 0, "Embedded order should expose orderId"); + } + } + + private static ArrayNode fetchCollection(String relativePath) throws Exception { + ResponseEntity response = executeGatewayGet(relativePath); + assertEquals(HttpStatus.OK, response.getStatusCode(), () -> "Expected 200 from " + relativePath); + String body = response.getBody(); + assertTrue(hasText(body), "Response body must not be empty"); + JsonNode root = OBJECT_MAPPER.readTree(body); + JsonNode collectionNode = root.get("collection"); + assertNotNull(collectionNode, "Response must include a 'collection' field"); + assertTrue(collectionNode.isArray(), "'collection' field must be an array"); + return (ArrayNode) collectionNode; + } + + private static JsonNode fetchObject(String relativePath) throws Exception { + ResponseEntity response = executeGatewayGet(relativePath); + assertEquals(HttpStatus.OK, response.getStatusCode(), () -> "Expected 200 from " + relativePath); + String body = response.getBody(); + assertTrue(hasText(body), "Response body must not be empty"); + JsonNode node = OBJECT_MAPPER.readTree(body); + assertTrue(node.isObject(), "Expected the response to be a JSON object"); + return node; + } + + private static RestTemplate createRestTemplate() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout((int) Duration.ofSeconds(30).toMillis()); + factory.setReadTimeout((int) Duration.ofSeconds(45).toMillis()); + return new RestTemplate(factory); + } + + // Retries help mask the occasional pause while port-forwarded gateway routes warm up. + private static ResponseEntity executeGatewayGet(String relativePath) throws Exception { + ResourceAccessException lastFailure = null; + for (int attempt = 1; attempt <= MAX_GATEWAY_ATTEMPTS; attempt++) { + try { + return REST_TEMPLATE.getForEntity(buildUrl(relativePath), String.class); + } catch (ResourceAccessException ex) { + lastFailure = ex; + if (attempt == MAX_GATEWAY_ATTEMPTS) { + break; + } + Thread.sleep(GATEWAY_RETRY_DELAY.toMillis()); + } + } + throw lastFailure; + } + + private static String resolveGatewayBaseUrl() { + String candidate = System.getProperty("API_GATEWAY_BASE_URL"); + if (!hasText(candidate)) { + candidate = System.getenv("API_GATEWAY_BASE_URL"); + } + if (!hasText(candidate)) { + candidate = "http://api-gateway-service.ecommerce.svc.cluster.local:8080"; + } + return stripTrailingSlash(candidate); + } + + private static String buildUrl(String relativePath) { + String cleanPath = relativePath.startsWith("/") ? relativePath : "/" + relativePath; + return GATEWAY_BASE_URL + cleanPath; + } + + private static String stripTrailingSlash(String value) { + if (value == null) { + return null; + } + int end = value.length(); + while (end > 0 && value.charAt(end - 1) == '/') { + end--; + } + return value.substring(0, end); + } + + private static boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } +} diff --git a/src/test/java/com/selimhorri/app/integration/MinikubeServiceCommunicationTest.java b/src/test/java/com/selimhorri/app/integration/MinikubeServiceCommunicationTest.java new file mode 100644 index 000000000..fe778421e --- /dev/null +++ b/src/test/java/com/selimhorri/app/integration/MinikubeServiceCommunicationTest.java @@ -0,0 +1,305 @@ +package com.selimhorri.app.integration; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * Verifies that each Minikube-hosted microservice enriches its payload with data obtained from other services. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MinikubeServiceCommunicationTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final RestTemplate REST_TEMPLATE = createRestTemplate(); + private static final int MAX_SERVICE_REACHABILITY_ATTEMPTS = 5; + private static final Duration SERVICE_RETRY_DELAY = Duration.ofSeconds(5); + + @Test + @Order(4) + void favouriteServiceShouldResolveUserDetails() throws Exception { + assertServiceReachable(ServiceTarget.FAVOURITE); + + ArrayNode favourites = fetchCollection(ServiceTarget.FAVOURITE, "/api/favourites"); + for (JsonNode favourite : favourites) { + JsonNode userNode = favourite.get("user"); + assertNotNull(userNode, "Expected favourite entry to include resolved user data"); + assertTrue(userNode.isObject(), "Expected user payload to be a JSON object"); + assertEquals( + favourite.path("userId").asInt(), + userNode.path("userId").asInt(), + "Favourite entry must expose the same user id received from user-service"); + assertTrue( + hasText(userNode.path("firstName").asText(null)), + "User payload should include the firstName coming from user-service"); + } + } + + @Test + @Order(5) + void favouriteServiceShouldResolveProductDetails() throws Exception { + assertServiceReachable(ServiceTarget.FAVOURITE); + + ArrayNode favourites = fetchCollection(ServiceTarget.FAVOURITE, "/api/favourites"); + for (JsonNode favourite : favourites) { + JsonNode productNode = favourite.get("product"); + assertNotNull(productNode, "Expected favourite entry to include resolved product data"); + assertTrue(productNode.isObject(), "Expected product payload to be a JSON object"); + assertEquals( + favourite.path("productId").asInt(), + productNode.path("productId").asInt(), + "Favourite entry must expose the same product id received from product-service"); + assertTrue( + hasText(productNode.path("productTitle").asText(null)), + "Product payload should include the productTitle coming from product-service"); + } + } + + @Test + @Order(6) + void shippingServiceShouldResolveProductDetails() throws Exception { + assertServiceReachable(ServiceTarget.SHIPPING); + + ArrayNode orderItems = fetchCollection(ServiceTarget.SHIPPING, "/api/shippings"); + for (JsonNode orderItem : orderItems) { + JsonNode productNode = orderItem.get("product"); + assertNotNull(productNode, "Expected order item entry to include resolved product data"); + assertTrue(productNode.isObject(), "Expected product payload to be a JSON object"); + assertEquals( + orderItem.path("productId").asInt(), + productNode.path("productId").asInt(), + "Order item must expose the same product id received from product-service"); + assertTrue( + hasText(productNode.path("productTitle").asText(null)), + "Product payload should include the productTitle coming from product-service"); + } + } + + @Test + @Order(7) + void shippingServiceShouldResolveOrderDetails() throws Exception { + assertServiceReachable(ServiceTarget.SHIPPING); + + ArrayNode orderItems = fetchCollection(ServiceTarget.SHIPPING, "/api/shippings"); + for (JsonNode orderItem : orderItems) { + JsonNode orderNode = orderItem.get("order"); + assertNotNull(orderNode, "Expected order item entry to include resolved order data"); + assertTrue(orderNode.isObject(), "Expected order payload to be a JSON object"); + assertEquals( + orderItem.path("orderId").asInt(), + orderNode.path("orderId").asInt(), + "Order item must expose the same order id received from order-service"); + assertTrue( + hasText(orderNode.path("orderDesc").asText(null)), + "Order payload should include the orderDesc coming from order-service"); + } + } + + @Test + @Order(8) + void paymentServiceShouldResolveOrderDetails() throws Exception { + assertServiceReachable(ServiceTarget.PAYMENT); + + ArrayNode payments = fetchCollection(ServiceTarget.PAYMENT, "/api/payments"); + for (JsonNode payment : payments) { + JsonNode orderNode = payment.get("order"); + assertNotNull(orderNode, "Expected payment entry to include resolved order data"); + assertTrue(orderNode.isObject(), "Expected order payload to be a JSON object"); + int orderId = orderNode.path("orderId").asInt(); + assertTrue( + orderId > 0, + "Order payload should include a valid orderId coming from order-service"); + assertTrue( + hasText(orderNode.path("orderDesc").asText(null)), + "Order payload should include the orderDesc coming from order-service"); + } + } + + private static ArrayNode fetchCollection(ServiceTarget target, String path) throws Exception { + ResponseEntity response = REST_TEMPLATE.getForEntity(buildUrl(target, path), String.class); + assertEquals(HttpStatus.OK, response.getStatusCode(), () -> "Expected 200 from " + buildUrl(target, path)); + String body = response.getBody(); + assertNotNull(body, "Response body must not be null"); + JsonNode root = OBJECT_MAPPER.readTree(body); + JsonNode collectionNode = root.get("collection"); + assertNotNull(collectionNode, "Response should wrap payload within a 'collection' field"); + assertTrue(collectionNode.isArray(), "Expected 'collection' to be a JSON array"); + ArrayNode collection = (ArrayNode) collectionNode; + assertTrue(collection.size() > 0, "Expected collection to return at least one element"); + return collection; + } + + private static boolean isServiceReachable(ServiceTarget target) { + try { + ResponseEntity response = REST_TEMPLATE.getForEntity(buildUrl(target, target.healthPath), String.class); + if (!response.getStatusCode().is2xxSuccessful()) { + return false; + } + String body = response.getBody(); + if (!hasText(body)) { + return true; + } + JsonNode root = OBJECT_MAPPER.readTree(body); + JsonNode status = root.get("status"); + return status != null ? "UP".equalsIgnoreCase(status.asText()) : true; + } catch (RestClientException | JsonProcessingException ex) { + return false; + } + } + + private static void assertServiceReachable(ServiceTarget target) { + for (int attempt = 1; attempt <= MAX_SERVICE_REACHABILITY_ATTEMPTS; attempt++) { + if (isServiceReachable(target)) { + return; + } + if (attempt < MAX_SERVICE_REACHABILITY_ATTEMPTS) { + try { + Thread.sleep(SERVICE_RETRY_DELAY.toMillis()); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + break; + } + } + } + assertTrue(false, target.unreachableMessage()); + } + + private static RestTemplate createRestTemplate() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout((int) Duration.ofSeconds(20).toMillis()); + factory.setReadTimeout((int) Duration.ofSeconds(20).toMillis()); + return new RestTemplate(factory); + } + + private static String buildUrl(ServiceTarget target, String relativePath) { + String baseUrl = resolveBaseUrl(target); + String cleanPath = relativePath.startsWith("/") ? relativePath : "/" + relativePath; + return baseUrl + cleanPath; + } + + private static String resolveBaseUrl(ServiceTarget target) { + String value = System.getProperty(target.systemProperty); + if (!hasText(value)) { + value = System.getenv(target.envVar); + } + if (!hasText(value)) { + value = target.defaultBaseUrl; + } + return stripTrailingSlash(value); + } + + private static String stripTrailingSlash(String value) { + if (value == null) { + return null; + } + int end = value.length(); + while (end > 0 && value.charAt(end - 1) == '/') { + end--; + } + return value.substring(0, end); + } + + private static boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + @Test + @Order(1) + void productServiceShouldListProducts() throws Exception { + assertServiceReachable(ServiceTarget.PRODUCT); + + ArrayNode products = fetchCollection(ServiceTarget.PRODUCT, "/api/products"); + for (JsonNode product : products) { + assertTrue(product.path("productId").asInt() > 0, "Expected productId to be populated"); + assertTrue(hasText(product.path("productTitle").asText(null)), "Product should expose a productTitle"); + } + } + + @Test + @Order(2) + void userServiceShouldListUsers() throws Exception { + assertServiceReachable(ServiceTarget.USER); + + ArrayNode users = fetchCollection(ServiceTarget.USER, "/api/users"); + for (JsonNode user : users) { + assertTrue(user.path("userId").asInt() > 0, "Expected userId to be populated"); + JsonNode credential = user.get("credential"); + assertNotNull(credential, "User payload should embed credential data"); + assertTrue(credential.isObject(), "Credential payload should be a JSON object"); + assertTrue( + hasText(credential.path("username").asText(null)), + "User should expose a credential.username field"); + } + } + + @Test + @Order(3) + void orderServiceShouldListOrders() throws Exception { + assertServiceReachable(ServiceTarget.ORDER); + + ArrayNode orders = fetchCollection(ServiceTarget.ORDER, "/api/orders"); + for (JsonNode order : orders) { + assertTrue(order.path("orderId").asInt() > 0, "Expected orderId to be populated"); + assertTrue(hasText(order.path("orderDesc").asText(null)), "Order should expose an orderDesc"); + } + } + + private enum ServiceTarget { + PRODUCT("PRODUCT_SERVICE_BASE_URL", "product.service.base-url", "http://product-service.ecommerce.svc.cluster.local:8500/product-service"), + USER("USER_SERVICE_BASE_URL", "user.service.base-url", "http://user-service.ecommerce.svc.cluster.local:8700/user-service"), + ORDER("ORDER_SERVICE_BASE_URL", "order.service.base-url", "http://order-service.ecommerce.svc.cluster.local:8300/order-service"), + FAVOURITE("FAVOURITE_SERVICE_BASE_URL", "favourite.service.base-url", "http://favourite-service.ecommerce.svc.cluster.local:8800/favourite-service"), + SHIPPING("SHIPPING_SERVICE_BASE_URL", "shipping.service.base-url", "http://shipping-service.ecommerce.svc.cluster.local:8600/shipping-service"), + PAYMENT("PAYMENT_SERVICE_BASE_URL", "payment.service.base-url", "http://payment-service.ecommerce.svc.cluster.local:8400/payment-service"); + + private final String envVar; + private final String systemProperty; + private final String defaultBaseUrl; + private final String healthPath = "/actuator/health"; + + ServiceTarget(String envVar, String systemProperty, String defaultBaseUrl) { + this.envVar = envVar; + this.systemProperty = systemProperty; + this.defaultBaseUrl = defaultBaseUrl; + } + + String unreachableMessage() { + return describe() + " not reachable at " + resolveBaseUrl(this); + } + + String describe() { + switch (this) { + case PRODUCT: + return "Product service"; + case USER: + return "User service"; + case ORDER: + return "Order service"; + case FAVOURITE: + return "Favourite service"; + case SHIPPING: + return "Shipping service"; + case PAYMENT: + return "Payment service"; + default: + return name(); + } + } + } +} diff --git a/tmp-cloud-config.yaml b/tmp-cloud-config.yaml new file mode 100644 index 000000000..08cd4b30d --- /dev/null +++ b/tmp-cloud-config.yaml @@ -0,0 +1,93 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + deployment.kubernetes.io/revision: "11" + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"cloud-config","namespace":"ecommerce"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"cloud-config"}},"template":{"metadata":{"labels":{"app":"cloud-config"}},"spec":{"containers":[{"env":[{"name":"SPRING_PROFILES_ACTIVE","value":"dev"},{"name":"SPRING_ZIPKIN_BASE_URL","valueFrom":{"configMapKeyRef":{"key":"ZIPKIN_BASE_URL","name":"zipkin-config"}}},{"name":"EUREKA_CLIENT_SERVICEURL_DEFAULTZONE","valueFrom":{"configMapKeyRef":{"key":"EUREKA_SERVER_URL","name":"eureka-config"}}},{"name":"EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE","valueFrom":{"configMapKeyRef":{"key":"EUREKA_SERVER_URL","name":"eureka-config"}}},{"name":"EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE","valueFrom":{"configMapKeyRef":{"key":"EUREKA_SERVER_URL","name":"eureka-config"}}},{"name":"JAVA_TOOL_OPTIONS","value":"-Deureka.client.service-url.defaultZone=http://service-discovery-service:8761/eureka/"}],"image":"selimhorri/cloud-config-ecommerce-boot:0.1.0","name":"cloud-config","ports":[{"containerPort":9296}],"resources":{"limits":{"cpu":"500m","memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}}]}}}} + creationTimestamp: "2025-10-31T01:00:33Z" + generation: 11 + name: cloud-config + namespace: ecommerce + resourceVersion: "44822" + uid: 382a6784-ec5e-4aef-8cdf-c013ffe32199 +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: cloud-config + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + annotations: + kubectl.kubernetes.io/restartedAt: "2025-11-01T17:24:32-05:00" + labels: + app: cloud-config + spec: + containers: + - env: + - name: SPRING_PROFILES_ACTIVE + value: dev + - name: SPRING_ZIPKIN_BASE_URL + value: http://zipkin-service.ecommerce:9411/ + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + key: EUREKA_SERVER_URL + name: eureka-config + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: http://service-discovery-service.ecommerce:8761/eureka + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + key: EUREKA_SERVER_URL + name: eureka-config + - name: JAVA_TOOL_OPTIONS + value: -Deureka.client.service-url.defaultZone=http://service-discovery-service:8761/eureka/ + - name: EUREKA_INSTANCE + value: service-discovery-service.ecommerce + image: selimhorri/cloud-config-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + name: cloud-config + ports: + - containerPort: 9296 + protocol: TCP + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +status: + availableReplicas: 1 + conditions: + - lastTransitionTime: "2025-11-03T14:12:00Z" + lastUpdateTime: "2025-11-03T14:12:00Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + - lastTransitionTime: "2025-10-31T01:00:33Z" + lastUpdateTime: "2025-11-03T18:03:26Z" + message: ReplicaSet "cloud-config-587cf4cb96" has successfully progressed. + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + observedGeneration: 11 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 diff --git a/tmp-user-deploy.yaml b/tmp-user-deploy.yaml new file mode 100644 index 000000000..e5430b9f2 --- /dev/null +++ b/tmp-user-deploy.yaml @@ -0,0 +1,106 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + deployment.kubernetes.io/revision: "5" + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"user-service","namespace":"ecommerce"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"user-service"}},"template":{"metadata":{"labels":{"app":"user-service"}},"spec":{"containers":[{"env":[{"name":"SPRING_CLOUD_CONFIG_NAME","value":"user-service"},{"name":"SPRING_PROFILES_ACTIVE","value":"dev"},{"name":"SPRING_ZIPKIN_BASE_URL","valueFrom":{"configMapKeyRef":{"key":"ZIPKIN_BASE_URL","name":"zipkin-config"}}},{"name":"EUREKA_CLIENT_SERVICEURL_DEFAULTZONE","valueFrom":{"configMapKeyRef":{"key":"EUREKA_SERVER_URL","name":"eureka-config"}}},{"name":"EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE","valueFrom":{"configMapKeyRef":{"key":"EUREKA_SERVER_URL","name":"eureka-config"}}},{"name":"EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE","valueFrom":{"configMapKeyRef":{"key":"EUREKA_SERVER_URL","name":"eureka-config"}}},{"name":"SPRING_CONFIG_IMPORT","value":"optional:configserver:http://cloud-config-service:9296/"},{"name":"SPRING_CLOUD_CONFIG_URI","value":"http://cloud-config-service:9296"},{"name":"EUREKA_INSTANCE_PREFER_IP_ADDRESS","value":"true"},{"name":"EUREKA_INSTANCE_HOSTNAME","value":"user-service"},{"name":"EUREKA_CLIENT_ENABLED","value":"true"},{"name":"EUREKA_CLIENT_FETCH_REGISTRY","value":"true"},{"name":"RIBBON_EUREKA_ENABLED","value":"true"}],"image":"selimhorri/user-service-ecommerce-boot:0.1.0","imagePullPolicy":"IfNotPresent","name":"user-service","ports":[{"containerPort":8700,"name":"http"}],"resources":{"limits":{"cpu":"500m","memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}}]}}}} + creationTimestamp: "2025-10-31T02:45:59Z" + generation: 5 + name: user-service + namespace: ecommerce + resourceVersion: "44665" + uid: b6742a0b-233d-40e1-a7d2-d2f4f6fe4341 +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: user-service + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + annotations: + kubectl.kubernetes.io/restartedAt: "2025-11-01T19:08:08-05:00" + labels: + app: user-service + spec: + containers: + - env: + - name: SPRING_CLOUD_CONFIG_NAME + value: user-service + - name: SPRING_PROFILES_ACTIVE + value: dev + - name: SPRING_ZIPKIN_BASE_URL + value: http://zipkin-service.ecommerce:9411/ + - name: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE + valueFrom: + configMapKeyRef: + key: EUREKA_SERVER_URL + name: eureka-config + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE + value: http://service-discovery-service.ecommerce:8761/eureka + - name: EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE + valueFrom: + configMapKeyRef: + key: EUREKA_SERVER_URL + name: eureka-config + - name: SPRING_CONFIG_IMPORT + value: optional:configserver:http://cloud-config-service.ecommerce:9296 + - name: SPRING_CLOUD_CONFIG_URI + value: http://cloud-config-service:9296 + - name: EUREKA_INSTANCE_PREFER_IP_ADDRESS + value: "true" + - name: EUREKA_INSTANCE_HOSTNAME + value: user-service + - name: EUREKA_CLIENT_ENABLED + value: "true" + - name: EUREKA_CLIENT_FETCH_REGISTRY + value: "true" + - name: RIBBON_EUREKA_ENABLED + value: "true" + image: selimhorri/user-service-ecommerce-boot:0.1.0 + imagePullPolicy: IfNotPresent + name: user-service + ports: + - containerPort: 8700 + name: http + protocol: TCP + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +status: + availableReplicas: 1 + conditions: + - lastTransitionTime: "2025-11-03T14:11:56Z" + lastUpdateTime: "2025-11-03T14:11:56Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + - lastTransitionTime: "2025-10-31T02:45:59Z" + lastUpdateTime: "2025-11-03T18:01:49Z" + message: ReplicaSet "user-service-847f67b5d6" has successfully progressed. + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + observedGeneration: 5 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 diff --git a/user-service/Dockerfile b/user-service/Dockerfile index fdc109e11..40b15406c 100644 --- a/user-service/Dockerfile +++ b/user-service/Dockerfile @@ -1,11 +1,9 @@ -FROM openjdk:11 +FROM eclipse-temurin:11-jre ARG PROJECT_VERSION=0.1.0 -RUN mkdir -p /home/app -WORKDIR /home/app ENV SPRING_PROFILES_ACTIVE dev -COPY user-service/ . -ADD user-service/target/user-service-v${PROJECT_VERSION}.jar user-service.jar +WORKDIR /home/app +COPY ./user-service/target/user-service-v${PROJECT_VERSION}.jar ./user-service.jar EXPOSE 8700 ENTRYPOINT ["java", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "-jar", "user-service.jar"] diff --git a/user-service/compose.yml b/user-service/compose.yml index 18c0f49a0..651cbb41d 100644 --- a/user-service/compose.yml +++ b/user-service/compose.yml @@ -1,12 +1,29 @@ + version: '3' services: user-service-container: image: selimhorri/user-service-ecommerce-boot:0.1.0 ports: - - 8700:8700 + - "8700:8700" + networks: + - microservices_network environment: - SPRING_PROFILES_ACTIVE=dev + - SPRING_ZIPKIN_BASE_URL=http://zipkin:9411 + - SPRING_CONFIG_IMPORT=optional:configserver:http://cloud-config-container:9296/ + - SPRING_CLOUD_CONFIG_URI=http://cloud-config-container:9296 + - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_SERVICE_URL_DEFAULT_ZONE=http://service-discovery-container:8761/eureka/ + - EUREKA_CLIENT_REGISTER_WITH_EUREKA=true + - EUREKA_CLIENT_FETCH_REGISTRY=true + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + - EUREKA_INSTANCE_HOSTNAME=user-service-container + +networks: + microservices_network: + external: true diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml index 27596cdd2..2b1ee3619 100644 --- a/user-service/src/main/resources/application.yml +++ b/user-service/src/main/resources/application.yml @@ -5,15 +5,20 @@ server: spring: zipkin: - base-url: ${SPRING_ZIPKIN_BASE_URL:http://localhost:9411/} + base-url: ${SPRING_ZIPKIN_BASE_URL:http://zipkin-service.ecommerce:9411/} config: - import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://localhost:9296} + import: ${SPRING_CONFIG_IMPORT:optional:configserver:http://cloud-config-service.ecommerce:9296} application: name: USER-SERVICE profiles: active: - dev +eureka: + client: + service-url: + defaultZone: ${EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE:http://service-discovery-service.ecommerce:8761/eureka} + resilience4j: circuitbreaker: instances: diff --git a/user-service/src/test/java/com/selimhorri/app/service/impl/UserServiceImplTest.java b/user-service/src/test/java/com/selimhorri/app/service/impl/UserServiceImplTest.java new file mode 100644 index 000000000..49cc3ce0a --- /dev/null +++ b/user-service/src/test/java/com/selimhorri/app/service/impl/UserServiceImplTest.java @@ -0,0 +1,209 @@ +package com.selimhorri.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.selimhorri.app.domain.Credential; +import com.selimhorri.app.domain.RoleBasedAuthority; +import com.selimhorri.app.domain.User; +import com.selimhorri.app.dto.CredentialDto; +import com.selimhorri.app.dto.UserDto; +import com.selimhorri.app.exception.wrapper.UserObjectNotFoundException; +import com.selimhorri.app.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +class UserServiceImplTest { + + @Mock + private UserRepository userRepository; + + private UserServiceImpl userService; + + private User user; + private UserDto userDto; + + @BeforeEach + void setUp() { + this.userService = new UserServiceImpl(this.userRepository); + + Credential credential = Credential.builder() + .credentialId(8) + .username("tester") + .password("secret") + .roleBasedAuthority(RoleBasedAuthority.ROLE_USER) + .isEnabled(Boolean.TRUE) + .isAccountNonExpired(Boolean.TRUE) + .isAccountNonLocked(Boolean.TRUE) + .isCredentialsNonExpired(Boolean.TRUE) + .build(); + + this.user = User.builder() + .userId(3) + .firstName("Jane") + .lastName("Doe") + .email("jane@example.com") + .phone("123456789") + .credential(credential) + .build(); + + this.userDto = UserDto.builder() + .userId(this.user.getUserId()) + .firstName(this.user.getFirstName()) + .lastName(this.user.getLastName()) + .email(this.user.getEmail()) + .phone(this.user.getPhone()) + .credentialDto(CredentialDto.builder() + .credentialId(credential.getCredentialId()) + .username(credential.getUsername()) + .password(credential.getPassword()) + .roleBasedAuthority(credential.getRoleBasedAuthority()) + .isEnabled(credential.getIsEnabled()) + .isAccountNonExpired(credential.getIsAccountNonExpired()) + .isAccountNonLocked(credential.getIsAccountNonLocked()) + .isCredentialsNonExpired(credential.getIsCredentialsNonExpired()) + .build()) + .build(); + } + + @Test + void findAllReturnsMappedUsers() { + when(this.userRepository.findAll()).thenReturn(List.of(this.user)); + + List result = this.userService.findAll(); + + assertEquals(1, result.size()); + UserDto dto = result.get(0); + assertEquals(this.user.getUserId(), dto.getUserId()); + assertEquals(this.user.getFirstName(), dto.getFirstName()); + assertEquals(this.user.getCredential().getUsername(), dto.getCredentialDto().getUsername()); + } + + @Test + void findByIdReturnsUserWhenPresent() { + when(this.userRepository.findById(this.user.getUserId())).thenReturn(Optional.of(this.user)); + + UserDto result = this.userService.findById(this.user.getUserId()); + + assertEquals(this.user.getUserId(), result.getUserId()); + assertEquals(this.user.getEmail(), result.getEmail()); + } + + @Test + void findByIdThrowsWhenMissing() { + when(this.userRepository.findById(this.user.getUserId())).thenReturn(Optional.empty()); + + assertThrows(UserObjectNotFoundException.class, () -> this.userService.findById(this.user.getUserId())); + } + + @Test + void savePersistsMappedUser() { + when(this.userRepository.save(any(User.class))).thenReturn(this.user); + + UserDto result = this.userService.save(this.userDto); + + assertEquals(this.user.getUserId(), result.getUserId()); + assertEquals(this.user.getFirstName(), result.getFirstName()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(this.userRepository).save(userCaptor.capture()); + User saved = userCaptor.getValue(); + assertEquals(this.userDto.getUserId(), saved.getUserId()); + assertEquals(this.userDto.getCredentialDto().getUsername(), saved.getCredential().getUsername()); + } + + @Test + void updatePersistsMappedUser() { + User updatedEntity = User.builder() + .userId(this.user.getUserId()) + .firstName("Updated") + .lastName(this.user.getLastName()) + .email(this.user.getEmail()) + .phone(this.user.getPhone()) + .credential(this.user.getCredential()) + .build(); + + when(this.userRepository.save(any(User.class))).thenReturn(updatedEntity); + + UserDto updatedDto = UserDto.builder() + .userId(this.userDto.getUserId()) + .firstName("Updated") + .lastName(this.userDto.getLastName()) + .email(this.userDto.getEmail()) + .phone(this.userDto.getPhone()) + .credentialDto(this.userDto.getCredentialDto()) + .build(); + + UserDto result = this.userService.update(updatedDto); + + assertEquals(updatedEntity.getFirstName(), result.getFirstName()); + } + + @Test + void updateByIdPersistsExistingUserState() { + when(this.userRepository.findById(this.user.getUserId())).thenReturn(Optional.of(this.user)); + when(this.userRepository.save(any(User.class))).thenReturn(this.user); + + UserDto updateRequest = UserDto.builder() + .userId(this.userDto.getUserId()) + .firstName("Ignored") + .lastName(this.userDto.getLastName()) + .email(this.userDto.getEmail()) + .phone(this.userDto.getPhone()) + .credentialDto(this.userDto.getCredentialDto()) + .build(); + + UserDto result = this.userService.update(this.user.getUserId(), updateRequest); + + assertEquals(this.user.getFirstName(), result.getFirstName()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(this.userRepository).save(userCaptor.capture()); + User saved = userCaptor.getValue(); + assertEquals(this.user.getUserId(), saved.getUserId()); + assertEquals(this.user.getCredential().getUsername(), saved.getCredential().getUsername()); + } + + @Test + void deleteByIdDelegatesToRepository() { + doNothing().when(this.userRepository).deleteById(this.user.getUserId()); + + this.userService.deleteById(this.user.getUserId()); + + verify(this.userRepository).deleteById(this.user.getUserId()); + } + + @Test + void findByUsernameReturnsUserWhenPresent() { + when(this.userRepository.findByCredentialUsername(this.user.getCredential().getUsername())) + .thenReturn(Optional.of(this.user)); + + UserDto result = this.userService.findByUsername(this.user.getCredential().getUsername()); + + assertSame(this.user.getUserId(), result.getUserId()); + assertEquals(this.user.getCredential().getUsername(), result.getCredentialDto().getUsername()); + } + + @Test + void findByUsernameThrowsWhenMissing() { + when(this.userRepository.findByCredentialUsername(this.user.getCredential().getUsername())) + .thenReturn(Optional.empty()); + + assertThrows(UserObjectNotFoundException.class, + () -> this.userService.findByUsername(this.user.getCredential().getUsername())); + } +} diff --git a/xd.txt b/xd.txt new file mode 100644 index 000000000..80100896a --- /dev/null +++ b/xd.txt @@ -0,0 +1,6 @@ +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCakNDQWU2Z0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwdGFXNXAKYTNWaVpVTkJNQjRYRFRJMU1UQXlOREUxTkRNME5sb1hEVE0xTVRBeU16RTFORE0wTmxvd0ZURVRNQkVHQTFVRQpBeE1LYldsdWFXdDFZbVZEUVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTHRBCm9Wc0RqL1V0MXp5Y1liZEdyalBJUmtDcmdZenVZbWI3WU41YVUyaUV6NE1JdDBsSkEvNENUSTJZMzNMU1didVUKTGRyN3Nia2tDdXpDbkh6Q3R4bXMvY1ZaUjZ1Nk0zcy93S0pvVVRFbVRGRVozM1NhSGVjZmZPRXE5WW5KQVJVSApoOGNmQ01kdE1JUkZXUUdMZlpXZFR0dTlPcEI3VlZwRDBwYm1LMitGZ1Q3R2hlMy9ER0tMZEJ1ckVSRXIrRm55CjV3eXRvVEU0V0FLd2E4aTJla09HNDk4NXJnSFcyL0N5R3RtSWlHQVBmamN1eVFsZXlVVXlVR3lRSzJrbGxKZWQKTGlpNFovWWNIOS92MzZXYi9hT1Zkemp6U2VzMnJGd0ZtaENHM2dkWHVlc0FHSGExNlRKWElhdTNyT3NSL0FkRQo2MVgvaUlKR2JobnRRbWlBYjdzQ0F3RUFBYU5oTUY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWRKUVFXCk1CUUdDQ3NHQVFVRkJ3TUNCZ2dyQmdFRkJRY0RBVEFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVcKQkJTdWJBdGc3cEFBR2ZueGg5cDZEMjl4V0Ntem1UQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFJdjJXL0NrRQoyYjBaRGVPdGlQY0Z2MllwQ3ZFOTBDS0Y1QS9tL09mL1dFK0w5a01TSkVTenN5U1o4MHVkL1YrVWxDUkRSTXNjCkxSVVdDbGJmdUt0dytReXZzT3QwYVkxYkRGdUlxL21Ba2hlK2FWc2puUDFQQktMQS82YW11dEVWYkJyb0NWMHAKMTlDUlM1dHV2dDNkbEZLbUVZRnFCY09nMFhCYStaM1NSWit0YzhTVmlrc096aUFZRklHeXI5T2JWMWI0QVVGWApZb3VFdnJEaTRoQTJQZi83eWN1YmpBa2NQQmxUYVZBcTRnckdDdnV2LzhKbEVjelh0OS9Ob3NEanBMT1Y1dTI2Clp0RmNWVGtlbUg3YVNRMjZVUW1Lbm1jVG9zVFc4R2l4bTFpdlZGYXpseVBRY2xteVZ6OXlENTM1cTJYL096eW8KQkpMbHlmL0VlSjk4dGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lCQWpBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwdGFXNXAKYTNWaVpVTkJNQjRYRFRJMU1UQXlOREUxTkRNME5sb1hEVEk0TVRBeU5ERTFORE0wTmxvd01URVhNQlVHQTFVRQpDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGakFVQmdOVkJBTVREVzFwYm1scmRXSmxMWFZ6WlhJd2dnRWlNQTBHCkNTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEeXlUV0ViVVBrcEl1dGc1RE11Z01RY3IyM3VLMnEKTEZPVDNtZ0IrRkJNVllsNjk3WDZsUlRMZC9SZ3JjdUJHS0FGdmN0OS80WW5ZMHBDVEpFN1BqMlNzWmJTMjVoWAo2aTdEcWhIbnVSbkxvV2hxd21HMnZXMUQrR2IxZ29mMXRjdndvNXFjYmF6MGMzUldtUDNQMkVxTS8yU2IxSlV2CnhBcW1YS2pGSEp5RUZqMngwWHMvbWZ3bzR4VzRWYVFubDA0Z0lDb2NDR2FScGdOWXhZamNtU2ZOR2Jhb2JpbkEKb3B4RFVqTUVmRjV4T1F6dU5BMEppeDArZlhKV09Mbk03YXlQalZEZUt6U0cxWXU0Z1lyUnFYcGdFM2pTS3J3TwpDc2xWbUtmUDVGOVluWTVFOW1nS1FHbGRUNThhajJhUkphMDRxcVRUbStGTkNLMDg1RTV2U3BuTEFnTUJBQUdqCllEQmVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JTdWJBdGc3cEFBR2ZueGg5cDZEMjl4V0NtegptVEFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBWEFITFBZVUJJZ0pRK0E0YWhaRzloSmJBa1JvNWYydWtzZFVBCllDQnNrY1hEb0ZNc05JTHJ0bkIzcVpCZG5uQzY4ZHBsQ2JoczZWS1VxbmhSRTA1Wi9qSkVOQk45MGt1OEwyMloKT3pHN2s3L1BlSmVKR1VldlpZNG1DdGhPTk45UFpSTlRyeGoxUUpCVEpsVnh3QVhyZ3NFZFVvTnp4SUJzU1BqZwoySDR4bERaZTYyVTBtZ2tDZHFsZFIvWVovaCthSEdWMVF5WDA3ZEh4U0JRYzVSN2s3K1VYWERnMkVXeFRkRlhwCmNFRjlNTzR6UDJTWTBWRjJ4ZDRHa2ZKL3FIYWx0UlpLdnowdzVVemRsdno2RkY3NU1CcmVlbk5UQmkrVDBvOVQKclQ4M3pPTW9BQ0dES2Q1K04raE0zWlB6WGJvZHVhNzJvYUVaenU5OWFEMTBLcWZ4akE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + + +LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBOHNrMWhHMUQ1S1NMcllPUXpMb0RFSEs5dDdpdHFpeFRrOTVvQWZoUVRGV0pldmUxCitwVVV5M2YwWUszTGdSaWdCYjNMZmYrR0oyTktRa3lST3o0OWtyR1cwdHVZVitvdXc2b1I1N2taeTZGb2FzSmgKdHIxdFEvaG05WUtIOWJYTDhLT2FuRzJzOUhOMFZwajl6OWhLalA5a205U1ZMOFFLcGx5b3hSeWNoQlk5c2RGNwpQNW44S09NVnVGV2tKNWRPSUNBcUhBaG1rYVlEV01XSTNKa256Um0ycUc0cHdLS2NRMUl6Qkh4ZWNUa003alFOCkNZc2RQbjF5VmppNXpPMnNqNDFRM2lzMGh0V0x1SUdLMGFsNllCTjQwaXE4RGdySlZaaW56K1JmV0oyT1JQWm8KQ2tCcFhVK2ZHbzlta1NXdE9LcWswNXZoVFFpdFBPUk9iMHFaeXdJREFRQUJBb0lCQUQxQ041YXFXeThkdFlDYQphYmErV0hlVisvaVhTSHBmcE5ieWp4dXdJOUtlNG9sOFp4Wks5MncvWUJIa3lDeFNvazNRamIvRThYeU91UFh4Cmk5Tis2TnBTcE5PZlJOa3A1WkxKSHF0eE1Pa0lXT2hYRTRDZ1d5bURTN2Myc0VkM0QzWXdVWnFXc2NVY1BJWjkKSTgyRnJZbldCdlNxdHRuT2hVRmtyOURNWDFHVUxUMkJrSisySXE3ZDI0ajdQa1JHaXpySDI1bzBiT3hxOU0yNQoxb2hTMHhKeFdwbm9Celh2eGRjSTFGcVQ5clRWVlJFM3RJemFnZ09WR2JlcXpObTlRT3djMjVQMTE5THNsRXZxCkp0R1RHVXNjN1RoL2grWWlZODg1UmlEVTRYNXJaRDlvR2dlelRXai9XckxGSGc5VTFUMzl1MS9CbGZnRU9qcnYKZ0hsa2YwRUNnWUVBK0ZEbW9hZE02VHNpUUM1UmduUkp5N05oeXdCbERKMnQvelFKSUtlLzhSRXRuUkRpZ0RWWgpxN1FUd3ZSTVBHU3cxMDZIWFdsWVF5SW1kUGFFdjFreE96U3pMMkNsU3lYSFJ0YjBMNjNRWGpGSkxYUkhhak96CjZqbll3T2NEWFl1VUQwOW1nNEQ0OXI0Yll6TndUYktPanZlQnpZaTlLR0xQTlpSWk1LblNuYVVDZ1lFQStreUEKSG85bksxK3dFN3IweVNlR1BhcSt4S0pEZ1JPdE1QZU5KMllldy9kdEV4bnJnWEtuZ04xS1pobzNESHlPQUVReQoyZW1JYzFnTXVKQVBFQTVGQWx6OTdiVTc2ZnlMWHVIUzZ2OUlTS3hIWVBUcG9VY0xpajdZTjhCMCtSU01IaEIrCmlBWEtaeWs1WU9iQXRiaEdpMUxTL2JtanpmSGVocWhFMXduaG5xOENnWUVBbUU3WEI3UmNZbnBZNmREL1Flc2UKRUltNHBCRmI2YjhWcUlFOFVBRlVwQUtMWGRtcmRFUkUraUo4TklvdjhLTTZMUFlINVczbUsxZFpDMGpoNTVSSAp0QzhBOFVCd1lvV0xrNUhwT1ExOXp5aWYzL2VBamsya1B6aTRFS29oMEZENndlRkwyY0ZUODlScmJBOXpCTlZPCng3WmNzYnI3ZXpnUUxLY1BoZC9FUmRrQ2dZRUEwNGUzZHh4R1dwZWhuT3NSV0xhNGNvRk1lNjJPVXhXbjlvd2YKbEw1dkhicUxTZXRMNXJoWTYwVVpUVFMyZFMwR2JSODF3b2ZrczJZa1NnNGxUTEppaTk2L0V4ZHBMUlFyVE9LWQpkOVdLNXpBQm11VWdWUlpQb0dRQUl3M1pUVythQXdaNWlNY3NDUWNENE9yaXpqQ1FIY3EvV1lKNzl0NkQ2MVZsCjZENEhtUzBDZ1lFQWt5MkI0SWVDaitYVjVGVi8rSVNnYngrR3ZaWnpLT1hucWxxRE1FZHhQNVlZeFU4TUU2ZlAKRjQ2dm01TzFFRmw0MHc5Qkt2WmJhVGFBbnVmcXlTdktnbktxTktBaXhPSjBXbWZ0enhiY1FMMjJ4d0d4d2FjQgpXWktldmpSZy9rcGY2WWZsTDUyb056UEpKaVRjSWJhU09wUWx0TExXNmNYc0c2MTd3TCt3K0tRPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= \ No newline at end of file