diff --git a/.github/workflows/pr_cdc.yml b/.github/workflows/pr_user_cdc.yml similarity index 93% rename from .github/workflows/pr_cdc.yml rename to .github/workflows/pr_user_cdc.yml index 02da9979..b03d1e0c 100644 --- a/.github/workflows/pr_cdc.yml +++ b/.github/workflows/pr_user_cdc.yml @@ -15,7 +15,7 @@ on: - ready_for_review paths: - 'apps/user-cdc/**' - - '.github/workflows/pr_cdc.yml' + - '.github/workflows/pr_user_cdc.yml' - '.github/workflows/call_code_review.yml' jobs: diff --git a/.github/workflows/pr_user_group_cdc.yml b/.github/workflows/pr_user_group_cdc.yml new file mode 100644 index 00000000..08b65cf4 --- /dev/null +++ b/.github/workflows/pr_user_group_cdc.yml @@ -0,0 +1,31 @@ +name: Code Review - user-group-cdc + +on: + workflow_dispatch: + + pull_request: + branches: + - main + - releases/** + types: + - opened + - edited + - synchronize + - reopened + - ready_for_review + paths: + - 'apps/user-group-cdc/**' + - '.github/workflows/pr_user_cdc.yml' + - '.github/workflows/call_code_review.yml' + +jobs: + code_review: + uses: ./.github/workflows/call_code_review.yml + name: User cdc Code Review + secrets: inherit + with: + pr_number: ${{ github.event.pull_request.number }} + source_branch: ${{ github.head_ref }} + target_branch: ${{ github.base_ref }} + sonar_key: 'pagopa_selfcare-user' + module: 'user-group-cdc' diff --git a/.github/workflows/release_cdc.yml b/.github/workflows/release_user_cdc.yml similarity index 100% rename from .github/workflows/release_cdc.yml rename to .github/workflows/release_user_cdc.yml diff --git a/.github/workflows/release_user_group_cdc.yml b/.github/workflows/release_user_group_cdc.yml new file mode 100644 index 00000000..644fa7c9 --- /dev/null +++ b/.github/workflows/release_user_group_cdc.yml @@ -0,0 +1,63 @@ +name: Deploy SELC - user-group-cdc + +on: + push: + branches: + - main + - releases/* + paths: + - "apps/user-group-cdc/**" + - "infra/container_apps/user-group-cdc/**" + - "apps/pom.xml" + - "pom.xml" + + workflow_dispatch: + inputs: + env: + type: choice + description: Environment + options: + - dev + - uat + - prod + +jobs: + + release_dev: + uses: pagopa/selfcare-commons/.github/workflows/call_release_docker.yml@main + name: '[Dev] User cdc Release' + if: ${{ (startsWith(github.ref_name, 'releases/') != true && inputs.env == null) || inputs.env == 'dev' }} + secrets: inherit + with: + environment: dev + tf_environment: dev + dir: 'infra/container_apps/user-group-cdc' + dockerfile_path: ./apps/user-group-cdc/Dockerfile + docker_image_name: pagopa/selfcare-user-group-cdc + upload_openapi_enabled: false + + release_uat: + uses: pagopa/selfcare-commons/.github/workflows/call_release_docker.yml@main + name: '[UAT] User cdc Release' + if: ${{ (startsWith(github.ref_name, 'releases/') == true && inputs.env == null) || inputs.env == 'uat' }} + secrets: inherit + with: + environment: uat + tf_environment: uat + dir: 'infra/container_apps/user-group-cdc' + dockerfile_path: ./apps/user-group-cdc/Dockerfile + docker_image_name: pagopa/selfcare-user-group-cdc + upload_openapi_enabled: false + + release_prod: + uses: pagopa/selfcare-commons/.github/workflows/call_release_docker.yml@main + name: '[Prod] User cdc Release' + if: ${{ inputs.env == 'prod' }} + secrets: inherit + with: + environment: prod + tf_environment: prod + dir: 'infra/container_apps/user-group-cdc' + dockerfile_path: ./apps/user-group-cdc/Dockerfile + docker_image_name: pagopa/selfcare-user-group-cdc + upload_openapi_enabled: false diff --git a/apps/pom.xml b/apps/pom.xml index e7f3bc5e..e4fc1f38 100644 --- a/apps/pom.xml +++ b/apps/pom.xml @@ -69,6 +69,17 @@ user-group-ms + + user-group-cdc + + + user-group-cdc/pom.xml + + + + user-group-cdc + + diff --git a/apps/user-group-cdc/Dockerfile b/apps/user-group-cdc/Dockerfile new file mode 100644 index 00000000..e328f481 --- /dev/null +++ b/apps/user-group-cdc/Dockerfile @@ -0,0 +1,56 @@ +# syntax=docker/dockerfile:1.6@sha256:ac85f380a63b13dfcefa89046420e1781752bab202122f8f50032edf31be0021 +FROM maven:3-eclipse-temurin-17@sha256:0d328fa6843bb26b60cf44d69833f241ffe96218fb29fa19df7a6603863eaae7 AS builder + +WORKDIR /src +COPY --link pom.xml . + +WORKDIR /src/libs +COPY --link ./libs/ . + +WORKDIR /src/test-coverage +COPY --link ./test-coverage/pom.xml . + +WORKDIR /src/apps +COPY --link ./apps/pom.xml . + +WORKDIR /src/apps/user-group-cdc +COPY --link ./apps/user-group-cdc/pom.xml . +COPY ./apps/user-group-cdc/src/main/ ./src/main/ + +WORKDIR /src + +RUN echo "\n" \ + "\n" \ + "\n" \ + "\${repositoryId}\n" \ + "\${repoLogin}\n" \ + "\${repoPwd}\n" \ + "\n" \ + "\n" \ + "\n" > settings.xml + +ARG REPO_ONBOARDING +ARG REPO_USERNAME +ARG REPO_PASSWORD + +RUN mvn --global-settings settings.xml --projects :user-group-cdc -DrepositoryId=${REPO_ONBOARDING} -DrepoLogin=${REPO_USERNAME} -DrepoPwd=${REPO_PASSWORD} --also-make clean package -DskipTests + +FROM openjdk:17-jdk@sha256:528707081fdb9562eb819128a9f85ae7fe000e2fbaeaf9f87662e7b3f38cb7d8 AS runtime + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +WORKDIR /app + +COPY --from=builder /src/apps/user-group-cdc/target/quarkus-app/lib/ ./lib/ +COPY --from=builder /src/apps/user-group-cdc/target/quarkus-app/*.jar ./ +COPY --from=builder /src/apps/user-group-cdc/target/quarkus-app/app/ ./app/ +COPY --from=builder /src/apps/user-group-cdc/target/quarkus-app/quarkus/ ./quarkus/ + +ADD https://github.com/microsoft/ApplicationInsights-Java/releases/download/3.2.11/applicationinsights-agent-3.2.11.jar ./applicationinsights-agent.jar +RUN chmod 755 ./applicationinsights-agent.jar + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTIONS -jar /app/quarkus-run.jar"] diff --git a/apps/user-group-cdc/README.md b/apps/user-group-cdc/README.md new file mode 100644 index 00000000..7a50aec8 --- /dev/null +++ b/apps/user-group-cdc/README.md @@ -0,0 +1,68 @@ +# Microservice User + +Our dedicated microservice is crafted to expertly manage all events related to operations, such as insertion, update, and deletion, +within the MongoDB collections residing in the user group domain, to send event message on sc-userGroup topic. +This specialized solution has been meticulously designed to mitigate potential concurrency issues arising from the presence of multiple active instances +on the main microservices. + +## Configuration Properties + +Before running you have to set these properties as environment variables. + +| **Property** | **Environment Variable** | **Default** | **Required** | +|-----------------------------------------------|----------------------------------------------|-------------|:------------:| +| quarkus.mongodb.connection-string | MONGODB-CONNECTION-STRING | | yes | +| user-group-cdc.app-insights.connection-string | USER-GROUP-CDC-APPINSIGHTS-CONNECTION-STRING | | yes | +| user-group-cdc.storage.connection-string | STORAGE_CONNECTION_STRING | | yes | +| quarkus.rest-client.event-hub.url | EVENT_HUB_BASE_PATH | | yes | +| eventhub.rest-client.keyName | SHARED_ACCESS_KEY_NAME | | yes | +| eventhub.rest-client.key | EVENTHUB-SC-USERS-GROUP-SELFCARE-WO-KEY-LC | | yes | +| user-group-cdc.send-events.watch.enabled | USER_GROUP_CDC_SEND_EVENTS_WATCH_ENABLED | false | no | + + +> **_NOTE:_** properties that contains secret must have the same name of its secret as uppercase. + + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./mvnw compile quarkus:dev +``` + +For some endpoints + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8083/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./mvnw package +``` +It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./mvnw package -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. + +## Related Guides + + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) + +### OpenAPI Generator + +Rest client are generated using a quarkus' extension. + +[Related guide section...](hhttps://github.com/quarkiverse/quarkus-openapi-generator) diff --git a/apps/user-group-cdc/pom.xml b/apps/user-group-cdc/pom.xml new file mode 100644 index 00000000..25df10c1 --- /dev/null +++ b/apps/user-group-cdc/pom.xml @@ -0,0 +1,224 @@ + + + 4.0.0 + + it.pagopa.selfcare + user-apps + 0.0.1 + + + user-group-cdc + 1.0.0-SNAPSHOT + + 3.12.1 + 1.18.28 + 1.5.5.Final + 17 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.11.0 + true + 3.1.2 + 0.1.3 + 2.4.1 + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-arc + + + org.projectlombok + lombok + ${lombok.version} + provided + + + io.quarkus + quarkus-mongodb-client + + + io.quarkus + quarkus-mongodb-panache + + + com.azure + azure-data-tables + 12.1.1 + + + io.quarkus + quarkus-test-common + test + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-test-mongodb + test + + + io.quarkus + quarkus-jacoco + test + + + io.quarkus + quarkus-test-vertx + test + + + + org.mockito + mockito-inline + 2.13.0 + test + + + io.quarkus + quarkus-panache-mock + test + + + org.mongodb + mongodb-driver-core + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.apache.commons + commons-collections4 + 4.3 + + + com.microsoft.azure + applicationinsights-web + 2.6.4 + + + io.quarkus + quarkus-smallrye-health + + + + io.quarkus + quarkus-rest-client-jackson + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator + ${quarkus-openapi-generator.version} + + + it.pagopa.selfcare + user-sdk-event + 0.0.1 + + + org.glassfish.jaxb + txw2 + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + native + + + native + + + + false + true + false + + + + diff --git a/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/UserGroupCdcLifecycle.java b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/UserGroupCdcLifecycle.java new file mode 100644 index 00000000..1509e449 --- /dev/null +++ b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/UserGroupCdcLifecycle.java @@ -0,0 +1,34 @@ +package it.pagopa.selfcare.user.event; + +import com.azure.data.tables.TableServiceClient; +import com.azure.data.tables.TableServiceClientBuilder; +import io.quarkus.runtime.StartupEvent; +import io.quarkus.runtime.configuration.ConfigUtils; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ApplicationScoped +public class UserGroupCdcLifecycle { + @ConfigProperty(name = "user-group-cdc.storage.connection-string") String storageConnectionString; + @ConfigProperty(name = "user-group-cdc.table.name") String tableName; + + + void onStart(@Observes StartupEvent ev) { + + if(ConfigUtils.getProfiles().contains("test")) { + //Not perform any action when testing + return; + } + + log.info("The application is starting..."); + + // Table CdCStartAt will be created + TableServiceClient tableServiceClient = new TableServiceClientBuilder() + .connectionString(storageConnectionString) + .buildClient(); + tableServiceClient.createTableIfNotExists(tableName); + } +} diff --git a/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/UserGroupCdcService.java b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/UserGroupCdcService.java new file mode 100644 index 00000000..3964281d --- /dev/null +++ b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/UserGroupCdcService.java @@ -0,0 +1,162 @@ +package it.pagopa.selfcare.user.event; + +import com.azure.data.tables.TableClient; +import com.azure.data.tables.models.TableEntity; +import com.azure.data.tables.models.TableServiceException; +import com.microsoft.applicationinsights.TelemetryClient; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.changestream.ChangeStreamDocument; +import com.mongodb.client.model.changestream.FullDocument; +import io.quarkus.mongodb.ChangeStreamOptions; +import io.quarkus.mongodb.reactive.ReactiveMongoClient; +import io.quarkus.mongodb.reactive.ReactiveMongoCollection; +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.Startup; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import it.pagopa.selfcare.user.client.EventHubRestClient; +import it.pagopa.selfcare.user.event.entity.UserGroupEntity; +import it.pagopa.selfcare.user.event.mapper.UserGroupNotificationMapper; +import it.pagopa.selfcare.user.model.TrackEventInput; +import it.pagopa.selfcare.user.model.UserGroupNotificationToSend; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import org.bson.BsonDocument; +import org.bson.conversions.Bson; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.time.Duration; +import java.util.*; + +import static com.mongodb.client.model.Projections.fields; +import static com.mongodb.client.model.Projections.include; +import static it.pagopa.selfcare.user.UserUtils.mapPropsForTrackEvent; +import static it.pagopa.selfcare.user.event.constant.CdcStartAtConstant.*; +import static it.pagopa.selfcare.user.model.TrackEventInput.toTrackEventInput; +import static it.pagopa.selfcare.user.model.TrackEventInput.toTrackEventInputForUserGroup; +import static it.pagopa.selfcare.user.model.constants.EventsMetric.*; +import static it.pagopa.selfcare.user.model.constants.EventsName.EVENT_USER_CDC_NAME; +import static it.pagopa.selfcare.user.model.constants.EventsName.EVENT_USER_GROUP_CDC_NAME; +import static java.util.Arrays.asList; + +@Startup +@Slf4j +@ApplicationScoped +public class UserGroupCdcService { + private static final String OPERATION_NAME = "USER-GROUP-CDC-sendUserGroupEvent"; + private static final String COLLECTION_NAME = "userGroups"; + + private final TelemetryClient telemetryClient; + private final Integer retryMinBackOff; + private final Integer retryMaxBackOff; + private final Integer maxRetry; + private final TableClient tableClient; + private final String mongodbDatabase; + private final ReactiveMongoClient mongoClient; + + @RestClient + @Inject + EventHubRestClient eventHubRestClient; + + private final UserGroupNotificationMapper userGroupNotificationMapper; + + public UserGroupCdcService(ReactiveMongoClient mongoClient, + @ConfigProperty(name = "quarkus.mongodb.database") String mongodbDatabase, + @ConfigProperty(name = "user-group-cdc.retry.min-backoff") Integer retryMinBackOff, + @ConfigProperty(name = "user-group-cdc.retry.max-backoff") Integer retryMaxBackOff, + @ConfigProperty(name = "user-group-cdc.retry") Integer maxRetry, + @ConfigProperty(name = "user-group-cdc.send-events.watch.enabled") Boolean sendEventsEnabled, + TelemetryClient telemetryClient, + TableClient tableClient, + + UserGroupNotificationMapper userGroupNotificationMapper) { + this.maxRetry = maxRetry; + this.mongoClient = mongoClient; + this.mongodbDatabase = mongodbDatabase; + this.retryMaxBackOff = retryMaxBackOff; + this.retryMinBackOff = retryMinBackOff; + this.telemetryClient = telemetryClient; + this.userGroupNotificationMapper = userGroupNotificationMapper; + this.tableClient = tableClient; + telemetryClient.getContext().getOperation().setName(OPERATION_NAME); + initOrderStream(sendEventsEnabled); + } + + private void initOrderStream(Boolean sendEventsEnabled) { + log.info("Starting initOrderStream ... "); + + String resumeToken = null; + + if (!ConfigUtils.getProfiles().contains("test")) { + try { + TableEntity cdcStartAtEntity = tableClient.getEntity(CDC_START_AT_PARTITION_KEY, CDC_START_AT_ROW_KEY); + if (Objects.nonNull(cdcStartAtEntity)) + resumeToken = (String) cdcStartAtEntity.getProperty(CDC_START_AT_PROPERTY); + } catch (TableServiceException e) { + log.warn("Table StarAt not found, it is starting from now ..."); + } + } + + ReactiveMongoCollection dataCollection = getCollection(); + ChangeStreamOptions options = new ChangeStreamOptions() + .fullDocument(FullDocument.UPDATE_LOOKUP); + if (Objects.nonNull(resumeToken)) + options = options.resumeAfter(BsonDocument.parse(resumeToken)); + + Bson match = Aggregates.match(Filters.in("operationType", asList("update", "replace", "insert"))); + Bson project = Aggregates.project(fields(include("_id", "ns", "documentKey", "fullDocument"))); + List pipeline = Arrays.asList(match, project); + + Multi> publisher = dataCollection.watch(pipeline, UserGroupEntity.class, options); + + if (sendEventsEnabled) { + publisher.subscribe().with( + this::consumerToSendScUserGroupEvent, + failure -> { + log.error("Error during subscribe collection, exception: {} , message: {}", failure.toString(), failure.getMessage()); + telemetryClient.trackEvent(EVENT_USER_GROUP_CDC_NAME, mapPropsForTrackEvent(TrackEventInput.builder().exception(failure.getClass().toString()).build()), Map.of(EVENTS_USER_GROUP_FAILURE, 1D)); + Quarkus.asyncExit(); + }); + } + + log.info("Completed initOrderStream ... "); + } + + private ReactiveMongoCollection getCollection() { + return mongoClient + .getDatabase(mongodbDatabase) + .getCollection(COLLECTION_NAME, UserGroupEntity.class); + } + + public void consumerToSendScUserGroupEvent(ChangeStreamDocument document) { + + assert document.getFullDocument() != null; + assert document.getDocumentKey() != null; + UserGroupEntity userGroupChanged = document.getFullDocument(); + + log.info("Starting consumerToSendScUserGroupEvent ... "); + + UserGroupNotificationToSend userGroupNotificationToSend = userGroupNotificationMapper.toUserGroupNotificationToSend(userGroupChanged); + + eventHubRestClient.sendUserGroupMessage(userGroupNotificationToSend) + .onFailure().retry().withBackOff(Duration.ofSeconds(retryMinBackOff), Duration.ofSeconds(retryMaxBackOff)).atMost(maxRetry) + .onItem().invoke(() -> telemetryClient.trackEvent(EVENT_USER_GROUP_CDC_NAME, + mapPropsForTrackEvent(toTrackEventInputForUserGroup(userGroupNotificationToSend)), Map.of(EVENTS_USER_GROUP_PRODUCT_SUCCESS, 1D))) + .onFailure().invoke(() -> telemetryClient.trackEvent(EVENT_USER_GROUP_CDC_NAME, + mapPropsForTrackEvent(toTrackEventInputForUserGroup(userGroupNotificationToSend)), Map.of(EVENTS_USER_GROUP_PRODUCT_FAILURE, 1D))) + .subscribe().with( + result -> { + log.info("SendEvents successfully performed from UserInstitution document having id: {}", document.getDocumentKey().toJson()); + telemetryClient.trackEvent(EVENT_USER_GROUP_CDC_NAME, mapPropsForTrackEvent(toTrackEventInputForUserGroup(userGroupNotificationToSend)), Map.of(EVENTS_USER_GROUP_SUCCESS, 1D)); + }, + failure -> { + log.error("Error during SendEvents from UserInstitution document having id: {} , message: {}", document.getDocumentKey().toJson(), failure.getMessage()); + telemetryClient.trackEvent(EVENT_USER_GROUP_CDC_NAME, mapPropsForTrackEvent(toTrackEventInputForUserGroup(userGroupNotificationToSend)), Map.of(EVENTS_USER_GROUP_FAILURE, 1D)); + }); + } + +} diff --git a/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/config/UserGroupCdcConfig.java b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/config/UserGroupCdcConfig.java new file mode 100644 index 00000000..bcd9f0f1 --- /dev/null +++ b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/config/UserGroupCdcConfig.java @@ -0,0 +1,29 @@ +package it.pagopa.selfcare.user.event.config; + +import com.azure.data.tables.TableClient; +import com.azure.data.tables.TableClientBuilder; +import com.microsoft.applicationinsights.TelemetryClient; +import com.microsoft.applicationinsights.TelemetryConfiguration; +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class UserGroupCdcConfig { + + @ApplicationScoped + public TelemetryClient telemetryClient(@ConfigProperty(name = "user-group-cdc.appinsights.connection-string") String appInsightsConnectionString) { + TelemetryConfiguration telemetryConfiguration = TelemetryConfiguration.createDefault(); + telemetryConfiguration.setConnectionString(appInsightsConnectionString); + return new TelemetryClient(telemetryConfiguration); + } + + @ApplicationScoped + public TableClient tableClient(@ConfigProperty(name = "user-group-cdc.storage.connection-string") String storageConnectionString, + @ConfigProperty(name = "user-group-cdc.table.name") String tableName){ + return new TableClientBuilder() + .connectionString(storageConnectionString) + .tableName(tableName) + .buildClient(); + } + +} diff --git a/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/constant/CdcStartAtConstant.java b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/constant/CdcStartAtConstant.java new file mode 100644 index 00000000..1f45eb86 --- /dev/null +++ b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/constant/CdcStartAtConstant.java @@ -0,0 +1,10 @@ +package it.pagopa.selfcare.user.event.constant; + +public class CdcStartAtConstant { + + + public static final String CDC_START_AT_PARTITION_KEY = "UserGroup"; + public static final String CDC_START_AT_ROW_KEY = "0001"; + public static final String CDC_START_AT_PROPERTY = "startAt"; + +} diff --git a/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/entity/UserGroupEntity.java b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/entity/UserGroupEntity.java new file mode 100644 index 00000000..d1994d87 --- /dev/null +++ b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/entity/UserGroupEntity.java @@ -0,0 +1,30 @@ +package it.pagopa.selfcare.user.event.entity; + +import io.quarkus.mongodb.panache.common.MongoEntity; +import io.quarkus.mongodb.panache.reactive.ReactivePanacheMongoEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.FieldNameConstants; + +import java.time.Instant; +import java.util.Set; + +@EqualsAndHashCode(callSuper = true) +@Data +@MongoEntity(collection = "userGroups") +@FieldNameConstants(asEnum = true) +public class UserGroupEntity extends ReactivePanacheMongoEntity { + + private String id; + private String institutionId; + private String productId; + private String name; + private String description; + private String status; + private Set members; + private Instant createdAt; + private String createdBy; + private Instant modifiedAt; + private String modifiedBy; + +} diff --git a/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/mapper/UserGroupNotificationMapper.java b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/mapper/UserGroupNotificationMapper.java new file mode 100644 index 00000000..f22d38cf --- /dev/null +++ b/apps/user-group-cdc/src/main/java/it/pagopa/selfcare/user/event/mapper/UserGroupNotificationMapper.java @@ -0,0 +1,11 @@ +package it.pagopa.selfcare.user.event.mapper; + +import it.pagopa.selfcare.user.event.entity.UserGroupEntity; +import it.pagopa.selfcare.user.model.UserGroupNotificationToSend; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "cdi") +public interface UserGroupNotificationMapper { + + UserGroupNotificationToSend toUserGroupNotificationToSend(UserGroupEntity userGroupEntity); +} diff --git a/apps/user-group-cdc/src/main/resources/application.properties b/apps/user-group-cdc/src/main/resources/application.properties new file mode 100644 index 00000000..85dc252f --- /dev/null +++ b/apps/user-group-cdc/src/main/resources/application.properties @@ -0,0 +1,21 @@ +quarkus.http.port=8080 + +quarkus.log.level=INFO +quarkus.http.limits.max-form-attribute-size=4096 + +quarkus.mongodb.connection-string = ${MONGODB-CONNECTION-STRING} +quarkus.mongodb.database = selcUserGroup + +user-group-cdc.send-events.watch.enabled=${USER_GROUP_CDC_SEND_EVENTS_WATCH_ENABLED:false} +user-group-cdc.appinsights.connection-string=${APPLICATIONINSIGHTS_CONNECTION_STRING:InstrumentationKey=00000000-0000-0000-0000-000000000000} +user-group-cdc.table.name=${START_AT_TABLE_NAME:CdCStartAt} +user-group-cdc.storage.connection-string=${STORAGE_CONNECTION_STRING:UseDevelopmentStorage=true;} + + +user-group-cdc.retry.min-backoff=${USER_GROUP_CDC-RETRY-MIN-BACKOFF:10} +user-group-cdc.retry.max-backoff=${USER_GROUP_CDC-RETRY-MAX-BACKOFF:12} +user-group-cdc.retry=${USER_GROUP_CDC-RETRY:3} + +quarkus.rest-client.event-hub.url=${EVENT_HUB_BASE_PATH:test} +eventhub.rest-client.keyName=${SHARED_ACCESS_KEY_NAME:test} +eventhub.rest-client.key=${EVENTHUB-SC-USERS-GROUP-SELFCARE-WO-KEY-LC:test} \ No newline at end of file diff --git a/apps/user-group-cdc/src/test/java/it/pagopa/selfcare/user/event/service/UserGroupCdcServiceTest.java b/apps/user-group-cdc/src/test/java/it/pagopa/selfcare/user/event/service/UserGroupCdcServiceTest.java new file mode 100644 index 00000000..03ba8484 --- /dev/null +++ b/apps/user-group-cdc/src/test/java/it/pagopa/selfcare/user/event/service/UserGroupCdcServiceTest.java @@ -0,0 +1,66 @@ +package it.pagopa.selfcare.user.event.service; + +import com.mongodb.client.model.changestream.ChangeStreamDocument; +import io.quarkus.test.InjectMock; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.mongodb.MongoTestResource; +import it.pagopa.selfcare.user.client.EventHubRestClient; +import it.pagopa.selfcare.user.event.UserGroupCdcService; +import it.pagopa.selfcare.user.event.entity.UserGroupEntity; +import it.pagopa.selfcare.user.event.mapper.UserGroupNotificationMapper; +import it.pagopa.selfcare.user.model.UserGroupNotificationToSend; +import jakarta.inject.Inject; +import org.bson.BsonDocument; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.Spy; + +import java.util.Set; + +import static org.mockito.Mockito.*; + +@QuarkusTest +@QuarkusTestResource(MongoTestResource.class) +public class UserGroupCdcServiceTest { + + @Inject + UserGroupCdcService userGroupCdcService; + + @RestClient + @InjectMock + EventHubRestClient eventHubRestClient; + + @Spy + UserGroupNotificationMapper userGroupNotificationMapper; + + @Test + void consumerToSendScUserGroupEventTest() { + UserGroupEntity userGroupEntity = dummyUserGroup(); + ChangeStreamDocument document = Mockito.mock(ChangeStreamDocument.class); + when(document.getFullDocument()).thenReturn(userGroupEntity); + BsonDocument bsonDocument = Mockito.mock(BsonDocument.class); + when(document.getDocumentKey()).thenReturn(bsonDocument); + + ArgumentCaptor notification = ArgumentCaptor.forClass(UserGroupNotificationToSend.class); + userGroupCdcService.consumerToSendScUserGroupEvent(document); + verify(eventHubRestClient, times(1)). + sendUserGroupMessage(notification.capture()); + Assertions.assertEquals(userGroupEntity.getId(), notification.getValue().getId()); + Assertions.assertEquals(userGroupEntity.getInstitutionId(), notification.getValue().getInstitutionId()); + Assertions.assertEquals(userGroupEntity.getProductId(), notification.getValue().getProductId()); + Assertions.assertEquals(2, notification.getValue().getMembers().size()); + } + + UserGroupEntity dummyUserGroup() { + UserGroupEntity userGroupEntity = new UserGroupEntity(); + userGroupEntity.setId("UserGroupId"); + userGroupEntity.setInstitutionId("InstitutionId"); + userGroupEntity.setProductId("ProductId"); + userGroupEntity.setMembers(Set.of("Member1", "Member2")); + return userGroupEntity; + } +} diff --git a/apps/user-group-cdc/src/test/resources/application.properties b/apps/user-group-cdc/src/test/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/apps/user-group-cdc/src/test/resources/application.properties @@ -0,0 +1 @@ + diff --git a/infra/container_apps/user-group-cdc/.terraform.lock.hcl b/infra/container_apps/user-group-cdc/.terraform.lock.hcl new file mode 100644 index 00000000..911037de --- /dev/null +++ b/infra/container_apps/user-group-cdc/.terraform.lock.hcl @@ -0,0 +1,48 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/azure/azapi" { + version = "1.9.0" + constraints = "~> 1.9.0" + hashes = [ + "h1:Ow1rr5fYBGSkplH/kcXeWz9y2wA81BnhZ7vTBzJfAAg=", + "h1:shpEoqcAbf+p6AvspiYO1YrX//8l1LV/owEcQpujWHw=", + "h1:yIJQVdnmGZdvS3yrw0M8ke9KiB/c0tjZ7KUXC46Hjx0=", + "h1:zaLH2Owmj61RX2G1Cy6VDy8Ttfzx+lDsSCyiu5cXkm4=", + "zh:349569471fbf387feaaf8b88da1690669e201147c342f905e5eb03df42b3cf87", + "zh:54346d5fb78cbad3eb7cfd96e1dd7ce4f78666cabaaccfec6ee9437476330018", + "zh:64b799da915ea3a9a58ac7a926c6a31c59fd0d911687804d8e815eda88c5580b", + "zh:9336ed9e112555e0fda8af6be9ba21478e30117d79ba662233311d9560d2b7c6", + "zh:a8aace9897b28ea0b2dbd7a3be3df033e158af40412c9c7670be0956f216ed7e", + "zh:ab23df7de700d9e785009a4ca9ceb38ae1ab894a13f5788847f15d018556f415", + "zh:b4f13f0b13560a67d427c71c85246f8920f98987120341830071df4535842053", + "zh:e58377bf36d8a14d28178a002657865ee17446182dac03525fd43435e41a1b5c", + "zh:ea5db4acc6413fd0fe6b35981e58cdc9850f5f3118031cc3d2581de511aee6aa", + "zh:f0b32c06c6bd4e4af2c02a62be07b947766aeeb09289a03f21aba16c2fd3c60f", + "zh:f1518e766a90c257d7eb36d360dafaf311593a4a9352ff8db0bcfe0ed8cf45ae", + "zh:fa89e84cff0776b5b61ff27049b1d8ed52040bd58c81c4628890d644a6fb2989", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.77.0" + constraints = "~> 3.77.0" + hashes = [ + "h1:/ImUzZZ+3Ax6HqkhVxSALK6mtyej3W0UlEEYy8A45XM=", + "h1:6KahvT54KTIS9rL5D4utjSCnxwDGClf59f/YqsYZbjY=", + "h1:7XzdPKIJoKazb3eMhQbjcVWRRu5RSsHeDNYUQW6zSAc=", + "h1:Ie29CiuuS6LeQLO0Cdf0k54oMgZd9rkuwOKjVAa4mfw=", + "zh:071c82025cda506af90302b3e89f61e086ad9e3b97b8c55382d5aed6f207cf10", + "zh:10464b6a85343fdcc5f8d3d60304c3e0bfcfff014b3067f6ead61d0c59fe8371", + "zh:5fd48cbeb13ead9158051e563e5354acbc94668fa3d5306c20d476e746e62991", + "zh:64b47c2150e1f0a8473c8f79d35be72786777772ab1a7e79d8039de4bc10e8c9", + "zh:6958e7d22e6efda97dde6e50d033c4a0f48da3c7e597482bf14774cf8d1f612e", + "zh:aad939983c1f28a27c01be636a8079e5f973f9bb640348cf264b35bf6a956bb8", + "zh:bb211cbcfb643a7d041afc597e1c8a10749e1d3a0141c16e5c643614b16895c8", + "zh:db30a46d335cc1c1e2dd0707c40c6b6b3c8dd72f8905e3177998e1b212d0ace0", + "zh:e04f4d086e546cc7ab565bde64e93ed6716c3764579918ecf9077e539b99dd4c", + "zh:ea92d15b18a17a31e9f7568efe3bbd07b77906f4585aec98e6d03942788efad8", + "zh:f03164fadd13e4991dde22ab31c09731a00aee6ad723c61d0d2835abc79a1a8c", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/infra/container_apps/user-group-cdc/env/dev/backend.ini b/infra/container_apps/user-group-cdc/env/dev/backend.ini new file mode 100644 index 00000000..200a37a1 --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/dev/backend.ini @@ -0,0 +1 @@ +subscription=DEV-SelfCare diff --git a/infra/container_apps/user-group-cdc/env/dev/backend.tfvars b/infra/container_apps/user-group-cdc/env/dev/backend.tfvars new file mode 100644 index 00000000..8d57c171 --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/dev/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "terraform-state-rg" +storage_account_name = "tfappdevselfcare" +container_name = "terraform-state" +key = "selfcare-user-group-cdc.user-group-app.tfstate" diff --git a/infra/container_apps/user-group-cdc/env/dev/terraform.tfvars b/infra/container_apps/user-group-cdc/env/dev/terraform.tfvars new file mode 100644 index 00000000..27570046 --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/dev/terraform.tfvars @@ -0,0 +1,63 @@ +env_short = "d" +suffix_increment = "-002" +cae_name = "cae-002" + +tags = { + CreatedBy = "Terraform" + Environment = "Dev" + Owner = "SelfCare" + Source = "https://github.com/pagopa/selfcare-user" + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} + +container_app = { + min_replicas = 0 + max_replicas = 1 + scale_rules = [ + { + custom = { + metadata = { + "desiredReplicas" = "1" + "start" = "0 8 * * MON-FRI" + "end" = "0 19 * * MON-FRI" + "timezone" = "Europe/Rome" + } + type = "cron" + } + name = "cron-scale-rule" + } + ] + cpu = 1 + memory = "2Gi" +} + +app_settings = [ + { + name = "JAVA_TOOL_OPTIONS" + value = "-javaagent:applicationinsights-agent.jar", + }, + { + name = "APPLICATIONINSIGHTS_ROLE_NAME" + value = "user-group-cdc", + }, + { + name = "EVENT_HUB_BASE_PATH" + value = "https://selc-d-eventhub-ns.servicebus.windows.net/sc-user-groups" + }, + { + name = "SHARED_ACCESS_KEY_NAME" + value = "selfcare-wo" + }, + { + name = "USER_GROUP_CDC_SEND_EVENTS_WATCH_ENABLED" + value = "true" + } +] + +secrets_names = { + "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" + "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" + "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" + "EVENTHUB-SC-USER-GROUPS-SELFCARE-WO-KEY-LC" = "eventhub-sc-usergroups-selfcare-wo-key-lc" +} + diff --git a/infra/container_apps/user-group-cdc/env/prod/backend.ini b/infra/container_apps/user-group-cdc/env/prod/backend.ini new file mode 100644 index 00000000..dc3318a8 --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/prod/backend.ini @@ -0,0 +1 @@ +subscription=PROD-SelfCare diff --git a/infra/container_apps/user-group-cdc/env/prod/backend.tfvars b/infra/container_apps/user-group-cdc/env/prod/backend.tfvars new file mode 100644 index 00000000..67d7218f --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/prod/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "terraform-state-rg" +storage_account_name = "tfappprodselfcare" +container_name = "terraform-state" +key = "selfcare-user-group-cdc.user-group-app.tfstate" diff --git a/infra/container_apps/user-group-cdc/env/prod/terraform.tfvars b/infra/container_apps/user-group-cdc/env/prod/terraform.tfvars new file mode 100644 index 00000000..6a4ea6e5 --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/prod/terraform.tfvars @@ -0,0 +1,51 @@ +prefix = "selc" +env_short = "p" +suffix_increment = "-002" +cae_name = "cae-002" + +tags = { + CreatedBy = "Terraform" + Environment = "Prod" + Owner = "SelfCare" + Source = "https://github.com/pagopa/selfcare-user" + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} + +container_app = { + min_replicas = 1 + max_replicas = 1 + scale_rules = [] + cpu = 1 + memory = "2Gi" +} + +app_settings = [ + { + name = "JAVA_TOOL_OPTIONS" + value = "-javaagent:applicationinsights-agent.jar", + }, + { + name = "APPLICATIONINSIGHTS_ROLE_NAME" + value = "user-group-cdc", + }, + { + name = "EVENT_HUB_BASE_PATH" + value = "https://selc-p-eventhub-ns.servicebus.windows.net/sc-user-groups" + }, + { + name = "SHARED_ACCESS_KEY_NAME" + value = "selfcare-wo" + }, + { + name = "USER_GROUP_CDC_SEND_EVENTS_WATCH_ENABLED" + value = "true" + } +] + +secrets_names = { + "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" + "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" + "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" + "EVENTHUB-SC-USER-GROUPS-SELFCARE-WO-KEY-LC" = "eventhub-sc-usergroups-selfcare-wo-key-lc" +} + diff --git a/infra/container_apps/user-group-cdc/env/uat/backend.ini b/infra/container_apps/user-group-cdc/env/uat/backend.ini new file mode 100644 index 00000000..8be57858 --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/uat/backend.ini @@ -0,0 +1 @@ +subscription=UAT-SelfCare diff --git a/infra/container_apps/user-group-cdc/env/uat/backend.tfvars b/infra/container_apps/user-group-cdc/env/uat/backend.tfvars new file mode 100644 index 00000000..d6da4704 --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/uat/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "terraform-state-rg" +storage_account_name = "tfappuatselfcare" +container_name = "terraform-state" +key = "selfcare-user-group-cdc.user-group-app.tfstate" diff --git a/infra/container_apps/user-group-cdc/env/uat/terraform.tfvars b/infra/container_apps/user-group-cdc/env/uat/terraform.tfvars new file mode 100644 index 00000000..ae164931 --- /dev/null +++ b/infra/container_apps/user-group-cdc/env/uat/terraform.tfvars @@ -0,0 +1,50 @@ +prefix = "selc" +env_short = "u" +suffix_increment = "-002" +cae_name = "cae-002" + +tags = { + CreatedBy = "Terraform" + Environment = "Uat" + Owner = "SelfCare" + Source = "https://github.com/pagopa/selfcare-user" + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} + +container_app = { + min_replicas = 1 + max_replicas = 1 + scale_rules = [] + cpu = 1 + memory = "2Gi" +} + +app_settings = [ + { + name = "JAVA_TOOL_OPTIONS" + value = "-javaagent:applicationinsights-agent.jar", + }, + { + name = "APPLICATIONINSIGHTS_ROLE_NAME" + value = "user-group-cdc", + }, + { + name = "EVENT_HUB_BASE_PATH" + value = "https://selc-u-eventhub-ns.servicebus.windows.net/sc-user-groups" + }, + { + name = "SHARED_ACCESS_KEY_NAME" + value = "selfcare-wo" + }, + { + name = "USER_GROUP_CDC_SEND_EVENTS_WATCH_ENABLED" + value = "true" + } +] + +secrets_names = { + "APPLICATIONINSIGHTS_CONNECTION_STRING" = "appinsights-connection-string" + "MONGODB-CONNECTION-STRING" = "mongodb-connection-string" + "STORAGE_CONNECTION_STRING" = "blob-storage-product-connection-string" + "EVENTHUB-SC-USER-GROUPS-SELFCARE-WO-KEY-LC" = "eventhub-sc-usergroups-selfcare-wo-key-lc" +} diff --git a/infra/container_apps/user-group-cdc/locals.tf b/infra/container_apps/user-group-cdc/locals.tf new file mode 100644 index 00000000..d93c886c --- /dev/null +++ b/infra/container_apps/user-group-cdc/locals.tf @@ -0,0 +1,6 @@ +locals { + project = "selc-${var.env_short}" + + container_app_environment_name = "${local.project}-${var.cae_name}" + ca_resource_group_name = "${local.project}-container-app${var.suffix_increment}-rg" +} \ No newline at end of file diff --git a/infra/container_apps/user-group-cdc/main.tf b/infra/container_apps/user-group-cdc/main.tf new file mode 100644 index 00000000..915afe98 --- /dev/null +++ b/infra/container_apps/user-group-cdc/main.tf @@ -0,0 +1,64 @@ +terraform { + required_version = ">= 1.6.0" + + backend "azurerm" {} +} + +provider "azurerm" { + features {} + skip_provider_registration = true + +} + +module "container_app_user_cdc" { + source = "github.com/pagopa/selfcare-commons//infra/terraform-modules/container_app_microservice?ref=main" + + env_short = var.env_short + resource_group_name = local.ca_resource_group_name + container_app = var.container_app + container_app_name = "user-group-cdc" + container_app_environment_name = local.container_app_environment_name + image_name = "selfcare-user-group-cdc" + image_tag = var.image_tag + app_settings = var.app_settings + secrets_names = var.secrets_names + workload_profile_name = var.workload_profile_name + + probes = [ + { + httpGet = { + path = "q/health/live" + port = 8080 + scheme = "HTTP" + } + timeoutSeconds = 5 + type = "Liveness" + failureThreshold = 3 + initialDelaySeconds = 1 + }, + { + httpGet = { + path = "q/health/ready" + port = 8080 + scheme = "HTTP" + } + timeoutSeconds = 5 + type = "Readiness" + failureThreshold = 30 + initialDelaySeconds = 3 + }, + { + httpGet = { + path = "q/health/started" + port = 8080 + scheme = "HTTP" + } + timeoutSeconds = 5 + failureThreshold = 5 + type = "Startup" + initialDelaySeconds = 5 + } + ] + + tags = var.tags +} diff --git a/infra/container_apps/user-group-cdc/terraform.sh b/infra/container_apps/user-group-cdc/terraform.sh new file mode 100644 index 00000000..15ca0bfc --- /dev/null +++ b/infra/container_apps/user-group-cdc/terraform.sh @@ -0,0 +1,324 @@ +#!/bin/bash +############################################################ +# Terraform script for managing infrastructure on Azure +# Fingerprint: d2hhdHlvdXdhbnQ/Cg== +############################################################ +# Global variables +# Version format x.y accepted +vers="1.11" +script_name=$(basename "$0") +git_repo="https://raw.githubusercontent.com/pagopa/eng-common-scripts/main/azure/${script_name}" +tmp_file="${script_name}.new" +# Check if the third parameter exists and is a file +if [ -n "$3" ] && [ -f "$3" ]; then + FILE_ACTION=true +else + FILE_ACTION=false +fi + +# Define functions +function clean_environment() { + rm -rf .terraform + rm tfplan 2>/dev/null + echo "cleaned!" +} + +function download_tool() { + #default value + cpu_type="intel" + os_type=$(uname) + + # only on MacOS + if [ "$os_type" == "Darwin" ]; then + cpu_brand=$(sysctl -n machdep.cpu.brand_string) + if grep -q -i "intel" <<< "$cpu_brand"; then + cpu_type="intel" + else + cpu_type="arm" + fi + fi + + echo $cpu_type + tool=$1 + git_repo="https://raw.githubusercontent.com/pagopa/eng-common-scripts/main/golang/${tool}_${cpu_type}" + if ! command -v $tool &> /dev/null; then + if ! curl -sL "$git_repo" -o "$tool"; then + echo "Error downloading ${tool}" + return 1 + else + chmod +x $tool + echo "${tool} downloaded! Please note this tool WON'T be copied in your **/bin folder for safety reasons. +You need to do it yourself!" + read -p "Press enter to continue" + + + fi + fi +} + +function extract_resources() { + TF_FILE=$1 + ENV=$2 + TARGETS="" + + # Check if the file exists + if [ ! -f "$TF_FILE" ]; then + echo "File $TF_FILE does not exist." + exit 1 + fi + + # Check if the directory exists + if [ ! -d "./env/$ENV" ]; then + echo "Directory ./env/$ENV does not exist." + exit 1 + fi + + TMP_FILE=$(mktemp) + grep -E '^resource|^module' $TF_FILE > $TMP_FILE + + while read -r line ; do + TYPE=$(echo $line | cut -d '"' -f 1 | tr -d ' ') + if [ "$TYPE" == "module" ]; then + NAME=$(echo $line | cut -d '"' -f 2) + TARGETS+=" -target=\"$TYPE.$NAME\"" + else + NAME1=$(echo $line | cut -d '"' -f 2) + NAME2=$(echo $line | cut -d '"' -f 4) + TARGETS+=" -target=\"$NAME1.$NAME2\"" + fi + done < $TMP_FILE + + rm $TMP_FILE + + echo "./terraform.sh $action $ENV $TARGETS" +} + +function help_usage() { + echo "terraform.sh Version ${vers}" + echo + echo "Usage: ./script.sh [ACTION] [ENV] [OTHER OPTIONS]" + echo "es. ACTION: init, apply, plan, etc." + echo "es. ENV: dev, uat, prod, etc." + echo + echo "Available actions:" + echo " clean Remove .terraform* folders and tfplan files" + echo " help This help" + echo " list List every environment available" + echo " update Update this script if possible" + echo " summ Generate summary of Terraform plan" + echo " tflist Generate an improved output of terraform state list" + echo " tlock Generate or update the dependency lock file" + echo " * any terraform option" +} + +function init_terraform() { + if [ -n "$env" ]; then + terraform init -reconfigure -backend-config="./env/$env/backend.tfvars" + else + echo "ERROR: no env configured!" + exit 1 + fi +} + +function list_env() { + # Check if env directory exists + if [ ! -d "./env" ]; then + echo "No environment directory found" + exit 1 + fi + + # List subdirectories under env directory + env_list=$(ls -d ./env/*/ 2>/dev/null) + + # Check if there are any subdirectories + if [ -z "$env_list" ]; then + echo "No environments found" + exit 1 + fi + + # Print the list of environments + echo "Available environments:" + for env in $env_list; do + env_name=$(echo "$env" | sed 's#./env/##;s#/##') + echo "- $env_name" + done +} + +function other_actions() { + if [ -n "$env" ] && [ -n "$action" ]; then + terraform "$action" -var-file="./env/$env/terraform.tfvars" -compact-warnings $other + else + echo "ERROR: no env or action configured!" + exit 1 + fi +} + +function state_output_taint_actions() { + if [ "$action" == "tflist" ]; then + # If 'tflist' is not installed globally and there is no 'tflist' file in the current directory, + # attempt to download the 'tflist' tool + if ! command -v tflist &> /dev/null && [ ! -f "tflist" ]; then + download_tool "tflist" + if [ $? -ne 0 ]; then + echo "Error: Failed to download tflist!!" + exit 1 + else + echo "tflist downloaded!" + fi + fi + if command -v tflist &> /dev/null; then + terraform state list | tflist + else + terraform state list | ./tflist + fi + else + terraform $action $other + fi +} + + +function parse_tfplan_option() { + # Create an array to contain arguments that do not start with '-tfplan=' + local other_args=() + + # Loop over all arguments + for arg in "$@"; do + # If the argument starts with '-tfplan=', extract the file name + if [[ "$arg" =~ ^-tfplan= ]]; then + echo "${arg#*=}" + else + # If the argument does not start with '-tfplan=', add it to the other_args array + other_args+=("$arg") + fi + done + + # Print all arguments in other_args separated by spaces + echo "${other_args[@]}" +} + +function tfsummary() { + local plan_file + plan_file=$(parse_tfplan_option "$@") + if [ -z "$plan_file" ]; then + plan_file="tfplan" + fi + action="plan" + other="-out=${plan_file}" + other_actions + if [ -n "$(command -v tf-summarize)" ]; then + tf-summarize -tree "${plan_file}" + else + echo "tf-summarize is not installed" + fi + if [ "$plan_file" == "tfplan" ]; then + rm $plan_file + fi +} + +function update_script() { + # Check if the repository was cloned successfully + if ! curl -sL "$git_repo" -o "$tmp_file"; then + echo "Error cloning the repository" + rm "$tmp_file" 2>/dev/null + return 1 + fi + + # Check if a newer version exists + remote_vers=$(sed -n '8s/vers="\(.*\)"/\1/p' "$tmp_file") + if [ "$(printf '%s\n' "$vers" "$remote_vers" | sort -V | tail -n 1)" == "$vers" ]; then + echo "The local script version is equal to or newer than the remote version." + rm "$tmp_file" 2>/dev/null + return 0 + fi + + # Check the fingerprint + local_fingerprint=$(sed -n '4p' "$0") + remote_fingerprint=$(sed -n '4p' "$tmp_file") + + if [ "$local_fingerprint" != "$remote_fingerprint" ]; then + echo "The local and remote file fingerprints do not match." + rm "$tmp_file" 2>/dev/null + return 0 + fi + + # Show the current and available versions to the user + echo "Current script version: $vers" + echo "Available script version: $remote_vers" + + # Ask the user if they want to update the script + read -rp "Do you want to update the script to version $remote_vers? (y/n): " answer + + if [ "$answer" == "y" ] || [ "$answer" == "Y" ]; then + # Replace the local script with the updated version + cp "$tmp_file" "$script_name" + chmod +x "$script_name" + rm "$tmp_file" 2>/dev/null + + echo "Script successfully updated to version $remote_vers" + else + echo "Update canceled by the user" + fi + + rm "$tmp_file" 2>/dev/null +} + +# Check arguments number +if [ "$#" -lt 1 ]; then + help_usage + exit 0 +fi + +# Parse arguments +action=$1 +env=$2 +filetf=$3 +shift 2 +other=$@ + +if [ -n "$env" ]; then + # shellcheck source=/dev/null + source "./env/$env/backend.ini" + if [ -z "$(command -v az)" ]; then + echo "az not found, cannot proceed" + exit 1 + fi + az account set -s "${subscription}" +fi + +# Call appropriate function based on action +case $action in + clean) + clean_environment + ;; + ?|help|-h) + help_usage + ;; + init) + init_terraform "$other" + ;; + list) + list_env + ;; + output|state|taint|tflist) + init_terraform + state_output_taint_actions $other + ;; + summ) + init_terraform + tfsummary "$other" + ;; + tlock) + terraform providers lock -platform=windows_amd64 -platform=darwin_amd64 -platform=darwin_arm64 -platform=linux_amd64 + ;; + update) + update_script + ;; + *) + if [ "$FILE_ACTION" = true ]; then + extract_resources "$filetf" "$env" + else + init_terraform + other_actions "$other" + fi + ;; +esac diff --git a/infra/container_apps/user-group-cdc/variables.tf b/infra/container_apps/user-group-cdc/variables.tf new file mode 100644 index 00000000..0f46be44 --- /dev/null +++ b/infra/container_apps/user-group-cdc/variables.tf @@ -0,0 +1,83 @@ +variable "prefix" { + description = "Domain prefix" + type = string + default = "selc" + validation { + condition = ( + length(var.prefix) <= 6 + ) + error_message = "Max length is 6 chars." + } +} + +variable "env_short" { + description = "Environment short name" + type = string + validation { + condition = ( + length(var.env_short) <= 1 + ) + error_message = "Max length is 1 chars." + } +} + +variable "tags" { + type = map(any) +} + + +variable "container_app" { + description = "Container App configuration" + type = object({ + min_replicas = number + max_replicas = number + + scale_rules = list(object({ + name = string + custom = object({ + metadata = map(string) + type = string + }) + })) + + cpu = number + memory = string + }) +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "app_settings" { + type = list(object({ + name = string + value = string + })) +} + +variable "secrets_names" { + type = map(string) + description = "KeyVault secrets to get values from" +} + +variable "workload_profile_name" { + type = string + description = "Workload Profile name to use" + default = null +} + +variable "cae_name" { + type = string + description = "Container App Environment name" + default = "cae-cp" +} + +variable "suffix_increment" { + type = string + description = "Suffix increment Container App Environment name" + default = "" +} + + diff --git a/libs/user-sdk-event/pom.xml b/libs/user-sdk-event/pom.xml index 703efd51..5b6f6c79 100644 --- a/libs/user-sdk-event/pom.xml +++ b/libs/user-sdk-event/pom.xml @@ -58,6 +58,12 @@ user-sdk-model 0.0.1 + + jakarta.validation + jakarta.validation-api + 3.0.2 + compile + diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/client/EventHubRestClient.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/client/EventHubRestClient.java index 05d9846d..1b15cc81 100644 --- a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/client/EventHubRestClient.java +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/client/EventHubRestClient.java @@ -2,6 +2,7 @@ import io.smallrye.mutiny.Uni; import it.pagopa.selfcare.user.auth.EventhubSasTokenAuthorization; +import it.pagopa.selfcare.user.model.UserGroupNotificationToSend; import it.pagopa.selfcare.user.model.UserNotificationToSend; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.POST; @@ -19,4 +20,8 @@ public interface EventHubRestClient { @Path("messages") Uni sendMessage(UserNotificationToSend notification); + @POST + @Path("messages") + Uni sendUserGroupMessage(UserGroupNotificationToSend notification); + } diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/TrackEventInput.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/TrackEventInput.java index d45b5c28..368eae09 100644 --- a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/TrackEventInput.java +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/TrackEventInput.java @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Data; +import java.util.List; import java.util.Optional; @Data @@ -16,6 +17,7 @@ public class TrackEventInput { private String institutionId; private String exception; private String productRole; + private List groupMembers; public static TrackEventInput toTrackEventInput(UserNotificationToSend userNotificationToSend) { return TrackEventInput.builder() @@ -26,4 +28,16 @@ public static TrackEventInput toTrackEventInput(UserNotificationToSend userNotif .productRole(Optional.ofNullable(userNotificationToSend.getUser()).map(UserToNotify::getProductRole).orElse(null)) .build(); } + + public static TrackEventInput toTrackEventInputForUserGroup(UserGroupNotificationToSend userGroupEntity) { + TrackEventInputBuilder trackEventInputBuilder = TrackEventInput.builder() + .documentKey(userGroupEntity.getId()) + .institutionId(userGroupEntity.getInstitutionId()) + .productId(userGroupEntity.getProductId()); + + if(userGroupEntity.getMembers() != null && !userGroupEntity.getMembers().isEmpty()) { + trackEventInputBuilder.groupMembers(userGroupEntity.getMembers().stream().toList()); + } + return trackEventInputBuilder.build(); + } } diff --git a/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/UserGroupNotificationToSend.java b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/UserGroupNotificationToSend.java new file mode 100644 index 00000000..a182cfff --- /dev/null +++ b/libs/user-sdk-event/src/main/java/it/pagopa/selfcare/user/model/UserGroupNotificationToSend.java @@ -0,0 +1,21 @@ +package it.pagopa.selfcare.user.model; + +import lombok.Data; + +import java.time.Instant; +import java.util.Set; + +@Data +public class UserGroupNotificationToSend { + private String id; + private String institutionId; + private String productId; + private String name; + private String description; + private String status; + private Set members; + private Instant createdAt; + private String createdBy; + private Instant modifiedAt; + private String modifiedBy; +} diff --git a/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/TrackEventInputTest.java b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/TrackEventInputTest.java new file mode 100644 index 00000000..48970c6a --- /dev/null +++ b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/TrackEventInputTest.java @@ -0,0 +1,51 @@ +package it.pagopa.selfcare.user; + +import it.pagopa.selfcare.user.model.TrackEventInput; +import it.pagopa.selfcare.user.model.UserGroupNotificationToSend; +import it.pagopa.selfcare.user.model.UserNotificationToSend; +import it.pagopa.selfcare.user.model.UserToNotify; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class TrackEventInputTest { + + @Test + void toTrackEventInput_withUserNotification() { + UserNotificationToSend userNotification = new UserNotificationToSend(); + userNotification.setId("docKey"); + UserToNotify user = new UserToNotify(); + user.setUserId("userId"); + user.setProductRole("productRole"); + userNotification.setUser(user); + userNotification.setInstitutionId("institutionId"); + userNotification.setProductId("productId"); + + TrackEventInput result = TrackEventInput.toTrackEventInput(userNotification); + + assertEquals("docKey", result.getDocumentKey()); + assertEquals("userId", result.getUserId()); + assertEquals("institutionId", result.getInstitutionId()); + assertEquals("productId", result.getProductId()); + assertEquals("productRole", result.getProductRole()); + } + + @Test + void toTrackEventInputForUserGroup_withUserGroupNotification() { + UserGroupNotificationToSend userGroupNotification = new UserGroupNotificationToSend(); + userGroupNotification.setId("docKey"); + userGroupNotification.setInstitutionId("institutionId"); + userGroupNotification.setProductId("productId"); + userGroupNotification.setMembers(Set.of("member1", "member2")); + + TrackEventInput result = TrackEventInput.toTrackEventInputForUserGroup(userGroupNotification); + + assertEquals("docKey", result.getDocumentKey()); + assertEquals("institutionId", result.getInstitutionId()); + assertEquals("productId", result.getProductId()); + } +} \ No newline at end of file diff --git a/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/UserGroupNotificationToSendTest.java b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/UserGroupNotificationToSendTest.java new file mode 100644 index 00000000..90834e80 --- /dev/null +++ b/libs/user-sdk-event/src/test/java/it/pagopa/selfcare/user/UserGroupNotificationToSendTest.java @@ -0,0 +1,39 @@ +package it.pagopa.selfcare.user; + +import it.pagopa.selfcare.user.model.UserGroupNotificationToSend; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import java.time.Instant; +import java.util.Set; + +class UserGroupNotificationToSendTest { + + @Test + void userGroupNotificationToSend_withValidData_shouldMapFieldsCorrectly() { + UserGroupNotificationToSend notification = new UserGroupNotificationToSend(); + notification.setId("id"); + notification.setInstitutionId("institutionId"); + notification.setProductId("productId"); + notification.setName("name"); + notification.setDescription("description"); + notification.setStatus("status"); + notification.setMembers(Set.of("member1", "member2")); + notification.setCreatedAt(Instant.now()); + notification.setCreatedBy("createdBy"); + notification.setModifiedAt(Instant.now()); + notification.setModifiedBy("modifiedBy"); + + assertEquals("id", notification.getId()); + assertEquals("institutionId", notification.getInstitutionId()); + assertEquals("productId", notification.getProductId()); + assertEquals("name", notification.getName()); + assertEquals("description", notification.getDescription()); + assertEquals("status", notification.getStatus()); + assertEquals(Set.of("member1", "member2"), notification.getMembers()); + assertNotNull(notification.getCreatedAt()); + assertEquals("createdBy", notification.getCreatedBy()); + assertNotNull(notification.getModifiedAt()); + assertEquals("modifiedBy", notification.getModifiedBy()); + } + +} diff --git a/libs/user-sdk-model/pom.xml b/libs/user-sdk-model/pom.xml index 8457c610..bbb60e81 100644 --- a/libs/user-sdk-model/pom.xml +++ b/libs/user-sdk-model/pom.xml @@ -25,7 +25,7 @@ it.pagopa.selfcare onboarding-sdk-common - 0.1.13 + 0.3.2 compile diff --git a/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsMetric.java b/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsMetric.java index 5e70a66e..01194e55 100644 --- a/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsMetric.java +++ b/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsMetric.java @@ -8,6 +8,11 @@ public class EventsMetric { public static final String EVENTS_USER_INSTITUTION_PRODUCT_FAILURE = "EventsUserInstitutionProduct_failures"; public static final String EVENTS_USER_INSTITUTION_PRODUCT_SUCCESS = "EventsUserInstitutionProduct_success"; + public static final String EVENTS_USER_GROUP_FAILURE = "EventsUserGroup_failures"; + public static final String EVENTS_USER_GROUP_SUCCESS = "EventsUserGroup_success"; + public static final String EVENTS_USER_GROUP_PRODUCT_FAILURE = "EventsUserGroupProduct_failures"; + public static final String EVENTS_USER_GROUP_PRODUCT_SUCCESS = "EventsUserGroupProduct_success"; + public static final String USER_INFO_UPDATE_FAILURE = "UserInfoUpdate_failure"; public static final String USER_INFO_UPDATE_SUCCESS = "UserInfoUpdate_success"; diff --git a/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsName.java b/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsName.java index 8a9e7d3e..37a0a53f 100644 --- a/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsName.java +++ b/libs/user-sdk-model/src/main/java/it.pagopa.selfcare.user.model/constants/EventsName.java @@ -4,4 +4,5 @@ public class EventsName { public static final String EVENT_USER_MS_NAME = "USER_MS"; public static final String EVENT_USER_CDC_NAME = "USER_CDC"; + public static final String EVENT_USER_GROUP_CDC_NAME = "USER_GROUP_CDC"; } diff --git a/test-coverage/pom.xml b/test-coverage/pom.xml index 4a68bcb0..7656c10c 100644 --- a/test-coverage/pom.xml +++ b/test-coverage/pom.xml @@ -68,6 +68,20 @@ + + + user-group-cdc + + false + + + + it.pagopa.selfcare + user-group-cdc + 1.0.0-SNAPSHOT + + + user-group-ms