From c4a40bfbfabfe129c06f6330e9332bbdc861cd17 Mon Sep 17 00:00:00 2001 From: Adrian Nowak Date: Tue, 13 Jun 2023 08:54:22 +0200 Subject: [PATCH] Move to GitHub --- .github/workflows/build.yaml | 50 ++ .github/workflows/helm-release.yaml | 45 ++ .github/workflows/release.yaml | 60 +++ .github/workflows/set-version.yaml | 82 ++++ .github/workflows/static.yaml | 42 ++ .github/workflows/test.yaml | 42 ++ .gitignore | 195 ++++++++ LICENSE-HEADER-TEMPLATE.txt | 16 + LICENSE-THIRD-PARTY.txt | 196 ++++++++ LICENSE.txt | 202 +++++++++ README.md | 78 ++++ appstore-bundle-service-api/pom.xml | 39 ++ .../appstorebundle/api/ApplicationParams.java | 43 ++ .../lgi/appstorebundle/api/Environment.java | 23 + .../api/model/ApplicationContext.java | 37 ++ .../lgi/appstorebundle/api/model/Bundle.java | 42 ++ .../api/model/BundleContext.java | 35 ++ .../api/model/BundleStatus.java | 40 ++ .../src/test/resources/logback-test.xml | 14 + appstore-bundle-service-application/pom.xml | 303 +++++++++++++ .../AppStoreBundleServiceApplication.java | 30 ++ .../AppStoreBundleServiceConfiguration.java | 79 ++++ .../configuration/DatabaseNodeType.java | 33 ++ .../DatasourceConfiguration.java | 113 +++++ .../configuration/FlywayConfiguration.java | 42 ++ .../RabbitMQConsumersConfiguration.java | 118 +++++ .../ApplicationNotFoundException.java | 32 ++ .../error/handler/GlobalExceptionHandler.java | 94 ++++ .../filters/CorrelationIdFilter.java | 60 +++ .../info/AppInfoContributor.java | 83 ++++ .../lgi/appstorebundle/info/AppProperty.java | 30 ++ .../resources/AppStoreBundleController.java | 119 +++++ .../service/ApplicationMetadataService.java | 45 ++ .../appstorebundle/service/BundleService.java | 90 ++++ .../appstorebundle/util/ConsumerFactory.java | 97 ++++ .../config/application-dev.properties | 2 + .../resources/config/application.properties | 77 ++++ .../static/appstore-bundle-service.yaml | 101 +++++ .../AppStoreBundleConfigurationTest.java | 98 ++++ ...StoreBundleControllerRealRequestsTest.java | 180 ++++++++ .../AppStoreBundleControllerTest.java | 164 +++++++ .../service/BundleServiceTest.java | 152 +++++++ .../src/test/resources/logback-test.xml | 14 + .../src/test/resources/unit-tests.properties | 13 + .../appstore-metadata-service-client/pom.xml | 63 +++ .../asms/AppstoreMetadataServiceClient.java | 144 ++++++ .../AsmsBulkheadConfiguration.java | 68 +++ .../AsmsCircuitBreakerConfiguration.java | 84 ++++ .../AsmsRestTemplateConfiguration.java | 46 ++ ...ppstoreMetadataServiceClientException.java | 26 ++ .../asms/model/ApplicationMetadata.java | 39 ++ .../ApplicationMetadataForMaintainer.java | 36 ++ .../external/asms/model/Header.java | 46 ++ .../asms/model/HeaderForMaintainer.java | 49 ++ .../external/asms/model/Maintainer.java | 36 ++ .../AppstoreMetadataServiceClientTest.java | 233 ++++++++++ .../client-common/pom.xml | 68 +++ .../lgi/appstorebundle/common/Headers.java | 27 ++ .../common/r4j/AsmsClientInvoker.java | 87 ++++ .../common/r4j/BulkheadFactory.java | 61 +++ .../common/r4j/CircuitBreakerFactory.java | 76 ++++ .../common/r4j/ClientInvokerFactory.java | 65 +++ .../configuration/BulkheadConfiguration.java | 32 ++ .../CircuitBreakerConfiguration.java | 37 ++ ...ltRecoverableUpstreamServiceException.java | 26 ++ .../r4j/exception/RecoverableException.java | 26 ++ .../common/r4j/ClientInvokerFactoryTest.java | 89 ++++ .../common/r4j/ClientInvokerTest.java | 219 +++++++++ appstore-bundle-service-external/pom.xml | 47 ++ .../rabbitmq-client/pom.xml | 73 +++ .../exception/RabbitMQException.java | 32 ++ .../external/ManagedRabbitMQ.java | 114 +++++ .../external/OptionalException.java | 51 +++ .../external/RabbitMQConfiguration.java | 69 +++ .../external/RabbitMQConsumer.java | 46 ++ .../external/RabbitMQConsumerMDC.java | 47 ++ .../external/RabbitMQService.java | 57 +++ .../model/EncryptionMessage.java | 49 ++ .../model/EncryptionMessageFactory.java | 57 +++ .../appstorebundle/model/ErrorMessage.java | 38 ++ .../appstorebundle/model/FeedbackMessage.java | 48 ++ .../model/GenerationMessage.java | 48 ++ .../model/EncryptionMessageFactoryTest.java | 73 +++ appstore-bundle-service-storage/pom.xml | 25 + .../storage-common/pom.xml | 27 ++ .../storage/persistent/BundleDao.java | 40 ++ .../src/test/resources/logback-test.xml | 14 + .../storage-persistent/pom.xml | 252 +++++++++++ .../storage/persistent/JooqBundleDao.java | 137 ++++++ .../OffsetDateTimeDateTimeConverter.java | 40 ++ .../resources/migration/V1__Add_schema.sql | 33 ++ .../src/test/resources/logback-test.xml | 12 + appstore-bundle-service-test/lombok.config | 1 + appstore-bundle-service-test/pom.xml | 170 +++++++ .../ApplicationConfiguration.java | 34 ++ .../configuration/BaseTestConfiguration.java | 46 ++ .../configuration/EndpointsConfiguration.java | 65 +++ .../core/MockedServiceUrlProviderImpl.java | 34 ++ .../test/core/ServiceUrlProvider.java | 24 + .../test/service/endpoints/BaseEndpoint.java | 41 ++ .../test/service/endpoints/InfoEndpoint.java | 44 ++ .../StartBundleGenerationEndpoint.java | 52 +++ .../test/service/model/BundleWithAudit.java | 33 ++ .../model/EncryptionMessageWithEnvelope.java | 31 ++ .../model/GenerationMessageWithEnvelope.java | 31 ++ .../test/service/model/InfoResponse.java | 60 +++ .../test/tests/BaseContainersIT.java | 100 ++++ .../tests/ConsumeFeedbackMessageMockedIT.java | 220 +++++++++ .../appstorebundle/test/tests/GetInfoIT.java | 61 +++ .../test/tests/MockedBaseIT.java | 72 +++ .../tests/StartBundleGenerationMockedIT.java | 215 +++++++++ .../test/utils/ASMSMockSteps.java | 100 ++++ .../test/utils/DatabaseSteps.java | 305 +++++++++++++ .../test/utils/RabbitMQSteps.java | 211 +++++++++ .../test/utils/TestLifecycleLogger.java | 58 +++ .../src/test/resources/allure.properties | 2 + .../resources/application-test.properties | 1 + .../src/test/resources/logback-test.xml | 15 + .../test/resources/testcontainers.properties | 1 + .../src/test/resources/tpl/request.ftl | 39 ++ .../src/test/resources/tpl/response.ftl | 33 ++ charts/appstore-bundle-service-0.15.0.tgz | Bin 0 -> 3241 bytes charts/index.yaml | 17 + helm/appstore-bundle-service/.helmignore | 23 + helm/appstore-bundle-service/Chart.yaml | 28 ++ .../templates/_helpers.tpl | 46 ++ .../templates/configmap.yaml | 54 +++ .../templates/deployment.yaml | 74 +++ .../templates/ingress.yaml | 40 ++ .../templates/sealedsecret.yaml | 46 ++ .../templates/service.yaml | 36 ++ helm/appstore-bundle-service/values.yaml | 57 +++ helm/config/lint-config.yaml | 19 + pom.xml | 426 ++++++++++++++++++ 134 files changed, 9527 insertions(+) create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/helm-release.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/set-version.yaml create mode 100644 .github/workflows/static.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .gitignore create mode 100644 LICENSE-HEADER-TEMPLATE.txt create mode 100644 LICENSE-THIRD-PARTY.txt create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 appstore-bundle-service-api/pom.xml create mode 100644 appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/ApplicationParams.java create mode 100644 appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/Environment.java create mode 100644 appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/ApplicationContext.java create mode 100644 appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/Bundle.java create mode 100644 appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/BundleContext.java create mode 100644 appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/BundleStatus.java create mode 100644 appstore-bundle-service-api/src/test/resources/logback-test.xml create mode 100644 appstore-bundle-service-application/pom.xml create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/AppStoreBundleServiceApplication.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/AppStoreBundleServiceConfiguration.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/DatabaseNodeType.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/DatasourceConfiguration.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/FlywayConfiguration.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/RabbitMQConsumersConfiguration.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/error/exception/ApplicationNotFoundException.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/error/handler/GlobalExceptionHandler.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/filters/CorrelationIdFilter.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/info/AppInfoContributor.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/info/AppProperty.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/resources/AppStoreBundleController.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/service/ApplicationMetadataService.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/service/BundleService.java create mode 100644 appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/util/ConsumerFactory.java create mode 100644 appstore-bundle-service-application/src/main/resources/config/application-dev.properties create mode 100644 appstore-bundle-service-application/src/main/resources/config/application.properties create mode 100644 appstore-bundle-service-application/src/main/resources/static/appstore-bundle-service.yaml create mode 100644 appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/AppStoreBundleConfigurationTest.java create mode 100644 appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/resources/AppStoreBundleControllerRealRequestsTest.java create mode 100644 appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/resources/AppStoreBundleControllerTest.java create mode 100644 appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/service/BundleServiceTest.java create mode 100644 appstore-bundle-service-application/src/test/resources/logback-test.xml create mode 100644 appstore-bundle-service-application/src/test/resources/unit-tests.properties create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/pom.xml create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/AppstoreMetadataServiceClient.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsBulkheadConfiguration.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsCircuitBreakerConfiguration.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsRestTemplateConfiguration.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/exception/AppstoreMetadataServiceClientException.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/ApplicationMetadata.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/ApplicationMetadataForMaintainer.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/Header.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/HeaderForMaintainer.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/Maintainer.java create mode 100644 appstore-bundle-service-external/appstore-metadata-service-client/src/test/java/com/lgi/appstorebundle/external/asms/AppstoreMetadataServiceClientTest.java create mode 100644 appstore-bundle-service-external/client-common/pom.xml create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/Headers.java create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/AsmsClientInvoker.java create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/BulkheadFactory.java create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/CircuitBreakerFactory.java create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/ClientInvokerFactory.java create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/configuration/BulkheadConfiguration.java create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/configuration/CircuitBreakerConfiguration.java create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/exception/DefaultRecoverableUpstreamServiceException.java create mode 100644 appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/exception/RecoverableException.java create mode 100644 appstore-bundle-service-external/client-common/src/test/java/com/lgi/appstorebundle/common/r4j/ClientInvokerFactoryTest.java create mode 100644 appstore-bundle-service-external/client-common/src/test/java/com/lgi/appstorebundle/common/r4j/ClientInvokerTest.java create mode 100644 appstore-bundle-service-external/pom.xml create mode 100644 appstore-bundle-service-external/rabbitmq-client/pom.xml create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/exception/RabbitMQException.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/ManagedRabbitMQ.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/OptionalException.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConfiguration.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConsumer.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConsumerMDC.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQService.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/EncryptionMessage.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/EncryptionMessageFactory.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/ErrorMessage.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/FeedbackMessage.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/GenerationMessage.java create mode 100644 appstore-bundle-service-external/rabbitmq-client/src/test/java/com/lgi/appstorebundle/model/EncryptionMessageFactoryTest.java create mode 100644 appstore-bundle-service-storage/pom.xml create mode 100644 appstore-bundle-service-storage/storage-common/pom.xml create mode 100644 appstore-bundle-service-storage/storage-common/src/main/java/com/lgi/appstorebundle/storage/persistent/BundleDao.java create mode 100644 appstore-bundle-service-storage/storage-common/src/test/resources/logback-test.xml create mode 100644 appstore-bundle-service-storage/storage-persistent/pom.xml create mode 100644 appstore-bundle-service-storage/storage-persistent/src/main/java/com/lgi/appstorebundle/storage/persistent/JooqBundleDao.java create mode 100644 appstore-bundle-service-storage/storage-persistent/src/main/java/com/lgi/appstorebundle/storage/persistent/converter/OffsetDateTimeDateTimeConverter.java create mode 100644 appstore-bundle-service-storage/storage-persistent/src/main/resources/migration/V1__Add_schema.sql create mode 100644 appstore-bundle-service-storage/storage-persistent/src/test/resources/logback-test.xml create mode 100644 appstore-bundle-service-test/lombok.config create mode 100644 appstore-bundle-service-test/pom.xml create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/ApplicationConfiguration.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/BaseTestConfiguration.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/EndpointsConfiguration.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/core/MockedServiceUrlProviderImpl.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/core/ServiceUrlProvider.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/BaseEndpoint.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/InfoEndpoint.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/StartBundleGenerationEndpoint.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/BundleWithAudit.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/EncryptionMessageWithEnvelope.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/GenerationMessageWithEnvelope.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/InfoResponse.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/BaseContainersIT.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/ConsumeFeedbackMessageMockedIT.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/GetInfoIT.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/MockedBaseIT.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/StartBundleGenerationMockedIT.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/ASMSMockSteps.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/DatabaseSteps.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/RabbitMQSteps.java create mode 100644 appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/TestLifecycleLogger.java create mode 100644 appstore-bundle-service-test/src/test/resources/allure.properties create mode 100644 appstore-bundle-service-test/src/test/resources/application-test.properties create mode 100644 appstore-bundle-service-test/src/test/resources/logback-test.xml create mode 100644 appstore-bundle-service-test/src/test/resources/testcontainers.properties create mode 100644 appstore-bundle-service-test/src/test/resources/tpl/request.ftl create mode 100644 appstore-bundle-service-test/src/test/resources/tpl/response.ftl create mode 100644 charts/appstore-bundle-service-0.15.0.tgz create mode 100644 charts/index.yaml create mode 100644 helm/appstore-bundle-service/.helmignore create mode 100644 helm/appstore-bundle-service/Chart.yaml create mode 100644 helm/appstore-bundle-service/templates/_helpers.tpl create mode 100644 helm/appstore-bundle-service/templates/configmap.yaml create mode 100644 helm/appstore-bundle-service/templates/deployment.yaml create mode 100644 helm/appstore-bundle-service/templates/ingress.yaml create mode 100644 helm/appstore-bundle-service/templates/sealedsecret.yaml create mode 100644 helm/appstore-bundle-service/templates/service.yaml create mode 100644 helm/appstore-bundle-service/values.yaml create mode 100644 helm/config/lint-config.yaml create mode 100644 pom.xml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..9bb1c0a --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,50 @@ +name: Builds the project +on: + workflow_call: + inputs: + java-version: + description: "JDK Version" + type: string + required: true + version: + description: "Version to build" + type: string + required: true + pushDockerImage: + description: "Push docker image" + default: false + type: boolean + secrets: + REGISTRY_USERNAME: + required: true + REGISTRY_PASSWORD: + required: true +env: + DOCKER_REGISTRY: docker.io + DOCKER_IMAGE_NAME: appstore-bundle-service + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + ref: v${{ inputs.version }} + - name: Setup JDK + uses: actions/setup-java@v3 + with: + java-version: ${{ inputs.java-version }} + distribution: 'temurin' + cache: maven + - name: Build + run: + mvn package -DskipTests -DskipJarUpload=true -DscmCommit=${{ github.sha }} -DscmBranch=${{ github.ref_name }} -Ddocker_registry.username=${{ secrets.REGISTRY_USERNAME }} -Ddocker_registry.domain=${{ env.DOCKER_REGISTRY }} -Ddocker.image=${{ env.DOCKER_IMAGE_NAME }} + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + - name: Push + run: + docker push --all-tags ${{ env.DOCKER_REGISTRY }}/${{ secrets.REGISTRY_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }} diff --git a/.github/workflows/helm-release.yaml b/.github/workflows/helm-release.yaml new file mode 100644 index 0000000..8ab2531 --- /dev/null +++ b/.github/workflows/helm-release.yaml @@ -0,0 +1,45 @@ +name: Release helm package +on: + workflow_call: + inputs: + version: + description: "Helm package version" + default: true + type: string + secrets: + token: + required: true +env: + CI_COMMIT_AUTHOR: Dac-Cloud-Bot + CI_COMMIT_AUTHOR_EMAIL: dac-cloud@libertyglobal.com + CI_COMMIT_MESSAGE: "[CI] Add Helm Chart" + HELM_REPOSITORY: https://libertyglobal.github.io/appstore-bundle-service/charts +jobs: + release-helm: + permissions: + contents: write + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Configure Git + run: | + git config user.name ${{ env.CI_COMMIT_AUTHOR }} + git config user.email ${{ env.CI_COMMIT_AUTHOR_EMAIL }} + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.5.0 + - name: Generate and Publish Helm + run: | + git pull + helm package --version ${{ inputs.version }} --app-version ${{ inputs.version }} helm/appstore-bundle-service -d charts/ + helm repo index charts/ --url ${{ env.HELM_REPOSITORY }} + git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" + git config --global user.email "{{ env.CI_COMMIT_AUTHOR_EMAIL }}" + git add charts/ + git commit -m "${{ env.CI_COMMIT_MESSAGE }}" + git push + diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..0ab415e --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,60 @@ +name: Release a new version +on: + workflow_dispatch: + inputs: + nextRelease: + description: "Version to release" + type: string + required: true + nextDevelopmentVersion: + description: "Next development version" + type: string + required: true + runTests: + description: "Run IT tests" + default: true + type: boolean + +jobs: + test: + if: inputs.runTests + uses: ./.github/workflows/test.yaml + with: + java-version: '17' + set-release-version: + permissions: write-all + uses: ./.github/workflows/set-version.yaml + if: always() && + !contains(needs.test.result, 'failure') && + !contains(needs.test.result, 'cancelled') + with: + java-version: '17' + version: ${{ inputs.nextRelease }} + commit_message: "Release ${{ inputs.nextRelease }}" + commit: true + create_tag: true + build: + uses: ./.github/workflows/build.yaml + needs: set-release-version + with: + pushDockerImage: true + version: ${{ inputs.nextRelease }} + java-version: '17' + secrets: inherit + helm-release: + permissions: write-all + uses: ./.github/workflows/helm-release.yaml + needs: build + with: + version: ${{ inputs.nextRelease }} + secrets: + token: ${{ secrets.GITHUB_TOKEN }} + set-dev-version: + permissions: write-all + uses: ./.github/workflows/set-version.yaml + needs: helm-release + with: + version: ${{ inputs.nextDevelopmentVersion }} + commit_message: "Prepare for next development iteration ${{ inputs.nextDevelopmentVersion }}" + commit: true + java-version: '17' diff --git a/.github/workflows/set-version.yaml b/.github/workflows/set-version.yaml new file mode 100644 index 0000000..2c34173 --- /dev/null +++ b/.github/workflows/set-version.yaml @@ -0,0 +1,82 @@ +name: Sets version on project +on: + workflow_call: + inputs: + java-version: + description: "JDK Version" + type: string + required: true + version: + description: "Version to set" + type: string + required: true + commit: + description: "Commit" + type: boolean + default: false + commit_message: + description: "Commit message" + type: string + create_tag: + description: "Create tag" + type: boolean + default: false +env: + CI_COMMIT_AUTHOR: Dac-Cloud-Bot + CI_COMMIT_AUTHOR_EMAIL: dac-cloud@libertyglobal.com + CI_COMMIT_MESSAGE: "[CI] ${{ inputs.commit_message }}" + +jobs: + set-version: + if: inputs.commit == false + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: ${{ inputs.java-version }} + distribution: 'temurin' + cache: maven + - name: Set next release version + run: mvn versions:set -DnewVersion=${{ inputs.version }} -DprocessAllModules + set-version-tag-and-commit: + if: inputs.commit && inputs.create_tag + runs-on: ubuntu-22.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: ${{ inputs.java-version }} + distribution: 'temurin' + cache: maven + - name: GIT commit and push all changed files + run: | + git pull + mvn versions:set -DnewVersion=${{ inputs.version }} -DprocessAllModules + git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" + git config --global user.email "${{ env.CI_COMMIT_AUTHOR_EMAIL }}" + git commit -a -m "${{ env.CI_COMMIT_MESSAGE }}" + git tag -a v${{ inputs.version }} -m "Version ${{ inputs.version }}" + git push --tags + set-version-and-commit: + if: inputs.commit && inputs.create_tag == false + runs-on: ubuntu-22.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: ${{ inputs.java-version }} + distribution: 'temurin' + cache: maven + - name: GIT commit and push all changed files + run: | + git pull + mvn versions:set -DnewVersion=${{ inputs.version }} -DprocessAllModules + git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" + git config --global user.email "${{ env.CI_COMMIT_AUTHOR_EMAIL }}" + git commit -a -m "${{ env.CI_COMMIT_MESSAGE }}" + git push diff --git a/.github/workflows/static.yaml b/.github/workflows/static.yaml new file mode 100644 index 0000000..fc2ed46 --- /dev/null +++ b/.github/workflows/static.yaml @@ -0,0 +1,42 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: '.' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..2e333c2 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,42 @@ +name: Test the project +on: + workflow_call: + inputs: + java-version: + description: "JDK Version" + type: string + required: true +jobs: + test: + runs-on: ubuntu-22.04 + services: + postgres: + image: postgres:12 + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + testcontainers: + image: testcontainers/ryuk:0.3.0 + ports: + - 46577:46577 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + steps: + - uses: actions/checkout@v3 + - name: Setup JDK + uses: actions/setup-java@v3 + with: + java-version: ${{ inputs.java-version }} + distribution: 'temurin' + cache: maven + - name: Test + run: + mvn verify -DskipJarUpload=true -Ddb.port=5432 -Ddocker.skip=true + mvn verify -Dci-stage=integration-tests -DskipJarUpload=true -Ddb.port=5432 -Ddocker.skip=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..646c820 --- /dev/null +++ b/.gitignore @@ -0,0 +1,195 @@ +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/dictionaries +.idea/**/shelf + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ +cmake-build-release/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ +*.iml +.idea/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + + +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) +!/.mvn/wrapper/maven-wrapper.jar + + +# Compiled class file +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# jEnv +*.java-version diff --git a/LICENSE-HEADER-TEMPLATE.txt b/LICENSE-HEADER-TEMPLATE.txt new file mode 100644 index 0000000..75f5655 --- /dev/null +++ b/LICENSE-HEADER-TEMPLATE.txt @@ -0,0 +1,16 @@ +If not stated otherwise in this file or this component's LICENSE file the +following copyright and licenses apply: + +Copyright ${license.year} ${license.owner} + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/LICENSE-THIRD-PARTY.txt b/LICENSE-THIRD-PARTY.txt new file mode 100644 index 0000000..dfaf1d9 --- /dev/null +++ b/LICENSE-THIRD-PARTY.txt @@ -0,0 +1,196 @@ + +Lists of 194 third-party dependencies. + (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.2.11 - http://logback.qos.ch/logback-classic) + (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.2.11 - http://logback.qos.ch/logback-core) + (Apache License, Version 2.0) ClassMate (com.fasterxml:classmate:1.5.1 - https://github.com/FasterXML/java-classmate) + (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.13.2 - http://github.com/FasterXML/jackson) + (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.14.2 - https://github.com/FasterXML/jackson-core) + (The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.13.2.2 - http://github.com/FasterXML/jackson) + (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.2 - https://github.com/FasterXML/jackson-dataformats-text) + (The Apache Software License, Version 2.0) Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) + (The Apache Software License, Version 2.0) Jackson datatype: Joda (com.fasterxml.jackson.datatype:jackson-datatype-joda:2.14.2 - https://github.com/FasterXML/jackson-datatype-joda) + (The Apache Software License, Version 2.0) Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) + (The Apache Software License, Version 2.0) Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) + (The Apache Software License, Version 2.0) zjsonpatch (com.flipkart.zjsonpatch:zjsonpatch:0.2.1 - https://github.com/flipkart-incubator/zjsonpatch/) + (The Apache Software License, Version 2.0) docker-java-api (com.github.docker-java:docker-java-api:3.3.0 - https://github.com/docker-java/docker-java) + (The Apache Software License, Version 2.0) docker-java-transport (com.github.docker-java:docker-java-transport:3.3.0 - https://github.com/docker-java/docker-java) + (The Apache Software License, Version 2.0) docker-java-transport-zerodep (com.github.docker-java:docker-java-transport-zerodep:3.3.0 - https://github.com/docker-java/docker-java) + (The Apache Software License, Version 2.0) Handlebars (com.github.jknack:handlebars:4.0.6 - https://github.com/jknack/handlebars.java/handlebars) + (The Apache Software License, Version 2.0) WireMock (com.github.tomakehurst:wiremock:2.5.1 - http://wiremock.org) + (Apache 2.0) Auto Common Libraries (com.google.auto:auto-common:1.2 - https://github.com/google/auto/tree/master/common) + (Apache 2.0) AutoService Processor (com.google.auto.service:auto-service:1.0.1 - https://github.com/google/auto/tree/master/service) + (Apache 2.0) AutoService (com.google.auto.service:auto-service-annotations:1.0.1 - https://github.com/google/auto/tree/master/service) + (Apache 2.0) AutoValue Processor (com.google.auto.value:auto-value:1.10.1 - https://github.com/google/auto/tree/master/value) + (Apache 2.0) AutoValue Annotations (com.google.auto.value:auto-value-annotations:1.10.1 - https://github.com/google/auto/tree/master/value) + (The Apache Software License, Version 2.0) FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/) + (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.11.0 - https://errorprone.info/error_prone_annotations) + (The Apache Software License, Version 2.0) Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess) + (Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:31.1-jre - https://github.com/google/guava) + (The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) + (The Apache Software License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/) + (The Apache Software License, Version 2.0) project ':json-path' (com.jayway.jsonpath:json-path:2.6.0 - https://github.com/jayway/JsonPath) + (Apache License, Version 2.0) AppStore Bundle Service :: API (com.lgi.appstorebundle:appstore-bundle-service-api:0.13.0-SNAPSHOT - https://spring.io/projects/spring-boot/appstore-bundle-service/appstore-bundle-service-api) + (Apache License, Version 2.0) AppStore Bundle Service :: Application (com.lgi.appstorebundle:appstore-bundle-service-application:0.13.0-SNAPSHOT - https://spring.io/projects/spring-boot/appstore-bundle-service/appstore-bundle-service-application) + (Apache License, Version 2.0) appstore-metadata-service-client (com.lgi.appstorebundle:appstore-metadata-service-client:0.13.0-SNAPSHOT - https://spring.io/projects/spring-boot/appstore-bundle-service/appstore-bundle-service-external/appstore-metadata-service-client) + (Apache License, Version 2.0) client-common (com.lgi.appstorebundle:client-common:0.13.0-SNAPSHOT - https://spring.io/projects/spring-boot/appstore-bundle-service/appstore-bundle-service-external/client-common) + (Apache License, Version 2.0) rabbitmq-client (com.lgi.appstorebundle:rabbitmq-client:0.13.0-SNAPSHOT - https://spring.io/projects/spring-boot/appstore-bundle-service/appstore-bundle-service-external/rabbitmq-client) + (Apache License, Version 2.0) storage-common (com.lgi.appstorebundle:storage-common:0.13.0-SNAPSHOT - https://spring.io/projects/spring-boot/appstore-bundle-service/appstore-bundle-service-storage/storage-common) + (Apache License, Version 2.0) storage-persistent (com.lgi.appstorebundle:storage-persistent:0.13.0-SNAPSHOT - https://spring.io/projects/spring-boot/appstore-bundle-service/appstore-bundle-service-storage/storage-persistent) + (ASL 2.0) (GPL v2) (MPL 2.0) RabbitMQ Java Client (com.rabbitmq:amqp-client:5.12.0 - https://www.rabbitmq.com) + (ASL 2.0) (GPL v2) (MPL 2.0) RabbitMQ Java Client (com.rabbitmq:amqp-client:5.13.1 - https://www.rabbitmq.com) + (Eclipse Distribution License - v 1.0) Old JAXB Runtime (com.sun.xml.bind:jaxb-impl:2.3.3 - https://eclipse-ee4j.github.io/jaxb-ri/jaxb-bundles/jaxb-impl) + (Apache License 2.0) JSON library from Android SDK (com.vaadin.external.google:android-json:0.0.20131108.vaadin1 - http://developer.android.com/sdk) + (The Apache Software License, Version 2.0) HikariCP (com.zaxxer:HikariCP:3.4.1 - https://github.com/brettwooldridge/HikariCP) + (The Apache Software License, Version 2.0) HikariCP (com.zaxxer:HikariCP:4.0.3 - https://github.com/brettwooldridge/HikariCP) + (Apache License, Version 2.0) Apache Commons Codec (commons-codec:commons-codec:1.15 - https://commons.apache.org/proper/commons-codec/) + (The MIT License (MIT)) ClassGraph (io.github.classgraph:classgraph:4.8.69 - https://github.com/classgraph/classgraph) + (Apache-2.0) resilience4j (io.github.resilience4j:resilience4j-bulkhead:1.6.1 - https://resilience4j.readme.io) + (Apache-2.0) resilience4j (io.github.resilience4j:resilience4j-circuitbreaker:1.6.1 - https://resilience4j.readme.io) + (Apache-2.0) resilience4j (io.github.resilience4j:resilience4j-core:1.6.1 - https://resilience4j.readme.io) + (Apache-2.0) resilience4j (io.github.resilience4j:resilience4j-prometheus:1.7.1 - https://resilience4j.readme.io) + (The Apache Software License, Version 2.0) micrometer-core (io.micrometer:micrometer-core:1.8.4 - https://github.com/micrometer-metrics/micrometer) + (The Apache Software License, Version 2.0) Prometheus Java Simpleclient (io.prometheus:simpleclient:0.16.0 - http://github.com/prometheus/client_java/simpleclient) + (The Apache Software License, Version 2.0) Prometheus Java Simpleclient Common (io.prometheus:simpleclient_common:0.12.0 - http://github.com/prometheus/client_java/simpleclient_common) + (The Apache Software License, Version 2.0) Prometheus Java Span Context Supplier - Common (io.prometheus:simpleclient_tracer_common:0.12.0 - http://github.com/prometheus/client_java/simpleclient_tracer/simpleclient_tracer_common) + (The Apache Software License, Version 2.0) Prometheus Java Span Context Supplier - OpenTelemetry (io.prometheus:simpleclient_tracer_otel:0.12.0 - http://github.com/prometheus/client_java/simpleclient_tracer/simpleclient_tracer_otel) + (The Apache Software License, Version 2.0) Prometheus Java Span Context Supplier - OpenTelemetry Agent (io.prometheus:simpleclient_tracer_otel_agent:0.12.0 - http://github.com/prometheus/client_java/simpleclient_tracer/simpleclient_tracer_otel_agent) + (The Apache Software License, Version 2.0) Allure Attachments (io.qameta.allure:allure-attachments:2.13.3 - https://github.com/allure-framework/allure-java) + (The Apache Software License, Version 2.0) Allure Java Commons (io.qameta.allure:allure-java-commons:2.13.3 - https://github.com/allure-framework/allure-java) + (The Apache Software License, Version 2.0) Allure JUnit Platform Integration (io.qameta.allure:allure-junit-platform:2.13.3 - https://github.com/allure-framework/allure-java) + (The Apache Software License, Version 2.0) Allure JUnit 5 Integration (io.qameta.allure:allure-junit5:2.13.3 - https://github.com/allure-framework/allure-java) + (The Apache Software License, Version 2.0) Allure Model Integration (io.qameta.allure:allure-model:2.13.3 - https://github.com/allure-framework/allure-java) + (The Apache Software License, Version 2.0) Allure Rest-Assured Integration (io.qameta.allure:allure-rest-assured:2.13.3 - https://github.com/allure-framework/allure-java) + (Apache License 2.0) Reactive Relational Database Connectivity - SPI (io.r2dbc:r2dbc-spi:0.8.6.RELEASE - https://r2dbc.io/r2dbc-spi) + (Apache 2.0) json-path (io.rest-assured:json-path:4.4.0 - http://maven.apache.org) + (Apache 2.0) REST Assured (io.rest-assured:rest-assured:4.4.0 - http://code.google.com/p/rest-assured) + (Apache 2.0) rest-assured-common (io.rest-assured:rest-assured-common:4.4.0 - http://maven.apache.org) + (Apache 2.0) xml-path (io.rest-assured:xml-path:4.4.0 - http://code.google.com/p/rest-assured/xml-path) + (Apache License 2.0) swagger-annotations (io.swagger:swagger-annotations:1.5.22 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations) + (Apache License 2.0) swagger-annotations (io.swagger.core.v3:swagger-annotations:2.2.8 - https://github.com/swagger-api/swagger-core/modules/swagger-annotations) + (Apache License 2.0) swagger-core (io.swagger.core.v3:swagger-core:2.1.3 - https://github.com/swagger-api/swagger-core/modules/swagger-core) + (Apache License 2.0) swagger-integration (io.swagger.core.v3:swagger-integration:2.1.3 - https://github.com/swagger-api/swagger-core/modules/swagger-integration) + (Apache License 2.0) swagger-models (io.swagger.core.v3:swagger-models:2.1.3 - https://github.com/swagger-api/swagger-core/modules/swagger-models) + (The Apache Software License, Version 2.0) Vavr (io.vavr:vavr:0.10.2 - http://vavr.io) + (The Apache Software License, Version 2.0) Vavr Match (io.vavr:vavr-match:0.10.2 - http://vavr.io) + (EDL 1.0) Jakarta Activation API jar (jakarta.activation:jakarta.activation-api:1.2.2 - https://github.com/eclipse-ee4j/jaf/jakarta.activation-api) + (EPL 2.0) (GPL2 w/ CPE) Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:1.3.5 - https://projects.eclipse.org/projects/ee4j.ca) + (Apache License 2.0) Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:2.0.2 - https://beanvalidation.org) + (Eclipse Distribution License - v 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 - https://github.com/eclipse-ee4j/jaxb-api/jakarta.xml.bind-api) + (Apache License, Version 2.0) Joda-Time (joda-time:joda-time:2.12.2 - https://www.joda.org/joda-time/) + (Eclipse Public License 1.0) JUnit (junit:junit:4.13.2 - http://junit.org) + (Apache License, Version 2.0) Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.11.22 - https://bytebuddy.net/byte-buddy) + (Apache License, Version 2.0) Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.11.22 - https://bytebuddy.net/byte-buddy-agent) + (Apache-2.0) (LGPL-2.1-or-later) Java Native Access (net.java.dev.jna:jna:5.12.1 - https://github.com/java-native-access/jna) + (The Apache Software License, Version 2.0) ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.4.8 - https://urielch.github.io/) + (The Apache Software License, Version 2.0) JSON Small and Fast Parser (net.minidev:json-smart:2.4.8 - https://urielch.github.io/) + (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:4.9 - http://pholser.github.com/jopt-simple) + (The BSD License) ANTLR 4 Runtime (org.antlr:antlr4-runtime:4.5.1-1 - http://www.antlr.org/antlr4-runtime) + (The Apache Software License, Version 2.0) Apache Commons Collections (org.apache.commons:commons-collections4:4.0 - http://commons.apache.org/proper/commons-collections/) + (Apache License, Version 2.0) Apache Commons Compress (org.apache.commons:commons-compress:1.22 - https://commons.apache.org/proper/commons-compress/) + (Apache License, Version 2.0) Apache Commons Lang (org.apache.commons:commons-lang3:3.12.0 - https://commons.apache.org/proper/commons-lang/) + (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.13 - http://hc.apache.org/httpcomponents-client) + (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.15 - http://hc.apache.org/httpcomponents-core-ga) + (Apache License, Version 2.0) Apache HttpClient Mime (org.apache.httpcomponents:httpmime:4.5.13 - http://hc.apache.org/httpcomponents-client) + (Apache License, Version 2.0) Apache Log4j API (org.apache.logging.log4j:log4j-api:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-api/) + (Apache License, Version 2.0) Apache Log4j to SLF4J Adapter (org.apache.logging.log4j:log4j-to-slf4j:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-to-slf4j/) + (Apache License, Version 2.0) Apache Tika core (org.apache.tika:tika-core:1.20 - http://tika.apache.org/) + (Apache License, Version 2.0) tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:9.0.60 - https://tomcat.apache.org/) + (Apache License, Version 2.0) tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:9.0.60 - https://tomcat.apache.org/) + (Apache License, Version 2.0) tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:9.0.60 - https://tomcat.apache.org/) + (The Apache License, Version 2.0) org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) + (Apache License, Version 2.0) AssertJ Core (org.assertj:assertj-core:3.24.2 - https://assertj.github.io/doc/#assertj-core) + (Apache 2.0) Awaitility (org.awaitility:awaitility:4.0.2 - http://awaitility.org) + (Apache 2.0) Awaitility (org.awaitility:awaitility:4.2.0 - http://awaitility.org) + (Apache License 2.0) TagSoup (org.ccil.cowan.tagsoup:tagsoup:1.2.1 - http://home.ccil.org/~cowan/XML/tagsoup/) + (The MIT License) Checker Qual (org.checkerframework:checker-qual:3.12.0 - https://checkerframework.org) + (The MIT License) Checker Qual (org.checkerframework:checker-qual:3.5.0 - https://checkerframework.org) + (The Apache Software License, Version 2.0) Apache Groovy (org.codehaus.groovy:groovy:3.0.10 - https://groovy-lang.org) + (The Apache Software License, Version 2.0) Apache Groovy (org.codehaus.groovy:groovy-json:3.0.10 - https://groovy-lang.org) + (The Apache Software License, Version 2.0) Apache Groovy (org.codehaus.groovy:groovy-xml:3.0.10 - https://groovy-lang.org) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-continuation) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-http) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-io) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-security) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-server) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-servlet) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-servlets) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-util) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-util-ajax) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-webapp) + (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.45.v20220203 - https://eclipse.org/jetty/jetty-xml) + (Apache License, Version 2.0) flyway-core (org.flywaydb:flyway-core:8.5.10 - https://flywaydb.org/flyway-core) + (Apache License, Version 2.0) Apache FreeMarker (org.freemarker:freemarker:2.3.31 - https://freemarker.apache.org/) + (BSD License 3) Hamcrest (org.hamcrest:hamcrest:2.2 - http://hamcrest.org/JavaHamcrest/) + (BSD License 3) Hamcrest Core (org.hamcrest:hamcrest-core:2.2 - http://hamcrest.org/JavaHamcrest/) + (BSD-2-Clause) (Public Domain, per Creative Commons CC0) HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.12 - http://hdrhistogram.github.io/HdrHistogram/) + (Apache License 2.0) Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:6.2.3.Final - http://hibernate.org/validator/hibernate-validator) + (Apache License, version 2.0) JBoss Logging 3 (org.jboss.logging:jboss-logging:3.4.3.Final - http://www.jboss.org) + (The Apache Software License, Version 2.0) JetBrains Java Annotations (org.jetbrains:annotations:17.0.0 - https://github.com/JetBrains/java-annotations) + (Apache License, Version 2.0) jOOQ (org.jooq:jooq:3.16.6 - http://www.jooq.org/jooq) + (Apache License, Version 2.0) jOOQ Meta (org.jooq:jooq-meta:3.16.6 - http://www.jooq.org/jooq-meta) + (Apache License, Version 2.0) jOOR (org.jooq:joor-java-8:0.9.10 - https://github.com/jOOQ/jOOR) + (Eclipse Public License v2.0) JUnit Jupiter (Aggregator) (org.junit.jupiter:junit-jupiter:5.8.2 - https://junit.org/junit5/) + (Eclipse Public License v2.0) JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.9.2 - https://junit.org/junit5/) + (Eclipse Public License v2.0) JUnit Jupiter Engine (org.junit.jupiter:junit-jupiter-engine:5.8.2 - https://junit.org/junit5/) + (Eclipse Public License v2.0) JUnit Jupiter Params (org.junit.jupiter:junit-jupiter-params:5.9.2 - https://junit.org/junit5/) + (Eclipse Public License v2.0) JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.8.2 - https://junit.org/junit5/) + (Eclipse Public License v2.0) JUnit Platform Engine API (org.junit.platform:junit-platform-engine:1.8.2 - https://junit.org/junit5/) + (Eclipse Public License v2.0) JUnit Platform Launcher (org.junit.platform:junit-platform-launcher:1.8.2 - https://junit.org/junit5/) + (Eclipse Public License v2.0) JUnit Vintage Engine (org.junit.vintage:junit-vintage-engine:5.9.2 - https://junit.org/junit5/) + (Public Domain, per Creative Commons CC0) LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/) + (The MIT License) mockito-core (org.mockito:mockito-core:4.0.0 - https://github.com/mockito/mockito) + (The MIT License) mockito-junit-jupiter (org.mockito:mockito-junit-jupiter:4.0.0 - https://github.com/mockito/mockito) + (Apache License, Version 2.0) Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis) + (The Apache License, Version 2.0) org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.2.0 - https://github.com/ota4j-team/opentest4j) + (BSD-3-Clause) asm (org.ow2.asm:asm:9.1 - http://asm.ow2.io/) + (BSD-2-Clause) PostgreSQL JDBC Driver (org.postgresql:postgresql:42.3.5 - https://jdbc.postgresql.org) + (The MIT License) Project Lombok (org.projectlombok:lombok:1.18.20 - https://projectlombok.org) + (CC0) reactive-streams (org.reactivestreams:reactive-streams:1.0.3 - http://www.reactive-streams.org/) + (MIT) Duct Tape (org.rnorth.duct-tape:duct-tape:1.0.8 - https://github.com/rnorth/duct-tape) + (The Apache Software License, Version 2.0) JSONassert (org.skyscreamer:jsonassert:1.5.0 - https://github.com/skyscreamer/JSONassert) + (MIT License) JUL to SLF4J bridge (org.slf4j:jul-to-slf4j:1.7.30 - http://www.slf4j.org) + (MIT License) SLF4J API Module (org.slf4j:slf4j-api:1.7.30 - http://www.slf4j.org) + (The Apache License, Version 2.0) Spring OpenAPI 3.0 documentation generator (org.springdoc:springdoc-openapi-common:1.4.3 - https://springdoc.org/) + (The Apache License, Version 2.0) Spring OpenAPI 3.0 documentation generator (org.springdoc:springdoc-openapi-ui:1.4.3 - https://springdoc.org/) + (The Apache License, Version 2.0) Spring OpenAPI 3.0 documentation generator (org.springdoc:springdoc-openapi-webmvc-core:1.4.3 - https://springdoc.org/) + (Apache License, Version 2.0) Spring AOP (org.springframework:spring-aop:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Beans (org.springframework:spring-beans:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Context (org.springframework:spring-context:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Core (org.springframework:spring-core:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Expression Language (SpEL) (org.springframework:spring-expression:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Commons Logging Bridge (org.springframework:spring-jcl:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring JDBC (org.springframework:spring-jdbc:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Messaging (org.springframework:spring-messaging:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring TestContext Framework (org.springframework:spring-test:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Transaction (org.springframework:spring-tx:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Web (org.springframework:spring-web:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Web MVC (org.springframework:spring-webmvc:5.3.18 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring AMQP Core (org.springframework.amqp:spring-amqp:2.4.3 - https://github.com/spring-projects/spring-amqp) + (Apache License, Version 2.0) Spring RabbitMQ Support (org.springframework.amqp:spring-rabbit:2.4.3 - https://github.com/spring-projects/spring-amqp) + (Apache License, Version 2.0) spring-boot (org.springframework.boot:spring-boot:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-actuator (org.springframework.boot:spring-boot-actuator:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter (org.springframework.boot:spring-boot-starter:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-jdbc (org.springframework.boot:spring-boot-starter-jdbc:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-jooq (org.springframework.boot:spring-boot-starter-jooq:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-logging (org.springframework.boot:spring-boot-starter-logging:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-validation (org.springframework.boot:spring-boot-starter-validation:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-test (org.springframework.boot:spring-boot-test:2.6.6 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:2.6.6 - https://spring.io/projects/spring-boot) + (Apache 2.0) Spring Retry (org.springframework.retry:spring-retry:1.3.2 - https://www.springsource.org) + (MIT) Testcontainers :: Database-Commons (org.testcontainers:database-commons:1.18.0 - https://testcontainers.org) + (MIT) Testcontainers :: JDBC (org.testcontainers:jdbc:1.18.0 - https://testcontainers.org) + (MIT) Testcontainers :: JDBC :: PostgreSQL (org.testcontainers:postgresql:1.18.0 - https://testcontainers.org) + (MIT) TestContainers :: RabbitMQ (org.testcontainers:rabbitmq:1.18.0 - https://testcontainers.org) + (MIT) Testcontainers Core (org.testcontainers:testcontainers:1.18.0 - https://testcontainers.org) + (Apache 2.0) Swagger UI (org.webjars:swagger-ui:3.28.0 - http://webjars.org) + (MIT) webjars-locator-core (org.webjars:webjars-locator-core:0.48 - http://webjars.org) + (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.8.4 - https://www.xmlunit.org/) + (The BSD 3-Clause License) org.xmlunit:xmlunit-legacy (org.xmlunit:xmlunit-legacy:2.8.4 - https://www.xmlunit.org/) + (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:1.29 - http://www.snakeyaml.org) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2757a2b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Liberty Global BV + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0fa02c6 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# AppStore Bundle Service + +How to start the appstore-bundle-service application +--- + +Prerequisites: + +1. Start postgres (as a docker instance): +``` +docker run --rm -d -p 5432:5432 -e "POSTGRES_HOST_AUTH_METHOD=trust" --name postgres postgres:12 +``` + +2. Start RabbitMQ (as a docker instance): +``` +docker run --rm -d -p 5672:5672 --name rabbitmq rabbitmq:3.8.19-management-alpine +docker exec -it rabbitmq rabbitmqadmin declare queue name=bundlegen-service-requests +docker exec -it rabbitmq rabbitmqadmin declare queue name=bundlegen-service-status +docker exec -it rabbitmq rabbitmqadmin declare queue name=bundlecrypt-service-requests +docker exec -it rabbitmq rabbitmqadmin declare queue name=bundlecrypt-service-status +``` +Run AppStore Bundle Service: + +1. Start application +``` +docker run --rm -d -p 8080:8080 --name appstore-bundle-service daccloud/appstore-bundle-service:latest +``` + +Run AppStore Bundle Service from code: + +1. Run `mvn clean package` to build your application +2. Start application on local machine with `mvn spring-boot:run -pl appstore-bundle-service-application -P development` +3. To check that your application is running enter url `http://localhost:8081` + +Integration tests +--- +**How to run integration tests?** + +Navigate to the parent maven module `appstore-bundle-service` and: +* In order to execute tests only against mocked application the `integration-tests` profile should be activated + * `mvn clean verify -Pintegration-tests` +
or + * `mvn clean verify -Dci-stage=integration-tests` + +**How to generate Allure test report?** + +1. After tests finished, navigate to the `appstore-bundle-service-tests` module +2. Run `mvn -pl appstore-bundle-service-test allure:serve` + + +Install on kubernetes: + +1. Create a file `appstore-bundle-service.yaml` and set properties + +```yaml +apiVersion: helm.fluxcd.io/v1 +kind: HelmRelease +metadata: + name: appstore-bundle-service + namespace: +spec: + chart: + repository: http://libertyglobal.github.io/appstore-bundle-service/charts/ + name: appstore-bundle-service + version: + values: + ingress: + domainName: + sealedSecret: + JDBC_PASSWORD: + JDBC_USER: + configMap: + JDBC_DATABASE_NAME: + ENVIRONMENT: + RABBITMQ_HOST: + APPSTORE_METADATA_SERVICE_URL: +``` + +2. Run `kubectl apply -f appstore-bundle-service.yaml` \ No newline at end of file diff --git a/appstore-bundle-service-api/pom.xml b/appstore-bundle-service-api/pom.xml new file mode 100644 index 0000000..8dffdd1 --- /dev/null +++ b/appstore-bundle-service-api/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + appstore-bundle-service + com.lgi.appstorebundle + 0.16.0-SNAPSHOT + + + appstore-bundle-service-api + + AppStore Bundle Service :: API + API + + + + src/main/java/com/lgi/appstorebundle/api/**/* + + + src/main/java/com/lgi/appstorebundle/api/**/* + + + + + + com.google.auto.value + auto-value-annotations + + + com.google.auto.value + auto-value + + + joda-time + joda-time + + + diff --git a/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/ApplicationParams.java b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/ApplicationParams.java new file mode 100644 index 0000000..7c9fea2 --- /dev/null +++ b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/ApplicationParams.java @@ -0,0 +1,43 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.api; + +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class ApplicationParams { + + public abstract String getApplicationId(); + + public abstract String getAppVersion(); + + public abstract String getPlatformName(); + + public abstract String getFirmwareVersion(); + + public abstract String getAppBundleName(); + + public static ApplicationParams create(String appId, String appVer, String platformName, String firmwareVersion, String appBundleName) { + return new AutoValue_ApplicationParams(appId, appVer, platformName, firmwareVersion, appBundleName); + } + + public String getFullyQualifiedApplicationName(){ + return getApplicationId() + ":" + getAppVersion(); + } +} diff --git a/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/Environment.java b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/Environment.java new file mode 100644 index 0000000..42c6498 --- /dev/null +++ b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/Environment.java @@ -0,0 +1,23 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.api; + +public enum Environment { + DEV, STAGING, PRODUCTION +} diff --git a/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/ApplicationContext.java b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/ApplicationContext.java new file mode 100644 index 0000000..06a8464 --- /dev/null +++ b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/ApplicationContext.java @@ -0,0 +1,37 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.api.model; + +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class ApplicationContext { + + public abstract String getApplicationId(); + + public abstract String getApplicationVersion(); + + public abstract String getPlatformName(); + + public abstract String getFirmwareVersion(); + + public static ApplicationContext create(String applicationId, String applicationVersion, String platformName, String firmwareVersion) { + return new AutoValue_ApplicationContext(applicationId, applicationVersion, platformName, firmwareVersion); + } +} diff --git a/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/Bundle.java b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/Bundle.java new file mode 100644 index 0000000..54adfae --- /dev/null +++ b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/Bundle.java @@ -0,0 +1,42 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.api.model; + +import com.google.auto.value.AutoValue; +import org.joda.time.DateTime; + +import java.util.UUID; + +@AutoValue +public abstract class Bundle { + + public abstract UUID getId(); + + public abstract ApplicationContext getApplicationContext(); + + public abstract BundleStatus getStatus(); + + public abstract String getXRequestId(); + + public abstract DateTime getMessageTimestamp(); + + public static Bundle create(UUID id, ApplicationContext applicationContext, BundleStatus status, String xRequestId, DateTime messageTimestamp) { + return new AutoValue_Bundle(id, applicationContext, status, xRequestId, messageTimestamp); + } +} diff --git a/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/BundleContext.java b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/BundleContext.java new file mode 100644 index 0000000..5cb8751 --- /dev/null +++ b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/BundleContext.java @@ -0,0 +1,35 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.api.model; + +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class BundleContext { + + public abstract Bundle getBundle(); + + public abstract String getOciImageUrl(); + + public abstract boolean getEncrypt(); + + public static BundleContext create(Bundle bundle, String ociImageUrl, boolean encrypt) { + return new AutoValue_BundleContext(bundle, ociImageUrl, encrypt); + } +} diff --git a/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/BundleStatus.java b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/BundleStatus.java new file mode 100644 index 0000000..72631dd --- /dev/null +++ b/appstore-bundle-service-api/src/main/java/com/lgi/appstorebundle/api/model/BundleStatus.java @@ -0,0 +1,40 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.api.model; + +import java.util.Optional; + +public enum BundleStatus { + + GENERATION_REQUESTED, + GENERATION_LAUNCHED, + GENERATION_COMPLETED, + ENCRYPTION_REQUESTED, + ENCRYPTION_LAUNCHED, + ENCRYPTION_COMPLETED, + BUNDLE_ERROR; + + public static Optional of(String status) { + try { + return Optional.of(valueOf(status)); + } catch (IllegalArgumentException ex) { + return Optional.empty(); + } + } +} diff --git a/appstore-bundle-service-api/src/test/resources/logback-test.xml b/appstore-bundle-service-api/src/test/resources/logback-test.xml new file mode 100644 index 0000000..cd13c36 --- /dev/null +++ b/appstore-bundle-service-api/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + + %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/appstore-bundle-service-application/pom.xml b/appstore-bundle-service-application/pom.xml new file mode 100644 index 0000000..8a9cf53 --- /dev/null +++ b/appstore-bundle-service-application/pom.xml @@ -0,0 +1,303 @@ + + + 4.0.0 + + + appstore-bundle-service + com.lgi.appstorebundle + 0.16.0-SNAPSHOT + + + appstore-bundle-service-application + + AppStore Bundle Service :: Application + + + com.lgi.appstorebundle.AppStoreBundleServiceApplication + + src/main/java/com/lgi/appstorebundle/configuration/*, + src/main/java/com/lgi/appstorebundle/error/exception/* + + + src/main/java/com/lgi/appstorebundle/configuration/*, + src/main/java/com/lgi/appstorebundle/error/exception/* + + com.lgi.appstorebundle + + + + + com.lgi.appstorebundle + appstore-bundle-service-api + ${project.version} + + + com.lgi.appstorebundle + appstore-metadata-service-client + ${project.version} + + + com.lgi.appstorebundle + rabbitmq-client + ${project.version} + + + com.lgi.appstorebundle + storage-persistent + ${project.version} + + + org.springframework.boot + spring-boot + + + com.google.auto.service + auto-service + + + io.swagger.core.v3 + swagger-annotations + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springdoc + springdoc-openapi-ui + ${springdoc-openapi-ui-version} + + + io.swagger + swagger-annotations + ${swagger-annotations-version} + + + jakarta.annotation + jakarta.annotation-api + + + org.assertj + assertj-core + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-test + + + org.testcontainers + testcontainers + + + org.testcontainers + rabbitmq + + + org.testcontainers + postgresql + + + org.flywaydb + flyway-core + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + ${project.groupId} + storage-persistent + ${project.version} + jar + */**.sql + + + ${project.build.directory}/classes + + + + package + + unpack + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + exec + + + + + + io.fabric8 + docker-maven-plugin + 0.40.2 + + + Build docker container + package + + build + + + + Push docker container + deploy + + push + + + + + + ${project.version} + + IfNotPresent + build + default + always + + + ${docker_registry.domain}/${docker_registry.username}/${docker.image} + + eclipse-temurin:17-jdk-jammy + + 8080 + + + ${project.version} + + + ${project.artifactId} + ${project.version} + ${scmCommit} + ${scmBranch} + ${maven.build.timestamp} + + + + java + -Dspring.profiles.active=prod + -jar + /app/app.jar + + + + + /app + + + + target/appstore-bundle-service-application-${project.version}-exec.jar + app.jar + + + + + + + + + + + + com.google.cloud.tools + jib-maven-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-remote-resources-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.openapitools + openapi-generator-maven-plugin + 5.1.0 + + + openapi-generate-spring + generate-sources + + generate + + + ${project.basedir}/src/main/resources/static/appstore-bundle-service.yaml + + spring + ${generated.source.package}.model + false + false + false + + true + true + false + java8 + is + + + + + + + + + + + application-release + + + performRelease + true + + + + + + se.bjurr.gitchangelog + git-changelog-maven-plugin + + + com.ruleoftech + markdown-page-generator-plugin + + + + + + diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/AppStoreBundleServiceApplication.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/AppStoreBundleServiceApplication.java new file mode 100644 index 0000000..1c4245d --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/AppStoreBundleServiceApplication.java @@ -0,0 +1,30 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AppStoreBundleServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AppStoreBundleServiceApplication.class, args); + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/AppStoreBundleServiceConfiguration.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/AppStoreBundleServiceConfiguration.java new file mode 100644 index 0000000..6d32024 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/AppStoreBundleServiceConfiguration.java @@ -0,0 +1,79 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.configuration; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import com.lgi.appstorebundle.api.Environment; +import com.lgi.appstorebundle.common.r4j.AsmsClientInvoker; +import com.lgi.appstorebundle.common.r4j.ClientInvokerFactory; +import com.lgi.appstorebundle.external.asms.AppstoreMetadataServiceClient; +import com.lgi.appstorebundle.external.asms.configuration.AsmsBulkheadConfiguration; +import com.lgi.appstorebundle.external.asms.configuration.AsmsCircuitBreakerConfiguration; +import com.lgi.appstorebundle.model.EncryptionMessageFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.validation.constraints.NotNull; + +@Configuration +public class AppStoreBundleServiceConfiguration { + + @Autowired + private AsmsCircuitBreakerConfiguration asmsCircuitBreakerConfiguration; + + @Autowired + private AsmsBulkheadConfiguration asmsBulkheadConfiguration; + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new JodaModule()) + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()) + .registerModule(new ParameterNamesModule()) + .setSerializationInclusion(JsonInclude.Include.NON_ABSENT) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } + + @Bean + public AsmsClientInvoker asmsClientInvoker() { + return ClientInvokerFactory.createClientInvoker( + asmsCircuitBreakerConfiguration, + asmsBulkheadConfiguration, + AppstoreMetadataServiceClient.class + ); + } + + @Bean + public EncryptionMessageFactory encryptionMessageFactory(@Value("${environment}") Environment environment, + @Value("${bundle.extension}") @NotNull String bundleExtension) { + return new EncryptionMessageFactory(environment, bundleExtension); + } + +} \ No newline at end of file diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/DatabaseNodeType.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/DatabaseNodeType.java new file mode 100644 index 0000000..72bee65 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/DatabaseNodeType.java @@ -0,0 +1,33 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.configuration; + +public enum DatabaseNodeType { + WRITE("writeNode"), READ("readNode"); + + DatabaseNodeType(String nodeType) { + this.nodeType = nodeType; + } + + private final String nodeType; + + public String getNodeType() { + return this.nodeType; + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/DatasourceConfiguration.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/DatasourceConfiguration.java new file mode 100644 index 0000000..81b68ab --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/DatasourceConfiguration.java @@ -0,0 +1,113 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.configuration; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.jooq.SQLDialect; +import org.jooq.conf.SettingsTools; +import org.jooq.impl.DataSourceConnectionProvider; +import org.jooq.impl.DefaultConfiguration; +import org.jooq.impl.DefaultDSLContext; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +@Configuration +public class DatasourceConfiguration { + + @Value("${query.timeout.seconds}") + @Min(1) + @Max(Integer.MAX_VALUE) + private int queryTimeoutInSeconds; + + @Value("${datasource.stack.name}") + private String stackName; + + @Value("${datasource.charset}") + private String charSet; + + @Configuration + @ConfigurationProperties(prefix = "spring.datasource.hikari.write") + public class WriteHikariConfig extends HikariConfig { + + @Bean("writeDataSource") + public HikariDataSource writeDataSource() { + return new HikariDataSource(this); + } + } + + @Configuration + @ConfigurationProperties(prefix = "spring.datasource.hikari.read") + public class ReadHikariConfig extends HikariConfig { + + @Bean("readDataSource") + public HikariDataSource readDataSource() { + return new HikariDataSource(this); + } + } + + @Bean("readConnectionProvider") + public DataSourceConnectionProvider readConnectionProvider(HikariDataSource readDataSource) { + return new DataSourceConnectionProvider + (new TransactionAwareDataSourceProxy(readDataSource)); + } + + @Bean("writeConnectionProvider") + public DataSourceConnectionProvider writeConnectionProvider(HikariDataSource writeDataSource) { + return new DataSourceConnectionProvider + (new TransactionAwareDataSourceProxy(writeDataSource)); + } + + @Bean + public DefaultDSLContext readDslContext(@Qualifier("readDataSource") HikariDataSource readDataSource) { + return new DefaultDSLContext(readConfiguration(readDataSource)); + } + + @Bean + public DefaultDSLContext writeDslContext(@Qualifier("writeDataSource") HikariDataSource writeDataSource) { + return new DefaultDSLContext(writeConfiguration(writeDataSource)); + } + + private DefaultConfiguration readConfiguration(HikariDataSource dataSource) { + DefaultConfiguration basicConfiguration = basicConfiguration(dataSource); + basicConfiguration.set(readConnectionProvider(dataSource)); + return basicConfiguration; + } + + private DefaultConfiguration writeConfiguration(HikariDataSource dataSource) { + DefaultConfiguration basicConfiguration = basicConfiguration(dataSource); + basicConfiguration.set(writeConnectionProvider(dataSource)); + return basicConfiguration; + } + + private DefaultConfiguration basicConfiguration(HikariDataSource dataSource) { + DefaultConfiguration configuration = new DefaultConfiguration(); + configuration.set(dataSource) + .set(SQLDialect.POSTGRES) + .set(SettingsTools.defaultSettings().withQueryTimeout(queryTimeoutInSeconds)); + return configuration; + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/FlywayConfiguration.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/FlywayConfiguration.java new file mode 100644 index 0000000..da81961 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/FlywayConfiguration.java @@ -0,0 +1,42 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.configuration; + +import com.zaxxer.hikari.HikariDataSource; +import org.flywaydb.core.Flyway; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FlywayConfiguration { + private static final String[] FLYWAY_MIGRATION_LOCATION = new String[] { + "classpath:/migration", + "classpath:/BOOT-INF/classes/migration" + }; + + @Bean + public Flyway flyway(HikariDataSource writeDataSource) { + final var flyway = Flyway.configure().dataSource( + writeDataSource.getJdbcUrl(), writeDataSource.getUsername(), writeDataSource.getPassword()) + .locations(FLYWAY_MIGRATION_LOCATION) + .load(); + flyway.migrate(); + return flyway; + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/RabbitMQConsumersConfiguration.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/RabbitMQConsumersConfiguration.java new file mode 100644 index 0000000..a3f0486 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/configuration/RabbitMQConsumersConfiguration.java @@ -0,0 +1,118 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.configuration; + +import com.google.common.base.Suppliers; +import com.lgi.appstorebundle.api.model.BundleStatus; +import com.lgi.appstorebundle.external.ManagedRabbitMQ; +import com.lgi.appstorebundle.external.RabbitMQConfiguration; +import com.lgi.appstorebundle.external.RabbitMQConsumer; +import com.lgi.appstorebundle.model.FeedbackMessage; +import com.lgi.appstorebundle.service.BundleService; +import com.lgi.appstorebundle.util.ConsumerFactory; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DeliverCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +@Configuration +public class RabbitMQConsumersConfiguration { + + private static final Logger LOG = LoggerFactory.getLogger(RabbitMQConsumersConfiguration.class); + private static final boolean AUTO_ACKNOWLEDGE = false; + + @Value("${bundle.encryption.enabled}") + private boolean encrypt; + + @Autowired + private RabbitMQConfiguration rabbitMQConfiguration; + + @Autowired + private ConsumerFactory consumerFactory; + + @Autowired + private BundleService bundleService; + + @Autowired + private ManagedRabbitMQ managedRabbitMQ; + + @PostConstruct + public void setUpConsumers() throws IOException { + List> rabbitMQConsumers = List.of( + new RabbitMQConsumer<>( + RabbitMQConfiguration::getGenerationStatusQueueName, + Suppliers.memoize(() -> consumerFactory.createConsumer(this::processGenerationStatus)) + ), + new RabbitMQConsumer<>( + RabbitMQConfiguration::getEncryptionStatusQueueName, + Suppliers.memoize(() -> consumerFactory.createConsumer(this::processEncryptionStatus)) + ) + ); + + for (RabbitMQConsumer rabbitMqConsumer : rabbitMQConsumers) { + Channel channel = managedRabbitMQ.getChannel(); + channel.basicConsume( + rabbitMqConsumer.getQueueNameSupplier().apply(rabbitMQConfiguration), + AUTO_ACKNOWLEDGE, + decorateBySendingAck(rabbitMqConsumer, channel), + consumerTag -> {} + ); + } + } + + private DeliverCallback decorateBySendingAck(RabbitMQConsumer consumer, Channel channel) { + return (consumerTag, delivery) -> { + consumer.getConsumer().handle(consumerTag, delivery); + channel.basicAck(delivery.getEnvelope().getDeliveryTag(), AUTO_ACKNOWLEDGE); + }; + } + + private void processGenerationStatus(FeedbackMessage feedbackMessage, String xRequestId) { + process(feedbackMessage, xRequestId, bundleStatus -> { + bundleService.updateBundleStatusIfNewer(feedbackMessage.getId(), bundleStatus, feedbackMessage.getMessageTimestamp()); + if (bundleStatus == BundleStatus.GENERATION_COMPLETED && encrypt) { + bundleService.triggerBundleEncryption(feedbackMessage.getId(), xRequestId); + } + }); + } + + private void processEncryptionStatus(FeedbackMessage feedbackMessage, String xRequestId) { + process(feedbackMessage, xRequestId, bundleStatus -> + bundleService.updateBundleStatusIfNewer(feedbackMessage.getId(), bundleStatus, feedbackMessage.getMessageTimestamp())); + } + + private void process(FeedbackMessage feedbackMessage, String + xRequestId, Consumer bundleStatusConsumer) { + Optional.ofNullable(feedbackMessage.getMessageTimestamp()) + .ifPresentOrElse( + messageTimestamp -> BundleStatus.of(feedbackMessage.getPhaseCode()) + .ifPresentOrElse(bundleStatusConsumer, + () -> LOG.warn("Message for 'x-request-id': '{}' does not have a valid 'phaseCode': '{}'. Cannot be processed.", xRequestId, feedbackMessage.getPhaseCode())), + () -> LOG.warn("Message for 'x-request-id': '{}' does not have a 'messageTimestamp'. Cannot be processed.", xRequestId)); + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/error/exception/ApplicationNotFoundException.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/error/exception/ApplicationNotFoundException.java new file mode 100644 index 0000000..a35f933 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/error/exception/ApplicationNotFoundException.java @@ -0,0 +1,32 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.error.exception; + +public class ApplicationNotFoundException extends RuntimeException { + + public static final String APP_NOT_FOUND = "Application id: '%s'. version: '%s', platformName: '%s', firmwareVersion: '%s' not found in AppStore Metadata Service"; + + public ApplicationNotFoundException(String message) { + super(message); + } + + public static ApplicationNotFoundException createDefault(String appId, String appVersion, String platformName, String firmwareVersion) { + return new ApplicationNotFoundException(String.format(APP_NOT_FOUND, appId, appVersion, platformName, firmwareVersion)); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/error/handler/GlobalExceptionHandler.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/error/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..1e50509 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/error/handler/GlobalExceptionHandler.java @@ -0,0 +1,94 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.error.handler; + +import com.lgi.appstorebundle.error.exception.ApplicationNotFoundException; +import com.lgi.appstorebundle.exception.RabbitMQException; +import com.lgi.appstorebundle.model.ErrorResponse; +import com.lgi.appstorebundle.model.ErrorResponseError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import static com.lgi.appstorebundle.filters.CorrelationIdFilter.X_REQUEST_ID_HEADER_NAME; + +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class); + private static final String APP_NOT_FOUND_MESSAGE = "Application not found!"; + + @ExceptionHandler(Exception.class) + public ResponseEntity handleAll(Exception ex, WebRequest request) { + return handleGenericResponse(ex, HttpStatus.INTERNAL_SERVER_ERROR, request, ex.getMessage()); + } + + @ExceptionHandler(ApplicationNotFoundException.class) + public ResponseEntity handleApplicationNotFound(Exception ex, WebRequest request) { + return handleGenericResponse(ex, HttpStatus.NOT_FOUND, request, APP_NOT_FOUND_MESSAGE); + } + + @ExceptionHandler(RabbitMQException.class) + public ResponseEntity handleRabbitMQException(Exception ex, WebRequest request) { + LOG.error("RabbitMQException message: {}", ex.getMessage()); + return handleGenericResponse(ex, HttpStatus.INTERNAL_SERVER_ERROR, request, ex.getMessage()); + } + + private ResponseEntity handleGenericResponse(Exception ex, + HttpStatus httpStatus, + WebRequest webRequest, + String message) { + return handleGenericResponse(ex, (HttpHeaders) null, httpStatus, webRequest, message); + } + + private ResponseEntity handleGenericResponse(Exception ex, + @Nullable HttpHeaders httpHeaders, + HttpStatus httpStatus, + WebRequest webRequest, + String message) { + return handleGenericResponse(ex, createResponse(ex, httpStatus, webRequest, message), httpHeaders, httpStatus, webRequest); + } + private ResponseEntity handleGenericResponse(Exception ex, ErrorResponse errorResponse, @Nullable HttpHeaders httpHeaders, HttpStatus httpStatus, + WebRequest webRequest) { + LOG.warn("Exception: ", ex); + HttpHeaders headers = httpHeaders != null ? httpHeaders : new HttpHeaders(); + return handleExceptionInternal(ex, errorResponse, headers, httpStatus, webRequest); + } + + private ErrorResponse createResponse(Exception ex, HttpStatus httpStatus, WebRequest webRequest, String message) { + String correlationId = webRequest.getHeader(X_REQUEST_ID_HEADER_NAME); + ErrorResponseError error = new ErrorResponseError(); + error.message(message) + .details(ex.getMessage()) + .httpStatusCode(httpStatus.value()) + .correlationId(correlationId); + + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.setError(error); + + return errorResponse; + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/filters/CorrelationIdFilter.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/filters/CorrelationIdFilter.java new file mode 100644 index 0000000..21ed97d --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/filters/CorrelationIdFilter.java @@ -0,0 +1,60 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.filters; + +import org.apache.logging.log4j.util.Strings; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.UUID; + +@Component +public class CorrelationIdFilter extends OncePerRequestFilter { + + public static final String X_REQUEST_ID_HEADER_NAME = "x-request-id"; + private static final String CORRELATION_ID = "correlationId"; + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + FilterChain filterChain) + throws ServletException, IOException { + String correlationId = httpServletRequest.getHeader(X_REQUEST_ID_HEADER_NAME); + if (Strings.isBlank(correlationId)) { + correlationId = generateUniqueCorrelationId(); + } + MDC.put(CORRELATION_ID, correlationId); + httpServletResponse.addHeader(X_REQUEST_ID_HEADER_NAME, correlationId); + try { + filterChain.doFilter(httpServletRequest, httpServletResponse); + } finally { + MDC.remove(CORRELATION_ID); + } + } + + private String generateUniqueCorrelationId() { + return UUID.randomUUID().toString(); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/info/AppInfoContributor.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/info/AppInfoContributor.java new file mode 100644 index 0000000..53f5cec --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/info/AppInfoContributor.java @@ -0,0 +1,83 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.info; + +import org.springframework.boot.actuate.info.Info; +import org.springframework.boot.actuate.info.InfoContributor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.TimeZone; + +@Component +public class AppInfoContributor implements InfoContributor { + + public static final String MISSING_DETAIL_VALUE = "not specified"; + + private static final Map DETAILS = new HashMap<>(); + + private final Environment environment; + + public AppInfoContributor(Environment environment) { + this.environment = environment; + } + + @PostConstruct + public void init() { + addAppPropertyDetailFromEnvironment(AppProperty.APP_BRANCH); + addAppPropertyDetailFromEnvironment(AppProperty.APP_BUILD_TIME); + addAppPropertyDetailFromEnvironment(AppProperty.APP_NAME); + addAppPropertyDetailFromEnvironment(AppProperty.APP_REVISION); + addAppPropertyDetail(AppProperty.APP_START_TIME, getCurrentTime()); + addAppPropertyDetailFromEnvironment(AppProperty.APP_VERSION); + addAppPropertyDetailFromEnvironment(AppProperty.HOST_NAME); + addAppPropertyDetailFromEnvironment(AppProperty.STACK_NAME); + } + + @Override + public void contribute(Info.Builder builder) { + builder.withDetails(DETAILS); + } + + private void addAppPropertyDetailFromEnvironment(AppProperty appProperty) { + addAppPropertyDetail(appProperty, getAppPropertyValueFromEnvironment(appProperty)); + } + + private void addAppPropertyDetail(AppProperty appProperty, Object value) { + DETAILS.put(appProperty.toString(), value); + } + + private Object getAppPropertyValueFromEnvironment(final AppProperty appProperty) { + return Optional.ofNullable(environment.getProperty(appProperty.toString())) + .orElse(MISSING_DETAIL_VALUE); + } + + public static String getCurrentTime() { + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + return df.format(new Date()); + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/info/AppProperty.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/info/AppProperty.java new file mode 100644 index 0000000..45d81ce --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/info/AppProperty.java @@ -0,0 +1,30 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.info; + +public enum AppProperty { + APP_BRANCH, + APP_BUILD_TIME, + APP_NAME, + APP_REVISION, + APP_START_TIME, + APP_VERSION, + HOST_NAME, + STACK_NAME +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/resources/AppStoreBundleController.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/resources/AppStoreBundleController.java new file mode 100644 index 0000000..8299a67 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/resources/AppStoreBundleController.java @@ -0,0 +1,119 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.resources; + +import com.lgi.appstorebundle.api.ApplicationParams; +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleContext; +import com.lgi.appstorebundle.error.exception.ApplicationNotFoundException; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadata; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadataForMaintainer; +import com.lgi.appstorebundle.service.ApplicationMetadataService; +import com.lgi.appstorebundle.service.BundleService; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.lgi.appstorebundle.api.model.BundleStatus.BUNDLE_ERROR; +import static com.lgi.appstorebundle.api.model.BundleStatus.GENERATION_REQUESTED; +import static com.lgi.appstorebundle.common.Headers.CORRELATION_ID; +import static java.util.UUID.randomUUID; +import static org.joda.time.DateTime.now; +import static org.springframework.http.HttpStatus.ACCEPTED; + +@RestController +@RequestMapping("/applications") +public class AppStoreBundleController { + + private static final Logger LOG = LoggerFactory.getLogger(AppStoreBundleController.class); + + private static final String RETRY_AFTER = "Retry-After"; + + private final Duration retryAfter; + private final ApplicationMetadataService applicationMetadataService; + private final BundleService bundleService; + private final boolean encrypt; + + private static final Predicate IS_NOT_BUNDLE_ERROR = application -> BUNDLE_ERROR != application.getStatus(); + + @Autowired + public AppStoreBundleController(@Value("${http.retry.after}") Duration retryAfter, + ApplicationMetadataService applicationMetadataService, + BundleService bundleService, + @Value("${bundle.encryption.enabled}") boolean encrypt) { + this.retryAfter = checkNotNull(retryAfter, "retryAfter"); + this.applicationMetadataService = applicationMetadataService; + this.bundleService = bundleService; + this.encrypt = encrypt; + } + + @GetMapping(value = "/{appId}/{appVersion}/{platformName}/{firmwareVersion}/{appBundleName}", produces = {"application/json"}) + public ResponseEntity startBundleGeneration(@Valid @PathVariable("appId") String appId, + @Valid @PathVariable("appVersion") String appVersion, + @Valid @PathVariable("platformName") String platformName, + @Valid @PathVariable("firmwareVersion") String firmwareVersion, + @Valid @PathVariable("appBundleName") String appBundleName, + @RequestHeader(CORRELATION_ID) String xRequestId) { + final ApplicationParams applicationParams = ApplicationParams.create(appId, appVersion, platformName, firmwareVersion, appBundleName); + final ApplicationMetadata applicationMetadataWithMaintainer = applicationMetadataService.getApplicationMetadata(applicationParams) + .orElseThrow(() -> ApplicationNotFoundException.createDefault(appId, appVersion, platformName, firmwareVersion)); + + final String maintainerCode = applicationMetadataWithMaintainer.getMaintainer().getCode(); + final ApplicationMetadataForMaintainer applicationMetadataForMaintainer = applicationMetadataService.getApplicationMetadataForMaintainerCode(applicationParams, maintainerCode) + .orElseThrow(() -> ApplicationNotFoundException.createDefault(appId, appVersion, platformName, firmwareVersion)); + + final Optional maybeBundle = bundleService.getLatestBundle(appId, appVersion, platformName, firmwareVersion); + + if (maybeBundle.filter(IS_NOT_BUNDLE_ERROR).isEmpty()) { + final UUID id = randomUUID(); + LOG.info("Starting a new bundle generation for bundle id:'{}', appId:'{}', appVersion:'{}', platformName:'{}', firmwareVersion:'{}'.", id, appId, appVersion, platformName, firmwareVersion); + final BundleContext bundleContext = createBundleContext(id, appId, appVersion, platformName, firmwareVersion, xRequestId, applicationMetadataForMaintainer.getHeader().getOciImageUrl()); + bundleService.triggerBundleGeneration(bundleContext); + } + + return ResponseEntity.status(ACCEPTED) + .header(RETRY_AFTER, String.valueOf(retryAfter.toSeconds())) + .header(CORRELATION_ID, xRequestId) + .build(); + } + + private BundleContext createBundleContext(UUID id, String appId, String appVersion, String platformName, String firmwareVersion, String xRequestId, String ociImageUrl) { + final DateTime messageTimestamp = now(DateTimeZone.UTC); + final ApplicationContext applicationContext = ApplicationContext.create(appId, appVersion, platformName, firmwareVersion); + final Bundle bundle = Bundle.create(id, applicationContext, GENERATION_REQUESTED, xRequestId, messageTimestamp); + return BundleContext.create(bundle, ociImageUrl, encrypt); + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/service/ApplicationMetadataService.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/service/ApplicationMetadataService.java new file mode 100644 index 0000000..8106f63 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/service/ApplicationMetadataService.java @@ -0,0 +1,45 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.service; + +import com.lgi.appstorebundle.api.ApplicationParams; +import com.lgi.appstorebundle.external.asms.AppstoreMetadataServiceClient; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadata; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadataForMaintainer; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ApplicationMetadataService { + + private final AppstoreMetadataServiceClient appstoreMetadataServiceClient; + + public ApplicationMetadataService(AppstoreMetadataServiceClient appstoreMetadataServiceClient) { + this.appstoreMetadataServiceClient = appstoreMetadataServiceClient; + } + + public Optional getApplicationMetadata(ApplicationParams app) { + return appstoreMetadataServiceClient.getApplicationByAppId(app); + } + + public Optional getApplicationMetadataForMaintainerCode(ApplicationParams app, String maintainerCode) { + return appstoreMetadataServiceClient.getApplicationByIdAndMaintainerCode(app, maintainerCode); + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/service/BundleService.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/service/BundleService.java new file mode 100644 index 0000000..f761952 --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/service/BundleService.java @@ -0,0 +1,90 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.service; + +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleContext; +import com.lgi.appstorebundle.api.model.BundleStatus; +import com.lgi.appstorebundle.external.OptionalException; +import com.lgi.appstorebundle.external.RabbitMQService; +import com.lgi.appstorebundle.model.EncryptionMessageFactory; +import com.lgi.appstorebundle.storage.persistent.BundleDao; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.UUID; + +import static com.lgi.appstorebundle.api.model.BundleStatus.BUNDLE_ERROR; + +@Service +public class BundleService { + + private static final Logger LOG = LoggerFactory.getLogger(BundleService.class); + + private final BundleDao bundleDao; + private final RabbitMQService rabbitMqService; + private final EncryptionMessageFactory encryptionMessageFactory; + + @Autowired + public BundleService(BundleDao bundleDao, RabbitMQService rabbitMqService, EncryptionMessageFactory encryptionMessageFactory) { + this.bundleDao = bundleDao; + this.rabbitMqService = rabbitMqService; + this.encryptionMessageFactory = encryptionMessageFactory; + } + + public Optional getLatestBundle(String applicationId, String applicationVersion, String platformName, String firmwareVersion) { + return bundleDao.getLatestBundle(applicationId, applicationVersion, platformName, firmwareVersion); + } + + public void triggerBundleGeneration(BundleContext bundleContext) { + final Bundle bundle = bundleContext.getBundle(); + bundleDao.saveBundleWithStatus(bundle); + final var maybeException = rabbitMqService.sendGenerationMessage(bundleContext); + maybeException.ifPresent(exception -> { + bundleDao.updateStatusForBundle(bundle.getId(), BUNDLE_ERROR, bundle.getMessageTimestamp()); + throw exception; + }); + } + + public void updateBundleStatusIfNewer(UUID id, BundleStatus status, DateTime messageTimestamp) { + final boolean wasUpdated = bundleDao.updateBundleStatusIfNewer(id, status, messageTimestamp); + if (wasUpdated) { + LOG.info("Bundle for id: '{}' was updated", id); + } + } + + public void triggerBundleEncryption(UUID id, String xRequestId) { + LOG.info("Triggering bundle encryption for bundle id '{}", id); + final Optional maybeBundle = bundleDao.getBundle(id); + maybeBundle.ifPresentOrElse(bundle -> { + bundleDao.updateStatusForBundle(bundle.getId(), BundleStatus.ENCRYPTION_REQUESTED, DateTime.now(DateTimeZone.UTC)); + final OptionalException optionalException = + rabbitMqService.sendEncryptionMessage(encryptionMessageFactory.fromBundle(bundle), xRequestId); + optionalException.ifPresent(exception -> { + bundleDao.updateStatusForBundle(bundle.getId(), BundleStatus.BUNDLE_ERROR, DateTime.now(DateTimeZone.UTC)); + throw exception; + }); + }, () -> LOG.warn("Could not find a bundle for id '{}', will not send it for encryption.", id)); + } +} diff --git a/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/util/ConsumerFactory.java b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/util/ConsumerFactory.java new file mode 100644 index 0000000..ed78dfe --- /dev/null +++ b/appstore-bundle-service-application/src/main/java/com/lgi/appstorebundle/util/ConsumerFactory.java @@ -0,0 +1,97 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lgi.appstorebundle.external.RabbitMQConsumerMDC; +import com.lgi.appstorebundle.model.FeedbackMessage; +import com.rabbitmq.client.DeliverCallback; +import com.rabbitmq.client.Delivery; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Optional; +import java.util.function.BiConsumer; + +import static com.lgi.appstorebundle.common.Headers.CORRELATION_ID; + +@Component +public class ConsumerFactory { + private static final Logger LOG = LoggerFactory.getLogger(ConsumerFactory.class); + + private final ObjectMapper objectMapper; + + @Autowired + public ConsumerFactory(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public DeliverCallback createConsumer(BiConsumer messageConsumer) { + return (consumerTag, delivery) -> { + try { + consumeMessage(delivery, messageConsumer); + } catch (Exception e) { + LOG.error("Received message cannot be processed. Exception: '{}'.", e.getMessage()); + } finally { + RabbitMQConsumerMDC.clear(); + } + }; + } + + private void consumeMessage(Delivery delivery, BiConsumer messageConsumer) { + final Optional maybeXRequestId = getXRequestIdFromReceivedMessage(delivery); + maybeXRequestId.ifPresentOrElse(xRequestId -> + { + RabbitMQConsumerMDC.populate(xRequestId); + Optional maybeFeedbackMessage = getFeedbackMessage(delivery); + maybeFeedbackMessage.ifPresent(feedbackMessage -> { + logMessage(feedbackMessage); + messageConsumer.accept(feedbackMessage, xRequestId); + }); + }, + () -> LOG.warn("Received message does not have an 'x-request-id'. Cannot be processed.") + ); + } + + private Optional getXRequestIdFromReceivedMessage(Delivery delivery) { + return Optional.ofNullable(delivery.getProperties().getHeaders()) + .map(headers -> headers.get(CORRELATION_ID)) + .map(Object::toString) + .filter(xRequestId -> !xRequestId.isEmpty()); + } + + private Optional getFeedbackMessage(Delivery delivery) { + try { + return Optional.ofNullable(objectMapper.readValue(delivery.getBody(), FeedbackMessage.class)); + } catch (IOException exception) { + LOG.error("Received message cannot be parsed to FeedbackMessage. Exception: '{}'.", exception.getMessage()); + return Optional.empty(); + } + } + + private void logMessage(FeedbackMessage feedbackMessage) { + Optional.ofNullable(feedbackMessage.getError()) + .ifPresentOrElse( + errorMessage -> LOG.warn("Received message contains 'ErrorCode': '{}' and 'ErrorMessage': '{}'", errorMessage.getCode(), errorMessage.getMessage()), + () -> LOG.info("Received a message with 'phaseCode': '{}'.", feedbackMessage.getPhaseCode())); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-application/src/main/resources/config/application-dev.properties b/appstore-bundle-service-application/src/main/resources/config/application-dev.properties new file mode 100644 index 0000000..9e34900 --- /dev/null +++ b/appstore-bundle-service-application/src/main/resources/config/application-dev.properties @@ -0,0 +1,2 @@ +server.port=8081 +asms.service.url=http://localhost:8080 \ No newline at end of file diff --git a/appstore-bundle-service-application/src/main/resources/config/application.properties b/appstore-bundle-service-application/src/main/resources/config/application.properties new file mode 100644 index 0000000..5142c28 --- /dev/null +++ b/appstore-bundle-service-application/src/main/resources/config/application.properties @@ -0,0 +1,77 @@ +http.retry.after=${HTTP_RETRY_AFTER:30s} +query.timeout.seconds=${QUERY_TIMEOUT_SECONDS:50} +bundle.encryption.enabled=${BUNDLE_ENCRYPTION_ENABLED:true} +bundle.extension=${BUNDLE_EXTENSION:tar.gz} +environment=${ENVIRONMENT:DEV} +logging.level.org.jooq.tools.LoggerListener=DEBUG + +springdoc.swagger-ui.path=/swagger +springdoc.swagger-ui.url=/appstore-bundle-service.yaml + +spring.profiles.active=@spring.profiles.active@ + +asms.service.url=${APPSTORE_METADATA_SERVICE_URL} +asms.r4j.cb.name="AppStore Metadata Service client circuit breaker" +asms.r4j.cb.failure_rate_threshold=100 +asms.r4j.cb.wait_duration_in_open_state=5s +asms.r4j.cb.ring_buffer_size_in_half_open_state=1 +asms.r4j.cb.ring_buffer_size_in_closed_state=5 +asms.r4j.cb.is_automatic_transition_from_open_to_half_open_enabled=true + +asms.r4j.bh.name="AppStore Metadata Service client bulkhead" +asms.r4j.bh.maxConcurrentCalls=100 +asms.r4j.bh.maxWaitDuration=500ms +asms.r4j.bh.writableStackTraceEnabled=true + +asms.request.timeout=1000ms +asms.idle.timeout=60s + +rabbitmq.generationQueueName=${GENERATION_QUEUE_NAME:bundlegen-service-requests} +rabbitmq.generationStatusQueueName=${GENERATION_STATUS_QUEUE_NAME:bundlegen-service-status} +rabbitmq.encryptionQueueName=${ENCRYPTION_QUEUE_NAME:bundlecrypt-service-requests} +rabbitmq.encryptionStatusQueueName=${ENCRYPTION_STATUS_QUEUE_NAME:bundlecrypt-service-status} +rabbitmq.host.name=${RABBITMQ_HOST:localhost} +rabbitmq.port=${RABBITMQ_PORT:5672} +rabbitmq.url=amqp://${rabbitmq.host.name}:${rabbitmq.port} + +database.host.read=${READ_NODE_JDBC_HOST:localhost} +database.host.write=${WRITE_NODE_JDBC_HOST:localhost} +database.port=${JDBC_PORT:5432} +database.name=${JDBC_DATABASE_NAME:postgres} +database.schema=${JDBC_SCHEMA:appstore_bundle_service} + +spring.datasource.hikari.read.jdbcUrl=jdbc:postgresql://${database.host.read}:${database.port}/${database.name}?currentSchema=${database.schema}&ApplicationName=AppstoreBundleService +spring.datasource.hikari.read.username=${JDBC_USER:postgres} +spring.datasource.hikari.read.password=${JDBC_PASSWORD:postgres} +spring.datasource.hikari.read.poolName=asbs_read_node +spring.datasource.hikari.read.maximumPoolSize=${MAX_READ_NODE_POOL_SIZE:10} +spring.datasource.hikari.read.minimumIdle=2 +spring.datasource.hikari.read.connectionTimeout=1000 +spring.datasource.hikari.read.readOnly=true +spring.datasource.hikari.read.leakDetectionThreshold=180000 +spring.datasource.hikari.write.jdbcUrl=jdbc:postgresql://${database.host.write}:${database.port}/${database.name}?currentSchema=${database.schema}&ApplicationName=AppstoreBundleService +spring.datasource.hikari.write.username=${JDBC_USER:postgres} +spring.datasource.hikari.write.password=${JDBC_PASSWORD:postgres} +spring.datasource.hikari.write.poolName=asbs_write_node +spring.datasource.hikari.write.maximumPoolSize=${MAX_WRITE_NODE_POOL_SIZE:10} +spring.datasource.hikari.write.minimumIdle=2 +spring.datasource.hikari.write.connectionTimeout=1000 +spring.datasource.hikari.write.leakDetectionThreshold=180000 + +spring.jackson.default-property-inclusion=NON_NULL + +datasource.stack.name=${STACK_NAME:} +datasource.charset=UTF-8 + +spring.mvc.pathmatch.matching-strategy=ant_path_matcher + +management.endpoints.jmx.exposure.exclude=info +management.endpoint.info.enabled=true +management.info.defaults.enabled=false +management.endpoints.web.exposure.include=health,info,prometheus +management.endpoint.prometheus.enabled=${PROMETHEUS_METRICS:false} +management.metrics.web.server.request.autotime.enabled=true +server.tomcat.mbeanregistry.enabled=true +server.error.whitelabel.enabled=false +server.error.include-stacktrace=never +webApplications.list=HTML5,LIGHTNING \ No newline at end of file diff --git a/appstore-bundle-service-application/src/main/resources/static/appstore-bundle-service.yaml b/appstore-bundle-service-application/src/main/resources/static/appstore-bundle-service.yaml new file mode 100644 index 0000000..b6ac513 --- /dev/null +++ b/appstore-bundle-service-application/src/main/resources/static/appstore-bundle-service.yaml @@ -0,0 +1,101 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +openapi: 3.0.1 +info: + title: AppStore Bundle Service API + description: AppStore Bundle Service handles bundle generation process by interacting with Bundle Generator and Bundle Cryptor services. + version: 0.0.1 +paths: + /applications/{appId}/{appVersion}/{platformName}/{firmwareVersion}/{appBundleName}: + get: + tags: + - Applications + parameters: + - name: appId + in: path + required: true + schema: + type: string + - name: appVersion + in: path + required: true + schema: + type: string + - name: platformName + in: path + required: true + schema: + type: string + - name: firmwareVersion + in: path + required: true + schema: + type: string + - name: appBundleName + in: path + required: true + schema: + type: string + - name: x-request-id + in: header + required: true + schema: + type: string + responses: + 202: + description: Bundle generation request accepted + 404: + description: Application not found in AppStore Metadata Service + 400: + description: Invalid request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + default: + description: Unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" +components: + schemas: + ErrorResponse: + title: ErrorResponse + required: + - error + type: object + properties: + error: + required: + - details + - httpStatusCode + - message + type: object + properties: + httpStatusCode: + type: integer + format: int32 + message: + type: string + details: + type: string + correlationId: + type: string diff --git a/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/AppStoreBundleConfigurationTest.java b/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/AppStoreBundleConfigurationTest.java new file mode 100644 index 0000000..3f54e0c --- /dev/null +++ b/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/AppStoreBundleConfigurationTest.java @@ -0,0 +1,98 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle; + +import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.PortBinding; +import com.github.dockerjava.api.model.Ports; +import com.lgi.appstorebundle.resources.AppStoreBundleController; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.RabbitMQContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.testcontainers.containers.PostgreSQLContainer.POSTGRESQL_PORT; + +@SpringBootTest +@ContextConfiguration(initializers = AppStoreBundleConfigurationTest.Initializer.class) +@TestPropertySource(locations="classpath:unit-tests.properties") +@ActiveProfiles("dev") +class AppStoreBundleConfigurationTest { + + @Autowired + private AppStoreBundleController bundleResource; + + @Test + void sanityCheck() { + assertThat(bundleResource).isNotNull(); + } + + public static class Initializer implements ApplicationContextInitializer { + + private static final String RABBIT_MQ_IMAGE = "rabbitmq:3.8.19-management-alpine"; + private static final Integer RABBIT_MQ_PORT = 5672; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + createAndRunPostgresContainer(); + createAndRunRabbitMqContainer(); + } + + private void createAndRunPostgresContainer() { + var postgres = new PostgreSQLContainer<>("postgres:12"); + postgres.withDatabaseName("postgres"); + postgres.setHostAccessible(true); + postgres.withExposedPorts(POSTGRESQL_PORT); + postgres.withCreateContainerCmdModifier(cmd -> cmd.withHostConfig( + new HostConfig().withPortBindings(new PortBinding( + Ports.Binding.bindPort(5433), + new ExposedPort(POSTGRESQL_PORT)) + ) + )); + postgres.withUsername("postgres"); + postgres.withPassword("postgres"); + postgres.start(); + postgres.getExposedPorts(); + postgres.getPortBindings(); + } + + public void createAndRunRabbitMqContainer() { + final RabbitMQContainer rabbitMQContainer = new RabbitMQContainer(RABBIT_MQ_IMAGE); + rabbitMQContainer.withQueue("bundlegen-service-requests") + .withQueue("bundlegen-service-status") + .withQueue("bundlecrypt-service-requests") + .withQueue("bundlecrypt-service-status"); + rabbitMQContainer + .withExposedPorts(RABBIT_MQ_PORT) + .withCreateContainerCmdModifier(cmd -> cmd + .withPortBindings(new PortBinding(Ports.Binding.bindPort(RABBIT_MQ_PORT), + new ExposedPort(RABBIT_MQ_PORT)))); + rabbitMQContainer.start(); + } + } + +} \ No newline at end of file diff --git a/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/resources/AppStoreBundleControllerRealRequestsTest.java b/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/resources/AppStoreBundleControllerRealRequestsTest.java new file mode 100644 index 0000000..78ba84e --- /dev/null +++ b/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/resources/AppStoreBundleControllerRealRequestsTest.java @@ -0,0 +1,180 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.resources; + +import com.lgi.appstorebundle.api.ApplicationParams; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleStatus; +import com.lgi.appstorebundle.exception.RabbitMQException; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadata; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadataForMaintainer; +import com.lgi.appstorebundle.external.asms.model.Header; +import com.lgi.appstorebundle.external.asms.model.HeaderForMaintainer; +import com.lgi.appstorebundle.external.asms.model.Maintainer; +import com.lgi.appstorebundle.service.ApplicationMetadataService; +import com.lgi.appstorebundle.service.BundleService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.Duration; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(AppStoreBundleController.class) +class AppStoreBundleControllerRealRequestsTest { + + private static final String GET_APPLICATION_PATH = "/applications/{appId}/{appVersion}/{platformName}/{firmwareVersion}/{appBundleName}"; + private static final String CORRELATION_ID = "x-request-id"; + private static final String APP_ID = "applicationId"; + private static final String APP_VER = "applicationVersion"; + private static final String APP_NAME = "applicationName"; + private static final String URL = "url"; + private static final String PLATFORM_NAME = "platformName"; + private static final String FIRMWARE_VER = "firmwareVersion"; + private static final String OCI_IMAGE_URL = "ociImageUrl"; + private static final String APP_BUNDLE_NAME = "applicationBundleName"; + private static final String X_REQUEST_ID = "0293e324-0558-4f23-b924-25a8b3f59583"; + private final HeaderForMaintainer applicationHeaderForMaintainer = HeaderForMaintainer.create(APP_ID, APP_NAME, APP_VER, URL, OCI_IMAGE_URL); + private final Header applicationHeader = Header.create(APP_ID, APP_NAME, APP_VER, URL); + private final Maintainer applicationMaintainer = Maintainer.create("maintainerCode"); + private final ApplicationMetadataForMaintainer applicationMetadataForMaintainer = ApplicationMetadataForMaintainer.create(applicationHeaderForMaintainer); + private final ApplicationMetadata applicationMetadata = ApplicationMetadata.create(applicationHeader, applicationMaintainer); + + @Value("${http.retry.after}") + private Duration retryAfter; + + @Autowired + private AppStoreBundleController bundleController; + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ApplicationMetadataService applicationMetadataServiceMock; + + @MockBean + private BundleService bundleServiceMock; + + @Test + void givenValidRequestWhenApplicationDoesNotExistThenNotFound() throws Exception { + // GIVEN + when(applicationMetadataServiceMock.getApplicationMetadata(any(ApplicationParams.class))).thenReturn(Optional.empty()); + + // WHEN THEN + mockMvc.perform(get(GET_APPLICATION_PATH, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, APP_BUNDLE_NAME) + .header(CORRELATION_ID, X_REQUEST_ID) + ).andExpectAll( + status().is(HttpStatus.NOT_FOUND.value()), + header().doesNotExist("Retry-After") + ); + } + + @Test + void givenValidRequestWhenApplicationForMaintainerDoesNotExistThenNotFound() throws Exception { + // GIVEN + when(applicationMetadataServiceMock.getApplicationMetadata(any(ApplicationParams.class))).thenReturn(Optional.of(applicationMetadata)); + when(applicationMetadataServiceMock.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.empty()); + + // WHEN THEN + mockMvc.perform(get(GET_APPLICATION_PATH, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, APP_BUNDLE_NAME) + .header(CORRELATION_ID, X_REQUEST_ID) + ).andExpectAll( + status().is(HttpStatus.NOT_FOUND.value()), + header().doesNotExist("Retry-After") + ); + } + + @Test + void givenValidRequestWhenGenerationNotStartedThenGenerationRequested() throws Exception { + // GIVEN + when(applicationMetadataServiceMock.getApplicationMetadata(any(ApplicationParams.class))).thenReturn(Optional.of(applicationMetadata)); + when(applicationMetadataServiceMock.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.of(applicationMetadataForMaintainer)); + when(bundleServiceMock.getLatestBundle(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER)).thenReturn(Optional.empty()); + + // WHEN THEN + mockMvc.perform(get(GET_APPLICATION_PATH, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, APP_BUNDLE_NAME) + .header(CORRELATION_ID, X_REQUEST_ID) + ).andExpectAll( + status().is(HttpStatus.ACCEPTED.value()), + header().string("Retry-After", String.valueOf(retryAfter.toSeconds())) + ); + } + + @Test + void givenValidRequestWhenGenerationErrorThenTriggerNewGeneration() throws Exception { + // GIVEN + final Bundle bundle = mock(Bundle.class); + when(bundle.getStatus()).thenReturn(BundleStatus.BUNDLE_ERROR); + when(applicationMetadataServiceMock.getApplicationMetadata(any(ApplicationParams.class))).thenReturn(Optional.of(applicationMetadata)); + when(applicationMetadataServiceMock.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.of(applicationMetadataForMaintainer)); + when(bundleServiceMock.getLatestBundle(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER)).thenReturn(Optional.of(bundle)); + + // WHEN THEN + mockMvc.perform(get(GET_APPLICATION_PATH, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, APP_BUNDLE_NAME) + .header(CORRELATION_ID, X_REQUEST_ID) + ).andExpectAll( + status().is(HttpStatus.ACCEPTED.value()), + header().string("Retry-After", String.valueOf(retryAfter.toSeconds())) + ); + } + + @Test + void givenValidRequestWhenGenerationRequestedThenGenerationSkipped() throws Exception { + // GIVEN + final Bundle bundle = mock(Bundle.class); + when(bundle.getStatus()).thenReturn(BundleStatus.GENERATION_REQUESTED); + when(applicationMetadataServiceMock.getApplicationMetadata(any(ApplicationParams.class))).thenReturn(Optional.of(applicationMetadata)); + when(applicationMetadataServiceMock.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.of(applicationMetadataForMaintainer)); + when(bundleServiceMock.getLatestBundle(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER)).thenReturn(Optional.of(bundle)); + + // WHEN THEN + mockMvc.perform(get(GET_APPLICATION_PATH, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, APP_BUNDLE_NAME) + .header(CORRELATION_ID, X_REQUEST_ID) + ).andExpectAll( + status().is(HttpStatus.ACCEPTED.value()), + header().string("Retry-After", String.valueOf(retryAfter.toSeconds())) + ); + } + + @Test + void givenValidRequestWhenTriggeringGenerationThrowsExceptionThenInternalServerError() throws Exception { + // GIVEN + when(applicationMetadataServiceMock.getApplicationMetadata(any(ApplicationParams.class))).thenReturn(Optional.of(applicationMetadata)); + when(applicationMetadataServiceMock.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.of(applicationMetadataForMaintainer)); + when(bundleServiceMock.getLatestBundle(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER)).thenReturn(Optional.empty()); + doThrow(RabbitMQException.createDefault(X_REQUEST_ID, "error")).when(bundleServiceMock).triggerBundleGeneration(any()); + + // WHEN THEN + mockMvc.perform(get(GET_APPLICATION_PATH, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, APP_BUNDLE_NAME) + .header(CORRELATION_ID, X_REQUEST_ID) + ).andExpect(status().is(HttpStatus.INTERNAL_SERVER_ERROR.value())); + } +} diff --git a/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/resources/AppStoreBundleControllerTest.java b/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/resources/AppStoreBundleControllerTest.java new file mode 100644 index 0000000..2ec3d35 --- /dev/null +++ b/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/resources/AppStoreBundleControllerTest.java @@ -0,0 +1,164 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.resources; + +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleStatus; +import com.lgi.appstorebundle.error.exception.ApplicationNotFoundException; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadata; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadataForMaintainer; +import com.lgi.appstorebundle.external.asms.model.Header; +import com.lgi.appstorebundle.external.asms.model.HeaderForMaintainer; +import com.lgi.appstorebundle.external.asms.model.Maintainer; +import com.lgi.appstorebundle.service.ApplicationMetadataService; +import com.lgi.appstorebundle.service.BundleService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.ResponseEntity; + +import java.time.Duration; +import java.util.Optional; + +import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class AppStoreBundleControllerTest { + + private static final String RETRY_AFTER = "Retry-After"; + private static final Duration RETRY_AFTER_IN_SECONDS = Duration.ofSeconds(30); + public static final String APP_ID = "appId"; + public static final String APP_NAME = "appName"; + public static final String APP_VER = "applicationVersion"; + public static final String URL = "url"; + public static final String OCI_IMAGE_URL = "ociImageUrl"; + public static final String PLATFORM_NAME = "platformName"; + public static final String FIRMWARE_VERSION = "firmwareVersion"; + public static final String BUNDLE_NAME = "bundleName"; + public static final String X_REQUEST_ID = "xRequestId"; + + private final ApplicationMetadataService asmsService = mock(ApplicationMetadataService.class); + private final BundleService bundleService = mock(BundleService.class); + private final AppStoreBundleController resource = new AppStoreBundleController(RETRY_AFTER_IN_SECONDS, asmsService, bundleService, true); + private final HeaderForMaintainer applicationHeaderForMaintainer = HeaderForMaintainer.create(APP_ID, APP_NAME, APP_VER, URL, OCI_IMAGE_URL); + private final Header applicationHeader = Header.create(APP_ID, APP_NAME, APP_VER, URL); + private final Maintainer applicationMaintainer = Maintainer.create("maintainerCode"); + private final ApplicationMetadataForMaintainer applicationMetadataForMaintainer = ApplicationMetadataForMaintainer.create(applicationHeaderForMaintainer); + private final ApplicationMetadata applicationMetadata = ApplicationMetadata.create(applicationHeader, applicationMaintainer); + + @BeforeEach + void setUp() { + reset(asmsService, bundleService); + } + + @Test + void returnErrorWhenApplicationNotExistsInAppstoreMetadataService() { + // GIVEN + when(asmsService.getApplicationMetadata(any())).thenReturn(Optional.empty()); + + // WHEN, THEN + assertThrows(ApplicationNotFoundException.class, () -> resource.startBundleGeneration( + APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VERSION, BUNDLE_NAME, X_REQUEST_ID)); + } + + @Test + void returnErrorWhenApplicationForMaintainerNotExistsInAppstoreMetadataService() { + // GIVEN + when(asmsService.getApplicationMetadata(any())).thenReturn(Optional.of(applicationMetadata)); + when(asmsService.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.empty()); + + // WHEN, THEN + assertThrows(ApplicationNotFoundException.class, () -> resource.startBundleGeneration( + APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VERSION, BUNDLE_NAME, X_REQUEST_ID)); + } + + @Test + void returnRetryAfterAndTriggerBundleGeneration() { + // GIVEN + when(asmsService.getApplicationMetadata(any())).thenReturn(Optional.of(applicationMetadata)); + when(asmsService.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.of(applicationMetadataForMaintainer)); + when(bundleService.getLatestBundle(any(), any(), any(), any())).thenReturn(Optional.empty()); + + // WHEN + ResponseEntity response = resource.startBundleGeneration( + APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VERSION, BUNDLE_NAME, X_REQUEST_ID); + + // THEN + verify(bundleService).triggerBundleGeneration(any()); + assertEquals(SC_ACCEPTED, response.getStatusCodeValue(), "Service should return ACCEPTED status code"); + assertNotNull(response.getHeaders().get(RETRY_AFTER), "Service should return ACCEPTED status code"); + assertEquals( + String.valueOf(RETRY_AFTER_IN_SECONDS.toSeconds()), + response.getHeaders().get(RETRY_AFTER).get(0), "Service should return header 'Retry-After'" + ); + } + + @Test + void returnRetryAfterAndTriggerNewBundleGenerationBecauseOfBundleErrorStatus() { + // GIVEN + when(asmsService.getApplicationMetadata(any())).thenReturn(Optional.of(applicationMetadata)); + when(asmsService.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.of(applicationMetadataForMaintainer)); + final Bundle bundle = mock(Bundle.class); + when(bundle.getStatus()).thenReturn(BundleStatus.BUNDLE_ERROR); + when(bundleService.getLatestBundle(any(), any(), any(), any())).thenReturn(Optional.of(bundle)); + + // WHEN + ResponseEntity response = resource.startBundleGeneration( + APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VERSION, BUNDLE_NAME, X_REQUEST_ID); + + // THEN + verify(bundleService).triggerBundleGeneration(any()); + assertEquals(SC_ACCEPTED, response.getStatusCodeValue(), "Service should return ACCEPTED status code"); + assertNotNull(response.getHeaders().get(RETRY_AFTER), "Service should return ACCEPTED status code"); + assertEquals(String.valueOf( + RETRY_AFTER_IN_SECONDS.toSeconds()), + response.getHeaders().get(RETRY_AFTER).get(0), "Service should return header 'Retry-After'" + ); + } + + @Test + void returnRetryAfterWithoutStartingBundleGeneration() { + // GIVEN + when(asmsService.getApplicationMetadata(any())).thenReturn(Optional.of(applicationMetadata)); + when(asmsService.getApplicationMetadataForMaintainerCode(any(), any())).thenReturn(Optional.of(applicationMetadataForMaintainer)); + final Bundle bundle = mock(Bundle.class); + when(bundle.getStatus()).thenReturn(BundleStatus.GENERATION_REQUESTED); + when(bundleService.getLatestBundle(any(), any(), any(), any())).thenReturn(Optional.of(bundle)); + + // WHEN + ResponseEntity response = resource.startBundleGeneration( + APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VERSION, BUNDLE_NAME, X_REQUEST_ID); + + // THEN + verify(bundleService, never()).triggerBundleGeneration(any()); + assertEquals(SC_ACCEPTED, response.getStatusCodeValue(), "Service should return ACCEPTED status code"); + assertNotNull(response.getHeaders().get(RETRY_AFTER), "Service should return ACCEPTED status code"); + assertEquals( + String.valueOf(RETRY_AFTER_IN_SECONDS.toSeconds()), + response.getHeaders().get(RETRY_AFTER).get(0), "Service should return header 'Retry-After'" + ); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/service/BundleServiceTest.java b/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/service/BundleServiceTest.java new file mode 100644 index 0000000..b076001 --- /dev/null +++ b/appstore-bundle-service-application/src/test/java/com/lgi/appstorebundle/service/BundleServiceTest.java @@ -0,0 +1,152 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.service; + +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleContext; +import com.lgi.appstorebundle.api.model.BundleStatus; +import com.lgi.appstorebundle.exception.RabbitMQException; +import com.lgi.appstorebundle.external.OptionalException; +import com.lgi.appstorebundle.external.RabbitMQService; +import com.lgi.appstorebundle.model.EncryptionMessageFactory; +import com.lgi.appstorebundle.storage.persistent.BundleDao; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; +import java.util.UUID; + +import static com.lgi.appstorebundle.api.model.BundleStatus.GENERATION_REQUESTED; +import static java.util.UUID.randomUUID; +import static org.joda.time.DateTime.now; +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.atMostOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class BundleServiceTest { + + public static final UUID ID = randomUUID(); + public static final String APP_ID = "appId"; + public static final String APP_VERSION = "appVersion"; + public static final String PLATFORM_NAME = "platformName"; + public static final String FIRMWARE_VERSION = "firmwareVersion"; + public static final String OCI_IMAGE_URL = "ociImageUrl"; + public static final String X_REQUEST_ID = "xRequestId"; + + private final BundleDao dao = mock(BundleDao.class); + private final RabbitMQService rabbitMQ = mock(RabbitMQService.class); + private final EncryptionMessageFactory encryptionMessageFactory = mock(EncryptionMessageFactory.class); + private final BundleService service = new BundleService(dao, rabbitMQ, encryptionMessageFactory); + private final DateTime messageTimestamp = now(DateTimeZone.UTC); + private final ApplicationContext applicationContext = ApplicationContext.create(APP_ID, APP_VERSION, PLATFORM_NAME, FIRMWARE_VERSION); + private final Bundle bundle = Bundle.create(ID, applicationContext, GENERATION_REQUESTED, X_REQUEST_ID, messageTimestamp); + private final BundleContext bundleContext = BundleContext.create(bundle, OCI_IMAGE_URL, true); + + @Captor + private ArgumentCaptor bundleStatusCaptor; + + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); + reset(dao, rabbitMQ); + } + + @Test + void triggerBundleGeneration_SavingToDBAndSendingMessageExecuted() { + // GIVEN + when(rabbitMQ.sendGenerationMessage(any())).thenReturn(OptionalException.empty()); + + // WHEN + service.triggerBundleGeneration(bundleContext); + + // THEN + verify(dao).saveBundleWithStatus(any()); + verify(rabbitMQ).sendGenerationMessage(any()); + verify(dao, never()).updateStatusForBundle(any(), any(), any()); + } + + @Test + void triggerBundleGenerationWithException_SaveAndUpdateRowAndSendingMessageExecuted() { + // GIVEN + when(rabbitMQ.sendGenerationMessage(any())).thenReturn(OptionalException.of(RabbitMQException.createDefault(X_REQUEST_ID, "error"))); + + // WHEN, THEN + assertThrows(RabbitMQException.class, + () -> service.triggerBundleGeneration(bundleContext)); + + verify(dao).saveBundleWithStatus(any()); + verify(rabbitMQ).sendGenerationMessage(any()); + verify(dao).updateStatusForBundle(any(), any(), any()); + } + + @Test + void triggerBundleEncryptionWithNotExistingBundle_skipTriggeringEncryption() { + // GIVEN + when(dao.getBundle(bundle.getId())).thenReturn(Optional.empty()); + + // WHEN, THEN + service.triggerBundleEncryption(bundle.getId(), X_REQUEST_ID); + + verify(dao, never()).updateStatusForBundle(any(), any(), any()); + verify(rabbitMQ, never()).sendEncryptionMessage(any(), any()); + } + + @Test + void triggerBundleEncryptionWithExistingBundle_sendsMqMessage() { + // GIVEN + when(dao.getBundle(bundle.getId())).thenReturn(Optional.of(bundle)); + when(rabbitMQ.sendEncryptionMessage(any(), any())).thenReturn(OptionalException.empty()); + + // WHEN, THEN + service.triggerBundleEncryption(bundle.getId(), X_REQUEST_ID); + + verify(dao, atMostOnce()).updateStatusForBundle(any(), bundleStatusCaptor.capture(), any()); + verify(rabbitMQ).sendEncryptionMessage(any(), any()); + assertEquals(BundleStatus.ENCRYPTION_REQUESTED, bundleStatusCaptor.getValue()); + } + + @Test + void triggerBundleEncryptionWithException_updatesBundleStatusToError() { + // GIVEN + UUID bundleId = bundle.getId(); + when(dao.getBundle(bundleId)).thenReturn(Optional.of(bundle)); + when(rabbitMQ.sendEncryptionMessage(any(), any())).thenReturn(OptionalException.of(RabbitMQException.createDefault(X_REQUEST_ID, "error"))); + + // WHEN, THEN + assertThrows(RabbitMQException.class, + () -> service.triggerBundleEncryption(bundleId, X_REQUEST_ID)); + + verify(dao, times(2)).updateStatusForBundle(any(), bundleStatusCaptor.capture(), any()); + verify(rabbitMQ).sendEncryptionMessage(any(), any()); + assertEquals(BundleStatus.BUNDLE_ERROR, bundleStatusCaptor.getValue()); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-application/src/test/resources/logback-test.xml b/appstore-bundle-service-application/src/test/resources/logback-test.xml new file mode 100644 index 0000000..cd13c36 --- /dev/null +++ b/appstore-bundle-service-application/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + + %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/appstore-bundle-service-application/src/test/resources/unit-tests.properties b/appstore-bundle-service-application/src/test/resources/unit-tests.properties new file mode 100644 index 0000000..1bdf74e --- /dev/null +++ b/appstore-bundle-service-application/src/test/resources/unit-tests.properties @@ -0,0 +1,13 @@ +database.port=5433 +spring.flyway.enabled=false +rabbitmq.host.name=localhost +rabbitmq.port=5672 +rabbitmq.url=amqp://${rabbitmq.host.name}:${rabbitmq.port} +database.host.read=localhost +database.host.write=localhost +database.name=postgres +database.schema=appstore_bundle_service +spring.datasource.hikari.read.username=postgres +spring.datasource.hikari.read.password=postgres +spring.datasource.hikari.write.username=postgres +spring.datasource.hikari.write.password=postgres \ No newline at end of file diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/pom.xml b/appstore-bundle-service-external/appstore-metadata-service-client/pom.xml new file mode 100644 index 0000000..a2d4763 --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/pom.xml @@ -0,0 +1,63 @@ + + + + com.lgi.appstorebundle + appstore-bundle-service-external + 0.16.0-SNAPSHOT + + 4.0.0 + + appstore-metadata-service-client + + + 2.5.1 + + src/main/java/com/lgi/appstorebundle/external/asms/exception/*, + src/main/java/com/lgi/appstorebundle/external/asms/model/* + + + src/main/java/com/lgi/appstorebundle/external/asms/exception/*, + src/main/java/com/lgi/appstorebundle/external/asms/model/* + + + + + + com.lgi.appstorebundle + client-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + + + com.google.auto.value + auto-value + + + org.springframework.boot + spring-boot-starter-test + test + + + com.fasterxml.jackson.core + jackson-core + + + com.github.tomakehurst + wiremock + test + ${wiremock.version} + + + javax.servlet + javax.servlet-api + + + + + + \ No newline at end of file diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/AppstoreMetadataServiceClient.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/AppstoreMetadataServiceClient.java new file mode 100644 index 0000000..2fd8586 --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/AppstoreMetadataServiceClient.java @@ -0,0 +1,144 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms; + +import com.lgi.appstorebundle.api.ApplicationParams; +import com.lgi.appstorebundle.common.r4j.AsmsClientInvoker; +import com.lgi.appstorebundle.external.asms.exception.AppstoreMetadataServiceClientException; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadata; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadataForMaintainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +@Service +public class AppstoreMetadataServiceClient { + + private static final Logger LOG = LoggerFactory.getLogger(AppstoreMetadataServiceClient.class); + + private static final String ABOUT_TO_GET_APPLICATION_METADATA_BY_APP_ID_MSG_FMT = + "About to request AppStore Metadata Service for application id: '{}' and version: '{}'"; + private static final String ABOUT_TO_GET_APPLICATION_METADATA_BY_MAINTAINER_CODE_MSG_FMT = + "About to request AppStore Metadata Service for application id: '{}', version: '{}' and maintainerCode: '{}'"; + private static final String SUCCESSFUL_CALL_TO_ASMS_LOG_MESSAGE = "Successfully executed request to AppStore Metadata Service"; + + private static final String APPLICATION_ID_PATH_PARAM = "applicationId"; + private static final String PLATFORM_NAME_QUERY_PARAM = "platformName"; + private static final String FIRMWARE_VER_QUERY_PARAM = "firmwareVer"; + private static final String MAINTAINER_CODE_PARAM = "maintainerCode"; + public static final String GET_APPLICATION_BY_APP_ID_PATH = "apps/{" + APPLICATION_ID_PATH_PARAM + "}"; + public static final String GET_APPLICATION_BY_APP_ID_AND_MAINTAINER_CODE_PATH = + "maintainers/{" + MAINTAINER_CODE_PARAM + "}/apps/{" + APPLICATION_ID_PATH_PARAM + "}"; + + private final RestTemplate asmsRestTemplate; + private final AsmsClientInvoker clientInvoker; + + public AppstoreMetadataServiceClient(RestTemplate asmsRestTemplate, AsmsClientInvoker clientInvoker) { + this.asmsRestTemplate = asmsRestTemplate; + this.clientInvoker = clientInvoker; + } + + public Optional getApplicationByAppId(ApplicationParams appParams) { + LOG.info(ABOUT_TO_GET_APPLICATION_METADATA_BY_APP_ID_MSG_FMT, appParams.getApplicationId(), appParams.getAppVersion()); + return clientInvoker.invoke(() -> getApplicationMetadataById(appParams)); + } + + private Optional getApplicationMetadataById(ApplicationParams appParams) { + ResponseEntity response; + + Map parameters = new HashMap<>(); + parameters.put(APPLICATION_ID_PATH_PARAM, appParams.getFullyQualifiedApplicationName()); + + String urlTemplate = UriComponentsBuilder.fromUriString(GET_APPLICATION_BY_APP_ID_PATH) + .queryParam(PLATFORM_NAME_QUERY_PARAM, appParams.getPlatformName()) + .queryParam(FIRMWARE_VER_QUERY_PARAM, appParams.getFirmwareVersion()) + .encode() + .toUriString(); + + URI uriToCall = asmsRestTemplate.getUriTemplateHandler().expand(urlTemplate, parameters); + + try { + response = asmsRestTemplate.getForEntity( + uriToCall, + ApplicationMetadata.class + ); + } catch (HttpStatusCodeException e) { + return handleException(e); + } + LOG.info(SUCCESSFUL_CALL_TO_ASMS_LOG_MESSAGE); + return Optional.ofNullable(response.getBody()); + } + + public Optional getApplicationByIdAndMaintainerCode(ApplicationParams appParams, String maintainerCode) { + LOG.info(ABOUT_TO_GET_APPLICATION_METADATA_BY_MAINTAINER_CODE_MSG_FMT, appParams.getApplicationId(), appParams.getAppVersion(), maintainerCode); + return clientInvoker.invoke(() -> getApplicationByMaintainerAndAppId(appParams, maintainerCode)); + } + + private Optional getApplicationByMaintainerAndAppId(ApplicationParams applicationParams, + String maintainerCode) { + ResponseEntity response; + + Map parameters = new HashMap<>(); + parameters.put(MAINTAINER_CODE_PARAM, maintainerCode); + parameters.put(APPLICATION_ID_PATH_PARAM, applicationParams.getFullyQualifiedApplicationName()); + + String urlTemplate = UriComponentsBuilder.fromUriString(GET_APPLICATION_BY_APP_ID_AND_MAINTAINER_CODE_PATH) + .queryParam(PLATFORM_NAME_QUERY_PARAM, applicationParams.getPlatformName()) + .queryParam(FIRMWARE_VER_QUERY_PARAM, applicationParams.getFirmwareVersion()) + .encode() + .toUriString(); + + URI uriToCall = asmsRestTemplate.getUriTemplateHandler().expand(urlTemplate, parameters); + + try { + response = asmsRestTemplate.getForEntity( + uriToCall, + ApplicationMetadataForMaintainer.class + ); + } catch (HttpStatusCodeException e) { + return handleException(e); + } + LOG.info(SUCCESSFUL_CALL_TO_ASMS_LOG_MESSAGE); + return Optional.ofNullable(response.getBody()); + } + + private static Optional handleException(HttpStatusCodeException e, + Supplier> fallback) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return fallback.get(); + } + LOG.warn("Request to AppStore Metadata Service failed, code {}, reason: {}", e.getRawStatusCode(), e.getCause()); + throw new AppstoreMetadataServiceClientException(e.getResponseBodyAsString()); + } + + private static Optional handleException(HttpStatusCodeException e) { + return handleException(e, Optional::empty); + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsBulkheadConfiguration.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsBulkheadConfiguration.java new file mode 100644 index 0000000..530029c --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsBulkheadConfiguration.java @@ -0,0 +1,68 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.configuration; + +import autovalue.shaded.org.jetbrains.annotations.NotNull; +import com.lgi.appstorebundle.common.r4j.configuration.BulkheadConfiguration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +@Configuration +public class AsmsBulkheadConfiguration implements BulkheadConfiguration { + + @Value("${asms.r4j.bh.name}") + @NotNull + private String name; + + @Value("${asms.r4j.bh.maxConcurrentCalls}") + @NotNull + private Integer maxConcurrentCalls; + + @Value("${asms.r4j.bh.maxWaitDuration}") + @NotNull + @DurationUnit(ChronoUnit.MILLIS) + private Duration maxWaitDuration; + + @Value("${asms.r4j.bh.writableStackTraceEnabled}") + private Boolean writableStackTraceEnabled; + + @Override + public String getName() { + return name; + } + + @Override + public int getMaxConcurrentCalls() { + return maxConcurrentCalls; + } + + @Override + public Duration getMaxWaitDuration() { + return maxWaitDuration; + } + + @Override + public boolean isWritableStackTraceEnabled() { + return writableStackTraceEnabled == null || writableStackTraceEnabled; + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsCircuitBreakerConfiguration.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsCircuitBreakerConfiguration.java new file mode 100644 index 0000000..baf6e88 --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsCircuitBreakerConfiguration.java @@ -0,0 +1,84 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.configuration; + +import autovalue.shaded.org.jetbrains.annotations.NotNull; +import com.lgi.appstorebundle.common.r4j.configuration.CircuitBreakerConfiguration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +@Configuration +public class AsmsCircuitBreakerConfiguration implements CircuitBreakerConfiguration { + + @Value("${asms.r4j.cb.name}") + @NotNull + private String name; + + @Value("${asms.r4j.cb.failure_rate_threshold}") + @NotNull + private Float failureRateThreshold; + + @Value("${asms.r4j.cb.wait_duration_in_open_state}") + @NotNull + private Duration waitDurationInOpenState; + + @Value("${asms.r4j.cb.ring_buffer_size_in_half_open_state}") + @NotNull + private Integer ringBufferSizeInHalfOpenState; + + @Value("${asms.r4j.cb.ring_buffer_size_in_closed_state}") + @NotNull + private Integer ringBufferSizeInClosedState; + + @Value("${asms.r4j.cb.is_automatic_transition_from_open_to_half_open_enabled}") + @NotNull + private Boolean isAutomaticTransitionFromOpenToHalfOpenEnabled; + + @Override + public String getName() { + return name; + } + + @Override + public float getFailureRateThreshold() { + return failureRateThreshold; + } + + @Override + public Duration getWaitDurationInOpenState() { + return waitDurationInOpenState; + } + + @Override + public int getRingBufferSizeInHalfOpenState() { + return ringBufferSizeInHalfOpenState; + } + + @Override + public int getRingBufferSizeInClosedState() { + return ringBufferSizeInClosedState; + } + + @Override + public boolean isAutomaticTransitionFromOpenToHalfOpenEnabled() { + return isAutomaticTransitionFromOpenToHalfOpenEnabled; + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsRestTemplateConfiguration.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsRestTemplateConfiguration.java new file mode 100644 index 0000000..5c876a6 --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/configuration/AsmsRestTemplateConfiguration.java @@ -0,0 +1,46 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import java.time.Duration; + +@Configuration +public class AsmsRestTemplateConfiguration { + + @Bean("asmsRestTemplate") + public RestTemplate restTemplate(RestTemplateBuilder builder, + @Value("${asms.service.url}") String endpointUrl, + @Value("${asms.request.timeout}") Duration requestTimeout, + @Value("${asms.idle.timeout}") Duration idleTimeout) { + return builder + .setReadTimeout(idleTimeout) + .uriTemplateHandler(new DefaultUriBuilderFactory(endpointUrl)) + .messageConverters(new MappingJackson2HttpMessageConverter()) + .setReadTimeout(requestTimeout) + .build(); + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/exception/AppstoreMetadataServiceClientException.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/exception/AppstoreMetadataServiceClientException.java new file mode 100644 index 0000000..7ec818a --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/exception/AppstoreMetadataServiceClientException.java @@ -0,0 +1,26 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.exception; + +public class AppstoreMetadataServiceClientException extends RuntimeException { + + public AppstoreMetadataServiceClientException(String message) { + super(message); + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/ApplicationMetadata.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/ApplicationMetadata.java new file mode 100644 index 0000000..d07ad9e --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/ApplicationMetadata.java @@ -0,0 +1,39 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.auto.value.AutoValue; + +@AutoValue +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class ApplicationMetadata { + + public abstract Header getHeader(); + + public abstract Maintainer getMaintainer(); + + @JsonCreator + public static ApplicationMetadata create(@JsonProperty("header") Header header, + @JsonProperty("maintainer") Maintainer maintainer) { + return new AutoValue_ApplicationMetadata(header, maintainer); + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/ApplicationMetadataForMaintainer.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/ApplicationMetadataForMaintainer.java new file mode 100644 index 0000000..aebd8db --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/ApplicationMetadataForMaintainer.java @@ -0,0 +1,36 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.auto.value.AutoValue; + +@AutoValue +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class ApplicationMetadataForMaintainer { + + public abstract HeaderForMaintainer getHeader(); + + @JsonCreator + public static ApplicationMetadataForMaintainer create(@JsonProperty("header") HeaderForMaintainer header) { + return new AutoValue_ApplicationMetadataForMaintainer(header); + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/Header.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/Header.java new file mode 100644 index 0000000..c0c0c06 --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/Header.java @@ -0,0 +1,46 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.auto.value.AutoValue; + +@AutoValue +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class Header { + + public abstract String getId(); + + public abstract String getName(); + + public abstract String getVersion(); + + public abstract String getUrl(); + + @JsonCreator + public static Header create( + @JsonProperty("id") String id, + @JsonProperty("name") String name, + @JsonProperty("version") String version, + @JsonProperty("url") String url) { + return new AutoValue_Header(id, name, version, url); + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/HeaderForMaintainer.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/HeaderForMaintainer.java new file mode 100644 index 0000000..0fee604 --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/HeaderForMaintainer.java @@ -0,0 +1,49 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.auto.value.AutoValue; + +@AutoValue +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class HeaderForMaintainer { + + public abstract String getId(); + + public abstract String getName(); + + public abstract String getVersion(); + + public abstract String getUrl(); + + public abstract String getOciImageUrl(); + + @JsonCreator + public static HeaderForMaintainer create( + @JsonProperty("id") String id, + @JsonProperty("name") String name, + @JsonProperty("version") String version, + @JsonProperty("url") String url, + @JsonProperty("ociImageUrl") String ociImageUrl) { + return new AutoValue_HeaderForMaintainer(id, name, version, url, ociImageUrl); + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/Maintainer.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/Maintainer.java new file mode 100644 index 0000000..ed7e2f6 --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/main/java/com/lgi/appstorebundle/external/asms/model/Maintainer.java @@ -0,0 +1,36 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.auto.value.AutoValue; + +@AutoValue +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class Maintainer { + + public abstract String getCode(); + + @JsonCreator + public static Maintainer create(@JsonProperty("code") String code) { + return new AutoValue_Maintainer(code); + } +} diff --git a/appstore-bundle-service-external/appstore-metadata-service-client/src/test/java/com/lgi/appstorebundle/external/asms/AppstoreMetadataServiceClientTest.java b/appstore-bundle-service-external/appstore-metadata-service-client/src/test/java/com/lgi/appstorebundle/external/asms/AppstoreMetadataServiceClientTest.java new file mode 100644 index 0000000..c31b715 --- /dev/null +++ b/appstore-bundle-service-external/appstore-metadata-service-client/src/test/java/com/lgi/appstorebundle/external/asms/AppstoreMetadataServiceClientTest.java @@ -0,0 +1,233 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external.asms; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.core.Options; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.github.tomakehurst.wiremock.matching.UrlPattern; +import com.lgi.appstorebundle.api.ApplicationParams; +import com.lgi.appstorebundle.common.r4j.AsmsClientInvoker; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadata; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadataForMaintainer; +import com.lgi.appstorebundle.external.asms.model.Header; +import com.lgi.appstorebundle.external.asms.model.HeaderForMaintainer; +import com.lgi.appstorebundle.external.asms.model.Maintainer; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import java.time.Duration; +import java.util.Optional; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AppstoreMetadataServiceClientTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private static final String APP_ID = "MY_APP_ID"; + private static final String APP_VER = "MY_APP_VER"; + private static final String PLATFORM_NAME = "PLATFORM_NAME"; + private static final String FIRMWARE_VER = "FIRMWARE_VER"; + private static final String APP_BUNDLE_NAME = "APP_BUNDLE_NAME"; + private static final String MAINTAINER_CODE = "MAINTAINER_CODE"; + private static final String PLATFORM_NAME_QUERY_PARAM = "platformName"; + private static final String FIRMWARE_VER_QUERY_PARAM = "firmwareVer"; + private static final WireMockClassRule WIREMOCK = new WireMockClassRule(Options.DYNAMIC_PORT); + private AppstoreMetadataServiceClient appstoreMetadataServiceClient; + private CircuitBreaker circuitBreaker; + private static final String APP_NAME = "APP_NAME"; + private static final String APP_URL = "http://hostname/demo.id.appl/2.2/platformName/firmwareVer/demo.id.appl_2.2_platformName_firmwareVer.tar.gz"; + private static final String OCI_IMAGE_URL = "OCI_URL"; + + private static final ApplicationParams VALID_APP_PARAMS = + ApplicationParams.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, APP_BUNDLE_NAME); + + @AfterAll + static void afterAll() { + WIREMOCK.stop(); + } + + @BeforeAll + static void beforeAll() { + WIREMOCK.start(); + } + + @BeforeEach + void beforeEach() { + WIREMOCK.resetAll(); + circuitBreaker = createCircuitBreaker(); + appstoreMetadataServiceClient = new AppstoreMetadataServiceClient(buildAsmsRestTemplate(), buildClientInvoker()); + } + + @Test + void shouldExtractApplicationMetadataByAppIdOnSuccess() throws JsonProcessingException { + // GIVEN + ApplicationMetadata expected = ApplicationMetadata.create(Header.create(APP_ID, APP_NAME, APP_VER, APP_URL), Maintainer.create(MAINTAINER_CODE)); + + WIREMOCK.stubFor(get(new UrlPattern(containing("/apps/" + APP_ID + "%3A" + APP_VER), false)) + .withQueryParam(PLATFORM_NAME_QUERY_PARAM, equalTo(PLATFORM_NAME)) + .withQueryParam(FIRMWARE_VER_QUERY_PARAM, equalTo(FIRMWARE_VER)) + .willReturn(aResponse() + .withBody(objectMapper.writeValueAsString(expected)) + .withStatus(HttpStatus.SC_OK) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString()))); + + // WHEN + ApplicationMetadata actual = appstoreMetadataServiceClient.getApplicationByAppId(VALID_APP_PARAMS).orElseThrow(); + + // THEN + assertEquals(expected, actual); + } + + @Test + void shouldReturnEmptyOptionalWhenApplicationWithAppIdNotFound() { + // GIVEN + WIREMOCK.stubFor(get(anyUrl()) + .willReturn(aResponse() + .withBody("") + .withStatus(HttpStatus.SC_NOT_FOUND) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString()))); + + // WHEN + Optional maybeApplicationMetadata = appstoreMetadataServiceClient.getApplicationByAppId(VALID_APP_PARAMS); + + // THEN + assertTrue(maybeApplicationMetadata.isEmpty()); + } + + @Test + void shouldCloseCircuitWhenFailureThresholdPassedOnGetApplicationByAppId() { + // GIVEN + WIREMOCK.stubFor(get(anyUrl()).willReturn(aResponse().withStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR))); + + assertSame(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); + + // WHEN + for (int i = 0; i < 2; i++) { + try { + appstoreMetadataServiceClient.getApplicationByAppId(VALID_APP_PARAMS); + } catch (Exception ex) { + // consume exception + } + } + + // WHEN + assertSame(CircuitBreaker.State.OPEN, circuitBreaker.getState()); + } + + @Test + void shouldExtractApplicationMetadataByMaintainerCodeOnSuccess() throws JsonProcessingException { + // GIVEN + ApplicationMetadataForMaintainer expected = + ApplicationMetadataForMaintainer.create(HeaderForMaintainer.create(APP_ID, APP_NAME, APP_VER, APP_URL, OCI_IMAGE_URL)); + + WIREMOCK.stubFor(get(new UrlPattern(containing("/maintainers/" + MAINTAINER_CODE +"/apps/" + APP_ID + "%3A" + APP_VER), false)) + .withQueryParam(PLATFORM_NAME_QUERY_PARAM, equalTo(PLATFORM_NAME)) + .withQueryParam(FIRMWARE_VER_QUERY_PARAM, equalTo(FIRMWARE_VER)) + .willReturn(aResponse() + .withBody(objectMapper.writeValueAsString(expected)) + .withStatus(HttpStatus.SC_OK) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString()))); + + // WHEN + ApplicationMetadataForMaintainer actual = + appstoreMetadataServiceClient.getApplicationByIdAndMaintainerCode(VALID_APP_PARAMS, MAINTAINER_CODE).get(); + + // THEN + assertEquals(expected, actual); + } + + @Test + void shouldReturnEmptyOptionalWhenApplicationWithMaintainerCodeNotFound() { + // GIVEN + WIREMOCK.stubFor(get(anyUrl()) + .willReturn(aResponse() + .withBody("") + .withStatus(HttpStatus.SC_NOT_FOUND) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString()))); + + // WHEN + Optional maybeApplicationMetadata = + appstoreMetadataServiceClient.getApplicationByIdAndMaintainerCode(VALID_APP_PARAMS, MAINTAINER_CODE); + + // THEN + assertTrue(maybeApplicationMetadata.isEmpty()); + } + + @Test + void shouldCloseCircuitWhenFailureThresholdPassedOnGetApplicationByMaintainerCode() { + // GIVEN + WIREMOCK.stubFor(get(anyUrl()).willReturn(aResponse().withStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR))); + + assertSame(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); + + // WHEN + for (int i = 0; i < 2; i++) { + try { + appstoreMetadataServiceClient.getApplicationByIdAndMaintainerCode(VALID_APP_PARAMS, MAINTAINER_CODE); + } catch (Exception ex) { + // consume exception + } + } + + // WHEN + assertSame(CircuitBreaker.State.OPEN, circuitBreaker.getState()); + } + + private RestTemplate buildAsmsRestTemplate() { + RestTemplateBuilder builder = new RestTemplateBuilder(); + return builder + .uriTemplateHandler(new DefaultUriBuilderFactory("http://localhost:" + WIREMOCK.port())) + .messageConverters(new MappingJackson2HttpMessageConverter()) + .setReadTimeout(Duration.ofSeconds(2)) + .build(); + } + + private AsmsClientInvoker buildClientInvoker() { + return new AsmsClientInvoker(circuitBreaker, null); + } + + private static CircuitBreaker createCircuitBreaker() { + CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() + .failureRateThreshold(100) + .ringBufferSizeInClosedState(2) + .ringBufferSizeInHalfOpenState(2) + .build(); + return CircuitBreaker.of("test circuit breaker name", circuitBreakerConfig); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-external/client-common/pom.xml b/appstore-bundle-service-external/client-common/pom.xml new file mode 100644 index 0000000..b1b82ed --- /dev/null +++ b/appstore-bundle-service-external/client-common/pom.xml @@ -0,0 +1,68 @@ + + + + com.lgi.appstorebundle + appstore-bundle-service-external + 0.16.0-SNAPSHOT + + 4.0.0 + + client-common + + + + src/main/java/com/lgi/appstorebundle/common/configuration/*, + src/main/java/com/lgi/appstorebundle/common/r4j/exception/* + + + src/main/java/com/lgi/appstorebundle/common/configuration/*, + src/main/java/com/lgi/appstorebundle/common/r4j/exception/* + + + + + + org.springframework.boot + spring-boot-starter + + + io.github.resilience4j + resilience4j-circuitbreaker + + + io.github.resilience4j + resilience4j-bulkhead + + + io.github.resilience4j + resilience4j-core + + + com.google.guava + guava + + + io.github.resilience4j + resilience4j-prometheus + + + io.prometheus + simpleclient + + + org.assertj + assertj-core + + + org.mockito + mockito-core + test + + + org.awaitility + awaitility + + + \ No newline at end of file diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/Headers.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/Headers.java new file mode 100644 index 0000000..5ee8e68 --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/Headers.java @@ -0,0 +1,27 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common; + +public final class Headers { + + private Headers() { + } + + public static final String CORRELATION_ID = "x-request-id"; +} diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/AsmsClientInvoker.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/AsmsClientInvoker.java new file mode 100644 index 0000000..44e2775 --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/AsmsClientInvoker.java @@ -0,0 +1,87 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j; + +import io.github.resilience4j.bulkhead.Bulkhead; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.core.SupplierUtils; +import org.springframework.lang.Nullable; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +public class AsmsClientInvoker { + + private final CircuitBreaker circuitBreaker; + private final Bulkhead bulkhead; + + public AsmsClientInvoker(CircuitBreaker circuitBreaker, @Nullable Bulkhead bulkhead) { + this.circuitBreaker = requireNonNull(circuitBreaker, "circuitBreaker"); + this.bulkhead = bulkhead; + } + + public CircuitBreaker getCircuitBreaker() { + return circuitBreaker; + } + + public T invoke(Supplier task) { + return decorateSupplier(task).get(); + } + + public T invoke(Supplier task, Function fallback) { + final Supplier decorated = decorateSupplier(task); + + return SupplierUtils.recover(decorated, fallback).get(); + } + + public void invoke(Runnable task) { + decorateRunnable(task).run(); + } + + public CompletableFuture invokeCompletionStage(Supplier> task) { + return decorateCompletionStage(task).get() + .toCompletableFuture(); + } + + public Supplier decorateSupplier(Supplier task) { + Supplier circuitBreakerDecorated = CircuitBreaker.decorateSupplier(circuitBreaker, task); + return Optional.ofNullable(bulkhead) + .map(bh -> Bulkhead.decorateSupplier(bh, circuitBreakerDecorated)) + .orElse(circuitBreakerDecorated); + } + + public Supplier> decorateCompletionStage(Supplier> task) { + Supplier> circuitBreakerDecorated = CircuitBreaker.decorateCompletionStage(circuitBreaker, task); + return Optional.ofNullable(bulkhead) + .map(bh -> Bulkhead.decorateCompletionStage(bh, circuitBreakerDecorated)) + .orElse(circuitBreakerDecorated); + } + + public Runnable decorateRunnable(Runnable task) { + Runnable circuitBreakerDecorated = CircuitBreaker.decorateRunnable(circuitBreaker, task); + return Optional.ofNullable(bulkhead) + .map(bh -> Bulkhead.decorateRunnable(bh, circuitBreakerDecorated)) + .orElse(circuitBreakerDecorated); + } +} diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/BulkheadFactory.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/BulkheadFactory.java new file mode 100644 index 0000000..2eb442e --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/BulkheadFactory.java @@ -0,0 +1,61 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j; + +import com.lgi.appstorebundle.common.r4j.configuration.BulkheadConfiguration; +import io.github.resilience4j.bulkhead.Bulkhead; +import io.github.resilience4j.bulkhead.BulkheadConfig; +import io.github.resilience4j.bulkhead.BulkheadRegistry; +import io.github.resilience4j.prometheus.collectors.BulkheadMetricsCollector; +import io.prometheus.client.CollectorRegistry; + +public final class BulkheadFactory { + + private static final BulkheadRegistry BULKHEAD_REGISTRY = BulkheadRegistry.ofDefaults(); + private static final BulkheadMetricsCollector BULKHEAD_METRICS_COLLECTOR = BulkheadMetricsCollector.ofBulkheadRegistry(BULKHEAD_REGISTRY); + + static { + CollectorRegistry.defaultRegistry.register(BULKHEAD_METRICS_COLLECTOR); + } + + private BulkheadFactory() { + } + + public static Bulkhead create(BulkheadConfiguration configuration) { + return create(configuration, CollectorRegistry.defaultRegistry); + } + + public static Bulkhead create(BulkheadConfiguration configuration, CollectorRegistry collectorRegistry) { + BulkheadConfig bulkheadConfig = new BulkheadConfig.Builder() + .maxConcurrentCalls(configuration.getMaxConcurrentCalls()) + .maxWaitDuration(configuration.getMaxWaitDuration()) + .writableStackTraceEnabled(configuration.isWritableStackTraceEnabled()) + .build(); + + if (CollectorRegistry.defaultRegistry.equals(collectorRegistry)) { + return BULKHEAD_REGISTRY.bulkhead(configuration.getName(), bulkheadConfig); + } + + final BulkheadRegistry bulkheadRegistry = BulkheadRegistry.of(bulkheadConfig); + collectorRegistry.register(BulkheadMetricsCollector.ofBulkheadRegistry(bulkheadRegistry)); + + return bulkheadRegistry.bulkhead(configuration.getName()); + } + +} diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/CircuitBreakerFactory.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/CircuitBreakerFactory.java new file mode 100644 index 0000000..d5e56a6 --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/CircuitBreakerFactory.java @@ -0,0 +1,76 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j; + +import com.lgi.appstorebundle.common.r4j.configuration.CircuitBreakerConfiguration; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.prometheus.collectors.CircuitBreakerMetricsCollector; +import io.prometheus.client.CollectorRegistry; + +import java.util.function.Consumer; + +public final class CircuitBreakerFactory { + + private static final CircuitBreakerRegistry CIRCUIT_BREAKER_REGISTRY = CircuitBreakerRegistry.ofDefaults(); + private static final CircuitBreakerMetricsCollector CIRCUIT_BREAKER_METRICS_COLLECTOR = + CircuitBreakerMetricsCollector.ofCircuitBreakerRegistry(CIRCUIT_BREAKER_REGISTRY); + + static { + CollectorRegistry.defaultRegistry.register(CIRCUIT_BREAKER_METRICS_COLLECTOR); + } + + private CircuitBreakerFactory() { + } + + public static CircuitBreaker create(CircuitBreakerConfiguration configuration) { + return create(configuration, CollectorRegistry.defaultRegistry); + } + + public static CircuitBreaker create(CircuitBreakerConfiguration configuration, Consumer additionalConfig) { + return create(configuration, CollectorRegistry.defaultRegistry, additionalConfig); + } + + public static CircuitBreaker create(CircuitBreakerConfiguration configuration, CollectorRegistry metricsRegistry) { + return create(configuration, metricsRegistry, builder -> {}); + } + + public static CircuitBreaker create(CircuitBreakerConfiguration configuration, + CollectorRegistry metricsRegistry, + Consumer additionalConfig) { + CircuitBreakerConfig.Builder builder = new CircuitBreakerConfig.Builder() + .failureRateThreshold(configuration.getFailureRateThreshold()) + .waitDurationInOpenState(configuration.getWaitDurationInOpenState()) + .permittedNumberOfCallsInHalfOpenState(configuration.getRingBufferSizeInHalfOpenState()) + .slidingWindowSize(configuration.getRingBufferSizeInClosedState()) + .automaticTransitionFromOpenToHalfOpenEnabled(configuration.isAutomaticTransitionFromOpenToHalfOpenEnabled()); + + additionalConfig.accept(builder); + + if (CollectorRegistry.defaultRegistry.equals(metricsRegistry)) { + return CIRCUIT_BREAKER_REGISTRY.circuitBreaker(configuration.getName(), builder.build()); + } + + CircuitBreakerRegistry cbRegistry = CircuitBreakerRegistry.of(builder.build()); + metricsRegistry.register(CircuitBreakerMetricsCollector.ofCircuitBreakerRegistry(cbRegistry)); + + return cbRegistry.circuitBreaker(configuration.getName()); + } +} diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/ClientInvokerFactory.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/ClientInvokerFactory.java new file mode 100644 index 0000000..d495b17 --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/ClientInvokerFactory.java @@ -0,0 +1,65 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j; + +import com.lgi.appstorebundle.common.r4j.configuration.BulkheadConfiguration; +import com.lgi.appstorebundle.common.r4j.configuration.CircuitBreakerConfiguration; +import com.lgi.appstorebundle.common.r4j.exception.RecoverableException; +import io.github.resilience4j.bulkhead.Bulkhead; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ClientInvokerFactory { + + private ClientInvokerFactory() { + } + + public static AsmsClientInvoker createClientInvoker(CircuitBreakerConfiguration circuitBreakerConfig, + Class clazz) { + CircuitBreaker circuitBreaker = createCircuitBreaker(circuitBreakerConfig, LoggerFactory.getLogger(clazz)); + return new AsmsClientInvoker(circuitBreaker, null); + } + + public static AsmsClientInvoker createClientInvoker(CircuitBreakerConfiguration circuitBreakerConfig, + BulkheadConfiguration bulkheadConfiguration, Class clazz) { + CircuitBreaker circuitBreaker = createCircuitBreaker(circuitBreakerConfig, LoggerFactory.getLogger(clazz)); + Bulkhead bulkhead = createBulkhead(bulkheadConfiguration, LoggerFactory.getLogger(clazz)); + return new AsmsClientInvoker(circuitBreaker, bulkhead); + } + + private static Bulkhead createBulkhead(BulkheadConfiguration bulkheadConfiguration, Logger logger) { + Bulkhead bulkhead = BulkheadFactory.create(bulkheadConfiguration); + bulkhead.getEventPublisher() + .onCallRejected(event -> logger.warn("Bulkhead execution rejected: name={}", bulkhead.getName())); + return bulkhead; + } + + private static CircuitBreaker createCircuitBreaker(CircuitBreakerConfiguration circuitBreakerConfig, Logger logger) { + CircuitBreaker circuitBreaker = CircuitBreakerFactory.create(circuitBreakerConfig, builder -> builder.ignoreException(RecoverableException.class::isInstance)); + circuitBreaker.getEventPublisher() + .onIgnoredError(event -> logger.warn("CircuitBreaker ignored exception occurred: name={}, message={}", circuitBreaker.getName(), event.getThrowable().getMessage())) + .onError(event -> logger.warn("CircuitBreaker execution failed: name={}", circuitBreaker.getName(), event.getThrowable())) + .onSuccess(event -> logger.debug("CircuitBreaker execution succeeded: name={}", circuitBreaker.getName())) + .onCallNotPermitted(event -> logger.debug("CircuitBreaker call is not permitted: name={}", circuitBreaker.getName())) + .onStateTransition(event -> + logger.info("CircuitBreaker state transition: name={}, transition={}", circuitBreaker.getName(), event.getStateTransition())); + return circuitBreaker; + } +} diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/configuration/BulkheadConfiguration.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/configuration/BulkheadConfiguration.java new file mode 100644 index 0000000..de9e8c3 --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/configuration/BulkheadConfiguration.java @@ -0,0 +1,32 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j.configuration; + +import java.time.Duration; + +public interface BulkheadConfiguration { + + String getName(); + + int getMaxConcurrentCalls(); + + Duration getMaxWaitDuration(); + + boolean isWritableStackTraceEnabled(); +} diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/configuration/CircuitBreakerConfiguration.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/configuration/CircuitBreakerConfiguration.java new file mode 100644 index 0000000..30aafac --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/configuration/CircuitBreakerConfiguration.java @@ -0,0 +1,37 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j.configuration; + +import java.time.Duration; + +public interface CircuitBreakerConfiguration { + + String getName(); + + float getFailureRateThreshold(); + + Duration getWaitDurationInOpenState(); + + int getRingBufferSizeInHalfOpenState(); + + int getRingBufferSizeInClosedState(); + + boolean isAutomaticTransitionFromOpenToHalfOpenEnabled(); + +} diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/exception/DefaultRecoverableUpstreamServiceException.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/exception/DefaultRecoverableUpstreamServiceException.java new file mode 100644 index 0000000..c190cfb --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/exception/DefaultRecoverableUpstreamServiceException.java @@ -0,0 +1,26 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j.exception; + +public class DefaultRecoverableUpstreamServiceException extends RuntimeException implements RecoverableException { + + public DefaultRecoverableUpstreamServiceException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/exception/RecoverableException.java b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/exception/RecoverableException.java new file mode 100644 index 0000000..a5c2441 --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/main/java/com/lgi/appstorebundle/common/r4j/exception/RecoverableException.java @@ -0,0 +1,26 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j.exception; + +/** + * Indicates to circuit breaker that the exception should not cause opening of the circuit and let the requests flow. + */ +public interface RecoverableException { + +} \ No newline at end of file diff --git a/appstore-bundle-service-external/client-common/src/test/java/com/lgi/appstorebundle/common/r4j/ClientInvokerFactoryTest.java b/appstore-bundle-service-external/client-common/src/test/java/com/lgi/appstorebundle/common/r4j/ClientInvokerFactoryTest.java new file mode 100644 index 0000000..1c164a7 --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/test/java/com/lgi/appstorebundle/common/r4j/ClientInvokerFactoryTest.java @@ -0,0 +1,89 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j; + +import com.lgi.appstorebundle.common.r4j.configuration.BulkheadConfiguration; +import com.lgi.appstorebundle.common.r4j.configuration.CircuitBreakerConfiguration; +import com.lgi.appstorebundle.common.r4j.exception.DefaultRecoverableUpstreamServiceException; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import org.junit.jupiter.api.Test; +import java.time.Duration; + +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ClientInvokerFactoryTest { + + private static final int RING_BUFFER_SIZE = 2; + private static final boolean WRITABLE_STACK_TRACE_ENABLED = true; + + @Test + void givenCircuitBreakerConfigWhenCreateInvokerThenCreated() { + //GIVEN, WHEN + AsmsClientInvoker invoker = ClientInvokerFactory.createClientInvoker(testCircuitBreaker(), ClientInvokerFactoryTest.class); + //THEN + assertNotNull(invoker); + } + + @Test + void givenCircuitBreakerConfigWhenRecoverableExceptionThenIgnored() { + //GIVEN + AsmsClientInvoker invoker = ClientInvokerFactory.createClientInvoker(testCircuitBreaker(), ClientInvokerFactoryTest.class); + //WHEN + for (int i = 0; i < RING_BUFFER_SIZE + 1; i++) { + invoker.invoke(() -> { + throw new DefaultRecoverableUpstreamServiceException("error"); + }, Function.identity()); + } + //THEN + assertEquals(CircuitBreaker.State.CLOSED, invoker.getCircuitBreaker().getState()); + } + + @Test + void givenCircuitBreakerConfigWithBulkheadWhenCreateInvokerThenCreated() { + //GIVEN, WHEN + AsmsClientInvoker invoker = ClientInvokerFactory.createClientInvoker(testCircuitBreaker(), testBulkhead(), ClientInvokerFactoryTest.class); + //THEN + assertNotNull(invoker); + } + + private BulkheadConfiguration testBulkhead() { + BulkheadConfiguration bulkheadConfigurationMock = mock(BulkheadConfiguration.class); + when(bulkheadConfigurationMock.getName()).thenReturn("bh"); + when(bulkheadConfigurationMock.getMaxConcurrentCalls()).thenReturn(2); + when(bulkheadConfigurationMock.getMaxWaitDuration()).thenReturn(Duration.ofMillis(500)); + when(bulkheadConfigurationMock.isWritableStackTraceEnabled()).thenReturn(WRITABLE_STACK_TRACE_ENABLED); + return bulkheadConfigurationMock; + } + + private CircuitBreakerConfiguration testCircuitBreaker() { + CircuitBreakerConfiguration cbMock = mock(CircuitBreakerConfiguration.class); + when(cbMock.getName()).thenReturn("cb"); + when(cbMock.getFailureRateThreshold()).thenReturn( 100f); + when(cbMock.getWaitDurationInOpenState()).thenReturn(Duration.ofMillis(500)); + when(cbMock.getRingBufferSizeInHalfOpenState()).thenReturn(RING_BUFFER_SIZE); + when(cbMock.getRingBufferSizeInClosedState()).thenReturn(RING_BUFFER_SIZE); + when(cbMock.isAutomaticTransitionFromOpenToHalfOpenEnabled()).thenReturn(true); + return cbMock; + } + +} \ No newline at end of file diff --git a/appstore-bundle-service-external/client-common/src/test/java/com/lgi/appstorebundle/common/r4j/ClientInvokerTest.java b/appstore-bundle-service-external/client-common/src/test/java/com/lgi/appstorebundle/common/r4j/ClientInvokerTest.java new file mode 100644 index 0000000..548b829 --- /dev/null +++ b/appstore-bundle-service-external/client-common/src/test/java/com/lgi/appstorebundle/common/r4j/ClientInvokerTest.java @@ -0,0 +1,219 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.common.r4j; + +import com.lgi.appstorebundle.common.r4j.exception.RecoverableException; +import io.github.resilience4j.bulkhead.Bulkhead; +import io.github.resilience4j.bulkhead.BulkheadConfig; +import io.github.resilience4j.bulkhead.BulkheadFullException; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +class ClientInvokerTest { + + private static final int CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD = 2; + + private static final int MAX_CONCURRENT_CALLS = 2; + private static final Duration MAX_WAIT_DURATION = Duration.ofMillis(100); + + private AsmsClientInvoker clientInvoker; + private CircuitBreaker circuitBreaker; + private Bulkhead bulkhead; + + @BeforeEach + void setUp() { + circuitBreaker = createCircuitBreaker(); + bulkhead = createBulkhead(MAX_CONCURRENT_CALLS, MAX_WAIT_DURATION); + clientInvoker = new AsmsClientInvoker(circuitBreaker, bulkhead); + } + + @Test + void circuitOpensOnExceptionWhenInvoke() { + //GIVEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED); + //WHEN + runDecoratedTask(() -> clientInvoker.invoke(this::throwRuntimeException)); + //THEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN); + } + + @Test + void callRejectedIfBulkheadFullWhenInvoke() { + //GIVEN + Bulkhead fullBulkhead = createBulkhead(0, Duration.ZERO); + CircuitBreaker circuitBreaker = createCircuitBreaker(); + clientInvoker = new AsmsClientInvoker(circuitBreaker, fullBulkhead); + //WHEN, THEN + assertThatExceptionOfType(BulkheadFullException.class) + .isThrownBy(() -> clientInvoker.invoke(() -> null)); + } + + @Test + void circuitOpensOnExceptionWhenInvokeWithFallback() { + //GIVEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED); + //WHEN + runDecoratedTask(() -> clientInvoker.invoke(this::throwRuntimeException, ex -> "fallback")); + //THEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN); + } + + @Test + void fallbackAppliedIfBulkheadFullWhenInvoke() { + //GIVEN + Bulkhead fullBulkhead = createBulkhead(0, Duration.ZERO); + CircuitBreaker circuitBreaker = createCircuitBreaker(); + clientInvoker = new AsmsClientInvoker(circuitBreaker, fullBulkhead); + //WHEN, THEN + String result = clientInvoker.invoke(() -> null, ex -> "fallback"); + assertThat(result).isEqualTo("fallback"); + } + + @Test + void circuitOpensOnExceptionWhenInvokeAsync() { + //GIVEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED); + //WHEN + runDecoratedTask(() -> clientInvoker.invokeCompletionStage(() -> CompletableFuture.failedFuture(new RuntimeException()))); + //THEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN); + } + + @Test + void callRejectedIfBulkheadFullWhenInvokeAsync() { + //GIVEN + Bulkhead fullBulkhead = createBulkhead(0, Duration.ZERO); + CircuitBreaker circuitBreaker = createCircuitBreaker(); + clientInvoker = new AsmsClientInvoker(circuitBreaker, fullBulkhead); + //WHEN, THEN + assertThat(clientInvoker.invokeCompletionStage(() -> CompletableFuture.completedFuture("result"))) + .hasFailedWithThrowableThat() + .isInstanceOf(BulkheadFullException.class); + } + + @Test + void circuitOpensOnExceptionWhenRunAsync() { + //GIVEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED); + //WHEN + runDecoratedTask(() -> clientInvoker.invoke(this::throwRuntimeException)); + //THEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN); + } + + @Test + void circuitClosesOnSuccessfulCallsWhenRunAsync() { + //GIVEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED); + runDecoratedTask(() -> clientInvoker.invoke(this::throwRuntimeException)); + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN); + await().atMost(300L, TimeUnit.MILLISECONDS) + .untilAsserted(() -> assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.HALF_OPEN)); + //WHEN + runDecoratedTask(() -> clientInvoker.invoke(() -> "")); + //THEN + assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED); + } + + @Test + void callRejectedIfBulkheadFullWhenInvokeRunnable() { + //GIVEN + Bulkhead fullBulkhead = createBulkhead(0, Duration.ZERO); + CircuitBreaker circuitBreaker = createCircuitBreaker(); + clientInvoker = new AsmsClientInvoker(circuitBreaker, fullBulkhead); + //WHEN, THEN + assertThatExceptionOfType(BulkheadFullException.class) + .isThrownBy(() -> clientInvoker.invoke(() -> {})); + } + + @Test + void runsCircuitBreakerOnlyIfNullBulkheadWhenInvoke() { + //GIVEN + CircuitBreaker circuitBreaker = createCircuitBreaker(); + clientInvoker = new AsmsClientInvoker(circuitBreaker, null); + //WHEN, THEN + String result = assertDoesNotThrow(() -> clientInvoker.invoke(() -> "result")); + assertThat(result).isEqualTo("result"); + } + + @Test + void runsCircuitBreakerOnlyIfNullBulkheadWhenInvokeCompletionStage() { + //GIVEN + CircuitBreaker circuitBreaker = createCircuitBreaker(); + clientInvoker = new AsmsClientInvoker(circuitBreaker, null); + //WHEN, THEN + CompletableFuture result = assertDoesNotThrow(() -> clientInvoker.invokeCompletionStage(() -> CompletableFuture.completedFuture("result"))); + assertThat(result).isCompleted(); + } + + @Test + void runsCircuitBreakerOnlyIfNullBulkheadWhenInvokeRunnable() { + //GIVEN + CircuitBreaker circuitBreaker = createCircuitBreaker(); + clientInvoker = new AsmsClientInvoker(circuitBreaker, null); + //WHEN, THEN + assertDoesNotThrow(() -> clientInvoker.invoke(() -> {})); + } + + + private void runDecoratedTask(Runnable task) { + for (int i = 0; i < CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD; i++) { + try { + task.run(); + } catch (Exception ex) { + //consumed + } + } + } + + private String throwRuntimeException() { + throw new RuntimeException(); + } + + private CircuitBreaker createCircuitBreaker() { + CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() + .failureRateThreshold(100) + .ringBufferSizeInClosedState(CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD) + .ringBufferSizeInHalfOpenState(CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD) + .ignoreException(RecoverableException.class::isInstance) + .enableAutomaticTransitionFromOpenToHalfOpen() + .waitDurationInOpenState(Duration.ofMillis(100)) + .build(); + return CircuitBreaker.of("test circuit breaker name", circuitBreakerConfig); + } + + private Bulkhead createBulkhead(int maxConcurrentCalls, Duration maxWaitDuration) { + BulkheadConfig bulkheadConfig = BulkheadConfig.custom() + .maxConcurrentCalls(maxConcurrentCalls) + .maxWaitDuration(maxWaitDuration) + .build(); + return Bulkhead.of("test bulkhead", bulkheadConfig); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-external/pom.xml b/appstore-bundle-service-external/pom.xml new file mode 100644 index 0000000..b9fa4b4 --- /dev/null +++ b/appstore-bundle-service-external/pom.xml @@ -0,0 +1,47 @@ + + + + 4.0.0 + + + com.lgi.appstorebundle + appstore-bundle-service + 0.16.0-SNAPSHOT + + + appstore-bundle-service-external + + AppStore Bundle Service :: External + External + + pom + + + client-common + appstore-metadata-service-client + rabbitmq-client + + + + + com.lgi.appstorebundle + appstore-bundle-service-api + ${project.version} + + + com.google.auto.value + auto-value + provided + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-params + + + \ No newline at end of file diff --git a/appstore-bundle-service-external/rabbitmq-client/pom.xml b/appstore-bundle-service-external/rabbitmq-client/pom.xml new file mode 100644 index 0000000..0b1ec0a --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/pom.xml @@ -0,0 +1,73 @@ + + + + com.lgi.appstorebundle + appstore-bundle-service-external + 0.16.0-SNAPSHOT + + 4.0.0 + + rabbitmq-client + + + 2.5.1 + + src/main/java/com/lgi/appstorebundle/exception/*, + src/main/java/com/lgi/appstorebundle/external/RabbitMQConfiguration.java, + src/main/java/com/lgi/appstorebundle/external/model/*Message.java + + + src/main/java/com/lgi/appstorebundle/exception/*, + src/main/java/com/lgi/appstorebundle/external/RabbitMQConfiguration.java, + src/main/java/com/lgi/appstorebundle/external/model/*Message.java + + + + + + com.lgi.appstorebundle + client-common + ${project.version} + + + org.springframework.amqp + spring-rabbit + + + org.springframework.boot + spring-boot-starter-web + + + com.rabbitmq + amqp-client + 5.12.0 + + + com.fasterxml.jackson.core + jackson-core + + + com.github.tomakehurst + wiremock + test + ${wiremock.version} + + + javax.servlet + javax.servlet-api + + + + + jakarta.validation + jakarta.validation-api + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + + + + \ No newline at end of file diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/exception/RabbitMQException.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/exception/RabbitMQException.java new file mode 100644 index 0000000..05b2999 --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/exception/RabbitMQException.java @@ -0,0 +1,32 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.exception; + +public class RabbitMQException extends RuntimeException { + + public static final String MESSAGE_NOT_SENT = "Error during sending a message for 'x-request-id': '%s'. Reason: %s"; + + public RabbitMQException(String message) { + super(message); + } + + public static RabbitMQException createDefault(String xRequestId, String reason) { + return new RabbitMQException(String.format(MESSAGE_NOT_SENT, xRequestId, reason)); + } +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/ManagedRabbitMQ.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/ManagedRabbitMQ.java new file mode 100644 index 0000000..114c596 --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/ManagedRabbitMQ.java @@ -0,0 +1,114 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lgi.appstorebundle.exception.RabbitMQException; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +import static com.lgi.appstorebundle.common.Headers.CORRELATION_ID; +import static java.util.Objects.requireNonNull; + +@Service +public class ManagedRabbitMQ { + + private static final Logger LOG = LoggerFactory.getLogger(ManagedRabbitMQ.class); + + private static final int PREFETCH_COUNT = 1; + + private final String generationQueueName; + private final String encryptionQueueName; + private Channel channel; + private Connection connection; + private final RabbitMQConfiguration configuration; + private final ObjectMapper objectMapper; + + @Autowired + public ManagedRabbitMQ(RabbitMQConfiguration configuration, ObjectMapper objectMapper) { + this.configuration = requireNonNull(configuration); + this.objectMapper = requireNonNull(objectMapper); + this.generationQueueName = requireNonNull(configuration.getGenerationQueueName()); + this.encryptionQueueName = requireNonNull(configuration.getEncryptionQueueName()); + } + + @PostConstruct + public void start() throws IOException, TimeoutException, URISyntaxException, NoSuchAlgorithmException, KeyManagementException { + LOG.info("RabbitMQ starting..."); + ConnectionFactory factory = new ConnectionFactory(); + factory.setUri(configuration.getUrl()); + connection = factory.newConnection(); + channel = connection.createChannel(); + channel.basicQos(PREFETCH_COUNT); + LOG.info("RabbitMQ started."); + } + + @PreDestroy + public void stop() { + try { + LOG.info("RabbitMQ channel closing..."); + channel.close(); + LOG.info("RabbitMQ channel closed."); + LOG.info("RabbitMQ connection closing..."); + connection.close(); + LOG.info("RabbitMQ connection closed."); + } catch (IOException | TimeoutException e) { + LOG.warn("RabbitMQ error during closing", e); + } + } + + public OptionalException sendGenerationMessage(T message, String xRequestId) { + return sendMessage(message, xRequestId, generationQueueName); + } + + public OptionalException sendEncryptionMessage(T message, String xRequestId) { + return sendMessage(message, xRequestId, encryptionQueueName); + } + + public OptionalException sendMessage(T message, String xRequestId, String queueName) { + final AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder(); + builder.headers(Map.of(CORRELATION_ID, xRequestId)); + try { + channel.basicPublish("", queueName, builder.build(), objectMapper.writeValueAsBytes(message)); + LOG.info("Message for 'x-request-id': '{}' was sent on queue: '{}'", xRequestId, queueName); + return OptionalException.empty(); + } catch (Exception e) { + return OptionalException.of(RabbitMQException.createDefault(xRequestId, e.getMessage())); + } + } + + public Channel getChannel() { + return channel; + } +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/OptionalException.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/OptionalException.java new file mode 100644 index 0000000..2118c76 --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/OptionalException.java @@ -0,0 +1,51 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external; + +import java.util.Objects; +import java.util.function.Consumer; + +public final class OptionalException { + + private static final OptionalException EMPTY = new OptionalException(); + + private final RuntimeException value; + + public static OptionalException empty() { + return EMPTY; + } + + public static OptionalException of(RuntimeException value) { + return new OptionalException(value); + } + + private OptionalException() { + this.value = null; + } + + private OptionalException(RuntimeException value) { + this.value = Objects.requireNonNull(value); + } + + public void ifPresent(Consumer action) { + if (value != null) { + action.accept(value); + } + } +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConfiguration.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConfiguration.java new file mode 100644 index 0000000..edda842 --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConfiguration.java @@ -0,0 +1,69 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import javax.validation.constraints.NotNull; + +@Configuration +public class RabbitMQConfiguration { + + @Value("${rabbitmq.generationQueueName}") + @NotNull + private String generationQueueName; + + @Value("${rabbitmq.generationStatusQueueName}") + @NotNull + private String generationStatusQueueName; + + @Value("${rabbitmq.encryptionQueueName}") + @NotNull + private String encryptionQueueName; + + @Value("${rabbitmq.encryptionStatusQueueName}") + @NotNull + private String encryptionStatusQueueName; + + @Value("${rabbitmq.url}") + @NotNull + private String url; + + public String getGenerationQueueName() { + return generationQueueName; + } + + public String getGenerationStatusQueueName() { + return generationStatusQueueName; + } + + public String getEncryptionQueueName() { + return encryptionQueueName; + } + + public String getEncryptionStatusQueueName() { + return encryptionStatusQueueName; + } + + public String getUrl() { + return url; + } + +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConsumer.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConsumer.java new file mode 100644 index 0000000..c8626e5 --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConsumer.java @@ -0,0 +1,46 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external; + +import com.rabbitmq.client.DeliverCallback; + +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +public class RabbitMQConsumer { + + private final Function queueNameSupplier; + private final Supplier consumerSupplier; + + public RabbitMQConsumer(Function queueNameSupplier, Supplier consumerSupplier) { + this.queueNameSupplier = requireNonNull(queueNameSupplier); + this.consumerSupplier = requireNonNull(consumerSupplier); + } + + public Function getQueueNameSupplier() { + return queueNameSupplier; + } + + public DeliverCallback getConsumer() { + return consumerSupplier.get(); + } + +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConsumerMDC.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConsumerMDC.java new file mode 100644 index 0000000..682e569 --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQConsumerMDC.java @@ -0,0 +1,47 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external; + +import org.slf4j.MDC; + +import static com.lgi.appstorebundle.common.Headers.CORRELATION_ID; + +public class RabbitMQConsumerMDC { + + static final String RABBITMQ_MDC_INDICATOR = "rabbitmq-mdc"; + + private RabbitMQConsumerMDC() { + //empty + } + + public static void populate(String xRequestId) { + if (MDC.get(RABBITMQ_MDC_INDICATOR) == null) { + MDC.put(RABBITMQ_MDC_INDICATOR, "true"); + MDC.put(CORRELATION_ID, xRequestId); + } + } + + public static void clear() { + String kafkaMdc = MDC.get(RABBITMQ_MDC_INDICATOR); + if ("true".equals(kafkaMdc)) { + MDC.remove(CORRELATION_ID); + MDC.remove(RABBITMQ_MDC_INDICATOR); + } + } +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQService.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQService.java new file mode 100644 index 0000000..387b078 --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/external/RabbitMQService.java @@ -0,0 +1,57 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.external; + +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleContext; +import com.lgi.appstorebundle.model.EncryptionMessage; +import com.lgi.appstorebundle.model.GenerationMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class RabbitMQService { + + private final ManagedRabbitMQ rabbitMQ; + + @Autowired + public RabbitMQService(ManagedRabbitMQ rabbitMQ) { + this.rabbitMQ = rabbitMQ; + } + + public OptionalException sendGenerationMessage(BundleContext bundleContext) { + final Bundle bundle = bundleContext.getBundle(); + final ApplicationContext applicationContext = bundle.getApplicationContext(); + GenerationMessage generationMessage = GenerationMessage.create( + bundle.getId(), + applicationContext.getApplicationId(), + applicationContext.getApplicationVersion(), + applicationContext.getPlatformName(), + applicationContext.getFirmwareVersion(), + bundleContext.getOciImageUrl(), + bundleContext.getEncrypt() + ); + return rabbitMQ.sendGenerationMessage(generationMessage, bundle.getXRequestId()); + } + + public OptionalException sendEncryptionMessage(EncryptionMessage encryptionMessage, String xRequestId) { + return rabbitMQ.sendEncryptionMessage(encryptionMessage, xRequestId); + } +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/EncryptionMessage.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/EncryptionMessage.java new file mode 100644 index 0000000..393946a --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/EncryptionMessage.java @@ -0,0 +1,49 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.google.auto.value.AutoValue; + +import java.util.UUID; + +@AutoValue +public abstract class EncryptionMessage { + + public abstract UUID getId(); + + public abstract String getAppId(); + + public abstract String getAppVersion(); + + public abstract String getPlatformName(); + + public abstract String getFirmwareVersion(); + + public abstract String getOciBundleUrl(); + + public abstract String getEnvironment(); + + @JsonCreator + public static EncryptionMessage create(UUID id, String appId, String appVersion, String platformName, String firmwareVersion, String ociBundleUrl, String environment) { + return new AutoValue_EncryptionMessage(id, appId, appVersion, platformName, firmwareVersion, ociBundleUrl, environment); + } + +} + diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/EncryptionMessageFactory.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/EncryptionMessageFactory.java new file mode 100644 index 0000000..ea47bbd --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/EncryptionMessageFactory.java @@ -0,0 +1,57 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.model; + +import com.lgi.appstorebundle.api.Environment; +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; + +import static java.util.Objects.requireNonNull; + +public class EncryptionMessageFactory { + + private final Environment environment; + private final String bundleExtension; + + public EncryptionMessageFactory(Environment env, String bundleExtension) { + this.environment = requireNonNull(env, "environment"); + this.bundleExtension = requireNonNull(bundleExtension, "bundleExtension"); + } + + public EncryptionMessage fromBundle(Bundle bundle) { + requireNonNull(bundle, "bundle"); + + final ApplicationContext appCtx = bundle.getApplicationContext(); + return EncryptionMessage.create(bundle.getId(), appCtx.getApplicationId(), appCtx.getApplicationVersion(), + appCtx.getPlatformName(), appCtx.getFirmwareVersion(), buildBundleUrl(appCtx), environment.toString()); + } + + private String buildBundleUrl(ApplicationContext appCtx) { + requireNonNull(appCtx, "applicationContext"); + + return String.format("/%s/%s/%s/%s/%s", appCtx.getApplicationId(), appCtx.getApplicationVersion(), appCtx.getPlatformName(), appCtx.getFirmwareVersion(), + buildBundleName(appCtx)); + } + + private String buildBundleName(ApplicationContext appCtx) { + requireNonNull(appCtx, "applicationContext"); + + return String.format("%s-%s-%s-%s.%s", appCtx.getApplicationId(), appCtx.getApplicationVersion(), appCtx.getPlatformName(), appCtx.getFirmwareVersion(), bundleExtension); + } +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/ErrorMessage.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/ErrorMessage.java new file mode 100644 index 0000000..ec6954e --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/ErrorMessage.java @@ -0,0 +1,38 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.google.auto.value.AutoValue; +import org.springframework.lang.Nullable; + +@AutoValue +public abstract class ErrorMessage { + + @Nullable + public abstract String getMessage(); + + @Nullable + public abstract String getCode(); + + @JsonCreator + public static ErrorMessage create(String message, String code) { + return new AutoValue_ErrorMessage(message, code); + } +} diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/FeedbackMessage.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/FeedbackMessage.java new file mode 100644 index 0000000..854d02b --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/FeedbackMessage.java @@ -0,0 +1,48 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.joda.ser.DateTimeSerializer; +import com.google.auto.value.AutoValue; +import org.joda.time.DateTime; +import org.springframework.lang.Nullable; +import java.util.UUID; + +@AutoValue +public abstract class FeedbackMessage { + + public abstract UUID getId(); + + public abstract String getPhaseCode(); + + @Nullable + @JsonSerialize(using = DateTimeSerializer.class) + public abstract DateTime getMessageTimestamp(); + + @Nullable + public abstract ErrorMessage getError(); + + @JsonCreator + public static FeedbackMessage create(UUID id, String phaseCode, DateTime messageTimestamp, ErrorMessage error) { + return new AutoValue_FeedbackMessage(id, phaseCode, messageTimestamp, error); + } +} + diff --git a/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/GenerationMessage.java b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/GenerationMessage.java new file mode 100644 index 0000000..3218e9d --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/main/java/com/lgi/appstorebundle/model/GenerationMessage.java @@ -0,0 +1,48 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.google.auto.value.AutoValue; + +import java.util.UUID; + +@AutoValue +public abstract class GenerationMessage { + + public abstract UUID getId(); + + public abstract String getAppId(); + + public abstract String getAppVersion(); + + public abstract String getPlatformName(); + + public abstract String getFirmwareVersion(); + + public abstract String getOciImageUrl(); + + public abstract boolean getEncrypt(); + + @JsonCreator + public static GenerationMessage create(UUID id, String appId, String appVersion, String platformName, String firmwareVersion, String ociImageUrl, boolean encrypt) { + return new AutoValue_GenerationMessage(id, appId, appVersion, platformName, firmwareVersion, ociImageUrl, encrypt); + } +} + diff --git a/appstore-bundle-service-external/rabbitmq-client/src/test/java/com/lgi/appstorebundle/model/EncryptionMessageFactoryTest.java b/appstore-bundle-service-external/rabbitmq-client/src/test/java/com/lgi/appstorebundle/model/EncryptionMessageFactoryTest.java new file mode 100644 index 0000000..59f49dc --- /dev/null +++ b/appstore-bundle-service-external/rabbitmq-client/src/test/java/com/lgi/appstorebundle/model/EncryptionMessageFactoryTest.java @@ -0,0 +1,73 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.model; + +import com.lgi.appstorebundle.api.Environment; +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleStatus; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static java.util.UUID.randomUUID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class EncryptionMessageFactoryTest { + + private static final String X_REQ_ID = "x-request-id-value"; + private static final Environment ENV = Environment.DEV; + private static final String BUNDLE_EXTENSION = "tar.gz"; + + private static final Bundle BUNDLE = Bundle.create(randomUUID(), + ApplicationContext.create("appId", "appVer", "platformName", "firmwareVer"), + BundleStatus.ENCRYPTION_LAUNCHED, X_REQ_ID, DateTime.now(DateTimeZone.UTC)); + + private EncryptionMessageFactory factory; + + @BeforeEach + void setUp() { + factory = new EncryptionMessageFactory(ENV, BUNDLE_EXTENSION); + } + + @Test + void doesNotAllowNullEnvironmentAndBundleExtension() { + assertThrows(NullPointerException.class, () -> new EncryptionMessageFactory(null, BUNDLE_EXTENSION)); + assertThrows(NullPointerException.class, () -> new EncryptionMessageFactory(ENV, null)); + } + + @Test + void whenCreateEncryptionMessageFromValidBundleThenCreatedWithValidOciBundleUrl() { + //WHEN + final EncryptionMessage encryptionMessage = factory.fromBundle(BUNDLE); + + //THEN + assertEquals(ENV.toString(), encryptionMessage.getEnvironment()); + + final ApplicationContext appCtx = BUNDLE.getApplicationContext(); + assertEquals(appCtx.getApplicationId(), encryptionMessage.getAppId()); + assertEquals(appCtx.getApplicationVersion(), encryptionMessage.getAppVersion()); + assertEquals(appCtx.getPlatformName(), encryptionMessage.getPlatformName()); + assertEquals(appCtx.getFirmwareVersion(), encryptionMessage.getFirmwareVersion()); + + assertEquals("/appId/appVer/platformName/firmwareVer/appId-appVer-platformName-firmwareVer.tar.gz", encryptionMessage.getOciBundleUrl()); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-storage/pom.xml b/appstore-bundle-service-storage/pom.xml new file mode 100644 index 0000000..851133b --- /dev/null +++ b/appstore-bundle-service-storage/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + appstore-bundle-service + com.lgi.appstorebundle + 0.16.0-SNAPSHOT + + + appstore-bundle-service-storage + + AppStore Bundle Service :: Storage + Storage + + pom + + + storage-common + storage-persistent + + + + diff --git a/appstore-bundle-service-storage/storage-common/pom.xml b/appstore-bundle-service-storage/storage-common/pom.xml new file mode 100644 index 0000000..9509717 --- /dev/null +++ b/appstore-bundle-service-storage/storage-common/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.lgi.appstorebundle + appstore-bundle-service-storage + 0.16.0-SNAPSHOT + + + storage-common + + + + com.lgi.appstorebundle + appstore-bundle-service-api + ${project.version} + + + joda-time + joda-time + + + com.google.guava + guava + + + diff --git a/appstore-bundle-service-storage/storage-common/src/main/java/com/lgi/appstorebundle/storage/persistent/BundleDao.java b/appstore-bundle-service-storage/storage-common/src/main/java/com/lgi/appstorebundle/storage/persistent/BundleDao.java new file mode 100644 index 0000000..8408462 --- /dev/null +++ b/appstore-bundle-service-storage/storage-common/src/main/java/com/lgi/appstorebundle/storage/persistent/BundleDao.java @@ -0,0 +1,40 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.storage.persistent; + +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleStatus; +import org.joda.time.DateTime; + +import java.util.Optional; +import java.util.UUID; + +public interface BundleDao { + + Optional getLatestBundle(String applicationId, String applicationVersion, String platformName, String firmwareVersion); + + Optional getBundle(UUID id); + + void saveBundleWithStatus(Bundle bundle); + + void updateStatusForBundle(UUID id, BundleStatus status, DateTime messageTimestamp); + + boolean updateBundleStatusIfNewer(UUID id, BundleStatus status, DateTime messageTimestamp); + +} diff --git a/appstore-bundle-service-storage/storage-common/src/test/resources/logback-test.xml b/appstore-bundle-service-storage/storage-common/src/test/resources/logback-test.xml new file mode 100644 index 0000000..bcdd36e --- /dev/null +++ b/appstore-bundle-service-storage/storage-common/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/appstore-bundle-service-storage/storage-persistent/pom.xml b/appstore-bundle-service-storage/storage-persistent/pom.xml new file mode 100644 index 0000000..79e6399 --- /dev/null +++ b/appstore-bundle-service-storage/storage-persistent/pom.xml @@ -0,0 +1,252 @@ + + + + 4.0.0 + + + com.lgi.appstorebundle + appstore-bundle-service-storage + 0.16.0-SNAPSHOT + + + storage-persistent + + jar + + + postgres + postgres + postgres + localhost + 5432 + 46577 + -Xdoclint:none + appstore_bundle_service + + jdbc:postgresql://${db.host}:${db.port}/postgres + 3.4.1 + **/migrator/** + /var/run/docker.sock + + + + + windows-specific-settings + + + Windows + + + + //var/run/docker.sock + + + + + + + org.springframework.boot + spring-boot-starter-jooq + + + jakarta.annotation + jakarta.annotation-api + + + org.postgresql + postgresql + + + org.jooq + jooq + + + org.jooq + jooq-meta + + + com.lgi.appstorebundle + storage-common + ${project.version} + + + org.junit.vintage + junit-vintage-engine + + + org.junit.jupiter + junit-jupiter-api + + + ch.qos.logback + logback-classic + + + com.zaxxer + HikariCP + ${hikari.test.version} + test + + + + + + + io.fabric8 + docker-maven-plugin + 0.42.1 + + true + true + + + ryuk-summoned + testcontainers/ryuk:0.4.0 + + + ryuk.port:8080 + + + + ${sock-dir-docker-host}:/var/run/docker.sock + + + true + + + + postgres + postgres:12 + + + ${db.password} + + + db.port:5432 + + + + + true + + true + + + + + + + + start-postgres + generate-sources + + start + + + + + + com.libertyglobal.common.maven + tcp-msg-maven-plugin + 1.0.0 + + + write-death-note-for-postgres + generate-sources + + tcpmsg + + + ${ryuk.port} + label=killme + 2 + + + + + + org.flywaydb + flyway-maven-plugin + + + generate-sources + + migrate + + + + + ${db.url} + ${db.user} + ${db.password} + + ${db.schema} + + + filesystem:src/main/resources/migration + + + + + org.postgresql + postgresql + ${postgresql.version} + + + + + org.jooq + jooq-codegen-maven + + + ASBS-model-generation + generate-sources + + generate + + + + ${db.url} + ${db.user} + + ${db.password} + + + + org.jooq.meta.postgres.PostgresDatabase + .* + flyway_schema_history + ${db.schema} + SELECT :schema_name || '_' || MAX("version") FROM + "${db.schema}"."flyway_schema_history" + + + + + 0 + org.joda.time.DateTime + + com.lgi.appstorebundle.storage.persistent.converter.OffsetDateTimeDateTimeConverter + + (?i:\Qtimestamp with time zone\E) + ALL + ALL + + + + + com.lgi.appstorebundle.jooq.generated + target/generated-sources/jooq + + + true + + + + + + + + + diff --git a/appstore-bundle-service-storage/storage-persistent/src/main/java/com/lgi/appstorebundle/storage/persistent/JooqBundleDao.java b/appstore-bundle-service-storage/storage-persistent/src/main/java/com/lgi/appstorebundle/storage/persistent/JooqBundleDao.java new file mode 100644 index 0000000..d8f2d4f --- /dev/null +++ b/appstore-bundle-service-storage/storage-persistent/src/main/java/com/lgi/appstorebundle/storage/persistent/JooqBundleDao.java @@ -0,0 +1,137 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.storage.persistent; + +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleStatus; +import com.lgi.appstorebundle.jooq.generated.tables.records.BundleRecord; +import org.joda.time.DateTime; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.UUID; + +import static com.lgi.appstorebundle.jooq.generated.Tables.BUNDLE; +import static java.util.Objects.requireNonNull; +import static org.joda.time.DateTime.now; +import static org.jooq.impl.DSL.coalesce; + +@Component +public class JooqBundleDao implements BundleDao { + + private final DSLContext readDslContext; + + private final DSLContext writeDslContext; + + @Autowired + public JooqBundleDao(DSLContext readDslContext, DSLContext writeDslContext) { + this.readDslContext = requireNonNull(readDslContext, "readDslContext"); + this.writeDslContext = requireNonNull(writeDslContext, "writeDslContext"); + } + + @Override + public Optional getLatestBundle(String applicationId, String applicationVersion, String platformName, String firmwareVersion) { + return readDslContext.selectFrom(BUNDLE) + .where(BUNDLE.APPLICATION_ID.eq(applicationId)) + .and(BUNDLE.APPLICATION_VERSION.eq(applicationVersion)) + .and(BUNDLE.PLATFORM_NAME.eq(platformName)) + .and(BUNDLE.FIRMWARE_VERSION.eq(firmwareVersion)) + .orderBy(coalesce(BUNDLE.UPDATED_AT, BUNDLE.CREATED_AT)) + .limit(1) + .fetchOptional(JooqBundleDao::toBundle); + } + + @Override + public Optional getBundle(UUID id) { + return readDslContext.selectFrom(BUNDLE) + .where(BUNDLE.ID.eq(id)) + .fetchOptional(JooqBundleDao::toBundle); + } + + @Override + public void saveBundleWithStatus(Bundle bundle) { + writeDslContext.transaction(configuration -> + DSL.using(configuration).insertInto(BUNDLE, + BUNDLE.ID, + BUNDLE.APPLICATION_ID, + BUNDLE.APPLICATION_VERSION, + BUNDLE.PLATFORM_NAME, + BUNDLE.FIRMWARE_VERSION, + BUNDLE.STATUS, + BUNDLE.X_REQUEST_ID, + BUNDLE.CREATED_AT, + BUNDLE.MESSAGE_TIMESTAMP + ).values( + bundle.getId(), + bundle.getApplicationContext().getApplicationId(), + bundle.getApplicationContext().getApplicationVersion(), + bundle.getApplicationContext().getPlatformName(), + bundle.getApplicationContext().getFirmwareVersion(), + bundle.getStatus().name(), + bundle.getXRequestId(), + now(), + bundle.getMessageTimestamp() + ) + .execute()); + } + + @Override + public void updateStatusForBundle(UUID id, BundleStatus status, DateTime messageTimestamp) { + writeDslContext.transaction(configuration -> + DSL.using(configuration) + .update(BUNDLE) + .set(BUNDLE.STATUS, status.toString()) + .set(BUNDLE.UPDATED_AT, now()) + .set(BUNDLE.MESSAGE_TIMESTAMP, messageTimestamp) + .where(BUNDLE.ID.eq(id)) + .execute()); + } + + @Override + public boolean updateBundleStatusIfNewer(UUID id, BundleStatus status, DateTime messageTimestamp) { + final Integer numberOfUpdatedRow = writeDslContext.transactionResult(configuration -> + DSL.using(configuration) + .update(BUNDLE) + .set(BUNDLE.STATUS, status.toString()) + .set(BUNDLE.UPDATED_AT, now()) + .set(BUNDLE.MESSAGE_TIMESTAMP, messageTimestamp) + .where(BUNDLE.ID.eq(id)) + .and(BUNDLE.MESSAGE_TIMESTAMP.lessThan(messageTimestamp)) + .execute()); + return numberOfUpdatedRow > 0; + } + + private static Bundle toBundle(BundleRecord bundleRecord) { + return Bundle.create( + bundleRecord.getId(), + ApplicationContext.create( + bundleRecord.get(BUNDLE.APPLICATION_ID), + bundleRecord.get(BUNDLE.APPLICATION_VERSION), + bundleRecord.get(BUNDLE.PLATFORM_NAME), + bundleRecord.get(BUNDLE.FIRMWARE_VERSION)), + BundleStatus.valueOf(bundleRecord.get(BUNDLE.STATUS)), + bundleRecord.get(BUNDLE.X_REQUEST_ID), + bundleRecord.get(BUNDLE.MESSAGE_TIMESTAMP) + ); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-storage/storage-persistent/src/main/java/com/lgi/appstorebundle/storage/persistent/converter/OffsetDateTimeDateTimeConverter.java b/appstore-bundle-service-storage/storage-persistent/src/main/java/com/lgi/appstorebundle/storage/persistent/converter/OffsetDateTimeDateTimeConverter.java new file mode 100644 index 0000000..45785fe --- /dev/null +++ b/appstore-bundle-service-storage/storage-persistent/src/main/java/com/lgi/appstorebundle/storage/persistent/converter/OffsetDateTimeDateTimeConverter.java @@ -0,0 +1,40 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.storage.persistent.converter; + +import org.joda.time.DateTime; +import org.jooq.impl.AbstractConverter; + +import java.time.OffsetDateTime; +import java.time.ZoneId; + +public class OffsetDateTimeDateTimeConverter extends AbstractConverter { + public OffsetDateTimeDateTimeConverter() { + super(OffsetDateTime.class, DateTime.class); + } + + public DateTime from(OffsetDateTime databaseObject) { + return databaseObject == null ? null : + new DateTime(databaseObject.toInstant().toEpochMilli()); + } + + public OffsetDateTime to(DateTime userObject) { + return userObject == null ? null : OffsetDateTime.ofInstant(java.time.Instant.ofEpochMilli(userObject.toInstant().getMillis()), ZoneId.systemDefault()); + } +} diff --git a/appstore-bundle-service-storage/storage-persistent/src/main/resources/migration/V1__Add_schema.sql b/appstore-bundle-service-storage/storage-persistent/src/main/resources/migration/V1__Add_schema.sql new file mode 100644 index 0000000..8abf3d8 --- /dev/null +++ b/appstore-bundle-service-storage/storage-persistent/src/main/resources/migration/V1__Add_schema.sql @@ -0,0 +1,33 @@ +-- +-- If not stated otherwise in this file or this component's LICENSE file the +-- following copyright and licenses apply: +-- +-- Copyright 2023 Liberty Global Technology Services BV +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE bundle ( + id UUID NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, + application_id VARCHAR(255) NOT NULL, + application_version VARCHAR(255) NOT NULL, + platform_name VARCHAR(255) NOT NULL, + firmware_version VARCHAR(255) NOT NULL, + status VARCHAR(255) NOT NULL CHECK (status IN ('GENERATION_REQUESTED', 'GENERATION_LAUNCHED', 'GENERATION_COMPLETED', 'ENCRYPTION_REQUESTED', 'ENCRYPTION_LAUNCHED', 'ENCRYPTION_COMPLETED', 'BUNDLE_ERROR')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ, + x_request_id VARCHAR(255) NOT NULL UNIQUE, + message_timestamp TIMESTAMPTZ NOT NULL +); \ No newline at end of file diff --git a/appstore-bundle-service-storage/storage-persistent/src/test/resources/logback-test.xml b/appstore-bundle-service-storage/storage-persistent/src/test/resources/logback-test.xml new file mode 100644 index 0000000..6359b7f --- /dev/null +++ b/appstore-bundle-service-storage/storage-persistent/src/test/resources/logback-test.xml @@ -0,0 +1,12 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + \ No newline at end of file diff --git a/appstore-bundle-service-test/lombok.config b/appstore-bundle-service-test/lombok.config new file mode 100644 index 0000000..f902278 --- /dev/null +++ b/appstore-bundle-service-test/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties=true \ No newline at end of file diff --git a/appstore-bundle-service-test/pom.xml b/appstore-bundle-service-test/pom.xml new file mode 100644 index 0000000..c3fd7bb --- /dev/null +++ b/appstore-bundle-service-test/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + + appstore-bundle-service + com.lgi.appstorebundle + 0.16.0-SNAPSHOT + + + appstore-bundle-service-test + + AppStore Bundle Service :: Integration Tests + Integration + + + **/tests/**/*.java + + 1.18.20 + 2.13.3 + 2.10.0 + 1.9.5 + 2.5.1 + 4.0.2 + + + + + + + com.lgi.appstorebundle + appstore-bundle-service-application + ${project.version} + test + + + + + org.assertj + assertj-core + + + + + org.springframework.boot + spring-boot-starter-test + + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + io.qameta.allure + allure-junit5 + ${allure.version} + test + + + io.qameta.allure + allure-rest-assured + ${allure.version} + test + + + javax.xml.bind + jaxb-api + + + org.hamcrest + hamcrest-library + + + + + + + org.awaitility + awaitility + ${awaitility.version} + test + + + + + com.github.tomakehurst + wiremock + ${wiremock.version} + test + + + javax.servlet + javax.servlet-api + + + + + + + org.testcontainers + postgresql + test + + + org.flywaydb + flyway-core + test + + + + + org.testcontainers + rabbitmq + test + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + true + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skipITs} + + ${testSet} + + + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + + + junit.jupiter.extensions.autodetection.enabled = true + + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + io.qameta.allure + allure-maven + ${allure-maven.version} + + ${allure.version} + ${project.basedir}/target/allure-results + + + + + diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/ApplicationConfiguration.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/ApplicationConfiguration.java new file mode 100644 index 0000000..fe63ca1 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/ApplicationConfiguration.java @@ -0,0 +1,34 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.configuration; + +import com.lgi.appstorebundle.test.core.MockedServiceUrlProviderImpl; +import com.lgi.appstorebundle.test.core.ServiceUrlProvider; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ApplicationConfiguration { + + @Bean + public ServiceUrlProvider mockUrlProvider(ServletWebServerApplicationContext webServerApplicationContext) { + return new MockedServiceUrlProviderImpl(webServerApplicationContext); + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/BaseTestConfiguration.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/BaseTestConfiguration.java new file mode 100644 index 0000000..4e1bc4c --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/BaseTestConfiguration.java @@ -0,0 +1,46 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(EndpointsConfiguration.class) +public class BaseTestConfiguration { + + @Bean + public ObjectMapper jsonMapper() { + var mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.registerModule(new JodaModule()); + mapper.registerModule(new Jdk8Module()); + mapper.registerModule(new ParameterNamesModule()); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/EndpointsConfiguration.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/EndpointsConfiguration.java new file mode 100644 index 0000000..fdae90c --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/configuration/EndpointsConfiguration.java @@ -0,0 +1,65 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lgi.appstorebundle.test.core.ServiceUrlProvider; +import com.lgi.appstorebundle.test.service.endpoints.InfoEndpoint; +import com.lgi.appstorebundle.test.service.endpoints.StartBundleGenerationEndpoint; +import io.qameta.allure.restassured.AllureRestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.config.RestAssuredConfig; +import io.restassured.filter.log.RequestLoggingFilter; +import io.restassured.filter.log.ResponseLoggingFilter; +import io.restassured.specification.RequestSpecification; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class EndpointsConfiguration { + + @Bean + public RequestSpecification requestSpecification(ObjectMapper jsonMapper) { + return new RequestSpecBuilder() + .addFilter(new RequestLoggingFilter()) + .addFilter(new ResponseLoggingFilter()) + .addFilter(new AllureRestAssured() + .setRequestTemplate("request.ftl") + .setResponseTemplate("response.ftl")) + .setConfig(RestAssuredConfig.config() + .objectMapperConfig(ObjectMapperConfig.objectMapperConfig() + .jackson2ObjectMapperFactory((type, s) -> jsonMapper))) + .build(); + } + + @Bean(name = "testInfoEndpoint") + public InfoEndpoint infoEndpoint( + ServiceUrlProvider serviceUrlProvider, + RequestSpecification requestSpecification) { + return new InfoEndpoint(serviceUrlProvider, requestSpecification); + } + + @Bean + public StartBundleGenerationEndpoint startBundleGenerationEndpoint( + ServiceUrlProvider serviceUrlProvider, + RequestSpecification requestSpecification) { + return new StartBundleGenerationEndpoint(serviceUrlProvider, requestSpecification); + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/core/MockedServiceUrlProviderImpl.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/core/MockedServiceUrlProviderImpl.java new file mode 100644 index 0000000..cd37868 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/core/MockedServiceUrlProviderImpl.java @@ -0,0 +1,34 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.core; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; + +@RequiredArgsConstructor +public class MockedServiceUrlProviderImpl implements ServiceUrlProvider { + @NonNull + private final ServletWebServerApplicationContext webServerApplicationContext; + + @Override + public String getBaseUrl() { + return String.format("http://localhost:%d", webServerApplicationContext.getWebServer().getPort()); + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/core/ServiceUrlProvider.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/core/ServiceUrlProvider.java new file mode 100644 index 0000000..217fb50 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/core/ServiceUrlProvider.java @@ -0,0 +1,24 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.core; + +public interface ServiceUrlProvider { + + String getBaseUrl(); +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/BaseEndpoint.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/BaseEndpoint.java new file mode 100644 index 0000000..a25ac15 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/BaseEndpoint.java @@ -0,0 +1,41 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.service.endpoints; + +import com.lgi.appstorebundle.test.core.ServiceUrlProvider; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import static io.restassured.RestAssured.UNDEFINED_PORT; + +@RequiredArgsConstructor +public abstract class BaseEndpoint> { + @NonNull + private final ServiceUrlProvider urlProvider; + @NonNull + private final RequestSpecification requestSpecification; + + protected RequestSpecification given() { + return RestAssured.given(requestSpecification) + .baseUri(urlProvider.getBaseUrl()) + .port(UNDEFINED_PORT); + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/InfoEndpoint.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/InfoEndpoint.java new file mode 100644 index 0000000..6223c47 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/InfoEndpoint.java @@ -0,0 +1,44 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.service.endpoints; + +import com.lgi.appstorebundle.test.core.ServiceUrlProvider; +import io.qameta.allure.Step; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import lombok.NonNull; + +public class InfoEndpoint extends BaseEndpoint { + private static final String INFO_PATH = "/actuator/info"; + + public InfoEndpoint(@NonNull ServiceUrlProvider urlProvider, + @NonNull RequestSpecification requestSpecification) { + super(urlProvider, requestSpecification); + } + + @Step("Get service info") + public Response getInfo() { + return given() + .when() + .get(INFO_PATH) + .then() + .extract() + .response(); + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/StartBundleGenerationEndpoint.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/StartBundleGenerationEndpoint.java new file mode 100644 index 0000000..9ccdfaf --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/endpoints/StartBundleGenerationEndpoint.java @@ -0,0 +1,52 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.service.endpoints; + +import com.lgi.appstorebundle.api.ApplicationParams; +import com.lgi.appstorebundle.test.core.ServiceUrlProvider; +import com.lgi.appstorebundle.test.utils.RabbitMQSteps; +import io.qameta.allure.Step; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import lombok.NonNull; + +public class StartBundleGenerationEndpoint extends BaseEndpoint { + private static final String REQUEST_BUNDLE_PATH = "/applications/{appId}/{appVersion}/{platformName}/{firmwareVersion}/{appBundleName}"; + + public StartBundleGenerationEndpoint(@NonNull ServiceUrlProvider urlProvider, + @NonNull RequestSpecification requestSpecification) { + super(urlProvider, requestSpecification); + } + + @Step("Request for starting bundle generation") + public Response startBundleGeneration(ApplicationParams appParams, String xRequestId) { + return given() + .when() + .pathParam("appId", appParams.getApplicationId()) + .pathParam("appVersion", appParams.getAppVersion()) + .pathParam("platformName", appParams.getPlatformName()) + .pathParam("firmwareVersion", appParams.getFirmwareVersion()) + .pathParam("appBundleName", appParams.getAppBundleName()) + .header(RabbitMQSteps.X_REQUEST_ID, xRequestId) + .get(REQUEST_BUNDLE_PATH) + .then() + .extract() + .response(); + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/BundleWithAudit.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/BundleWithAudit.java new file mode 100644 index 0000000..7f130e8 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/BundleWithAudit.java @@ -0,0 +1,33 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.service.model; + +import com.lgi.appstorebundle.api.model.Bundle; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.joda.time.DateTime; + +@Value +@AllArgsConstructor(staticName = "create") +public class BundleWithAudit { + + Bundle bundle; + DateTime createdAt; + DateTime updatedAt; +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/EncryptionMessageWithEnvelope.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/EncryptionMessageWithEnvelope.java new file mode 100644 index 0000000..728d698 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/EncryptionMessageWithEnvelope.java @@ -0,0 +1,31 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.service.model; + +import com.lgi.appstorebundle.model.EncryptionMessage; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "create") +public class EncryptionMessageWithEnvelope { + + EncryptionMessage encryptionMessage; + String xRequestId; +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/GenerationMessageWithEnvelope.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/GenerationMessageWithEnvelope.java new file mode 100644 index 0000000..5380e15 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/GenerationMessageWithEnvelope.java @@ -0,0 +1,31 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.service.model; + +import com.lgi.appstorebundle.model.GenerationMessage; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "create") +public class GenerationMessageWithEnvelope { + + GenerationMessage generationMessage; + String xRequestId; +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/InfoResponse.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/InfoResponse.java new file mode 100644 index 0000000..347546d --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/service/model/InfoResponse.java @@ -0,0 +1,60 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.service.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; + +@Value +@Builder +@Accessors(fluent = true) +@JsonInclude(Include.NON_NULL) +public class InfoResponse { + + @JsonProperty("APP_DEPLOY_TIME") + String appDeployTime; + + @JsonProperty("APP_START_TIME") + String appStartTime; + + @JsonProperty("HOST_NAME") + String hostName; + + @JsonProperty("APP_NAME") + String appName; + + @JsonProperty("APP_BRANCH") + String appBranch; + + @JsonProperty("APP_BUILD_TIME") + String appBuildTime; + + @JsonProperty("APP_VERSION") + String appVersion; + + @JsonProperty("STACK_NAME") + String stackName; + + @JsonProperty("APP_REVISION") + String appRevision; +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/BaseContainersIT.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/BaseContainersIT.java new file mode 100644 index 0000000..0121668 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/BaseContainersIT.java @@ -0,0 +1,100 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.tests; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.Options; +import lombok.extern.slf4j.Slf4j; +import org.flywaydb.core.Flyway; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DSL; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.utility.DockerImageName; + +import java.sql.DriverManager; +import java.sql.SQLException; + +import static com.lgi.appstorebundle.jooq.generated.AppstoreBundleService.APPSTORE_BUNDLE_SERVICE; + +@Slf4j +public abstract class BaseContainersIT { + + protected static final PostgreSQLContainer postgres; + protected static final RabbitMQContainer rabbitMQ; + protected static final DSLContext dslContext; + protected static final WireMockServer wireMockServer; + + static { + log.info("Starting Postgres database container"); + postgres = new PostgreSQLContainer<>( + DockerImageName.parse("postgres:15") + .asCompatibleSubstituteFor("postgres")) + .withReuse(true); + postgres.start(); + + log.info("Starting database migration"); + Flyway.configure() + .schemas(APPSTORE_BUNDLE_SERVICE.getName()) + .defaultSchema(APPSTORE_BUNDLE_SERVICE.getName()) + .dataSource(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()) + .locations("migration") + .load() + .migrate(); + + log.info("Creating DSL context"); + try { + dslContext = DSL.using(DriverManager.getConnection( + postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()), SQLDialect.POSTGRES); + } catch (SQLException e) { + throw new RuntimeException(e); + } + //custom composite array type qualification error workaround - https://github.com/jOOQ/jOOQ/issues/5571 + dslContext.query("ALTER ROLE " + postgres.getUsername() + " SET search_path TO " + APPSTORE_BUNDLE_SERVICE.getName()).execute(); + dslContext.query("ALTER DATABASE " + postgres.getDatabaseName() + " SET search_path TO " + APPSTORE_BUNDLE_SERVICE.getName()).execute(); + + log.info("Starting RabbitMQ container"); + rabbitMQ = new RabbitMQContainer("rabbitmq:3.8.19-management-alpine") + .withReuse(true) + .withQueue("bundlegen-service-requests") + .withQueue("bundlegen-service-status") + .withQueue("bundlecrypt-service-requests") + .withQueue("bundlecrypt-service-status"); + rabbitMQ.start(); + + log.info("Starting WireMock server"); + wireMockServer = new WireMockServer(Options.DYNAMIC_PORT); + wireMockServer.start(); + } + + @DynamicPropertySource + static void registerProperties(DynamicPropertyRegistry registry) { + registry.add("database.port", postgres::getFirstMappedPort); + registry.add("database.name", postgres::getDatabaseName); + registry.add("spring.datasource.hikari.read.username", postgres::getUsername); + registry.add("spring.datasource.hikari.read.password", postgres::getPassword); + registry.add("spring.datasource.hikari.write.username", postgres::getUsername); + registry.add("spring.datasource.hikari.write.password", postgres::getPassword); + registry.add("rabbitmq.port", rabbitMQ::getAmqpPort); + registry.add("asms.service.url", () -> String.format("http://localhost:%s", wireMockServer.port())); + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/ConsumeFeedbackMessageMockedIT.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/ConsumeFeedbackMessageMockedIT.java new file mode 100644 index 0000000..6d76229 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/ConsumeFeedbackMessageMockedIT.java @@ -0,0 +1,220 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.tests; + +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.model.EncryptionMessageFactory; +import com.lgi.appstorebundle.model.FeedbackMessage; +import com.lgi.appstorebundle.test.service.model.BundleWithAudit; +import io.qameta.allure.Epic; +import io.qameta.allure.Feature; +import io.qameta.allure.Issue; +import io.qameta.allure.Story; +import org.assertj.core.api.SoftAssertions; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static com.lgi.appstorebundle.api.model.BundleStatus.ENCRYPTION_REQUESTED; +import static com.lgi.appstorebundle.api.model.BundleStatus.GENERATION_COMPLETED; +import static com.lgi.appstorebundle.api.model.BundleStatus.GENERATION_LAUNCHED; +import static com.lgi.appstorebundle.api.model.BundleStatus.GENERATION_REQUESTED; +import static java.util.UUID.randomUUID; +import static org.joda.time.DateTime.now; +import static org.junit.jupiter.api.Assertions.fail; + +@Epic("Mocked application") +@Feature("Consume feedback message") +@Story("Consume feedback message") +public class ConsumeFeedbackMessageMockedIT extends MockedBaseIT { + + private static final String APP_ID = "APP_ID"; + private static final String APP_VER = "APP_VER"; + private static final String PLATFORM_NAME = "PLATFORM_NAME"; + private static final String FIRMWARE_VER = "FIRMWARE_VER"; + private static final String X_REQUEST_ID = "1111-1111111-1111"; + + @Autowired + EncryptionMessageFactory encryptionMessageFactory; + + @BeforeEach + void setUp() { + databaseSteps.clearBundleTable(); + rabbitMQSteps.clearStack(); + + } + + @Test + @Issue("SPARK-36508") + @DisplayName("Saving a new status for application when Feedback Message has greater message timestamp") + void receivedNewerFeedbackMessage_saveNewStatus(SoftAssertions softly) throws IOException { + // Given + final UUID id = randomUUID(); + final String xRequestId = "1111-1111111-2222"; + final DateTime now = now(DateTimeZone.UTC); + final DateTime messageTimestampForOldRow = now.minusMinutes(3); + final DateTime createdAtForOldRow = now.minusMinutes(2); + final BundleWithAudit bundleWithAudit = BundleWithAudit.create( + Bundle.create( + id, + ApplicationContext.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER), + GENERATION_REQUESTED, + xRequestId, + messageTimestampForOldRow), + createdAtForOldRow, + null); + databaseSteps.saveBundleWithStatus(bundleWithAudit); + + // When + rabbitMQSteps.pushMessage(FeedbackMessage.create(bundleWithAudit.getBundle().getId(), GENERATION_LAUNCHED.name(), now, null), xRequestId); + waitDelay(); + + // Then + databaseSteps.verifyBundleWasUpdated(softly, bundleWithAudit.getBundle().getId(), APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, GENERATION_LAUNCHED, xRequestId, messageTimestampForOldRow, createdAtForOldRow, null); + } + + @Test + @Issue("SPARK-36509") + @DisplayName("Sends encryption message when bundle generation is done and encryption is enabled") + void receivedNewerFeedbackMessage_whenEncryptionEnabledAndBundleStatusIsGenerationCompleted_publishEncryptionMessage(SoftAssertions softly) throws IOException { + // Given + final UUID id = randomUUID(); + final DateTime now = now(DateTimeZone.UTC); + final DateTime messageTimestampForOldRow = now.minusMinutes(3); + final DateTime createdAtForOldRow = now.minusMinutes(2); + final DateTime updatedAtForOldRow = now.minusMinutes(1); + final BundleWithAudit bundleWithAudit = BundleWithAudit.create( + Bundle.create( + id, + ApplicationContext.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER), + GENERATION_LAUNCHED, + X_REQUEST_ID, + messageTimestampForOldRow), + createdAtForOldRow, + updatedAtForOldRow); + databaseSteps.saveBundleWithStatus(bundleWithAudit); + + // When + rabbitMQSteps.pushMessage(FeedbackMessage.create(bundleWithAudit.getBundle().getId(), GENERATION_COMPLETED.name(), now, null), X_REQUEST_ID); + waitDelay(); + + // Then + rabbitMQSteps.verifySentEncryptionMessage(softly, encryptionMessageFactory.fromBundle(bundleWithAudit.getBundle()), X_REQUEST_ID); + databaseSteps.verifyBundleWasUpdated(softly, bundleWithAudit.getBundle().getId(), APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, ENCRYPTION_REQUESTED, X_REQUEST_ID, messageTimestampForOldRow, createdAtForOldRow, updatedAtForOldRow); + } + + @Test + @Issue("SPARK-36508") + @DisplayName("Do not saving a new status for application when Feedback Message has lower message timestamp") + void receivedOlderFeedbackMessage_doNotSaveNewStatus(SoftAssertions softly) throws IOException { + // Given + final UUID id = randomUUID(); + final DateTime now = now(DateTimeZone.UTC); + final DateTime messageTimestampForOldRow = now.minusMinutes(2); + final DateTime createdAtForOldRow = now.minusMinutes(3); + final DateTime updatedAtForOldRow = now.minusMinutes(1); + final BundleWithAudit bundleWithAudit = BundleWithAudit.create( + Bundle.create( + id, + ApplicationContext.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER), + GENERATION_COMPLETED, + X_REQUEST_ID, + messageTimestampForOldRow), + createdAtForOldRow, + updatedAtForOldRow); + databaseSteps.saveBundleWithStatus(bundleWithAudit); + + // When + rabbitMQSteps.pushMessage(FeedbackMessage.create(bundleWithAudit.getBundle().getId(), GENERATION_LAUNCHED.name(), messageTimestampForOldRow.minusMinutes(1), null), X_REQUEST_ID); + waitDelay(); + + // Then + databaseSteps.verifyBundleWasNotUpdated(softly, bundleWithAudit.getBundle().getId(), GENERATION_COMPLETED, messageTimestampForOldRow, updatedAtForOldRow); + } + + @Test + @Issue("SPARK-36508") + @DisplayName("Do not saving a new status for application when message not contains a x-request-id") + void receivedFeedbackMessageWithoutXRequestId_doNotSaveNewStatus(SoftAssertions softly) throws IOException { + // Given + final UUID id = randomUUID(); + final DateTime now = now(DateTimeZone.UTC); + final DateTime messageTimestampForOldRow = now.minusMinutes(3); + final DateTime createdAtForOldRow = now.minusMinutes(2); + final BundleWithAudit bundleWithAudit = BundleWithAudit.create( + Bundle.create( + id, + ApplicationContext.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER), + GENERATION_REQUESTED, + X_REQUEST_ID, + messageTimestampForOldRow), + createdAtForOldRow, + null); + databaseSteps.saveBundleWithStatus(bundleWithAudit); + + rabbitMQSteps.pushMessage(FeedbackMessage.create(bundleWithAudit.getBundle().getId(), GENERATION_LAUNCHED.name(), now, null), null); + waitDelay(); + + // Then + databaseSteps.verifyBundleWasNotUpdated(softly, bundleWithAudit.getBundle().getId(), GENERATION_REQUESTED, messageTimestampForOldRow, null); + } + + @Test + @Issue("SPARK-36508") + @DisplayName("Do not saving a new status for application when message not contains a messageTimestamp") + void receivedFeedbackMessageWithoutMessageTimestamp_doNotSaveNewStatus(SoftAssertions softly) throws IOException { + // Given + final UUID id = randomUUID(); + final DateTime now = now(DateTimeZone.UTC); + final DateTime messageTimestampForOldRow = now.minusMinutes(3); + final DateTime createdAtForOldRow = now.minusMinutes(2); + final BundleWithAudit bundleWithAudit = BundleWithAudit.create( + Bundle.create( + id, + ApplicationContext.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER), + GENERATION_REQUESTED, + X_REQUEST_ID, + messageTimestampForOldRow), + createdAtForOldRow, + null); + databaseSteps.saveBundleWithStatus(bundleWithAudit); + + rabbitMQSteps.pushMessage(FeedbackMessage.create(bundleWithAudit.getBundle().getId(), GENERATION_LAUNCHED.name(), null, null), X_REQUEST_ID); + waitDelay(); + + // Then + databaseSteps.verifyBundleWasNotUpdated(softly, bundleWithAudit.getBundle().getId(), GENERATION_REQUESTED, messageTimestampForOldRow, null); + } + + private void waitDelay() { + try { + TimeUnit.MILLISECONDS.sleep(1000); + } catch (InterruptedException e) { + fail(e); + } + } +} \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/GetInfoIT.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/GetInfoIT.java new file mode 100644 index 0000000..3412498 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/GetInfoIT.java @@ -0,0 +1,61 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.tests; + +import com.lgi.appstorebundle.test.service.endpoints.InfoEndpoint; +import com.lgi.appstorebundle.test.service.model.InfoResponse; +import io.qameta.allure.Epic; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.apache.http.HttpStatus; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +@Epic("Mocked application") +@Feature("Get info") +@Story("Get info") +public class GetInfoIT extends MockedBaseIT { + + @Autowired + @Qualifier("testInfoEndpoint") + private InfoEndpoint infoEndpoint; + + @Test + @Disabled("SPARK-51317") // TODO unignore this test when ASBS infrastructure is updated + @DisplayName("Call service info - verify successful response") + void callServiceInfo_verifySuccessfulResponse(SoftAssertions softly) { + // Given + var expectedAppName = "appstore-bundle-service-application"; + + // When + var response = infoEndpoint.getInfo(); + + // Then + softly.assertThat(response.getStatusCode()) + .as("Wrong HTTP status code received from the service") + .isEqualTo(HttpStatus.SC_OK); + softly.assertThat(response.as(InfoResponse.class).appName()) + .as("Wrong app name received from the service info") + .isEqualTo(expectedAppName); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/MockedBaseIT.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/MockedBaseIT.java new file mode 100644 index 0000000..8de300c --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/MockedBaseIT.java @@ -0,0 +1,72 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.tests; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lgi.appstorebundle.api.Environment; +import com.lgi.appstorebundle.test.service.endpoints.StartBundleGenerationEndpoint; +import com.lgi.appstorebundle.test.utils.ASMSMockSteps; +import com.lgi.appstorebundle.test.utils.DatabaseSteps; +import com.lgi.appstorebundle.test.utils.RabbitMQSteps; +import com.lgi.appstorebundle.test.utils.TestLifecycleLogger; +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.TimeoutException; + +@TestExecutionListeners(DependencyInjectionTestExecutionListener.class) +@ExtendWith(SoftAssertionsExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureWebTestClient +@ActiveProfiles("test") +public abstract class MockedBaseIT extends BaseContainersIT implements TestLifecycleLogger { + + protected static DatabaseSteps databaseSteps; + protected static RabbitMQSteps rabbitMQSteps; + protected static ASMSMockSteps asmsMockSteps; + private static boolean stepsInitialized = false; + @Autowired + protected StartBundleGenerationEndpoint startBundleGenerationEndpoint; + @Autowired + private ObjectMapper jsonMapper; + @Value("${environment}") + private Environment environment; + + @BeforeAll + void beforeAll() throws URISyntaxException, NoSuchAlgorithmException, IOException, KeyManagementException, TimeoutException { + if (!stepsInitialized) { + databaseSteps = new DatabaseSteps(dslContext); + rabbitMQSteps = new RabbitMQSteps(rabbitMQ, jsonMapper, environment); + asmsMockSteps = new ASMSMockSteps(wireMockServer, jsonMapper); + stepsInitialized = true; + } + } +} diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/StartBundleGenerationMockedIT.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/StartBundleGenerationMockedIT.java new file mode 100644 index 0000000..d6f6e57 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/tests/StartBundleGenerationMockedIT.java @@ -0,0 +1,215 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.tests; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.lgi.appstorebundle.api.ApplicationParams; +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.model.GenerationMessage; +import com.lgi.appstorebundle.test.service.model.BundleWithAudit; +import io.qameta.allure.Epic; +import io.qameta.allure.Feature; +import io.qameta.allure.Issue; +import io.qameta.allure.Story; +import org.apache.http.HttpStatus; +import org.assertj.core.api.SoftAssertions; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static com.lgi.appstorebundle.api.model.BundleStatus.BUNDLE_ERROR; +import static com.lgi.appstorebundle.api.model.BundleStatus.GENERATION_REQUESTED; +import static java.util.UUID.randomUUID; +import static org.joda.time.DateTime.now; +import static org.junit.jupiter.api.Assertions.fail; + +@Epic("Mocked application") +@Feature("Start bundle generation") +@Story("Start bundle generation") +public class StartBundleGenerationMockedIT extends MockedBaseIT { + + private static final UUID DUMMY_ID = randomUUID(); + private static final String APP_ID = "APP_ID"; + private static final String APP_VER = "APP_VER"; + private static final String PLATFORM_NAME = "PLATFORM_NAME"; + private static final String FIRMWARE_VER = "FIRMWARE_VER"; + private static final String OCI_IMAGE_URL = "OCI_IMAGE_URL"; + private static final boolean ENCRYPT = true; + private static final String X_REQUEST_ID = "1111-1111111-1111"; + private static final String SECOND_X_REQUEST_ID = "2222-22222222-2222"; + private static final String APP_BUNDLE_NAME = "APP_BUNDLE_NAME"; + private static final String MAINTAINER_CODE = "MAINTAINER_CODE"; + + private static final String APP_NAME = "APP_NAME"; + private static final String APP_URL = "http://hostname/demo.id.appl/2.2/platformName/firmwareVer/demo.id.appl_2.2_platformName_firmwareVer.tar.gz"; + + private static final ApplicationParams VALID_APP_PARAMS = + ApplicationParams.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, APP_BUNDLE_NAME); + + @BeforeEach + void setUp() { + databaseSteps.clearBundleTable(); + rabbitMQSteps.clearStack(); + asmsMockSteps.resetAll(); + } + + @Test + @Issue("SPARK-36508") + @DisplayName("GET: Call startBundleGeneration - Status code 404 because of missing application metadata from ASMS") + void callRequestBundleWhenAppNotExistsInASMS_verifyNotFoundResponse(SoftAssertions softly) { + // Given + asmsMockSteps.stubASMSWithNotFoundForApplicationMetadata(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER); + + // When + var response = startBundleGenerationEndpoint.startBundleGeneration(VALID_APP_PARAMS, X_REQUEST_ID); + waitDelay(); + + // Then + softly.assertThat(response.getStatusCode()) + .as("Wrong HTTP status code received from the service.") + .isEqualTo(HttpStatus.SC_NOT_FOUND); + databaseSteps.verifyNotStoredBundle(softly, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER); + rabbitMQSteps.verifyNotSentGenerationMessage(softly); + } + + @Test + @Issue("SPARK-36508") + @DisplayName("GET: Call startBundleGeneration - Status code 404 because of missing application metadata for maintainer from ASMS") + void callRequestBundleWhenAppForMaintainerNotExistsInASMS_verifyNotFoundResponse(SoftAssertions softly) throws JsonProcessingException { + // Given + asmsMockSteps.stubASMSWithApplicationMetadataInResponseBody(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, MAINTAINER_CODE, APP_NAME, APP_URL); + asmsMockSteps.stubASMSWithNotFoundForApplicationMetadataForMaintainer(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, MAINTAINER_CODE); + + // When + var response = startBundleGenerationEndpoint.startBundleGeneration(VALID_APP_PARAMS, X_REQUEST_ID); + waitDelay(); + + // Then + softly.assertThat(response.getStatusCode()) + .as("Wrong HTTP status code received from the service.") + .isEqualTo(HttpStatus.SC_NOT_FOUND); + databaseSteps.verifyNotStoredBundle(softly, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER); + rabbitMQSteps.verifyNotSentGenerationMessage(softly); + } + + @Test + @Issue("SPARK-36508") + @DisplayName("GET: Call startBundleGeneration - Status code 202, bundle stored in DB and message sent") + void callRequestBundleWhenGenerationNotTriggered_verifyStartingGenerationWithSendingMessageAndSavingToDB(SoftAssertions softly) throws JsonProcessingException { + // Given + asmsMockSteps.stubASMSWithApplicationMetadataInResponseBody(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, MAINTAINER_CODE, APP_NAME, APP_URL); + asmsMockSteps.stubASMSWithApplicationMetadataForMaintainerInResponseBody(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, MAINTAINER_CODE, APP_NAME, APP_URL, OCI_IMAGE_URL); + + // When + var response = startBundleGenerationEndpoint.startBundleGeneration(VALID_APP_PARAMS, X_REQUEST_ID); + waitDelay(); + + // Then + softly.assertThat(response.getStatusCode()) + .as("Wrong HTTP status code received from the service.") + .isEqualTo(HttpStatus.SC_ACCEPTED); + databaseSteps.verifyBundleWasCreated(softly, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, GENERATION_REQUESTED, X_REQUEST_ID); + final GenerationMessage expectedGenerationMessage = GenerationMessage.create(DUMMY_ID, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, OCI_IMAGE_URL, ENCRYPT); + rabbitMQSteps.verifySentGenerationMessage(softly, expectedGenerationMessage, X_REQUEST_ID); + } + + @Test + @Issue("SPARK-36508") + @DisplayName("GET: Call startBundleGeneration - Status code 202, started a new bundle generation due to a previous error") + void callRequestBundleWhenGenerationHadError_verifyStartingNewGenerationWithSendingMessageAndSavingToDB(SoftAssertions softly) throws JsonProcessingException { + // Given + final UUID id = randomUUID(); + final DateTime now = now(DateTimeZone.UTC); + final DateTime messageTimestampForOldRow = now.minusMinutes(2); + final DateTime createdAtForOldRow = now.minusMinutes(3); + final DateTime updatedAtForOldRow = now.minusMinutes(1); + final BundleWithAudit bundleWithAudit = BundleWithAudit.create( + Bundle.create( + id, + ApplicationContext.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER), + BUNDLE_ERROR, + X_REQUEST_ID, + messageTimestampForOldRow), + createdAtForOldRow, + updatedAtForOldRow); + databaseSteps.saveBundleWithStatus(bundleWithAudit); + asmsMockSteps.stubASMSWithApplicationMetadataInResponseBody(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, MAINTAINER_CODE, APP_NAME, APP_URL); + asmsMockSteps.stubASMSWithApplicationMetadataForMaintainerInResponseBody(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, MAINTAINER_CODE, APP_NAME, APP_URL, OCI_IMAGE_URL); + + // When + var response = startBundleGenerationEndpoint.startBundleGeneration(VALID_APP_PARAMS, SECOND_X_REQUEST_ID); + waitDelay(); + + // Then + softly.assertThat(response.getStatusCode()) + .as("Wrong HTTP status code received from the service.") + .isEqualTo(HttpStatus.SC_ACCEPTED); + databaseSteps.verifySecondBundleWasAdded(softly, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, GENERATION_REQUESTED, SECOND_X_REQUEST_ID, messageTimestampForOldRow, updatedAtForOldRow); + final GenerationMessage expectedGenerationMessage = GenerationMessage.create(DUMMY_ID, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, OCI_IMAGE_URL, ENCRYPT); + rabbitMQSteps.verifySentGenerationMessage(softly, expectedGenerationMessage, SECOND_X_REQUEST_ID); + } + + @Test + @Issue("SPARK-36508") + @DisplayName("GET: Call startBundleGeneration - Status code 202, start a new generation skipped due to existing one") + void callRequestBundleWhenGenerationWasAlreadyTriggered_verifyNotSendingMessageAndNotSavingToDB(SoftAssertions softly) throws JsonProcessingException { + // Given + final UUID id = randomUUID(); + final DateTime now = now(DateTimeZone.UTC); + final DateTime messageTimestampForOldRow = now.minusMinutes(3); + final DateTime createdAtForOldRow = now.minusMinutes(2); + final BundleWithAudit bundleWithAudit = BundleWithAudit.create( + Bundle.create( + id, + ApplicationContext.create(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER), + GENERATION_REQUESTED, + X_REQUEST_ID, + messageTimestampForOldRow), + createdAtForOldRow, + null); + databaseSteps.saveBundleWithStatus(bundleWithAudit); + asmsMockSteps.stubASMSWithApplicationMetadataInResponseBody(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, MAINTAINER_CODE, APP_NAME, APP_URL); + asmsMockSteps.stubASMSWithApplicationMetadataForMaintainerInResponseBody(APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER, MAINTAINER_CODE, APP_NAME, APP_URL, OCI_IMAGE_URL); + + // When + var response = startBundleGenerationEndpoint.startBundleGeneration(VALID_APP_PARAMS, SECOND_X_REQUEST_ID); + waitDelay(); + + // Then + softly.assertThat(response.getStatusCode()) + .as("Wrong HTTP status code received from the service.") + .isEqualTo(HttpStatus.SC_ACCEPTED); + databaseSteps.verifySecondBundleWasNotAdded(softly, APP_ID, APP_VER, PLATFORM_NAME, FIRMWARE_VER); + rabbitMQSteps.verifyNotSentGenerationMessage(softly); + } + + private void waitDelay() { + try { + TimeUnit.MILLISECONDS.sleep(1000); + } catch (InterruptedException e) { + fail(e); + } + } +} \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/ASMSMockSteps.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/ASMSMockSteps.java new file mode 100644 index 0000000..052ef17 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/ASMSMockSteps.java @@ -0,0 +1,100 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadata; +import com.lgi.appstorebundle.external.asms.model.ApplicationMetadataForMaintainer; +import com.lgi.appstorebundle.external.asms.model.Header; +import com.lgi.appstorebundle.external.asms.model.HeaderForMaintainer; +import com.lgi.appstorebundle.external.asms.model.Maintainer; +import io.qameta.allure.Step; +import lombok.AllArgsConstructor; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.http.entity.ContentType; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +@AllArgsConstructor +public class ASMSMockSteps { + + private static final String SEMICOLON_URL_ENC = "%3A"; + + @Autowired + private WireMockServer asmsWiremock; + + @Autowired + private ObjectMapper objectMapper; + + @Step("Stub Appstore Metadata Service for getting a application metadata with status code NOT_FOUND for AppId: {0}, AppVer: {1}, PlatformName: {2}, FirmwareVer: {3}") + public void stubASMSWithNotFoundForApplicationMetadata(String appId, String appVer, String platformName, String firmwareVersion) { + asmsWiremock.stubFor(get(urlPathEqualTo("/apps/" + appId + ":" + appVer)) + .withQueryParam("firmwareVer", equalTo(firmwareVersion)) + .withQueryParam("platformName", equalTo(platformName)) + .willReturn(aResponse() + .withStatus(HttpStatus.SC_NOT_FOUND) + .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()))); + } + + @Step("Stub Appstore Metadata Service for getting a application metadata for maintainer with status code NOT_FOUND for AppId: {0}, AppVer: {1}, PlatformName: {2}, FirmwareVer: {3}, MaintainerCode: {4}") + public void stubASMSWithNotFoundForApplicationMetadataForMaintainer(String appId, String appVer, String platformName, String firmwareVersion, String maintainerCode) { + asmsWiremock.addStubMapping(get(urlPathEqualTo("/maintainers/" + maintainerCode + "/apps/" + appId + ":" + appVer)) + .withQueryParam("firmwareVer", equalTo(firmwareVersion)) + .withQueryParam("platformName", equalTo(platformName)) + .willReturn(aResponse() + .withStatus(HttpStatus.SC_NOT_FOUND) + .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType())).build()); + } + + @Step("Stub Appstore Metadata Service for getting a application metadata with status code SC_OK for AppId: {0}, AppVer: {1}, PlatformName: {2}, FirmwareVer: {3}") + public void stubASMSWithApplicationMetadataInResponseBody(String appId, String appVer, String platformName, String firmwareVersion, String maintainerCode, String appName, String appUrl) throws JsonProcessingException { + ApplicationMetadata expected = ApplicationMetadata.create(Header.create(appId, appName, appVer, appUrl), Maintainer.create(maintainerCode)); + asmsWiremock.stubFor(get(urlPathEqualTo("/apps/" + appId + SEMICOLON_URL_ENC + appVer)) + .withQueryParam("firmwareVer", equalTo(firmwareVersion)) + .withQueryParam("platformName", equalTo(platformName)) + .willReturn(aResponse() + .withBody(objectMapper.writeValueAsString(expected)) + .withStatus(HttpStatus.SC_OK) + .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()))); + } + + @Step("Stub Appstore Metadata Service for getting a application metadata for maintainer with status code SC_OK for AppId: {0}, AppVer: {1}, PlatformName: {2}, FirmwareVer: {3}, MaintainerCode: {4}") + public void stubASMSWithApplicationMetadataForMaintainerInResponseBody(String appId, String appVer, String platformName, String firmwareVersion, String maintainerCode, String appName, String appUrl, String ociImageUrl) throws JsonProcessingException { + ApplicationMetadataForMaintainer expected = ApplicationMetadataForMaintainer.create(HeaderForMaintainer.create(appId, appName, appVer, appUrl, ociImageUrl)); + asmsWiremock.addStubMapping(get(urlPathEqualTo("/maintainers/" + maintainerCode + "/apps/" + appId + SEMICOLON_URL_ENC + appVer)) + .withQueryParam("firmwareVer", equalTo(firmwareVersion)) + .withQueryParam("platformName", equalTo(platformName)) + .willReturn(aResponse() + .withBody(objectMapper.writeValueAsString(expected)) + .withStatus(HttpStatus.SC_OK) + .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType())).build()); + } + + @Step("Reset all stubs for Appstore Metadata Service") + public void resetAll() { + asmsWiremock.resetAll(); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/DatabaseSteps.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/DatabaseSteps.java new file mode 100644 index 0000000..75012da --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/DatabaseSteps.java @@ -0,0 +1,305 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.utils; + +import com.lgi.appstorebundle.api.model.ApplicationContext; +import com.lgi.appstorebundle.api.model.Bundle; +import com.lgi.appstorebundle.api.model.BundleStatus; +import com.lgi.appstorebundle.test.service.model.BundleWithAudit; +import io.qameta.allure.Step; +import org.assertj.core.api.SoftAssertions; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static com.lgi.appstorebundle.jooq.generated.Tables.BUNDLE; +import static java.util.Comparator.comparing; + +public class DatabaseSteps { + + private final DSLContext dslContext; + + public DatabaseSteps(DSLContext dslContext) { + this.dslContext = dslContext; + } + + @Step("Get Bundle from DB AppId: {0}, AppVer: {1}, PlatformName: {2}, FirmwareVer: {3}") + public Optional getBundleWithAudit(String applicationId, String applicationVersion, String platformName, String firmwareVersion) { + return dslContext.selectFrom(BUNDLE) + .where(BUNDLE.APPLICATION_ID.eq(applicationId)) + .and(BUNDLE.APPLICATION_VERSION.eq(applicationVersion)) + .and(BUNDLE.PLATFORM_NAME.eq(platformName)) + .and(BUNDLE.FIRMWARE_VERSION.eq(firmwareVersion)) + .fetchOptional(r -> BundleWithAudit.create( + Bundle.create( + r.getId(), + ApplicationContext.create( + r.getApplicationId(), + r.getApplicationVersion(), + r.getPlatformName(), + r.getFirmwareVersion()), + BundleStatus.valueOf(r.getStatus()), + r.getXRequestId(), + r.getMessageTimestamp() + ), + r.getCreatedAt(), + r.getUpdatedAt()) + ); + } + + @Step("Get Bundle from DB Id: {0}") + public Optional getBundleWithAudit(UUID id) { + return dslContext.selectFrom(BUNDLE) + .where(BUNDLE.ID.eq(id)) + .fetchOptional(r -> BundleWithAudit.create( + Bundle.create( + r.getId(), + ApplicationContext.create( + r.getApplicationId(), + r.getApplicationVersion(), + r.getPlatformName(), + r.getFirmwareVersion()), + BundleStatus.valueOf(r.getStatus()), + r.getXRequestId(), + r.getMessageTimestamp() + ), + r.getCreatedAt(), + r.getUpdatedAt()) + ); + } + + @Step("Get list of Bundle rows from DB AppId: {0}, AppVer: {1}, PlatformName: {2}, FirmwareVer: {3}") + public List getBundlesWithAudit(String applicationId, String applicationVersion, String platformName, String firmwareVersion) { + return dslContext.selectFrom(BUNDLE) + .where(BUNDLE.APPLICATION_ID.eq(applicationId)) + .and(BUNDLE.APPLICATION_VERSION.eq(applicationVersion)) + .and(BUNDLE.PLATFORM_NAME.eq(platformName)) + .and(BUNDLE.FIRMWARE_VERSION.eq(firmwareVersion)) + .fetch(r -> BundleWithAudit.create( + Bundle.create( + r.getId(), + ApplicationContext.create( + r.getApplicationId(), + r.getApplicationVersion(), + r.getPlatformName(), + r.getFirmwareVersion()), + BundleStatus.valueOf(r.getStatus()), + r.getXRequestId(), + r.getMessageTimestamp() + ), + r.getCreatedAt(), + r.getUpdatedAt()) + ); + } + + @Step("Save Bundle in DB: {0}") + public void saveBundleWithStatus(BundleWithAudit bundleWithAudit) { + dslContext.transaction(configuration -> + { + final Bundle bundle = bundleWithAudit.getBundle(); + final ApplicationContext applicationContext = bundle.getApplicationContext(); + DSL.using(configuration).insertInto(BUNDLE, + BUNDLE.ID, + BUNDLE.APPLICATION_ID, + BUNDLE.APPLICATION_VERSION, + BUNDLE.PLATFORM_NAME, + BUNDLE.FIRMWARE_VERSION, + BUNDLE.STATUS, + BUNDLE.X_REQUEST_ID, + BUNDLE.MESSAGE_TIMESTAMP, + BUNDLE.CREATED_AT, + BUNDLE.UPDATED_AT + ).values( + bundle.getId(), + applicationContext.getApplicationId(), + applicationContext.getApplicationVersion(), + applicationContext.getPlatformName(), + applicationContext.getFirmwareVersion(), + bundle.getStatus().name(), + bundle.getXRequestId(), + bundle.getMessageTimestamp(), + bundleWithAudit.getCreatedAt(), + bundleWithAudit.getUpdatedAt() + ) + .execute(); + }); + } + + @Step("Clear 'bundle' table") + public void clearBundleTable() { + dslContext.deleteFrom(BUNDLE).execute(); + } + + @Step("Verify bundle AppId: {1}, AppVer: {2}, PlatformName: {3}, FirmwareVer: {4} not store in DB") + public void verifyNotStoredBundle(SoftAssertions softly, String applicationId, String applicationVersion, String platformName, String firmwareVersion) { + final Optional application = getBundleWithAudit(applicationId, applicationVersion, platformName, firmwareVersion); + softly.assertThat(application) + .as("Bundle stored in DB but should not be.") + .isEmpty(); + } + + @Step("Verify bundle was created in DB, AppId: {1}, AppVer: {2}, PlatformName: {3}, FirmwareVer: {4}, Status: {5}, xRequestId: {6}") + public void verifyBundleWasCreated(SoftAssertions softly, String applicationId, String applicationVersion, String platformName, String firmwareVersion, BundleStatus status, String xRequestId) { + final Optional actualBundleWithAudit = getBundleWithAudit( + applicationId, + applicationVersion, + platformName, + firmwareVersion); + softly.assertThat(actualBundleWithAudit) + .as("Bundle not stored in DB.") + .isNotEmpty(); + + final Bundle actualBundle = actualBundleWithAudit.get().getBundle(); + + softly.assertThat(actualBundle.getStatus()) + .as("Bundle from DB contains a wrong Status.") + .isEqualTo(status); + softly.assertThat(actualBundle.getXRequestId()) + .as("Bundle from DB contains a wrong xRequestId.") + .isEqualTo(xRequestId); + softly.assertThat(actualBundle.getMessageTimestamp()) + .as("Bundle from DB should contain a MessageTimestamp.") + .isNotNull(); + softly.assertThat(actualBundleWithAudit.get().getCreatedAt()) + .as("Bundle from DB should contain a createdAt.") + .isNotNull(); + softly.assertThat(actualBundleWithAudit.get().getUpdatedAt()) + .as("Bundle from DB should not contain a updatedAt.") + .isNull(); + } + + @Step("Verify bundle was updated in DB, Id: {1}, AppId: {2}, AppVer: {3}, PlatformName: {4}, FirmwareVer: {5}, Status: {6}, xRequestId: {7}, messageTimestampBeforeUpdate: {8}, createdAtBeforeUpdate: {9}, updatedAtBeforeUpdate: {10}") + public void verifyBundleWasUpdated(SoftAssertions softly, UUID id, String expectedAppId, String expectedAppVer, String expectedPlatformName, String expectedFirmwareVersion, BundleStatus expectedStatus, String expectedXRequestId, DateTime messageTimestampForOldRow, DateTime createdAtForOldRow, DateTime updatedAtForOldRow) { + final Optional actualBundleWithAudit = getBundleWithAudit(id); + softly.assertThat(actualBundleWithAudit) + .as("Bundle not stored in DB.") + .isNotEmpty(); + + final Bundle actualBundle = actualBundleWithAudit.get().getBundle(); + final ApplicationContext applicationContext = actualBundle.getApplicationContext(); + softly.assertThat(applicationContext.getApplicationId()) + .as("Bundle from DB contains a wrong AppId.") + .isEqualTo(expectedAppId); + softly.assertThat(applicationContext.getApplicationVersion()) + .as("Bundle from DB contains a wrong AppVersion.") + .isEqualTo(expectedAppVer); + softly.assertThat(applicationContext.getPlatformName()) + .as("Bundle from DB contains a wrong PlatformName.") + .isEqualTo(expectedPlatformName); + softly.assertThat(applicationContext.getFirmwareVersion()) + .as("Bundle from DB contains a wrong FirmwareVersion.") + .isEqualTo(expectedFirmwareVersion); + softly.assertThat(actualBundle.getStatus()) + .as("Bundle from DB contains a wrong Status.") + .isEqualTo(expectedStatus); + softly.assertThat(actualBundle.getXRequestId()) + .as("Bundle from DB contains a wrong xRequestId.") + .isEqualTo(expectedXRequestId); + softly.assertThat(actualBundle.getMessageTimestamp().withZone(DateTimeZone.UTC)) + .as("Bundle from DB contains a wrong MessageTimestamp.") + .isGreaterThan(messageTimestampForOldRow); + softly.assertThat(actualBundleWithAudit.get().getCreatedAt().withZone(DateTimeZone.UTC)) + .as("Bundle from DB contains a wrong createdAt.") + .isEqualTo(createdAtForOldRow); + softly.assertThat(actualBundleWithAudit.get().getUpdatedAt().withZone(DateTimeZone.UTC)) + .as("Bundle from DB should contain a updatedAt.") + .isNotNull(); + Optional.ofNullable(updatedAtForOldRow).ifPresent(dateTime -> + softly.assertThat(actualBundleWithAudit.get().getUpdatedAt().withZone(DateTimeZone.UTC)) + .as("Bundle from DB contains a wrong updatedAt.") + .isGreaterThan(dateTime) + ); + } + + @Step("Verify second bundle was added in DB, AppId: {1}, AppVer: {2}, PlatformName: {3}, FirmwareVer: {4}, Status: {5}, xRequestId: {6}, messageTimestampBeforeUpdate: {7}, createdAtBeforeUpdate: {8}, updatedAtBeforeUpdate: {9}") + public void verifySecondBundleWasAdded(SoftAssertions softly, String applicationId, String applicationVersion, String platformName, String firmwareVersion, BundleStatus expectedStatus, String expectedSecondXRequestId, DateTime messageTimestampForOldRow, DateTime updatedAtForOldRow) { + final List actualBundlesWithAudit = getBundlesWithAudit( + applicationId, + applicationVersion, + platformName, + firmwareVersion); + softly.assertThat(actualBundlesWithAudit) + .as("Two bundles should be stored in DB.") + .hasSize(2); + + final BundleWithAudit actualSecondBundleWithAudit = actualBundlesWithAudit.stream() + .max(comparing(o -> (o.getUpdatedAt() != null ? o.getUpdatedAt() : o.getCreatedAt()))) + .get(); + final Bundle actualSecondBundle = actualSecondBundleWithAudit + .getBundle(); + + softly.assertThat(actualSecondBundle.getStatus()) + .as("Bundle from DB contains a wrong Status.") + .isEqualTo(expectedStatus); + softly.assertThat(actualSecondBundle.getXRequestId()) + .as("Bundle from DB contains a wrong xRequestId.") + .isEqualTo(expectedSecondXRequestId); + softly.assertThat(actualSecondBundle.getMessageTimestamp().withZone(DateTimeZone.UTC)) + .as("Bundle from DB contains a wrong MessageTimestamp.") + .isGreaterThan(messageTimestampForOldRow); + Optional.ofNullable(updatedAtForOldRow).ifPresent(dateTime -> + softly.assertThat(actualSecondBundleWithAudit.getCreatedAt().withZone(DateTimeZone.UTC)) + .as("Bundle from DB should contain a createdAt greater then an updatedAt for old row.") + .isGreaterThan(dateTime)); + softly.assertThat(actualSecondBundleWithAudit.getUpdatedAt()) + .as("Bundle from DB should not contain a updatedAt.") + .isNull(); + } + + @Step("Verify bundle was not updated in DB, Id: {1}, Status: {2}, messageTimestamp: {3}, updatedAt: {4}") + public void verifyBundleWasNotUpdated(SoftAssertions softly, UUID id, BundleStatus expectedStatus, DateTime expectedMessageTimestamp, DateTime updatedAtForOldRow) { + final Optional actualBundleWithAudit = getBundleWithAudit(id); + softly.assertThat(actualBundleWithAudit) + .as("Bundle not stored in DB.") + .isNotEmpty(); + + final Bundle actualBundle = actualBundleWithAudit.get().getBundle(); + softly.assertThat(actualBundle.getStatus()) + .as("Bundle from DB contains a wrong Status.") + .isEqualTo(expectedStatus); + softly.assertThat(actualBundle.getMessageTimestamp().withZone(DateTimeZone.UTC)) + .as("Bundle from DB contains a wrong MessageTimestamp.") + .isEqualTo(expectedMessageTimestamp); + Optional.ofNullable(updatedAtForOldRow).ifPresentOrElse( + dateTime -> softly.assertThat(actualBundleWithAudit.get().getUpdatedAt().withZone(DateTimeZone.UTC)) + .as("Bundle from DB contains a wrong updatedAt.") + .isEqualTo(dateTime), + () -> softly.assertThat(actualBundleWithAudit.get().getUpdatedAt()) + .as("Bundle from DB should not contain a updatedAt.") + .isNull() + ); + } + + @Step("Verify second bundle was not added in DB, AppId: {1}, AppVer: {2}, PlatformName: {3}, FirmwareVer: {4}, Status: {5}, xRequestId: {6}, messageTimestampBeforeDoingUpdate: {7}, createdAtBeforeDoingUpdate: {8}, updatedAtBeforeDoingUpdate: {9}") + public void verifySecondBundleWasNotAdded(SoftAssertions softly, String applicationId, String applicationVersion, String platformName, String firmwareVersion) { + final List actualBundleWithAudit = getBundlesWithAudit( + applicationId, + applicationVersion, + platformName, + firmwareVersion); + softly.assertThat(actualBundleWithAudit) + .as("Second bundle not stored in DB.") + .hasSize(1); + } +} \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/RabbitMQSteps.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/RabbitMQSteps.java new file mode 100644 index 0000000..68566c4 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/RabbitMQSteps.java @@ -0,0 +1,211 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lgi.appstorebundle.api.Environment; +import com.lgi.appstorebundle.model.EncryptionMessage; +import com.lgi.appstorebundle.model.FeedbackMessage; +import com.lgi.appstorebundle.model.GenerationMessage; +import com.lgi.appstorebundle.test.service.model.EncryptionMessageWithEnvelope; +import com.lgi.appstorebundle.test.service.model.GenerationMessageWithEnvelope; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DeliverCallback; +import io.qameta.allure.Step; +import org.assertj.core.api.SoftAssertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.RabbitMQContainer; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.Optional; +import java.util.Stack; +import java.util.concurrent.TimeoutException; + +public class RabbitMQSteps { + + private static final Logger LOG = LoggerFactory.getLogger(RabbitMQSteps.class); + + public static final String X_REQUEST_ID = "x-request-id"; + + private final ObjectMapper objectMapper; + private final Channel generationChannel; + private final Channel encryptionChannel; + private final Connection connection; + private final Stack generationMsgsStack; + private final Stack encryptionMsgsStack; + private final Environment environment; + + public RabbitMQSteps(RabbitMQContainer rabbitMQContainer, ObjectMapper objectMapper, Environment environment) throws URISyntaxException, NoSuchAlgorithmException, KeyManagementException, IOException, TimeoutException { + this.objectMapper = objectMapper; + this.environment = environment; + this.generationMsgsStack = new Stack<>(); + this.encryptionMsgsStack = new Stack<>(); + ConnectionFactory factory = new ConnectionFactory(); + factory.setUri(rabbitMQContainer.getAmqpUrl()); + connection = factory.newConnection(); + generationChannel = connection.createChannel(); + generationChannel.basicConsume("bundlegen-service-requests", false, createGenerationConsumer(), consumerTag -> { + }); + encryptionChannel = connection.createChannel(); + encryptionChannel.basicConsume("bundlecrypt-service-requests", false, createEncryptionConsumer(), consumerTag -> { + }); + } + + private DeliverCallback createGenerationConsumer() { + return (consumerTag, delivery) -> { + final GenerationMessage generationMessage = objectMapper.readValue(delivery.getBody(), GenerationMessage.class); + final String xRequestId = Optional.ofNullable(delivery.getProperties().getHeaders()) + .map(headers -> headers.get(X_REQUEST_ID)) + .map(Object::toString) + .filter(maybeXRequestId -> !maybeXRequestId.isEmpty()) + .orElse(null); + final GenerationMessageWithEnvelope generationMessageWithEnvelope = GenerationMessageWithEnvelope.create(generationMessage, xRequestId); + LOG.info("Message from RabbitMQContainer received: {}", generationMessageWithEnvelope); + generationMsgsStack.push(generationMessageWithEnvelope); + }; + } + + private DeliverCallback createEncryptionConsumer() { + return (consumerTag, delivery) -> { + final EncryptionMessage encryptionMessage = objectMapper.readValue(delivery.getBody(), EncryptionMessage.class); + final String xRequestId = Optional.ofNullable(delivery.getProperties().getHeaders()) + .map(headers -> headers.get(X_REQUEST_ID)) + .map(Object::toString) + .orElseThrow(); + final EncryptionMessageWithEnvelope encryptionMessageWithEnvelope = EncryptionMessageWithEnvelope.create(encryptionMessage, xRequestId); + LOG.info("Message from RabbitMQContainer received: {}", encryptionMessageWithEnvelope); + encryptionMsgsStack.push(encryptionMessageWithEnvelope); + }; + } + + @Step("Clear stack with all received messages") + public void clearStack() { + generationMsgsStack.removeAllElements(); + encryptionMsgsStack.removeAllElements(); + } + + @Step("Push massage to the RabbitMQ: {0}, xRequestId: {1}") + public void pushMessage(FeedbackMessage feedbackMessage, String xRequestId) throws IOException { + final AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder(); + Optional.ofNullable(xRequestId).ifPresent(x -> builder.headers(Map.of(X_REQUEST_ID, x))); + generationChannel.basicPublish("", "bundlegen-service-status", builder.build(), objectMapper.writeValueAsBytes(feedbackMessage)); + } + + @Step("Verify sent message to the RabbitMQ: {1}, xRequestId: {2}") + public void verifySentGenerationMessage(SoftAssertions softly, GenerationMessage expected, String xRequestId) { + softly.assertThat(generationMsgsStack.isEmpty()) + .as("GenerationMessage was not received.") + .isFalse(); + final GenerationMessageWithEnvelope generationMessageWithEnvelope = generationMsgsStack.pop(); + final GenerationMessage generationMessage = generationMessageWithEnvelope.getGenerationMessage(); + softly.assertThat(generationMessage) + .as("Message for requesting bundle generation not sent.") + .isNotNull(); + softly.assertThat(generationMessage.getId()) + .as("Generation Message not contains an Id.") + .isNotNull(); + softly.assertThat(generationMessage.getAppId()) + .as("Generation Message contains a wrong AppId.") + .isEqualTo(expected.getAppId()); + softly.assertThat(generationMessage.getAppVersion()) + .as("Generation Message contains a wrong AppVer.") + .isEqualTo(expected.getAppVersion()); + softly.assertThat(generationMessage.getPlatformName()) + .as("Generation Message contains a wrong PlatformName.") + .isEqualTo(expected.getPlatformName()); + softly.assertThat(generationMessage.getFirmwareVersion()) + .as("Generation Message contains a wrong FirmwareVersion.") + .isEqualTo(expected.getFirmwareVersion()); + softly.assertThat(generationMessage.getOciImageUrl()) + .as("Generation Message contains a wrong OciImageUrl.") + .isEqualTo(expected.getOciImageUrl()); + softly.assertThat(generationMessage.getEncrypt()) + .as("Generation Message contains a wrong Encrypt.") + .isEqualTo(expected.getEncrypt()); + softly.assertThat(generationMessageWithEnvelope.getXRequestId()) + .as("Generation Message contains a wrong xRequestId.") + .isEqualTo(xRequestId); + } + + @Step("Verify sent message to the RabbitMQ: {1}, xRequestId: {2}") + public void verifySentEncryptionMessage(SoftAssertions softly, EncryptionMessage expected, String xRequestId) { + softly.assertThat(encryptionMsgsStack.isEmpty()) + .as("EncryptionMessage was not received.") + .isFalse(); + final EncryptionMessageWithEnvelope generationMessageWithEnvelope = encryptionMsgsStack.pop(); + final var encryptionMessage = generationMessageWithEnvelope.getEncryptionMessage(); + softly.assertThat(encryptionMessage) + .as("Message for requesting bundle encryption not sent.") + .isNotNull(); + softly.assertThat(encryptionMessage.getId()) + .as("Encryption Message contains a wrong Id.") + .isEqualTo(expected.getId()); + softly.assertThat(encryptionMessage.getAppId()) + .as("Encryption Message contains a wrong AppId.") + .isEqualTo(expected.getAppId()); + softly.assertThat(encryptionMessage.getAppVersion()) + .as("Encryption Message contains a wrong AppVer.") + .isEqualTo(expected.getAppVersion()); + softly.assertThat(encryptionMessage.getPlatformName()) + .as("Encryption Message contains a wrong PlatformName.") + .isEqualTo(expected.getPlatformName()); + softly.assertThat(encryptionMessage.getFirmwareVersion()) + .as("Encryption Message contains a wrong FirmwareVersion.") + .isEqualTo(expected.getFirmwareVersion()); + softly.assertThat(encryptionMessage.getOciBundleUrl()) + .as("Encryption Message contains a wrong OciBundleUrl.") + .isEqualTo(expected.getOciBundleUrl()); + softly.assertThat(generationMessageWithEnvelope.getXRequestId()) + .as("Encryption Message contains a wrong xRequestId.") + .isEqualTo(xRequestId); + softly.assertThat(encryptionMessage.getEnvironment()) + .as("Encryption Message contains a wrong environment.") + .isEqualTo(environment.toString()); + } + + @Step("Verify not sent message to the RabbitMQ") + public void verifyNotSentGenerationMessage(SoftAssertions softly) { + final boolean messageNotReceived = generationMsgsStack.empty(); + softly.assertThat(messageNotReceived) + .as("Generation Message for requesting bundle generation was sent but should not be.") + .isTrue(); + } + + public void stop() { + try { + LOG.info("RabbitMQContainer channel closing..."); + generationChannel.close(); + encryptionChannel.close(); + LOG.info("RabbitMQContainer channel closed."); + LOG.info("RabbitMQContainer connection closing..."); + connection.close(); + LOG.info("RabbitMQContainer connection closed."); + } catch (IOException | TimeoutException e) { + LOG.warn("RabbitMQContainer error during closing", e); + } + } +} \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/TestLifecycleLogger.java b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/TestLifecycleLogger.java new file mode 100644 index 0000000..1ee7ab0 --- /dev/null +++ b/appstore-bundle-service-test/src/test/java/com/lgi/appstorebundle/test/utils/TestLifecycleLogger.java @@ -0,0 +1,58 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Liberty Global Technology Services BV + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lgi.appstorebundle.test.utils; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; + +@TestInstance(Lifecycle.PER_CLASS) +public interface TestLifecycleLogger { + Logger log = LoggerFactory.getLogger(TestLifecycleLogger.class); + + @BeforeAll + default void logStartTestClass(TestInfo testInfo) { + String className = testInfo.getTestClass().map(Class::getSimpleName).orElse(""); + log.info("Test class started: {}", className); + } + + @AfterAll + default void logEndTestClass(TestInfo testInfo) { + String className = testInfo.getTestClass().map(Class::getSimpleName).orElse(""); + log.info("Test class finished: {}", className); + } + + @BeforeEach + default void logStartTest(TestInfo testInfo) { + log.info("Test started: [{}] :: {}", testInfo.getTestMethod().map(Method::getName).orElse(""), testInfo.getDisplayName()); + } + + @AfterEach + default void logEndTest(TestInfo testInfo) { + log.info("Test finished: [{}] :: {}", testInfo.getTestMethod().map(Method::getName).orElse(""), testInfo.getDisplayName()); + } +} diff --git a/appstore-bundle-service-test/src/test/resources/allure.properties b/appstore-bundle-service-test/src/test/resources/allure.properties new file mode 100644 index 0000000..1d528a0 --- /dev/null +++ b/appstore-bundle-service-test/src/test/resources/allure.properties @@ -0,0 +1,2 @@ +allure.results.directory=target/allure-results +allure.link.issue.pattern=https://jira.lgi.io/browse/{} \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/resources/application-test.properties b/appstore-bundle-service-test/src/test/resources/application-test.properties new file mode 100644 index 0000000..78a3266 --- /dev/null +++ b/appstore-bundle-service-test/src/test/resources/application-test.properties @@ -0,0 +1 @@ +spring.flyway.enabled=false \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/resources/logback-test.xml b/appstore-bundle-service-test/src/test/resources/logback-test.xml new file mode 100644 index 0000000..5cd1877 --- /dev/null +++ b/appstore-bundle-service-test/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/resources/testcontainers.properties b/appstore-bundle-service-test/src/test/resources/testcontainers.properties new file mode 100644 index 0000000..d535a5f --- /dev/null +++ b/appstore-bundle-service-test/src/test/resources/testcontainers.properties @@ -0,0 +1 @@ +testcontainers.reuse.enable=true \ No newline at end of file diff --git a/appstore-bundle-service-test/src/test/resources/tpl/request.ftl b/appstore-bundle-service-test/src/test/resources/tpl/request.ftl new file mode 100644 index 0000000..c74fe57 --- /dev/null +++ b/appstore-bundle-service-test/src/test/resources/tpl/request.ftl @@ -0,0 +1,39 @@ + +<#ftl output_format="HTML"> + <#-- @ftlvariable name="data" type="io.qameta.allure.attachment.http.HttpRequestAttachment" --> +
<#if data.method??>${data.method}<#else>GET to <#if data.url??>${data.url}<#else>Unknown
+ + <#if data.body??> +

Body

+
+
+        ${data.body}<#t>
+      
+
+ + + <#if (data.headers)?has_content> +

Headers

+
+ <#list data.headers as name, value> +
${name}: ${value}
+ +
+ + + + <#if (data.cookies)?has_content> +

Cookies

+
+ <#list data.cookies as name, value> +
${name}: ${value}
+ +
+ + + <#if data.curl??> +

Curl

+
+ ${data.curl} +
+ diff --git a/appstore-bundle-service-test/src/test/resources/tpl/response.ftl b/appstore-bundle-service-test/src/test/resources/tpl/response.ftl new file mode 100644 index 0000000..fa40ae0 --- /dev/null +++ b/appstore-bundle-service-test/src/test/resources/tpl/response.ftl @@ -0,0 +1,33 @@ + +<#ftl output_format="HTML"> + <#-- @ftlvariable name="data" type="io.qameta.allure.attachment.http.HttpResponseAttachment" --> +
Status code ${(data.responseCode)!"Unknown"}
+ <#if data.url??>
${data.url}
+ + <#if data.body??> +

Body

+
+
+        ${data.body}<#t>
+      
+
+ + + <#if (data.headers)?has_content> +

Headers

+
+ <#list data.headers as name, value> +
${name}: ${value}
+ +
+ + + + <#if (data.cookies)?has_content> +

Cookies

+
+ <#list data.cookies as name, value> +
${name}: ${value}
+ +
+ diff --git a/charts/appstore-bundle-service-0.15.0.tgz b/charts/appstore-bundle-service-0.15.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a3fcbbdc837e4d6683e06133daef16325763f4a9 GIT binary patch literal 3241 zcmV;a3|8|WiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PI}0j-wa9ETizx>G3nmEc{8lA z51<=aawRz_yW97-zmaT%fl$(PU$WbaerUixI^T43&PQJ;iDRXO#PT@dA;Yplc|!xN zoXv>T?wmyI*;hBu^SqOK-Tw7FZ~d=VuhpJa>-BogJAPF^K7Qs^8?TNV&%pb7(Ysrs zG?CA|ua>oL++U=Tn7%`)sNhYwsX8Q%7hgTM+HgH5L={LHYx}H~g$tb8Z~;FJ7iT6S zY{E?IST!q^Db=%N>;@vLT+lH}J^u$2W5Oz1qRqKZ#<-As_@A$fLf@T%3d`F~P$g(^JwxXJCW0|>OZgN6 z5zi%^&NL7n0;2(Ph0082Hg7sc?%9H(=GBfNo34<~R~TY2sJR|IHw2~ZRq5zIoCBDP1R^rGb|(r8cH+kXV{L3VmNH6s zfN)Fo%!40dfut`qMO6B(U)+JU^SIB84_emx5W4Y#f zmi&q{R0<@1NT|dR#&hd78jvwVVC2?}2AN945VSD5-b$(|pS}boCi<30GZZ0JTGDZ% zR~(gVM%4;|;6ON(T0V6A5}daDj{nj*fS)_VH@&MN{M;H0THRr%?L%(>XT9!uXV~d= zedzrNt?qmHsnb1w2}n)s0Dg?6(O*cQ#)TLffBA?j3QR<*kcu&&6B+;`e43Cc!c^R# zKr4ZEE8^1!iNMUMdbjTUUkne+M{;&Z1BEsveBsB zZJl1U&zo?lWrCKam`+(52I+(-1git^k9M~`XxYARui97n^h-&n$aCM6O)5sEm0?Nb z6t&SKSz$ayj*@IL_J^(EmA`|(slAEe?ps9rIqs0s+PS%~e3QGi@=NqzaZG{rR{d8c z?6jbeB^DM8TBoO-;pN*b106O_Ue(O7rKtrj-{xs(L72{^EL&$z5wa}P4#l2!Ze=+# z50!ro+g-oY>zW85(w+Wlgtb3v)lLuveJUt~#{D0M4jn6tj}%_RM5wuwUd)IfCK zo}_JC|7nb3Ml`C*Xof6CsazdDa4EQB{dZh#c&qEbnsz!BRe6MJ#yl~DW5{+ObTkvX=XSPKRM44n|^MQ%_l!}otGS_)g$r$eN zhW%5zj~&_PJd^$o5@SXJgwo$jP#TrsOU!%fI=63kx83+J-K>g8{NTfYUHGqF+rWP( z$0vLI_ZV%D|MvLr+rocqC)(GPhfO%kcif$P+q3Baq=(DD{k9VVEFpvo7Pgo#DkeeN z-5nr9qR`e&`#7btrP-b#fEmDti2|4*i(ECUSc-cw$%?$?kQ!@hu_Szo@ZB}eU&43x zfY-pE9H1=@B~q~&48#xc+_=GGTz!fG@G`E7fh#`hQ z3i*yO8m2p%&DYCVf1V?fOEd5`j~Klm)%TZ$mhSh;wa==n1a&(@KWxXO>yO=xD7$uwJi22xMx$Z^h@F6Q`h6iZoBbch%poM z2>FArgYCe7^~T9M{;PT?`~AO1X?y&)$A8~0{xjLFvH*eSi%j z_S+3Lo_a$~|M`V+ZWBBQKq13G3z@1N5gp7f3U+*jC3$7ZI2Q)OG`1q-s~nb_Y}(=) z&VqIM{^P#J*R=Fjk?Fjcqzl0MOOB{5k-NNi^h*i0y)vl9giyNW)>_=c-H_Q&80!m0 zgLxBL?3T>c;(aWnE_kkd=iQ_Ht;EIMS%Brk^p-&JnHi$UUYaUZy;}W}mR4Q{angiR zt>Hz*=XREUIh(5I75N~)`B92!&7>6m4{)-|{^7HDA*?7UWw$tFFNm_Za#b%IZT5)$ zC!B9+R?saU{`DILMMctfjq|(2uPByZEOwcDIFh>-(o$U+l*J*-gl=xC*`l z|JNF|R~zU5jrtz{KStZ*|2_WycJV()eJkWON;M%Sb0$c*#QzCPF<;qUkbHm&?luG60*1)7Sl*pqEMv@6$SP(` z^sE5LV_S49OF&EXY-q37_a#o~$Hj{=39gZc#mZf{k$(rYc_1c)3{`JHrX(7pTmwy3 zzH7+g)2F9S>-TQE>wgLdQv~@i5U{KMziOPU@BbY)_WOU2()RU#U;n?|`ahuZJIA@prAW|Co)w?B^C3o@aLLJ^?N&w0<4%bLYTmQQUG6 zykN6(I=B6}H!U>zac@?3zE^jj@~#M9J!IT@#<_MJcuyYNk2v!$pWojMB--d5(g>9% zQQQQc*xyFMm+i&eFaGl1C-!3UA;CL#AMR8CQ)Ki1xBfq_H#XM)b#Gt)AEWK-|Gxfz zyY>HOS?$vw=>6qZ`+pQ1NXx(-3s&v`r3PgyN becsu(ecQJ`bNjyl00960m7Z!|08#(|ww8VW literal 0 HcmV?d00001 diff --git a/charts/index.yaml b/charts/index.yaml new file mode 100644 index 0000000..caa5d4b --- /dev/null +++ b/charts/index.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +entries: + appstore-bundle-service: + - apiVersion: v1 + appVersion: 0.15.0 + created: "2023-06-13T10:04:11.118000893Z" + description: Appstore Bundle Service + digest: 56b0dcf922c832f732216487dbda7bef483ae519250706623a320661bcb05840 + home: https://github.com/LibertyGlobal/appstore-bundle-service + kubeVersion: '>=1.12.0-0' + name: appstore-bundle-service + sources: + - https://github.com/LibertyGlobal/appstore-bundle-service + urls: + - https://libertyglobal.github.io/appstore-bundle-service/charts/appstore-bundle-service-0.15.0.tgz + version: 0.15.0 +generated: "2023-06-13T10:04:11.116365867Z" diff --git a/helm/appstore-bundle-service/.helmignore b/helm/appstore-bundle-service/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/appstore-bundle-service/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/appstore-bundle-service/Chart.yaml b/helm/appstore-bundle-service/Chart.yaml new file mode 100644 index 0000000..b4defae --- /dev/null +++ b/helm/appstore-bundle-service/Chart.yaml @@ -0,0 +1,28 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: v1 +appVersion: snapshot +description: Appstore Bundle Service +home: https://github.com/LibertyGlobal/appstore-bundle-service +kubeVersion: ">=1.12.0-0" +name: appstore-bundle-service +sources: + - https://github.com/LibertyGlobal/appstore-bundle-service +version: snapshot diff --git a/helm/appstore-bundle-service/templates/_helpers.tpl b/helm/appstore-bundle-service/templates/_helpers.tpl new file mode 100644 index 0000000..9d26a9e --- /dev/null +++ b/helm/appstore-bundle-service/templates/_helpers.tpl @@ -0,0 +1,46 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +{{- define "appstore-bundle-charts.name" -}} +{{- .Chart.Name | trunc 40 | trimSuffix "-" -}} +{{- end -}} + +{{- define "appstore-bundle-charts.appSuffix" -}} +{{- $globalAppSuffix := "" -}} +{{- if not (empty .Values.appSuffix) -}} +{{- $globalAppSuffix = .Values.appSuffix -}} +{{- else if not (empty .Values.global) -}} + {{- if not (empty .Values.global.appSuffix) -}} + {{- $globalAppSuffix = .Values.global.appSuffix -}} + {{- end -}} +{{- end -}} +{{- $globalAppSuffix -}} +{{- end -}} + +{{- define "appstore-bundle-charts.fullname" -}} +{{- $appSuffix := include "appstore-bundle-charts.appSuffix" . -}} +{{- printf "%s-%s" .Chart.Name $appSuffix| trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "appstore-bundle-charts.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/helm/appstore-bundle-service/templates/configmap.yaml b/helm/appstore-bundle-service/templates/configmap.yaml new file mode 100644 index 0000000..736dfa3 --- /dev/null +++ b/helm/appstore-bundle-service/templates/configmap.yaml @@ -0,0 +1,54 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "appstore-bundle-charts.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ include "appstore-bundle-charts.fullname" . }} + helm.sh/chart: {{ include "appstore-bundle-charts.chart" . }} +data: +{{- range $key, $value := .Values.configMap }} + {{- if eq (kindOf $value) "map" }} + + {{- if $value.required -}} + {{- $dummyValueNeededForValidation := required (printf "Missing config value `configMap.%s.value`" $key) $value.value -}} + {{- end -}} +{{/* +Quotes is $value.indent != true and $value.quote = true or $value.quote does not exist +*/}} + {{- if and (or (not (hasKey $value "quote")) $value.quote) (not $value.indent) }} + {{ $key }}: {{ $value.value | quote }} + {{- else if not $value.indent }} + {{ $key }}: {{ $value.value }} + {{- else }} + {{ $key }}: |+ +{{ $value.value | indent 4 }} + {{- end }} + {{- else }} + {{- if contains "\n" $value }} + {{ $key }}: |- +{{ $value | indent 4 }} + {{- else }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/appstore-bundle-service/templates/deployment.yaml b/helm/appstore-bundle-service/templates/deployment.yaml new file mode 100644 index 0000000..f6dc8ce --- /dev/null +++ b/helm/appstore-bundle-service/templates/deployment.yaml @@ -0,0 +1,74 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "appstore-bundle-charts.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ include "appstore-bundle-charts.fullname" . }} + helm.sh/chart: {{ include "appstore-bundle-charts.chart" . }} +spec: + replicas: 1 + revisionHistoryLimit: 0 + selector: + matchLabels: + app: {{ include "appstore-bundle-charts.fullname" . }} + template: + metadata: + labels: + app: {{ include "appstore-bundle-charts.fullname" . }} + spec: + containers: + - name: {{ include "appstore-bundle-charts.name" . }} + image: {{ required "Missing `.Values.image.repository`" .Values.image.repository }}:{{ .Chart.AppVersion }} + imagePullPolicy: Always + ports: + - containerPort: 8080 + resources: + requests: + memory: "1024Mi" + cpu: "250m" + limits: + memory: "1024Mi" + cpu: "1000m" + envFrom: + - configMapRef: + name: {{ include "appstore-bundle-charts.fullname" . }} + env: + - name: JDBC_USER + valueFrom: + secretKeyRef: + name: {{ .Values.sealedSecretName }} + key: JDBC_USER + - name: JDBC_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.sealedSecretName }} + key: JDBC_PASSWORD + - name: STACK_NAME + value: {{ include "appstore-bundle-charts.fullname" . }}-{{ .Release.Namespace }} + - name: HOST_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + value: {{ .Release.Namespace }} + diff --git a/helm/appstore-bundle-service/templates/ingress.yaml b/helm/appstore-bundle-service/templates/ingress.yaml new file mode 100644 index 0000000..cbb4d2f --- /dev/null +++ b/helm/appstore-bundle-service/templates/ingress.yaml @@ -0,0 +1,40 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "appstore-bundle-charts.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ include "appstore-bundle-charts.fullname" . }} + annotations: + kubernetes.io/ingress.class: payload +spec: + rules: + - host: {{ include "appstore-bundle-charts.fullname" . }}.{{ .Release.Namespace }}.{{ .Values.ingress.domainName }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "appstore-bundle-charts.fullname" . }} + port: + number: {{ .Values.service.port }} \ No newline at end of file diff --git a/helm/appstore-bundle-service/templates/sealedsecret.yaml b/helm/appstore-bundle-service/templates/sealedsecret.yaml new file mode 100644 index 0000000..e2dc94b --- /dev/null +++ b/helm/appstore-bundle-service/templates/sealedsecret.yaml @@ -0,0 +1,46 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: {{ .Values.sealedSecretName }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Values.sealedSecretName }} + helm.sh/chart: {{ include "appstore-bundle-charts.chart" . }} +spec: + encryptedData: + {{- range $key, $value := .Values.sealedSecret -}} + {{- if eq (kindOf $value) "map" }} + {{- if $value.required }} + {{ $key }}: {{ required (printf "Missing secret value `secret.%s`" $key) $value.value }} + {{- else }} + {{ $key }}: {{ $value.value }} + {{- end }} + {{- else }} + {{ $key }}: {{ $value }} + {{- end }} + {{- end }} + template: + metadata: + creationTimestamp: null + name: {{ .Values.sealedSecretName }} + namespace: {{ .Release.Namespace }} + type: Opaque diff --git a/helm/appstore-bundle-service/templates/service.yaml b/helm/appstore-bundle-service/templates/service.yaml new file mode 100644 index 0000000..eaa9b31 --- /dev/null +++ b/helm/appstore-bundle-service/templates/service.yaml @@ -0,0 +1,36 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "appstore-bundle-charts.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ include "appstore-bundle-charts.fullname" . }} + helm.sh/chart: {{ include "appstore-bundle-charts.chart" . }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + name: app-connectors + targetPort: {{ .Values.service.containerPort }} + protocol: TCP + selector: + app: {{ include "appstore-bundle-charts.fullname" . }} diff --git a/helm/appstore-bundle-service/values.yaml b/helm/appstore-bundle-service/values.yaml new file mode 100644 index 0000000..d015ead --- /dev/null +++ b/helm/appstore-bundle-service/values.yaml @@ -0,0 +1,57 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +appSuffix: '' + +image: + repository: daccloud/appstore-bundle-service + +ingress: + domainName: local + +service: + type: "ClusterIP" + port: 80 + containerPort: 8080 + +configMap: + JDBC_DATABASE_NAME: "appstore_bundle_service" # Database name + JDBC_SCHEMA: "appstore_bundle_service" # Schema in database + JDBC_PORT: "5432" # JDBC port + ENVIRONMENT: nil # Part of RabbitMQ encryption message + MAX_WRITE_NODE_POOL_SIZE: '10' # The max pool size for write mode in the database + MAX_READ_NODE_POOL_SIZE: '10' # The max pool size for read-only mode in the database + WRITE_NODE_JDBC_HOST: postgres-write # The host for write mode in the database + READ_NODE_JDBC_HOST: postgres-read # The host for read-only mode in the database + APPSTORE_METADATA_SERVICE_URL: http://appstore-metadata-service # URL to appstore-metadata-service + HTTP_RETRY_AFTER: '30s' # Indication to client how long it should wait till the retry API call + QUERY_TIMEOUT_SECONDS: '50' # The default timeout for long-running queries + BUNDLE_ENCRYPTION_ENABLED: 'true' # Toggle for bundle encryption + GENERATION_QUEUE_NAME: bundlegen-service-requests # Target queue for bundle generation + GENERATION_STATUS_QUEUE_NAME: bundlegen-service-status # Source queue for bundle generation status + ENCRYPTION_QUEUE_NAME: bundlecrypt-service-requests # Target queue for bundle encryption + ENCRYPTION_STATUS_QUEUE_NAME: bundlecrypt-service-status # Source queue for bundle encryption status + RABBITMQ_PORT: '5672' # Rabbit MQ port + RABBITMQ_HOST: bundle-generator-rabbit # Rabbit MQ host name + BUNDLE_EXTENSION: 'tar.gz' # Extension of a generated bundle + +sealedSecretName: appstore-bundle-service +sealedSecret: + JDBC_PASSWORD: setYourSealedSecretUsingYourSealedSecretController # Sealed JDBC password + JDBC_USER: setYourSealedSecretUsingYourSealedSecretController # Sealed JDBC user \ No newline at end of file diff --git a/helm/config/lint-config.yaml b/helm/config/lint-config.yaml new file mode 100644 index 0000000..c20cd32 --- /dev/null +++ b/helm/config/lint-config.yaml @@ -0,0 +1,19 @@ +# +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Liberty Global Technology Services BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f1e3a63 --- /dev/null +++ b/pom.xml @@ -0,0 +1,426 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.6.6 + + + + com.lgi.appstorebundle + appstore-bundle-service + 0.16.0-SNAPSHOT + pom + AppStore Bundle Service + + + appstore-bundle-service-api + appstore-bundle-service-application + appstore-bundle-service-test + appstore-bundle-service-storage + appstore-bundle-service-external + + + + 11 + com.lgi.appstorebundle + 42.3.5 + 8.5.10 + 3.16.6 + 1.18.0 + 1.16 + 5.1 + 2.2.2 + 1.7.30 + true + 1.10.1 + 2.14.2 + 2.14.2 + 1.3.5 + 5.9.2 + 5.9.2 + 5.9.2 + 2.12.2 + 4.2.0 + 1.0.1 + 2.2.8 + 2.6.6 + 31.1-jre + 1.7.1 + 0.16.0 + 1.4.3 + 2.0.2 + 1.6.1 + 3.24.2 + 1.5.22 + 31.1-jre + true + true + LICENSE-THIRD-PARTY.txt + Apache2|${line.separator} + Apache 2|${line.separator} + Apache 2.0|${line.separator} + Apache-2.0|${line.separator} + Apache License 2.0|${line.separator} + Apache License v2.0|${line.separator} + Apache License, version 2.0|${line.separator} + Apache License, Version 2.0|${line.separator} + Apache Software License - Version 2.0|${line.separator} + ASL 2.0|${line.separator} + The Apache License, Version 2.0|${line.separator} + The Apache Software License, Version 2.0|${line.separator} + BSD|${line.separator} + BSD License 3|${line.separator} + BSD-2-Clause|${line.separator} + The BSD 3-Clause License|${line.separator} + BSD-3-Clause|${line.separator} + The BSD License|${line.separator} + CC0|${line.separator} + CC0 1.0 Universal License|${line.separator} + Public Domain, per Creative Commons CC0|${line.separator} + COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0|${line.separator} + CDDL/GPLv2+CE|${line.separator} + Eclipse Public License 1.0|${line.separator} + Eclipse Public License - v 1.0|${line.separator} + Eclipse Public License, Version 1.0|${line.separator} + Eclipse Public License v2.0|${line.separator} + EDL 1.0|${line.separator} + Eclipse Distribution License - v 1.0|${line.separator} + GPL2 w/ CPE|${line.separator} + MIT|${line.separator} + MIT License|${line.separator} + The MIT License|${line.separator} + The MIT License (MIT) + + true + set_repository_username + set_repository_domain + ${project.artifactId} + + + + + + integration-tests + + + ci-stage + integration-tests + + + + false + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + + development + + true + + + dev + + + + + com.mycila + license-maven-plugin + 4.0.rc2 + + + add-license-headers + + format + + process-sources + + + + + + + + prod + + prod + + + + + + + + org.springframework.boot + spring-boot + ${spring-boot.version} + + + com.google.auto.value + auto-value-annotations + ${auto.value.version} + + + com.google.auto.value + auto-value + ${auto.value.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.core.version} + + + jakarta.annotation + jakarta.annotation-api + ${jakarta.annotation-api.version} + + + org.junit.vintage + junit-vintage-engine + test + ${junit-vintage-engine.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter-api.version} + test + + + org.junit.jupiter + junit-jupiter-params + test + ${junit-jupiter-params} + + + joda-time + joda-time + ${joda-time.version} + + + org.awaitility + awaitility + ${awaitility.version} + test + + + com.google.auto.service + auto-service + provided + ${auto-service.version} + + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + test + + + com.google.guava + guava + ${google.guava.version} + + + io.github.resilience4j + resilience4j-prometheus + ${r4j.prometeus.version} + + + io.prometheus + simpleclient + ${prometeus.simpleclient.version} + + + jakarta.validation + jakarta.validation-api + ${jakarta.validation.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + ${jackson.datatype.version} + + + io.github.resilience4j + resilience4j-circuitbreaker + ${resilience4j.version} + + + io.github.resilience4j + resilience4j-bulkhead + ${resilience4j.version} + + + io.github.resilience4j + resilience4j-core + ${resilience4j.version} + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.flywaydb + flyway-core + ${flywaydb.version} + + + org.postgresql + postgresql + ${postgresql.version} + + + org.jooq + jooq-meta + ${jooq.version} + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + org.testcontainers + rabbitmq + ${testcontainers.version} + test + + + org.testcontainers + postgresql + ${testcontainers.version} + test + + + javax.annotation + javax.annotation-api + + + javax.xml.bind + jaxb-api + + + junit + junit + + + + + org.jooq + jooq + ${jooq.version} + + + javax.xml.bind + jaxb-api + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + org.codehaus.mojo + build-helper-maven-plugin + + + timestamp-property + + timestamp-property + + validate + + current.year + yyyy + + + + + + org.codehaus.mojo + license-maven-plugin + 2.0.0 + + + generate-third-party-dependency-licenses-report + + aggregate-add-third-party + + validate + + ./.. + + + + + + com.mycila + license-maven-plugin + 4.0.rc2 + + + + ${current.year} + Liberty Global Technology Services BV + + true + true + + SCRIPT_STYLE + SCRIPT_STYLE + SCRIPT_STYLE + + + +
LICENSE-HEADER-TEMPLATE.txt
+ **/*.java,**/*.sql,**/*.conf,**/Dockerfile,**/*.yaml,**/*.groovy,**/*.yml,**/*.tpl +
+
+
+ + + check-license-headers + + check + + prepare-package + + +
+
+
+ +