diff --git a/.github/workflows/cart-ci.yaml b/.github/workflows/cart-ci.yaml index a35d87b1ae..d25a8e49bf 100644 --- a/.github/workflows/cart-ci.yaml +++ b/.github/workflows/cart-ci.yaml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - uses: ./.github/workflows/actions - name: Run Maven Build Command - run: mvn clean install -DskipTests -f cart + run: mvn clean install -DskipTests - name: Run Maven Test run: mvn test -f cart - name: Unit Test Results diff --git a/.github/workflows/order-ci.yaml b/.github/workflows/order-ci.yaml index f9fb5f781e..0364eb8e0e 100644 --- a/.github/workflows/order-ci.yaml +++ b/.github/workflows/order-ci.yaml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - uses: ./.github/workflows/actions - name: Run Maven Build Command - run: mvn clean install -DskipTests -f order + run: mvn clean install -DskipTests - name: Run Maven Test run: mvn test -f order - name: Unit Test Results diff --git a/cart/pom.xml b/cart/pom.xml index ef8316ecd1..cfdd5584ad 100644 --- a/cart/pom.xml +++ b/cart/pom.xml @@ -109,6 +109,12 @@ org.liquibase liquibase-core + + com.yas + saga + 0.0.1-SNAPSHOT + compile + diff --git a/cart/src/main/java/com/yas/cart/controller/CartController.java b/cart/src/main/java/com/yas/cart/controller/CartController.java index e060fffbef..57dd0bf8a2 100644 --- a/cart/src/main/java/com/yas/cart/controller/CartController.java +++ b/cart/src/main/java/com/yas/cart/controller/CartController.java @@ -11,6 +11,8 @@ import jakarta.validation.constraints.NotEmpty; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -40,7 +42,7 @@ public ResponseEntity> listCartDetailByCustomerId(@PathVar public ResponseEntity getLastCart(Principal principal) { if (principal == null) return ResponseEntity.ok(null); - return ResponseEntity.ok(cartService.getLastCart()); + return ResponseEntity.ok(cartService.getLastCart(principal.getName())); } @PostMapping(path = "/storefront/carts") @@ -56,18 +58,21 @@ public ResponseEntity createCart(@Valid @RequestBody @NotEmpty @PutMapping("cart-item") public ResponseEntity updateCart(@Valid @RequestBody CartItemVm cartItemVm) { - return new ResponseEntity<>(cartService.updateCartItems(cartItemVm), HttpStatus.OK); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return new ResponseEntity<>(cartService.updateCartItems(cartItemVm, auth.getName()), HttpStatus.OK); } @DeleteMapping("/storefront/cart-item") public ResponseEntity removeCartItemByProductId(@RequestParam Long productId) { - cartService.removeCartItemByProductId(productId); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + cartService.removeCartItemByProductId(productId, auth.getName()); return ResponseEntity.noContent().build(); } @DeleteMapping("/storefront/cart-item/multi-delete") public ResponseEntity removeCartItemListByProductIdList(@RequestParam List productIds) { - cartService.removeCartItemListByProductIdList(productIds); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + cartService.removeCartItemListByProductIdList(productIds, auth.getName()); return ResponseEntity.noContent().build(); } diff --git a/cart/src/main/java/com/yas/cart/repository/CartItemRepository.java b/cart/src/main/java/com/yas/cart/repository/CartItemRepository.java index 3bb93e1088..97a3ab8486 100644 --- a/cart/src/main/java/com/yas/cart/repository/CartItemRepository.java +++ b/cart/src/main/java/com/yas/cart/repository/CartItemRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -16,8 +17,10 @@ public interface CartItemRepository extends JpaRepository { Optional findByCartIdAndProductId(Long cartId, Long productId); + @Transactional void deleteByCartIdAndProductId(Long cartId, Long productId); + @Transactional void deleteByCartIdAndProductIdIn(Long cartId, List productIds); @Query("select sum(ci.quantity) from CartItem ci where ci.cart.id = ?1") diff --git a/cart/src/main/java/com/yas/cart/saga/HandlerDispatcherRegister.java b/cart/src/main/java/com/yas/cart/saga/HandlerDispatcherRegister.java new file mode 100644 index 0000000000..3f4fc016df --- /dev/null +++ b/cart/src/main/java/com/yas/cart/saga/HandlerDispatcherRegister.java @@ -0,0 +1,17 @@ +package com.yas.cart.saga; + +import com.yas.cart.saga.handler.CartItemCommandHandler; +import io.eventuate.tram.commands.consumer.CommandDispatcher; +import io.eventuate.tram.sagas.participant.SagaCommandDispatcherFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class HandlerDispatcherRegister { + @Bean + public CommandDispatcher consumerCommandDispatcher(CartItemCommandHandler target, + SagaCommandDispatcherFactory sagaCommandDispatcherFactory) { + + return sagaCommandDispatcherFactory.make("cartItemCommandDispatcher", target.commandHandlerDefinitions()); + } +} diff --git a/cart/src/main/java/com/yas/cart/saga/handler/CartItemCommandHandler.java b/cart/src/main/java/com/yas/cart/saga/handler/CartItemCommandHandler.java new file mode 100644 index 0000000000..bd59435e6c --- /dev/null +++ b/cart/src/main/java/com/yas/cart/saga/handler/CartItemCommandHandler.java @@ -0,0 +1,43 @@ +package com.yas.cart.saga.handler; + +import com.yas.cart.exception.BadRequestException; +import com.yas.cart.exception.NotFoundException; +import com.yas.cart.service.CartService; +import com.yas.saga.cart.command.DeleteCartItemCommand; +import com.yas.saga.cart.reply.DeleteCartItemFailure; +import com.yas.saga.cart.reply.DeleteCartItemSuccess; +import io.eventuate.tram.commands.consumer.CommandHandlers; +import io.eventuate.tram.commands.consumer.CommandMessage; +import io.eventuate.tram.messaging.common.Message; +import io.eventuate.tram.sagas.participant.SagaCommandHandlersBuilder; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import static io.eventuate.tram.commands.consumer.CommandHandlerReplyBuilder.withFailure; +import static io.eventuate.tram.commands.consumer.CommandHandlerReplyBuilder.withSuccess; + +@Component +@RequiredArgsConstructor +public class CartItemCommandHandler { + + private final CartService cartService; + + public CommandHandlers commandHandlerDefinitions() { + return SagaCommandHandlersBuilder + .fromChannel("cartService") + .onMessage(DeleteCartItemCommand.class, this::deleteCartItem) + .build(); + } + + private Message deleteCartItem(CommandMessage cm) { + DeleteCartItemCommand cmd = cm.getCommand(); + try { + this.cartService.removeCartItemListByProductIdList(cmd.getProductIds(), cmd.getCustomerId()); + String productIdsStr = StringUtils.join(cmd.getProductIds(), ","); + return withSuccess(new DeleteCartItemSuccess("Delete all card items of product ids [" + productIdsStr + "] success")); + } catch (BadRequestException | NotFoundException e) { + return withFailure(new DeleteCartItemFailure(e.getMessage())); + } + } +} diff --git a/cart/src/main/java/com/yas/cart/service/CartService.java b/cart/src/main/java/com/yas/cart/service/CartService.java index be4415a75a..2ba674221c 100644 --- a/cart/src/main/java/com/yas/cart/service/CartService.java +++ b/cart/src/main/java/com/yas/cart/service/CartService.java @@ -8,7 +8,6 @@ import com.yas.cart.repository.CartRepository; import com.yas.cart.utils.Constants; import com.yas.cart.viewmodel.*; -import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -88,9 +87,8 @@ public CartGetDetailVm addToCart(List cartItemVms) { return CartGetDetailVm.fromModel(cart); } - public CartGetDetailVm getLastCart() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - return cartRepository.findByCustomerIdAndOrderIdIsNull(auth.getName()) + public CartGetDetailVm getLastCart(String customerId) { + return cartRepository.findByCustomerIdAndOrderIdIsNull(customerId) .stream().reduce((first, second) -> second) .map(CartGetDetailVm::fromModel).orElse(CartGetDetailVm.fromModel(new Cart())); } @@ -103,8 +101,8 @@ private CartItem getCartItemByProductId(Set cartItems, Long productId) return new CartItem(); } - public CartItemPutVm updateCartItems(CartItemVm cartItemVm) { - CartGetDetailVm currentCart = getLastCart(); + public CartItemPutVm updateCartItems(CartItemVm cartItemVm, String customerId) { + CartGetDetailVm currentCart = getLastCart(customerId); validateCart(currentCart, cartItemVm.productId()); @@ -124,11 +122,9 @@ public CartItemPutVm updateCartItems(CartItemVm cartItemVm) { } } - @Transactional - public void removeCartItemListByProductIdList(List productIdList) { - CartGetDetailVm currentCart = getLastCart(); - productIdList.stream().forEach(id -> validateCart(currentCart, id)); - + public void removeCartItemListByProductIdList(List productIdList, String customerId) { + CartGetDetailVm currentCart = getLastCart(customerId); + productIdList.forEach(id -> validateCart(currentCart, id)); cartItemRepository.deleteByCartIdAndProductIdIn(currentCart.id(), productIdList); } @@ -145,8 +141,8 @@ private void validateCart(CartGetDetailVm cart, Long productId) { } @Transactional - public void removeCartItemByProductId(Long productId) { - CartGetDetailVm currentCart = getLastCart(); + public void removeCartItemByProductId(Long productId, String customerId) { + CartGetDetailVm currentCart = getLastCart(customerId); validateCart(currentCart, productId); diff --git a/cart/src/main/resources/application.properties b/cart/src/main/resources/application.properties index 6842d9ff6f..f70805f268 100644 --- a/cart/src/main/resources/application.properties +++ b/cart/src/main/resources/application.properties @@ -8,6 +8,11 @@ management.endpoints.web.exposure.include=prometheus management.metrics.distribution.percentiles-histogram.http.server.requests=true management.metrics.tags.application=${spring.application.name} +eventuatelocal.kafka.bootstrap.servers=kafka:9092 +eventuatelocal.zookeeper.connection.string=zookeeper:2181 +eventuate.database.schema=eventuate +spring.liquibase.parameters.eventualSlotName=cart + logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] spring.security.oauth2.resourceserver.jwt.issuer-uri=http://identity/realms/Yas @@ -26,6 +31,9 @@ spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialec # Hibernate ddl auto (none, create, create-drop, validate, update) spring.jpa.hibernate.ddl-auto = none +# Disable open in view transaction +spring.jpa.open-in-view=false + #Enable liquibase spring.liquibase.enabled=true diff --git a/cart/src/main/resources/db/changelog/db.changelog-master.yaml b/cart/src/main/resources/db/changelog/db.changelog-master.yaml index 419ad29d64..d9ef4f57ba 100644 --- a/cart/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/cart/src/main/resources/db/changelog/db.changelog-master.yaml @@ -2,4 +2,6 @@ databaseChangeLog: - includeAll: path: db/changelog/ddl/ - includeAll: - path: db/changelog/data/ \ No newline at end of file + path: db/changelog/data/ + - includeAll: + path: db/changelog/eventuate-dll/ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2ae323ebfc..1f832ee01b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -220,10 +220,12 @@ services: networks: - yas-network postgres: - image: debezium/postgres:15-alpine + image: debezium/postgres:15-alpine-custom + build: ./docker/postgres ports: - "5432:5432" volumes: + - ./docker/postgres/postgresql.conf.sample:/usr/share/postgresql/postgresql.conf.sample - ./postgres_init.sql:/docker-entrypoint-initdb.d/postgres_init.sql - postgres:/var/lib/postgresql/data command: postgres -c 'max_connections=500' @@ -261,7 +263,7 @@ services: environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://kafka:29092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 @@ -302,6 +304,52 @@ services: networks: - yas-network + eventuate-cdc: + image: eventuateio/eventuate-cdc-service:0.15.0.RELEASE + ports: + - "8099:8080" + depends_on: + - postgres + - kafka + - zookeeper + networks: + - yas-network + environment: + CDC_OPTS: "--debug" + + EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 + EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING: zookeeper:2181 + + EVENTUATE_CDC_READER_READER1_TYPE: postgres-wal + EVENTUATE_CDC_READER_READER1_DATASOURCEURL: jdbc:postgresql://postgres/order + EVENTUATE_CDC_READER_READER1_MONITORINGSCHEMA: eventuate + EVENTUATE_CDC_READER_READER1_DATASOURCEUSERNAME: admin + EVENTUATE_CDC_READER_READER1_DATASOURCEPASSWORD: admin + EVENTUATE_CDC_READER_READER1_DATASOURCEDRIVERCLASSNAME: org.postgresql.Driver + EVENTUATE_CDC_READER_READER1_LEADERSHIPLOCKPATH: /eventuate/cdc/leader/order + EVENTUATE_CDC_READER_READER1_OFFSETSTORAGETOPICNAME: db.history.common + EVENTUATE_CDC_READER_READER1_OUTBOXID: 1 + EVENTUATE_CDC_READER_READER1_POSTGRESREPLICATIONSLOTNAME: eventuate_slot_order + + EVENTUATE_CDC_READER_READER2_TYPE: postgres-wal + EVENTUATE_CDC_READER_READER2_DATASOURCEURL: jdbc:postgresql://postgres/cart + EVENTUATE_CDC_READER_READER2_MONITORINGSCHEMA: eventuate + EVENTUATE_CDC_READER_READER2_DATASOURCEUSERNAME: admin + EVENTUATE_CDC_READER_READER2_DATASOURCEPASSWORD: admin + EVENTUATE_CDC_READER_READER2_DATASOURCEDRIVERCLASSNAME: org.postgresql.Driver + EVENTUATE_CDC_READER_READER2_LEADERSHIPLOCKPATH: /eventuate/cdc/leader/cart + EVENTUATE_CDC_READER_READER2_OFFSETSTORAGETOPICNAME: db.history.common + EVENTUATE_CDC_READER_READER2_OUTBOXID: 2 + EVENTUATE_CDC_READER_READER2_POSTGRESREPLICATIONSLOTNAME: eventuate_slot_cart + + EVENTUATE_CDC_PIPELINE_PIPELINE1_TYPE: eventuate-tram + EVENTUATE_CDC_PIPELINE_PIPELINE1_READER: reader1 + EVENTUATE_CDC_PIPELINE_PIPELINE1_EVENTUATEDATABASESCHEMA: eventuate + + EVENTUATE_CDC_PIPELINE_PIPELINE2_TYPE: eventuate-tram + EVENTUATE_CDC_PIPELINE_PIPELINE2_READER: reader2 + EVENTUATE_CDC_PIPELINE_PIPELINE2_EVENTUATEDATABASESCHEMA: eventuate + networks: yas-network: driver: bridge diff --git a/docker/postgres/Dockerfile b/docker/postgres/Dockerfile new file mode 100644 index 0000000000..5254934b13 --- /dev/null +++ b/docker/postgres/Dockerfile @@ -0,0 +1,7 @@ +FROM debezium/postgres:15-alpine +ENV WAL2JSON_TAG=wal2json_2_5 +RUN apk add --no-cache --virtual .debezium-build-deps gcc clang15 llvm15 git make musl-dev pkgconf \ + && git clone https://github.com/eulerto/wal2json -b master --single-branch \ + && (cd /wal2json && git checkout tags/$WAL2JSON_TAG -b $WAL2JSON_TAG && make && make install) \ + && rm -rf wal2json \ + && apk del .debezium-build-deps \ No newline at end of file diff --git a/docker/postgres/postgresql.conf.sample b/docker/postgres/postgresql.conf.sample new file mode 100644 index 0000000000..3857e7fb3d --- /dev/null +++ b/docker/postgres/postgresql.conf.sample @@ -0,0 +1,16 @@ +# LOGGING +# log_min_error_statement = fatal +# log_min_messages = DEBUG1 + +# CONNECTION +listen_addresses = '*' + +# MODULES +shared_preload_libraries = 'decoderbufs,wal2json' + +# REPLICATION +wal_level = logical # minimal, archive, hot_standby, or logical (change requires restart) +max_wal_senders = 20 # max number of walsender processes (change requires restart) +#wal_keep_segments = 4 # in logfile segments, 16MB each; 0 disables +#wal_sender_timeout = 60s # in milliseconds; 0 disables +max_replication_slots = 20 # max number of replication slots (change requires restart) \ No newline at end of file diff --git a/k8s/charts/cart/Chart.yaml b/k8s/charts/cart/Chart.yaml index 9cefd83f01..1acc5c4771 100644 --- a/k8s/charts/cart/Chart.yaml +++ b/k8s/charts/cart/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/k8s/charts/cart/values.yaml b/k8s/charts/cart/values.yaml index 52de1c1cd3..e4cb799ab3 100644 --- a/k8s/charts/cart/values.yaml +++ b/k8s/charts/cart/values.yaml @@ -10,3 +10,12 @@ backend: enabled: true host: api.yas.local.com path: /cart + extraVolumes: + - name: cart-application-config + configMap: + name: cart-application-configmap + extraVolumeMounts: + - name: cart-application-config + mountPath: /opt/yas/cart + extraApplicationConfigPaths: + - /opt/yas/cart/cart-application.yaml diff --git a/k8s/charts/eventuate-cdc/.helmignore b/k8s/charts/eventuate-cdc/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/k8s/charts/eventuate-cdc/.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/k8s/charts/eventuate-cdc/Chart.yaml b/k8s/charts/eventuate-cdc/Chart.yaml new file mode 100644 index 0000000000..9a1c34871a --- /dev/null +++ b/k8s/charts/eventuate-cdc/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: eventuate-cdc +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/k8s/charts/eventuate-cdc/templates/NOTES.txt b/k8s/charts/eventuate-cdc/templates/NOTES.txt new file mode 100644 index 0000000000..70d12d7f5d --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "eventuate-cdc.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "eventuate-cdc.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "eventuate-cdc.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "eventuate-cdc.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/k8s/charts/eventuate-cdc/templates/_helpers.tpl b/k8s/charts/eventuate-cdc/templates/_helpers.tpl new file mode 100644 index 0000000000..501fe99b9b --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "eventuate-cdc.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "eventuate-cdc.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "eventuate-cdc.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "eventuate-cdc.labels" -}} +helm.sh/chart: {{ include "eventuate-cdc.chart" . }} +{{ include "eventuate-cdc.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "eventuate-cdc.selectorLabels" -}} +app.kubernetes.io/name: {{ include "eventuate-cdc.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "eventuate-cdc.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "eventuate-cdc.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/k8s/charts/eventuate-cdc/templates/deployment.yaml b/k8s/charts/eventuate-cdc/templates/deployment.yaml new file mode 100644 index 0000000000..6b58b5178f --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "eventuate-cdc.fullname" . }} + labels: + {{- include "eventuate-cdc.labels" . | nindent 4 }} + annotations: + reloader.stakater.com/search: "true" +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "eventuate-cdc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "eventuate-cdc.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "eventuate-cdc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: SPRING_APPLICATION_NAME + value: eventuate-cdc + - name: LOGGING_PATTERN_LEVEL + value: application=${spring.application.name} traceId=%X{traceId:-} spanId=%X{spanId:-} level=%level + envFrom: + - secretRef: + name: eventuate-cdc-env-secret + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /actuator/health + port: http + readinessProbe: + httpGet: + path: /actuator/health + port: http + initialDelaySeconds: 15 + periodSeconds: 5 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/k8s/charts/eventuate-cdc/templates/hpa.yaml b/k8s/charts/eventuate-cdc/templates/hpa.yaml new file mode 100644 index 0000000000..b9db5b29e6 --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "eventuate-cdc.fullname" . }} + labels: + {{- include "eventuate-cdc.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "eventuate-cdc.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/k8s/charts/eventuate-cdc/templates/ingress.yaml b/k8s/charts/eventuate-cdc/templates/ingress.yaml new file mode 100644 index 0000000000..77bdefddeb --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "eventuate-cdc.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "eventuate-cdc.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/k8s/charts/eventuate-cdc/templates/service.yaml b/k8s/charts/eventuate-cdc/templates/service.yaml new file mode 100644 index 0000000000..9bb9ec62af --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "eventuate-cdc.fullname" . }} + labels: + {{- include "eventuate-cdc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "eventuate-cdc.selectorLabels" . | nindent 4 }} diff --git a/k8s/charts/eventuate-cdc/templates/serviceaccount.yaml b/k8s/charts/eventuate-cdc/templates/serviceaccount.yaml new file mode 100644 index 0000000000..bea90b30bb --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "eventuate-cdc.serviceAccountName" . }} + labels: + {{- include "eventuate-cdc.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/k8s/charts/eventuate-cdc/templates/servicemonitoring.yaml b/k8s/charts/eventuate-cdc/templates/servicemonitoring.yaml new file mode 100644 index 0000000000..12052c0050 --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/servicemonitoring.yaml @@ -0,0 +1,13 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "eventuate-cdc.fullname" . }} + labels: + release: prometheus +spec: + selector: + matchLabels: + app.kubernetes.io/name: {{ include "eventuate-cdc.fullname" . }} + endpoints: + - port: 'http' + path: '/actuator/prometheus' \ No newline at end of file diff --git a/k8s/charts/eventuate-cdc/templates/tests/test-connection.yaml b/k8s/charts/eventuate-cdc/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..ea95818fd5 --- /dev/null +++ b/k8s/charts/eventuate-cdc/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "eventuate-cdc.fullname" . }}-test-connection" + labels: + {{- include "eventuate-cdc.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "eventuate-cdc.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/k8s/charts/eventuate-cdc/values.yaml b/k8s/charts/eventuate-cdc/values.yaml new file mode 100644 index 0000000000..c1f012e2a7 --- /dev/null +++ b/k8s/charts/eventuate-cdc/values.yaml @@ -0,0 +1,82 @@ +# Default values for eventuate-cdc. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: eventuateio/eventuate-cdc-service + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "0.15.0.RELEASE" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/k8s/charts/order/Chart.yaml b/k8s/charts/order/Chart.yaml index 543c0b0c9c..e95d954fc8 100644 --- a/k8s/charts/order/Chart.yaml +++ b/k8s/charts/order/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/k8s/charts/order/values.yaml b/k8s/charts/order/values.yaml index 5b73f36e93..6b72445297 100644 --- a/k8s/charts/order/values.yaml +++ b/k8s/charts/order/values.yaml @@ -10,3 +10,12 @@ backend: enabled: true host: api.yas.local.com path: /order + extraVolumes: + - name: order-application-config + configMap: + name: order-application-configmap + extraVolumeMounts: + - name: order-application-config + mountPath: /opt/yas/order + extraApplicationConfigPaths: + - /opt/yas/order/order-application.yaml \ No newline at end of file diff --git a/k8s/charts/yas-configuration/Chart.yaml b/k8s/charts/yas-configuration/Chart.yaml index 33097903d6..03075dc8c6 100644 --- a/k8s/charts/yas-configuration/Chart.yaml +++ b/k8s/charts/yas-configuration/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/k8s/charts/yas-configuration/templates/eventuate-cdc-env.secret.yaml b/k8s/charts/yas-configuration/templates/eventuate-cdc-env.secret.yaml new file mode 100644 index 0000000000..5debccbf5e --- /dev/null +++ b/k8s/charts/yas-configuration/templates/eventuate-cdc-env.secret.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: eventuate-cdc-env-secret + annotations: + reloader.stakater.com/match: "true" +type: Opaque +stringData: + CDC_OPTS: "--debug" + EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: "kafka-cluster-kafka-brokers.kafka:9092" + EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING: "zookeeper.zookeeper:2181" + {{- range $index, $config := .Values.eventuateCdcConfig }} + EVENTUATE_CDC_READER_READER{{ $index }}_TYPE: "postgres-wal" + EVENTUATE_CDC_READER_READER{{ $index }}_DATASOURCEURL: "jdbc:postgresql://postgresql.postgres/{{ $config.database }}" + EVENTUATE_CDC_READER_READER{{ $index }}_MONITORINGSCHEMA: "eventuate" + EVENTUATE_CDC_READER_READER{{ $index }}_DATASOURCEUSERNAME: "{{ $.Values.credentials.postgresql.username }}" + EVENTUATE_CDC_READER_READER{{ $index }}_DATASOURCEPASSWORD: "{{ $.Values.credentials.postgresql.password }}" + EVENTUATE_CDC_READER_READER{{ $index }}_DATASOURCEDRIVERCLASSNAME: "org.postgresql.Driver" + EVENTUATE_CDC_READER_READER{{ $index }}_LEADERSHIPLOCKPATH: "/eventuate/cdc/leader/eventuate/{{ $config.database }}" + EVENTUATE_CDC_READER_READER{{ $index }}_OFFSETSTORAGETOPICNAME: "db.history.common" + EVENTUATE_CDC_READER_READER{{ $index }}_OUTBOXID: "{{ $index }}" + EVENTUATE_CDC_READER_READER{{ $index }}_POSTGRESREPLICATIONSLOTNAME: "eventuate_slot_{{ $config.database }}" + EVENTUATE_CDC_PIPELINE_PIPELINE{{ $index }}_TYPE: "eventuate-tram" + EVENTUATE_CDC_PIPELINE_PIPELINE{{ $index }}_READER: "reader{{ $index }}" + EVENTUATE_CDC_PIPELINE_PIPELINE{{ $index }}_EVENTUATEDATABASESCHEMA: "eventuate" + {{- end }} \ No newline at end of file diff --git a/k8s/charts/yas-configuration/templates/yas-configurations.configmap.yaml b/k8s/charts/yas-configuration/templates/yas-configurations.configmap.yaml index 4443f73444..db34f91db2 100644 --- a/k8s/charts/yas-configuration/templates/yas-configurations.configmap.yaml +++ b/k8s/charts/yas-configuration/templates/yas-configurations.configmap.yaml @@ -44,7 +44,7 @@ data: customer-application.yaml: | {{ toYaml .Values.customerApplicationConfig | nindent 4 }} --- -# Configmap of search application config custo +# Configmap of search application config custom apiVersion: v1 kind: ConfigMap metadata: @@ -53,4 +53,26 @@ metadata: reloader.stakater.com/match: "true" data: search-application.yaml: | - {{ toYaml .Values.searchApplicationConfig | nindent 4 }} \ No newline at end of file + {{ toYaml .Values.searchApplicationConfig | nindent 4 }} +--- +# Configmap of cart application config custom +apiVersion: v1 +kind: ConfigMap +metadata: + name: cart-application-configmap + annotations: + reloader.stakater.com/match: "true" +data: + cart-application.yaml: | + {{ toYaml .Values.cartApplicationConfig | nindent 4 }} +--- +# Configmap of order application config custom +apiVersion: v1 +kind: ConfigMap +metadata: + name: order-application-configmap + annotations: + reloader.stakater.com/match: "true" +data: + order-application.yaml: | + {{ toYaml .Values.orderApplicationConfig | nindent 4 }} \ No newline at end of file diff --git a/k8s/charts/yas-configuration/values.yaml b/k8s/charts/yas-configuration/values.yaml index 5ec52d2f99..0d8485a769 100644 --- a/k8s/charts/yas-configuration/values.yaml +++ b/k8s/charts/yas-configuration/values.yaml @@ -201,6 +201,30 @@ searchApplicationConfig: username: ${ELASTICSEARCH_USERNAME} password: ${ELASTICSEARCH_PASSWORD} +# Cart application config custom +cartApplicationConfig: + eventuatelocal: + kafka: + bootstrap: + servers: kafka-cluster-kafka-brokers.kafka:9092 + zookeeper: + connection: + string: kafka-cluster-zookeeper-nodes.kafka:2181 + +# Order application config custom +orderApplicationConfig: + eventuatelocal: + kafka: + bootstrap: + servers: kafka-cluster-kafka-brokers.kafka:9092 + zookeeper: + connection: + string: kafka-cluster-zookeeper-nodes.kafka:2181 + +eventuateCdcConfig: + - database: order + - database: cart + logbackConfig: | @@ -210,7 +234,7 @@ logbackConfig: | - + diff --git a/k8s/deploy/deploy-yas-applications.sh b/k8s/deploy/deploy-yas-applications.sh index 139fed0b17..106100c9f0 100644 --- a/k8s/deploy/deploy-yas-applications.sh +++ b/k8s/deploy/deploy-yas-applications.sh @@ -49,3 +49,6 @@ for chart in {"cart","customer","inventory","location","media","order","payment" --set backend.ingress.host="api.$DOMAIN" sleep 60 done + +helm upgrade --install eventuate-cdc ../charts/eventuate-cdc \ +--namespace yas --create-namespace \ No newline at end of file diff --git a/k8s/deploy/postgres/postgresql/templates/postgresql.yaml b/k8s/deploy/postgres/postgresql/templates/postgresql.yaml index 954be7629d..960709e1b4 100644 --- a/k8s/deploy/postgres/postgresql/templates/postgresql.yaml +++ b/k8s/deploy/postgres/postgresql/templates/postgresql.yaml @@ -16,6 +16,9 @@ spec: version: {{ .Values.postgresqlVersion | quote }} parameters: max_connections: {{ .Values.maxConnections | quote }} + wal_level: logical + max_replication_slots: "20" + max_wal_senders: "20" numberOfInstances: {{ .Values.replicas }} volume: size: {{ .Values.volumeSize }} @@ -41,8 +44,53 @@ spec: rating: {{ .Values.username }} tax: {{ .Values.username }} grafana: {{ .Values.username }} + patroni: + slots: + eventuate_slot_cart: + type: logical + database: cart + plugin: wal2json + eventuate_slot_customer: + type: logical + database: customer + plugin: wal2json + eventuate_slot_inventory: + type: logical + database: inventory + plugin: wal2json + eventuate_slot_location: + type: logical + database: location + plugin: wal2json + eventuate_slot_media: + type: logical + database: media + plugin: wal2json + eventuate_slot_order: + type: logical + database: order + plugin: wal2json + eventuate_slot_payment: + type: logical + database: payment + plugin: wal2json + eventuate_slot_product: + type: logical + database: product + plugin: wal2json + eventuate_slot_promotion: + type: logical + database: promotion + plugin: wal2json + eventuate_slot_rating: + type: logical + database: rating + plugin: wal2json + eventuate_slot_tax: + type: logical + database: tax + plugin: wal2json allowedSourceRanges: # IP ranges to access your cluster go here - resources: {{ toYaml .Values.resources | nindent 4 }} \ No newline at end of file diff --git a/k8s/deploy/setup-cluster.sh b/k8s/deploy/setup-cluster.sh index 28c1359050..6fdc513b01 100644 --- a/k8s/deploy/setup-cluster.sh +++ b/k8s/deploy/setup-cluster.sh @@ -133,4 +133,7 @@ helm upgrade --install grafana ./observability/grafana \ --set grafana.username="$GRAFANA_USERNAME" \ --set grafana.password="$GRAFANA_PASSWORD" \ --set postgresql.username="$POSTGRESQL_USERNAME" \ ---set postgresql.password="$POSTGRESQL_PASSWORD" \ No newline at end of file +--set postgresql.password="$POSTGRESQL_PASSWORD" + +helm upgrade --install zookeeper ./zookeeper \ + --namespace zookeeper --create-namespace \ No newline at end of file diff --git a/k8s/deploy/zookeeper/.helmignore b/k8s/deploy/zookeeper/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/k8s/deploy/zookeeper/.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/k8s/deploy/zookeeper/Chart.yaml b/k8s/deploy/zookeeper/Chart.yaml new file mode 100644 index 0000000000..fa5cc184f6 --- /dev/null +++ b/k8s/deploy/zookeeper/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: zookeeper +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "3.8.2" diff --git a/k8s/deploy/zookeeper/templates/NOTES.txt b/k8s/deploy/zookeeper/templates/NOTES.txt new file mode 100644 index 0000000000..c86c53e0b8 --- /dev/null +++ b/k8s/deploy/zookeeper/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "zookeeper.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "zookeeper.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "zookeeper.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "zookeeper.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/k8s/deploy/zookeeper/templates/_helpers.tpl b/k8s/deploy/zookeeper/templates/_helpers.tpl new file mode 100644 index 0000000000..64d8ce47e9 --- /dev/null +++ b/k8s/deploy/zookeeper/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "zookeeper.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "zookeeper.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "zookeeper.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "zookeeper.labels" -}} +helm.sh/chart: {{ include "zookeeper.chart" . }} +{{ include "zookeeper.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "zookeeper.selectorLabels" -}} +app.kubernetes.io/name: {{ include "zookeeper.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "zookeeper.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "zookeeper.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/k8s/deploy/zookeeper/templates/deployment.yaml b/k8s/deploy/zookeeper/templates/deployment.yaml new file mode 100644 index 0000000000..fa546a5cc9 --- /dev/null +++ b/k8s/deploy/zookeeper/templates/deployment.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "zookeeper.fullname" . }} + labels: + {{- include "zookeeper.labels" . | nindent 4 }} +spec: + serviceName: {{ include "zookeeper.fullname" . }} + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "zookeeper.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "zookeeper.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "zookeeper.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + volumeMounts: + - name: {{ include "zookeeper.fullname" . }}-persistent-storage + mountPath: /usr/local/zookeeper-data + env: + - name: ZOOKEEPER_CLIENT_PORT + value: "2181" + - name: KAFKA_HEAP_OPTS + value: -Xmx64m + ports: + - name: zkport + containerPort: 2181 + protocol: TCP + livenessProbe: + exec: + command: + - bash + - -c + - "(echo ruok | nc localhost 2181) || exit 1" + readinessProbe: + exec: + command: + - bash + - -c + - "(echo ruok | nc localhost 2181) || exit 1" + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumeClaimTemplates: + - metadata: + name: {{ include "zookeeper.fullname" . }}-persistent-storage + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi \ No newline at end of file diff --git a/k8s/deploy/zookeeper/templates/hpa.yaml b/k8s/deploy/zookeeper/templates/hpa.yaml new file mode 100644 index 0000000000..df40aae3bb --- /dev/null +++ b/k8s/deploy/zookeeper/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "zookeeper.fullname" . }} + labels: + {{- include "zookeeper.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "zookeeper.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/k8s/deploy/zookeeper/templates/ingress.yaml b/k8s/deploy/zookeeper/templates/ingress.yaml new file mode 100644 index 0000000000..751e1fb0ed --- /dev/null +++ b/k8s/deploy/zookeeper/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "zookeeper.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "zookeeper.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/k8s/deploy/zookeeper/templates/service.yaml b/k8s/deploy/zookeeper/templates/service.yaml new file mode 100644 index 0000000000..6fc4f81f1e --- /dev/null +++ b/k8s/deploy/zookeeper/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "zookeeper.fullname" . }} + labels: + {{- include "zookeeper.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: zkport + protocol: TCP + name: zkport + selector: + {{- include "zookeeper.selectorLabels" . | nindent 4 }} diff --git a/k8s/deploy/zookeeper/templates/serviceaccount.yaml b/k8s/deploy/zookeeper/templates/serviceaccount.yaml new file mode 100644 index 0000000000..aee12d9bcb --- /dev/null +++ b/k8s/deploy/zookeeper/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "zookeeper.serviceAccountName" . }} + labels: + {{- include "zookeeper.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/k8s/deploy/zookeeper/templates/tests/test-connection.yaml b/k8s/deploy/zookeeper/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..6bfda6d840 --- /dev/null +++ b/k8s/deploy/zookeeper/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "zookeeper.fullname" . }}-test-connection" + labels: + {{- include "zookeeper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "zookeeper.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/k8s/deploy/zookeeper/values.yaml b/k8s/deploy/zookeeper/values.yaml new file mode 100644 index 0000000000..bdd6aacb84 --- /dev/null +++ b/k8s/deploy/zookeeper/values.yaml @@ -0,0 +1,82 @@ +# Default values for zookeeper. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: zookeeper + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 2181 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/order/pom.xml b/order/pom.xml index a62d48b013..74673038d7 100644 --- a/order/pom.xml +++ b/order/pom.xml @@ -52,7 +52,6 @@ springdoc-openapi-starter-webmvc-ui ${springdoc-openapi-starter-webmvc-ui.version} - org.springframework.boot spring-boot-starter-test @@ -117,6 +116,11 @@ keycloak-spring-security-adapter ${keycloak-spring-security-adapter.version} + + com.yas + saga + 0.0.1-SNAPSHOT + diff --git a/order/src/main/java/com/yas/order/controller/OrderController.java b/order/src/main/java/com/yas/order/controller/OrderController.java index c8ebcdfd75..baf0e332b6 100644 --- a/order/src/main/java/com/yas/order/controller/OrderController.java +++ b/order/src/main/java/com/yas/order/controller/OrderController.java @@ -1,27 +1,31 @@ package com.yas.order.controller; import com.yas.order.model.enumeration.EOrderStatus; +import com.yas.order.service.OrderSagaService; import com.yas.order.service.OrderService; import com.yas.order.viewmodel.order.*; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.time.ZonedDateTime; import java.util.List; @RestController +@RequiredArgsConstructor public class OrderController { private final OrderService orderService; - public OrderController(OrderService orderService) { - this.orderService = orderService; - } + private final OrderSagaService orderSagaService; @PostMapping("/storefront/orders") public ResponseEntity createOrder(@Valid @RequestBody OrderPostVm orderPostVm) { - return ResponseEntity.ok(orderService.createOrder(orderPostVm)); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return ResponseEntity.ok(orderSagaService.createOrder(orderPostVm, auth.getName())); } @GetMapping("/storefront/orders/completed") diff --git a/order/src/main/java/com/yas/order/saga/CreateOrderSaga.java b/order/src/main/java/com/yas/order/saga/CreateOrderSaga.java new file mode 100644 index 0000000000..f72201e369 --- /dev/null +++ b/order/src/main/java/com/yas/order/saga/CreateOrderSaga.java @@ -0,0 +1,60 @@ +package com.yas.order.saga; + +import com.yas.order.saga.data.CreateOrderSagaData; +import com.yas.order.service.CartService; +import com.yas.order.service.OrderService; +import com.yas.order.viewmodel.order.OrderItemPostVm; +import com.yas.order.viewmodel.order.OrderPostVm; +import com.yas.order.viewmodel.order.OrderVm; +import com.yas.saga.cart.reply.DeleteCartItemFailure; +import com.yas.saga.cart.reply.DeleteCartItemSuccess; +import io.eventuate.tram.commands.consumer.CommandWithDestination; +import io.eventuate.tram.sagas.orchestration.SagaDefinition; +import io.eventuate.tram.sagas.simpledsl.SimpleSaga; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CreateOrderSaga implements SimpleSaga { + + private final OrderService orderService; + private final CartService cartService; + + private final SagaDefinition sagaDefinition = + step() + .invokeLocal(this::createOrder) + .withCompensation(this::compensateOrder) + .step() + .invokeParticipant(this::deleteCartItem) + .onReply(DeleteCartItemSuccess.class, (data, reply) -> log.info(reply.getMessage())) + .onReply(DeleteCartItemFailure.class, (data, reply) -> log.warn(reply.getMessage())) + .build(); + + @Override + public SagaDefinition getSagaDefinition() { + return this.sagaDefinition; + } + + private void createOrder(CreateOrderSagaData data) { + OrderVm orderVm = this.orderService.createOrder(data.getOrderPostVm()); + data.setOrderVm(orderVm); + } + + private CommandWithDestination deleteCartItem(CreateOrderSagaData data) { + OrderPostVm orderPostVm = data.getOrderPostVm(); + List productIds = orderPostVm.orderItemPostVms() + .stream() + .map(OrderItemPostVm::productId) + .toList(); + return this.cartService.deleteCartItem(productIds, data.getCustomerId()); + } + + private void compensateOrder(CreateOrderSagaData data) { + log.info("Compensate Order"); + } +} diff --git a/order/src/main/java/com/yas/order/saga/data/CreateOrderSagaData.java b/order/src/main/java/com/yas/order/saga/data/CreateOrderSagaData.java new file mode 100644 index 0000000000..7c2a7f88a8 --- /dev/null +++ b/order/src/main/java/com/yas/order/saga/data/CreateOrderSagaData.java @@ -0,0 +1,18 @@ +package com.yas.order.saga.data; + +import com.yas.order.viewmodel.order.OrderPostVm; +import com.yas.order.viewmodel.order.OrderVm; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CreateOrderSagaData { + private OrderPostVm orderPostVm; + private OrderVm orderVm; + private String customerId; +} diff --git a/order/src/main/java/com/yas/order/service/CartService.java b/order/src/main/java/com/yas/order/service/CartService.java index 5f800c5e79..68445f2d74 100644 --- a/order/src/main/java/com/yas/order/service/CartService.java +++ b/order/src/main/java/com/yas/order/service/CartService.java @@ -1,48 +1,22 @@ package com.yas.order.service; -import com.yas.order.config.ServiceUrlConfig; -import com.yas.order.exception.BadRequestException; -import com.yas.order.exception.NotFoundException; -import com.yas.order.viewmodel.ResponeStatusVm; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.jwt.Jwt; +import com.yas.saga.cart.command.DeleteCartItemCommand; +import io.eventuate.tram.commands.consumer.CommandWithDestination; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.util.UriComponentsBuilder; -import java.net.URI; + import java.util.List; +import static io.eventuate.tram.commands.consumer.CommandWithDestinationBuilder.send; + @Service public class CartService { - private final WebClient webClient; - private final ServiceUrlConfig serviceUrlConfig; - - public CartService(WebClient webClient, ServiceUrlConfig serviceUrlConfig) { - this.webClient = webClient; - this.serviceUrlConfig = serviceUrlConfig; - } - - public Void deleteCartItemByProductId(List productIds) { - final String jwt = ((Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getTokenValue(); - final URI url = UriComponentsBuilder - .fromHttpUrl(serviceUrlConfig.cart()) - .path("/storefront/cart-item/multi-delete") - .queryParam("productIds", productIds) - .buildAndExpand() - .toUri(); - return webClient.delete() - .uri(url) - .headers(h->h.setBearerAuth(jwt)) - .retrieve() - .onStatus( - HttpStatus.BAD_REQUEST::equals, - response -> response.bodyToMono(String.class).map(BadRequestException::new)) - .onStatus( - HttpStatus.INTERNAL_SERVER_ERROR::equals, - response -> response.bodyToMono(String.class).map(BadRequestException::new)) - .bodyToMono(Void.class) - .block(); + public CommandWithDestination deleteCartItem(List productIds, String customerId) { + var deleteCartItemCommand = DeleteCartItemCommand.builder() + .customerId(customerId) + .productIds(productIds) + .build(); + return send(deleteCartItemCommand) + .to("cartService") + .build(); } } \ No newline at end of file diff --git a/order/src/main/java/com/yas/order/service/OrderSagaService.java b/order/src/main/java/com/yas/order/service/OrderSagaService.java new file mode 100644 index 0000000000..69775f6d10 --- /dev/null +++ b/order/src/main/java/com/yas/order/service/OrderSagaService.java @@ -0,0 +1,28 @@ +package com.yas.order.service; + +import com.yas.order.saga.CreateOrderSaga; +import com.yas.order.saga.data.CreateOrderSagaData; +import com.yas.order.viewmodel.order.OrderPostVm; +import com.yas.order.viewmodel.order.OrderVm; +import io.eventuate.tram.sagas.orchestration.SagaInstanceFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class OrderSagaService { + + private final SagaInstanceFactory sagaInstanceFactory; + + private final CreateOrderSaga createOrderSaga; + + public OrderVm createOrder(OrderPostVm orderPostVm, String customerId) { + + CreateOrderSagaData data = CreateOrderSagaData.builder() + .customerId(customerId) + .orderPostVm(orderPostVm) + .build(); + sagaInstanceFactory.create(createOrderSaga, data); + return data.getOrderVm(); + } +} diff --git a/order/src/main/java/com/yas/order/service/OrderService.java b/order/src/main/java/com/yas/order/service/OrderService.java index 8d6b88f8ae..30b4a4262e 100644 --- a/order/src/main/java/com/yas/order/service/OrderService.java +++ b/order/src/main/java/com/yas/order/service/OrderService.java @@ -1,7 +1,6 @@ package com.yas.order.service; import com.yas.order.exception.NotFoundException; -import com.yas.order.model.Checkout; import com.yas.order.model.Order; import com.yas.order.model.OrderAddress; import com.yas.order.model.OrderItem; @@ -21,13 +20,15 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.time.ZonedDateTime; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @Slf4j @@ -38,7 +39,6 @@ public class OrderService { private final CheckoutRepository checkoutRepository; private final OrderRepository orderRepository; private final OrderItemRepository orderItemRepository; - private final CartService cartService; private final ProductService productService; public OrderVm createOrder(OrderPostVm orderPostVm) { @@ -114,13 +114,6 @@ public OrderVm createOrder(OrderPostVm orderPostVm) { //setOrderItems so that we able to return order with orderItems order.setOrderItems(orderItems); - // delete Item in Cart - try { - cartService.deleteCartItemByProductId(orderItems.stream().map(i -> i.getProductId()).toList()); - } catch (Exception ex) { - log.error("Delete products in cart fail: " + ex.getMessage()); - } - // TO-DO: decrement inventory when inventory is complete // ************ diff --git a/order/src/main/resources/application.properties b/order/src/main/resources/application.properties index d75125aaa6..77a9003f10 100644 --- a/order/src/main/resources/application.properties +++ b/order/src/main/resources/application.properties @@ -8,6 +8,11 @@ management.endpoints.web.exposure.include=prometheus management.metrics.distribution.percentiles-histogram.http.server.requests=true management.metrics.tags.application=${spring.application.name} +eventuatelocal.kafka.bootstrap.servers=kafka:9092 +eventuatelocal.zookeeper.connection.string=zookeeper:2181 +eventuate.database.schema=eventuate +spring.liquibase.parameters.eventualSlotName=order + logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] spring.security.oauth2.resourceserver.jwt.issuer-uri=http://identity/realms/Yas @@ -28,6 +33,9 @@ spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialec # Hibernate ddl auto (none, create, create-drop, validate, update) spring.jpa.hibernate.ddl-auto = none +# Disable open in view transaction +spring.jpa.open-in-view=false + #Enable liquibase spring.liquibase.enabled=true diff --git a/order/src/main/resources/db/changelog/db.changelog-master.yaml b/order/src/main/resources/db/changelog/db.changelog-master.yaml index 419ad29d64..d9ef4f57ba 100644 --- a/order/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/order/src/main/resources/db/changelog/db.changelog-master.yaml @@ -2,4 +2,6 @@ databaseChangeLog: - includeAll: path: db/changelog/ddl/ - includeAll: - path: db/changelog/data/ \ No newline at end of file + path: db/changelog/data/ + - includeAll: + path: db/changelog/eventuate-dll/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000..ba3056b475 --- /dev/null +++ b/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + com.yas + yas + 0.0.1-SNAPSHOT + pom + yas + + + cart + + order + + saga + + + \ No newline at end of file diff --git a/saga/.gitignore b/saga/.gitignore new file mode 100644 index 0000000000..549e00a2a9 --- /dev/null +++ b/saga/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/saga/.mvn/wrapper/maven-wrapper.jar b/saga/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000..cb28b0e37c Binary files /dev/null and b/saga/.mvn/wrapper/maven-wrapper.jar differ diff --git a/saga/.mvn/wrapper/maven-wrapper.properties b/saga/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..462686e25d --- /dev/null +++ b/saga/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/saga/mvnw b/saga/mvnw new file mode 100755 index 0000000000..66df285428 --- /dev/null +++ b/saga/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/saga/mvnw.cmd b/saga/mvnw.cmd new file mode 100644 index 0000000000..95ba6f54ac --- /dev/null +++ b/saga/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/saga/pom.xml b/saga/pom.xml new file mode 100644 index 0000000000..bde99273f3 --- /dev/null +++ b/saga/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + com.yas + saga + 0.0.1-SNAPSHOT + saga + Saga model common yas project + + 17 + 17 + 17 + 1.18.26 + 1.4.1.Final + 2023.0.RELEASE + + + + org.projectlombok + lombok + ${org.lombok.version} + true + + + io.eventuate.tram.core + eventuate-tram-spring-optimistic-locking + + + io.eventuate.tram.core + eventuate-tram-spring-jdbc-kafka + + + io.eventuate.tram.sagas + eventuate-tram-sagas-spring-orchestration-simple-dsl-starter + + + io.eventuate.tram.sagas + eventuate-tram-sagas-spring-participant-starter + + + + + + io.eventuate.platform + eventuate-platform-dependencies + ${eventuate-platform.version} + pom + import + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.projectlombok + lombok + ${org.lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + + diff --git a/saga/src/main/java/com/yas/saga/SagaConfiguration.java b/saga/src/main/java/com/yas/saga/SagaConfiguration.java new file mode 100644 index 0000000000..1c28ba68ea --- /dev/null +++ b/saga/src/main/java/com/yas/saga/SagaConfiguration.java @@ -0,0 +1,10 @@ +package com.yas.saga; + +import io.eventuate.tram.spring.optimisticlocking.OptimisticLockingDecoratorConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(OptimisticLockingDecoratorConfiguration.class) +public class SagaConfiguration { +} diff --git a/saga/src/main/java/com/yas/saga/cart/command/DeleteCartItemCommand.java b/saga/src/main/java/com/yas/saga/cart/command/DeleteCartItemCommand.java new file mode 100644 index 0000000000..39749a7b66 --- /dev/null +++ b/saga/src/main/java/com/yas/saga/cart/command/DeleteCartItemCommand.java @@ -0,0 +1,18 @@ +package com.yas.saga.cart.command; + +import io.eventuate.tram.commands.common.Command; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DeleteCartItemCommand implements Command { + private List productIds; + private String customerId; +} diff --git a/saga/src/main/java/com/yas/saga/cart/reply/DeleteCartItemFailure.java b/saga/src/main/java/com/yas/saga/cart/reply/DeleteCartItemFailure.java new file mode 100644 index 0000000000..cf30d3ba65 --- /dev/null +++ b/saga/src/main/java/com/yas/saga/cart/reply/DeleteCartItemFailure.java @@ -0,0 +1,14 @@ +package com.yas.saga.cart.reply; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DeleteCartItemFailure { + private String message; +} diff --git a/saga/src/main/java/com/yas/saga/cart/reply/DeleteCartItemSuccess.java b/saga/src/main/java/com/yas/saga/cart/reply/DeleteCartItemSuccess.java new file mode 100644 index 0000000000..aa3960778d --- /dev/null +++ b/saga/src/main/java/com/yas/saga/cart/reply/DeleteCartItemSuccess.java @@ -0,0 +1,14 @@ +package com.yas.saga.cart.reply; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DeleteCartItemSuccess { + private String message; +} diff --git a/saga/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/saga/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..dc2956999a --- /dev/null +++ b/saga/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,11 @@ +com.yas.saga.SagaConfiguration +io.eventuate.tram.sagas.spring.participant.autoconfigure.SpringParticipantAutoConfiguration +io.eventuate.tram.sagas.spring.orchestration.autoconfigure.SpringOrchestratorSimpleDslAutoConfiguration +io.eventuate.tram.spring.messaging.autoconfigure.EventuateTramActiveMQMessageConsumerAutoConfiguration +io.eventuate.tram.spring.messaging.autoconfigure.EventuateTramKafkaMessageConsumerAutoConfiguration +io.eventuate.tram.spring.messaging.autoconfigure.EventuateTramRabbitMQMessageConsumerAutoConfiguration +io.eventuate.tram.spring.messaging.autoconfigure.EventuateTramRedisMessageConsumerAutoConfiguration +io.eventuate.tram.spring.messaging.autoconfigure.TramMessageProducerJdbcAutoConfiguration +io.eventuate.tram.spring.messaging.common.TramMessagingCommonAutoConfiguration +io.eventuate.tram.spring.commands.common.TramCommandsCommonAutoConfiguration +io.eventuate.tram.spring.consumer.jdbc.TramConsumerJdbcAutoConfiguration \ No newline at end of file diff --git a/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0001.sql b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0001.sql new file mode 100644 index 0000000000..f727fcd602 --- /dev/null +++ b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0001.sql @@ -0,0 +1,52 @@ +--liquibase formatted sql +--changeset bangnguyen:eventuate-dll-0001 +--comment: init eventuate tables + +CREATE SCHEMA eventuate; + +DROP TABLE IF EXISTS eventuate.events CASCADE; +DROP TABLE IF EXISTS eventuate.entities CASCADE; +DROP TABLE IF EXISTS eventuate.snapshots CASCADE; +DROP TABLE IF EXISTS eventuate.cdc_monitoring CASCADE; + +CREATE TABLE eventuate.events +( + event_id VARCHAR(1000) PRIMARY KEY, + event_type VARCHAR(1000), + event_data VARCHAR(1000) NOT NULL, + entity_type VARCHAR(1000) NOT NULL, + entity_id VARCHAR(1000) NOT NULL, + triggering_event VARCHAR(1000), + metadata VARCHAR(1000), + published SMALLINT DEFAULT 0 +); + +CREATE INDEX events_idx ON eventuate.events (entity_type, entity_id, event_id); +CREATE INDEX events_published_idx ON eventuate.events (published, event_id); + +CREATE TABLE eventuate.entities +( + entity_type VARCHAR(1000), + entity_id VARCHAR(1000), + entity_version VARCHAR(1000) NOT NULL, + PRIMARY KEY (entity_type, entity_id) +); + +CREATE INDEX entities_idx ON eventuate.entities (entity_type, entity_id); + +CREATE TABLE eventuate.snapshots +( + entity_type VARCHAR(1000), + entity_id VARCHAR(1000), + entity_version VARCHAR(1000), + snapshot_type VARCHAR(1000) NOT NULL, + snapshot_json VARCHAR(1000) NOT NULL, + triggering_events VARCHAR(1000), + PRIMARY KEY (entity_type, entity_id, entity_version) +); + +CREATE TABLE eventuate.cdc_monitoring +( + reader_id VARCHAR(1000) PRIMARY KEY, + last_time BIGINT +); \ No newline at end of file diff --git a/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0002.sql b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0002.sql new file mode 100644 index 0000000000..d3c6cce717 --- /dev/null +++ b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0002.sql @@ -0,0 +1,34 @@ +--liquibase formatted sql +--changeset bangnguyen:eventuate-dll-0002 +--comment: init eventuate tables + +DROP TABLE IF EXISTS eventuate.message CASCADE; +DROP TABLE IF EXISTS eventuate.received_messages CASCADE; + +CREATE TABLE eventuate.message +( + id VARCHAR(1000) PRIMARY KEY, + destination TEXT NOT NULL, + headers TEXT NOT NULL, + payload TEXT NOT NULL, + published SMALLINT DEFAULT 0, + message_partition SMALLINT, + creation_time BIGINT +); + +CREATE INDEX message_published_idx ON eventuate.message (published, id); + +CREATE TABLE eventuate.received_messages +( + consumer_id VARCHAR(1000), + message_id VARCHAR(1000), + creation_time BIGINT, + published SMALLINT DEFAULT 0, + PRIMARY KEY (consumer_id, message_id) +); + +CREATE TABLE eventuate.offset_store +( + client_name VARCHAR(255) NOT NULL PRIMARY KEY, + serialized_offset VARCHAR(255) +); \ No newline at end of file diff --git a/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0003.sql b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0003.sql new file mode 100644 index 0000000000..34479d1b7e --- /dev/null +++ b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0003.sql @@ -0,0 +1,9 @@ +--liquibase formatted sql +--changeset bangnguyen:eventuate-dll-0003 +--comment: init eventuate tables + +ALTER TABLE eventuate.message DROP COLUMN payload; +ALTER TABLE eventuate.message ADD COLUMN payload JSON; + +ALTER TABLE eventuate.message DROP COLUMN headers; +ALTER TABLE eventuate.message ADD COLUMN headers JSON; \ No newline at end of file diff --git a/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0004.sql b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0004.sql new file mode 100644 index 0000000000..e0f89e18a9 --- /dev/null +++ b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0004.sql @@ -0,0 +1,71 @@ +--liquibase formatted sql +--changeset bangnguyen:eventuate-dll-0004 +--comment: init eventuate tables + +CREATE SEQUENCE eventuate.message_table_id_sequence START 1; + +select setval('eventuate.message_table_id_sequence', (ROUND(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000))::BIGINT); + +CREATE TABLE eventuate.new_message +( + dbid BIGINT NOT NULL DEFAULT nextval('eventuate.message_table_id_sequence') PRIMARY KEY, + id VARCHAR(1000), + destination TEXT NOT NULL, + headers TEXT NOT NULL, + payload TEXT NOT NULL, + published SMALLINT DEFAULT 0, + message_partition SMALLINT, + creation_time BIGINT +); + +ALTER SEQUENCE eventuate.message_table_id_sequence OWNED BY eventuate.new_message.dbid; + +INSERT INTO eventuate.new_message (id, destination, headers, payload, published, message_partition, creation_time) +SELECT id, destination, headers, payload, published, message_partition, creation_time +FROM eventuate.message +ORDER BY id; + +DROP TABLE eventuate.message; + +ALTER TABLE eventuate.new_message + RENAME TO message; + +CREATE SEQUENCE eventuate.events_table_id_sequence START 1; + +select setval('eventuate.events_table_id_sequence', (ROUND(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000))::BIGINT); + +CREATE TABLE eventuate.new_events +( + id BIGINT NOT NULL DEFAULT nextval('eventuate.events_table_id_sequence') PRIMARY KEY, + event_id VARCHAR(1000), + event_type VARCHAR(1000), + event_data VARCHAR(1000) NOT NULL, + entity_type VARCHAR(1000) NOT NULL, + entity_id VARCHAR(1000) NOT NULL, + triggering_event VARCHAR(1000), + metadata VARCHAR(1000), + published SMALLINT DEFAULT 0 +); + +ALTER SEQUENCE eventuate.events_table_id_sequence OWNED BY eventuate.new_events.id; + +INSERT INTO eventuate.new_events (event_id, event_type, event_data, entity_type, entity_id, triggering_event, metadata, + published) +SELECT event_id, + event_type, + event_data, + entity_type, + entity_id, + triggering_event, + metadata, + published +FROM eventuate.events +ORDER BY event_id; + +DROP TABLE eventuate.events; + +ALTER TABLE eventuate.new_events + RENAME TO events; + +CREATE INDEX events_idx ON eventuate.events (entity_type, entity_id, id); +CREATE INDEX events_published_idx ON eventuate.events (published, id); \ No newline at end of file diff --git a/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0005.sql b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0005.sql new file mode 100644 index 0000000000..4fda04ba3d --- /dev/null +++ b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0005.sql @@ -0,0 +1,48 @@ +--liquibase formatted sql +--changeset bangnguyen:eventuate-dll-0006 +--comment: init eventuate tables + +DROP Table IF Exists eventuate.saga_instance_participants; +DROP Table IF Exists eventuate.saga_instance; +DROP Table IF Exists eventuate.saga_lock_table; +DROP Table IF Exists eventuate.saga_stash_table; + +CREATE TABLE eventuate.saga_instance_participants +( + saga_type VARCHAR(255) NOT NULL, + saga_id VARCHAR(100) NOT NULL, + destination VARCHAR(100) NOT NULL, + resource VARCHAR(100) NOT NULL, + PRIMARY KEY (saga_type, saga_id, destination, resource) +); + +CREATE TABLE eventuate.saga_instance +( + saga_type VARCHAR(255) NOT NULL, + saga_id VARCHAR(100) NOT NULL, + state_name VARCHAR(100) NOT NULL, + last_request_id VARCHAR(100), + end_state BOOLEAN, + compensating BOOLEAN, + failed BOOLEAN, + saga_data_type TEXT NOT NULL, + saga_data_json TEXT NOT NULL, + PRIMARY KEY (saga_type, saga_id) +); + +create table eventuate.saga_lock_table +( + target VARCHAR(100) PRIMARY KEY, + saga_type VARCHAR(255) NOT NULL, + saga_Id VARCHAR(100) NOT NULL +); + +create table eventuate.saga_stash_table +( + message_id VARCHAR(100) PRIMARY KEY, + target VARCHAR(100) NOT NULL, + saga_type VARCHAR(255) NOT NULL, + saga_id VARCHAR(100) NOT NULL, + message_headers VARCHAR(1000) NOT NULL, + message_payload VARCHAR(1000) NOT NULL +); \ No newline at end of file diff --git a/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0006.sql b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0006.sql new file mode 100644 index 0000000000..2d63bc4b6d --- /dev/null +++ b/saga/src/main/resources/db/changelog/eventuate-dll/changelog-0006.sql @@ -0,0 +1,13 @@ +--liquibase formatted sql +--changeset bangnguyen:eventuate-dll-0006 +--comment: create replication slot +DO + ' + DECLARE + eventuate_slot VARCHAR := ''eventuate_slot_${eventualSlotName}''; + BEGIN + IF NOT EXISTS (SELECT * FROM pg_replication_slots WHERE slot_name = eventuate_slot) THEN + PERFORM pg_create_logical_replication_slot(eventuate_slot, ''wal2json''); + END IF; + END + '; \ No newline at end of file