diff --git a/.github/workflows/sonarqube.yaml b/.github/workflows/sonarqube.yaml index 935581b..1442e4c 100644 --- a/.github/workflows/sonarqube.yaml +++ b/.github/workflows/sonarqube.yaml @@ -8,8 +8,13 @@ on: jobs: build: name: Build and analyze - runs-on: windows-latest + runs-on: ubuntu-latest steps: + - name: Set up Docker + run: | + docker version + docker pull postgres:16 --platform=linux/amd64 + docker pull rabbitmq:3.11 --platform=linux/amd64 - name: Set up JDK 17 uses: actions/setup-java@v3 with: @@ -18,34 +23,33 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Setup Android SDK + uses: android-actions/setup-android@v3 - name: Cache SonarCloud packages uses: actions/cache@v3 with: path: ~\sonar\cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - - name: Cache SonarCloud scanner - id: cache-sonar-scanner + - name: Cache SonarCloud packages uses: actions/cache@v3 with: - path: .\.sonar\scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner + path: ~\sonar\cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar - name: Install SonarCloud scanner if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: powershell run: | - New-Item -Path .\.sonar\scanner -ItemType Directory - dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner + dotnet tool install --global dotnet-sonarscanner - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: powershell run: | - .\.sonar\scanner\dotnet-sonarscanner begin /k:"MadWorldNL_MantaRayPlan" /o:"madworldnl" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths=TestResults/*/coverage.opencover.xml /d:sonar.coverage.exclusions="**Test*.cs" /d:sonar.exclusions="**/Clients.DefaultStyle/wwwroot/lib/**, **/Clients.DefaultStyle/wwwroot/app/**" + dotnet-sonarscanner begin /k:"MadWorldNL_MantaRayPlan" /o:"madworldnl" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths=TestResults/*/coverage.opencover.xml /d:sonar.coverage.exclusions="**Test*.cs" /d:sonar.exclusions="**/Clients.DefaultStyle/wwwroot/lib/**, **/Clients.DefaultStyle/wwwroot/app/**" + dotnet workload install android dotnet workload restore ./src/MadWorldNL.MantaRayPlan.sln; dotnet restore ./src/MadWorldNL.MantaRayPlan.sln; dotnet build ./src/MadWorldNL.MantaRayPlan.sln --no-restore -c Release; dotnet test ./src/MadWorldNL.MantaRayPlan.sln --no-build --no-restore -c Release --blame-hang-timeout 5min --collect:"XPlat Code Coverage" --results-directory TestResults/ -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover; - .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ No newline at end of file + dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ No newline at end of file diff --git a/README.md b/README.md index 1cfa957..52e397d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ docker compose up *This project is required to set as startup project -`dotnet workload restore` is required to install the necessary workloads for the project. You don't need to run this command if you have already installed the workloads. +`dotnet workload restore` is required to install the necessary workloads for the project. However, when you download a new update, +it may be necessary to run this command to ensure all workloads are up to date. ### Running the tests Before running the tests, ensure Docker is up and running. diff --git a/deployment/MantaRayPlanCloud/environments/values-production.yaml b/deployment/MantaRayPlanCloud/environments/values-production.yaml index 18ff046..3d47a8f 100644 --- a/deployment/MantaRayPlanCloud/environments/values-production.yaml +++ b/deployment/MantaRayPlanCloud/environments/values-production.yaml @@ -1,7 +1,7 @@ namespace: "manta-ray-plan-production" image: - tag: "v0.4.7" + tag: "v0.5.0" clusterIssuer: enabled: true diff --git a/deployment/MantaRayPlanCloud/templates/admin-bff.yaml b/deployment/MantaRayPlanCloud/templates/admin-bff.yaml index 2c1000f..c2a4e9b 100644 --- a/deployment/MantaRayPlanCloud/templates/admin-bff.yaml +++ b/deployment/MantaRayPlanCloud/templates/admin-bff.yaml @@ -18,10 +18,21 @@ spec: app: {{ .Values.admin.bff.app }} task: {{ .Values.admin.bff.name }} spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false containers: - name: {{ .Values.admin.bff.app }} image: "{{ .Values.admin.bff.image }}:{{ .Values.image.tag }}" imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" livenessProbe: httpGet: path: /healthz @@ -31,13 +42,27 @@ spec: timeoutSeconds: 1 failureThreshold: 3 env: + - name: Api__Address + value: "{{ .Values.api.grpc.loadBalancer }}:8080" - name: OpenTelemetry__LoggerEndpoint value: "{{ .Values.logging.seq.host.internal }}" - name: OpenTelemetry__LoggerApiKey valueFrom: secretKeyRef: name: {{ .Values.logging.seq.secrets.name }} - key: apiKeyAdmin + key: apiKeyAdmin + - name: MessageBus__Host + value: {{ .Values.messageBus.loadBalancer }} + - name: MessageBus__Username + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: username + - name: MessageBus__Password + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: password ports: - containerPort: 8080 --- diff --git a/deployment/MantaRayPlanCloud/templates/admin-web.yaml b/deployment/MantaRayPlanCloud/templates/admin-web.yaml index 49c1712..d202b3c 100644 --- a/deployment/MantaRayPlanCloud/templates/admin-web.yaml +++ b/deployment/MantaRayPlanCloud/templates/admin-web.yaml @@ -6,18 +6,13 @@ metadata: data: default.conf: | server { - listen 80; + listen 80; listen [::]:80; server_name localhost; location / { - root /usr/share/nginx/html; - index index.html index.htm; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html =404; } add_header X-Frame-Options "DENY" always; @@ -27,6 +22,19 @@ data: add_header content-security-policy "default-src 'self' https://{{ .Values.admin.bff.host }}; img-src 'self' data: ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://{{ .Values.admin.bff.host }}; upgrade-insecure-requests; frame-ancestors 'self'"; } --- +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ .Values.admin.web.app }}-appsetting-config" + namespace: {{ .Values.namespace }} +data: + default.conf: | + { + "Api": { + "Address": "https://{{ .Values.admin.bff.host }}/" + } + } +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -47,10 +55,21 @@ spec: app: {{ .Values.admin.web.app }} task: {{ .Values.admin.web.name }} spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false containers: - name: {{ .Values.admin.web.app }} image: "{{ .Values.admin.web.image }}:{{ .Values.image.tag }}" imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" livenessProbe: httpGet: path: /healthz.txt @@ -64,12 +83,19 @@ spec: mountPath: /etc/nginx/conf.d/default.conf subPath: default.conf readOnly: true + - name: "{{ .Values.admin.web.app }}-appsettings-config-volume" + mountPath: /usr/share/nginx/html/appsettings.json + subPath: appsettings.json + readOnly: true ports: - containerPort: 80 volumes: - name: "{{ .Values.admin.web.app }}-nginx-config-volume" configMap: name: "{{ .Values.admin.web.app }}-nginx-config" + - name: "{{ .Values.admin.web.app }}-appsettings-config-volume" + configMap: + name: "{{ .Values.admin.web.app }}-appsettings-config" --- apiVersion: v1 kind: Service diff --git a/deployment/MantaRayPlanCloud/templates/api-grpc.yaml b/deployment/MantaRayPlanCloud/templates/api-grpc.yaml index 044a3f6..c1dad47 100644 --- a/deployment/MantaRayPlanCloud/templates/api-grpc.yaml +++ b/deployment/MantaRayPlanCloud/templates/api-grpc.yaml @@ -18,10 +18,21 @@ spec: app: {{ .Values.api.grpc.app }} task: {{ .Values.api.grpc.name }} spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false containers: - name: {{ .Values.api.grpc.app }} image: "{{ .Values.api.grpc.image }}:{{ .Values.image.tag }}" imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" livenessProbe: httpGet: path: /healthz @@ -55,7 +66,19 @@ spec: name: {{ .Values.logging.seq.secrets.name }} key: apiKeyGrpc - name: Kestrel__EndpointDefaults__Protocols - value: "Http1AndHttp2" + value: "Http1AndHttp2" + - name: MessageBus__Host + value: {{ .Values.messageBus.loadBalancer }} + - name: MessageBus__Username + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: username + - name: MessageBus__Password + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: password ports: - containerPort: 8080 --- diff --git a/deployment/MantaRayPlanCloud/templates/api-message-bus.yaml b/deployment/MantaRayPlanCloud/templates/api-message-bus.yaml index 37b1e6a..75a987d 100644 --- a/deployment/MantaRayPlanCloud/templates/api-message-bus.yaml +++ b/deployment/MantaRayPlanCloud/templates/api-message-bus.yaml @@ -18,10 +18,21 @@ spec: app: {{ .Values.api.messageBus.app }} task: {{ .Values.api.messageBus.name }} spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false containers: - name: {{ .Values.api.messageBus.app }} image: "{{ .Values.api.messageBus.image }}:{{ .Values.image.tag }}" imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" livenessProbe: httpGet: path: /healthz @@ -56,5 +67,17 @@ spec: key: apiKeyMessageBus - name: Kestrel__EndpointDefaults__Protocols value: "Http1AndHttp2" + - name: MessageBus__Host + value: {{ .Values.messageBus.loadBalancer }} + - name: MessageBus__Username + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: username + - name: MessageBus__Password + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: password ports: - containerPort: 8080 \ No newline at end of file diff --git a/deployment/MantaRayPlanCloud/templates/messagebus.yaml b/deployment/MantaRayPlanCloud/templates/messagebus.yaml new file mode 100644 index 0000000..c77f748 --- /dev/null +++ b/deployment/MantaRayPlanCloud/templates/messagebus.yaml @@ -0,0 +1,128 @@ +{{- $messageBusSecrets := (lookup "v1" "Secret" .Values.namespace .Values.messageBus.secret.name ) -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.messageBusSecrets.secret.name }} + namespace: {{ .Values.namespace }} +type: kubernetes.io/basic-auth +data: + username: {{ .Values.pgadmin.username | b64enc }} +{{- if and $messageBusSecrets $messageBusSecrets.data }} + {{- if $messageBusSecrets.data.password }} + password: {{ $messageBusSecrets.data.password }} + {{- else }} + password: {{ "nonExistingKey1234" | b64enc }} + {{- end }} +{{- else }} + password: {{ "nonExistingKey1234" | b64enc }} +{{- end }} +--- +kind: PersistentVolume +apiVersion: v1 +metadata: + name: rabbitmq-pv-volume + namespace: {{ .Values.namespace }} + labels: + type: local + app: {{ .Values.messageBus.app }} +spec: + storageClassName: manual + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteMany + hostPath: + path: "/MantaRayPlan/RabbitMQ" +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: rabbitmq-pv-claim + namespace: {{ .Values.namespace }} + labels: + app: {{ .Values.messageBus.app }} +spec: + storageClassName: manual + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.messageBus.deployment }} + namespace: {{ .Values.namespace }} + labels: + app: {{ .Values.messageBus.app }} + name: {{ .Values.messageBus.name }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Values.messageBus.app }} + task: {{ .Values.messageBus.name }} + template: + metadata: + labels: + app: {{ .Values.messageBus.app }} + task: {{ .Values.messageBus.name }} + spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false + containers: + - name: {{ .Values.messageBus.app }} + image: "{{ .Values.messageBus.image }}:{{ .Values.messageBus.tag }}" + imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" + env: + - name: RABBITMQ_DEFAULT_USER + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: username + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: password + ports: + - containerPort: 5672 + - containerPort: 15672 + volumeMounts: + - mountPath: "/var/lib/rabbitmq" + name: rabbitmqvolume + volumes: + - name: rabbitmqvolume + persistentVolumeClaim: + claimName: rabbitmq-pv-claim +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.messageBus.loadBalancer }} + namespace: {{ .Values.namespace }} +spec: + selector: + app: {{ .Values.messageBus.app }} + task: {{ .Values.messageBus.name }} + ports: + - protocol: TCP + name: http + port: 5672 + targetPort: 5672 + - protocol: TCP + name: http + port: 15672 + targetPort: 15672 + type: LoadBalancer \ No newline at end of file diff --git a/deployment/MantaRayPlanCloud/templates/pg-admin.yaml b/deployment/MantaRayPlanCloud/templates/pg-admin.yaml index aec9746..786bfa4 100644 --- a/deployment/MantaRayPlanCloud/templates/pg-admin.yaml +++ b/deployment/MantaRayPlanCloud/templates/pg-admin.yaml @@ -70,10 +70,21 @@ spec: app: {{ .Values.pgadmin.app }} task: {{ .Values.pgadmin.name }} spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false containers: - name: {{ .Values.pgadmin.app }} image: "dpage/pgadmin4:latest" imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" securityContext: runAsUser: 0 runAsGroup: 0 diff --git a/deployment/MantaRayPlanCloud/templates/seq.yaml b/deployment/MantaRayPlanCloud/templates/seq.yaml index af40bd5..c635936 100644 --- a/deployment/MantaRayPlanCloud/templates/seq.yaml +++ b/deployment/MantaRayPlanCloud/templates/seq.yaml @@ -87,10 +87,21 @@ spec: app: {{ .Values.logging.seq.app }} task: {{ .Values.logging.seq.name }} spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false containers: - name: {{ .Values.logging.seq.app }} image: "{{ .Values.logging.seq.image }}:{{ .Values.logging.seq.tag }}" imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" env: - name: ACCEPT_EULA value: "Y" diff --git a/deployment/MantaRayPlanCloud/templates/service-account.yaml b/deployment/MantaRayPlanCloud/templates/service-account.yaml new file mode 100644 index 0000000..bfd4a19 --- /dev/null +++ b/deployment/MantaRayPlanCloud/templates/service-account.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.serveraccount.name }} + namespace: {{ .Values.namespace }} +automountServiceAccountToken: false \ No newline at end of file diff --git a/deployment/MantaRayPlanCloud/templates/viewer-bff.yaml b/deployment/MantaRayPlanCloud/templates/viewer-bff.yaml index c0342f5..d5faf82 100644 --- a/deployment/MantaRayPlanCloud/templates/viewer-bff.yaml +++ b/deployment/MantaRayPlanCloud/templates/viewer-bff.yaml @@ -18,10 +18,21 @@ spec: app: {{ .Values.viewer.bff.app }} task: {{ .Values.viewer.bff.name }} spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false containers: - name: {{ .Values.viewer.bff.app }} image: "{{ .Values.viewer.bff.image }}:{{ .Values.image.tag }}" imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" livenessProbe: httpGet: path: /healthz @@ -31,13 +42,27 @@ spec: timeoutSeconds: 1 failureThreshold: 3 env: + - name: Api__Address + value: "{{ .Values.api.grpc.loadBalancer }}:8080" - name: OpenTelemetry__LoggerEndpoint value: "{{ .Values.logging.seq.host.internal }}" - name: OpenTelemetry__LoggerApiKey valueFrom: secretKeyRef: name: {{ .Values.logging.seq.secrets.name }} - key: apiKeyViewer + key: apiKeyViewer + - name: MessageBus__Host + value: {{ .Values.messageBus.loadBalancer }} + - name: MessageBus__Username + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: username + - name: MessageBus__Password + valueFrom: + secretKeyRef: + name: {{ .Values.messageBus.secret.name }} + key: password ports: - containerPort: 8080 --- diff --git a/deployment/MantaRayPlanCloud/templates/viewer-web.yaml b/deployment/MantaRayPlanCloud/templates/viewer-web.yaml index 84c07ad..2d6f723 100644 --- a/deployment/MantaRayPlanCloud/templates/viewer-web.yaml +++ b/deployment/MantaRayPlanCloud/templates/viewer-web.yaml @@ -6,25 +6,33 @@ metadata: data: default.conf: | server { - listen 80; + listen 80; listen [::]:80; server_name localhost; location / { - root /usr/share/nginx/html; - index index.html index.htm; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html =404; } add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "no-referrer"; add_header X-Content-Type-Options "nosniff"; add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()"; - add_header content-security-policy "default-src 'self' https://{{ .Values.viewer.bff.host }}; img-src 'self' data: ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://{{ .Values.viewer.bff.host }}; upgrade-insecure-requests; frame-ancestors 'self'"; + add_header content-security-policy "default-src 'self' https://{{ .Values.viewer.bff.host }}; img-src 'self' data: ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://{{ .Values.viewer.bff.host }}; upgrade-insecure-requests; frame-ancestors 'self'"; + } +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ .Values.admin.web.app }}-appsetting-config" + namespace: {{ .Values.namespace }} +data: + default.conf: | + { + "Api": { + "Address": "https://{{ .Values.viewer.bff.host }}/" + } } --- apiVersion: apps/v1 @@ -47,10 +55,21 @@ spec: app: {{ .Values.viewer.web.app }} task: {{ .Values.viewer.web.name }} spec: + serviceAccountName: {{ .Values.serveraccount.name }} + automountServiceAccountToken: false containers: - name: {{ .Values.viewer.web.app }} image: "{{ .Values.viewer.web.image }}:{{ .Values.image.tag }}" imagePullPolicy: Always + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 0.5 + memory: "500Mi" + limits: + ephemeral-storage: "2Gi" + cpu: 1 + memory: "1Gi" livenessProbe: httpGet: path: /healthz.txt @@ -64,12 +83,19 @@ spec: mountPath: /etc/nginx/conf.d/default.conf subPath: default.conf readOnly: true + Y- name: "{{ .Values.admin.web.app }}-appsettings-config-volume" + mountPath: /usr/share/nginx/html/appsettings.json + subPath: appsettings.json + readOnly: true ports: - containerPort: 80 volumes: - name: "{{ .Values.viewer.web.app }}-nginx-config-volume" configMap: name: "{{ .Values.viewer.web.app }}-nginx-config" + - name: "{{ .Values.admin.web.app }}-appsettings-config-volume" + configMap: + name: "{{ .Values.admin.web.app }}-appsettings-config" --- apiVersion: v1 kind: Service diff --git a/deployment/MantaRayPlanCloud/values.yaml b/deployment/MantaRayPlanCloud/values.yaml index 846147b..9d28ceb 100644 --- a/deployment/MantaRayPlanCloud/values.yaml +++ b/deployment/MantaRayPlanCloud/values.yaml @@ -17,6 +17,9 @@ domain: ingress: name: "ingress" +serveraccount: + name: manta-plan-account + api: grpc: deployment: "api-grpc-deployment" @@ -101,3 +104,14 @@ pgadmin: host: "database.mantarayplan" secret: name: pgadmin-secret + +messageBus: + app: rabbitmq + deployment: rabbitmq-deployment + loadBalancer: "rabbitmq-loadbalancer" + image: rabbitmq + tag: 3-management + name: rabbitmq + secret: + name: rabbitmq-secret + diff --git a/src/ClientSdk.Admin.Bff/ClientSdk.Admin.Bff.csproj b/src/ClientSdk.Admin.Bff/ClientSdk.Admin.Bff.csproj new file mode 100644 index 0000000..9da90f1 --- /dev/null +++ b/src/ClientSdk.Admin.Bff/ClientSdk.Admin.Bff.csproj @@ -0,0 +1,8 @@ + + + + net8.0 + MadWorldNL.MantaRayPlan.ClientSdk.Admin.Bff + + + diff --git a/src/ClientSdk.Admin.Bff/MessageBuses/GetStatusResponse.cs b/src/ClientSdk.Admin.Bff/MessageBuses/GetStatusResponse.cs new file mode 100644 index 0000000..16f3f4d --- /dev/null +++ b/src/ClientSdk.Admin.Bff/MessageBuses/GetStatusResponse.cs @@ -0,0 +1,3 @@ +namespace MadWorldNL.MantaRayPlan.MessageBuses; + +public record GetStatusResponse(string message, int counter); \ No newline at end of file diff --git a/src/ClientSdk.Admin.Bff/MessageBuses/PostStatusResponse.cs b/src/ClientSdk.Admin.Bff/MessageBuses/PostStatusResponse.cs new file mode 100644 index 0000000..6c56936 --- /dev/null +++ b/src/ClientSdk.Admin.Bff/MessageBuses/PostStatusResponse.cs @@ -0,0 +1,3 @@ +namespace MadWorldNL.MantaRayPlan.MessageBuses; + +public record PostStatusResponse(string message); \ No newline at end of file diff --git a/src/ClientSdk.Api.Grpc/ClientSdk.Api.Grpc.csproj b/src/ClientSdk.Api.Grpc/ClientSdk.Api.Grpc.csproj new file mode 100644 index 0000000..849f536 --- /dev/null +++ b/src/ClientSdk.Api.Grpc/ClientSdk.Api.Grpc.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + MadWorldNL.MantaRayPlan.ClientSdk.Api.Grpc + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/ClientSdk.Api.Grpc/Protos/events.proto b/src/ClientSdk.Api.Grpc/Protos/events.proto new file mode 100644 index 0000000..bdd9d2c --- /dev/null +++ b/src/ClientSdk.Api.Grpc/Protos/events.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option csharp_namespace = "MadWorldNL.MantaRayPlan.Api"; + +package events; + +import "google/protobuf/empty.proto"; + +service EventService { + rpc Subscribe (google.protobuf.Empty) returns (stream newEvent); +} + +message newEvent { + string type = 1; + string json = 2; +} \ No newline at end of file diff --git a/src/ClientSdk.Api.Grpc/Protos/messagebus.proto b/src/ClientSdk.Api.Grpc/Protos/messagebus.proto new file mode 100644 index 0000000..630a9d8 --- /dev/null +++ b/src/ClientSdk.Api.Grpc/Protos/messagebus.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +option csharp_namespace = "MadWorldNL.MantaRayPlan.Api"; + +package messagebus; + +import "google/protobuf/empty.proto"; + +service MessageBusService { + rpc GetStatus (google.protobuf.Empty) returns (GetMessageBusStatusReply); + rpc PostStatus (google.protobuf.Empty) returns (PostMessageBusStatusReply); +} + +message GetMessageBusStatusReply { + string message = 1; + int32 counter = 2; +} + +message PostMessageBusStatusReply { + string message = 1; +} \ No newline at end of file diff --git a/src/ClientSdk.Api.MessageBus/ClientSdk.Api.MessageBus.csproj b/src/ClientSdk.Api.MessageBus/ClientSdk.Api.MessageBus.csproj new file mode 100644 index 0000000..9e12404 --- /dev/null +++ b/src/ClientSdk.Api.MessageBus/ClientSdk.Api.MessageBus.csproj @@ -0,0 +1,8 @@ + + + + net8.0 + MadWorldNL.MantaRayPlan.ClientSdk.Api.MessageBus + + + diff --git a/src/ClientSdk.Api.MessageBus/Events/IEvent.cs b/src/ClientSdk.Api.MessageBus/Events/IEvent.cs new file mode 100644 index 0000000..d093abc --- /dev/null +++ b/src/ClientSdk.Api.MessageBus/Events/IEvent.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.MantaRayPlan.Events; + +public interface IEvent +{ + +} \ No newline at end of file diff --git a/src/ClientSdk.Api.MessageBus/Events/MessageBusStatusEvent.cs b/src/ClientSdk.Api.MessageBus/Events/MessageBusStatusEvent.cs new file mode 100644 index 0000000..5fb72bb --- /dev/null +++ b/src/ClientSdk.Api.MessageBus/Events/MessageBusStatusEvent.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.MantaRayPlan.Events; + +public class MessageBusStatusEvent() : IEvent +{ + public int Count { get; set; } +} \ No newline at end of file diff --git a/src/Clients.Admin.Bff.IntegrationTests/Admin.Bff.IntegrationTests.csproj b/src/Clients.Admin.Bff.IntegrationTests/Admin.Bff.IntegrationTests.csproj index d081186..9a0dcc1 100644 --- a/src/Clients.Admin.Bff.IntegrationTests/Admin.Bff.IntegrationTests.csproj +++ b/src/Clients.Admin.Bff.IntegrationTests/Admin.Bff.IntegrationTests.csproj @@ -13,6 +13,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/src/Clients.Admin.Bff.IntegrationTests/Base/AdminBffFactory.cs b/src/Clients.Admin.Bff.IntegrationTests/Base/AdminBffFactory.cs new file mode 100644 index 0000000..25caf1d --- /dev/null +++ b/src/Clients.Admin.Bff.IntegrationTests/Base/AdminBffFactory.cs @@ -0,0 +1,61 @@ +using MassTransit; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Testcontainers.RabbitMq; + +namespace MadWorldNL.MantaRayPlan.Base; + +public class AdminBffFactory : WebApplicationFactory, IAsyncLifetime +{ + private const string BusUser = "development"; + private const string BusPassword = "Password1234"; + + private RabbitMqContainer? _rabbitMqContainer; + + public async Task InitializeAsync() + { + _rabbitMqContainer = new RabbitMqBuilder() + .WithImage("rabbitmq:3.11") + .WithUsername(BusUser) + .WithPassword(BusPassword) + .Build(); + + await _rabbitMqContainer.StartAsync(); + } + + protected override IHost CreateHost(IHostBuilder builder) + { + var newSettings = new Dictionary + { + ["MessageBus:Host"] = _rabbitMqContainer!.Hostname, + ["MessageBus:Port"] = _rabbitMqContainer.GetMappedPublicPort(5672).ToString(), + ["MessageBus:Username"] = BusUser, + ["MessageBus:Password"] = BusPassword + }; + + builder.ConfigureHostConfiguration(config => + { + config.AddInMemoryCollection(newSettings!); + }); + + builder.ConfigureServices(services => + { + // For more info about testing message bus: + // https://masstransit.io/documentation/concepts/testing + services.AddMassTransitTestHarness(); + }); + + return base.CreateHost(builder); + } + + public new async Task DisposeAsync() + { + if (_rabbitMqContainer is not null) + { + await _rabbitMqContainer.DisposeAsync(); + } + + await base.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/Clients.Admin.Bff.IntegrationTests/Base/SharedTestCollection.cs b/src/Clients.Admin.Bff.IntegrationTests/Base/SharedTestCollection.cs new file mode 100644 index 0000000..52c5ab4 --- /dev/null +++ b/src/Clients.Admin.Bff.IntegrationTests/Base/SharedTestCollection.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.MantaRayPlan.Base; + +[CollectionDefinition(TestDefinitions.Default)] +public class SharedTestCollection : ICollectionFixture +{ +} \ No newline at end of file diff --git a/src/Clients.Admin.Bff.IntegrationTests/HealthCheckTests.cs b/src/Clients.Admin.Bff.IntegrationTests/Endpoints/HealthCheckTests.cs similarity index 68% rename from src/Clients.Admin.Bff.IntegrationTests/HealthCheckTests.cs rename to src/Clients.Admin.Bff.IntegrationTests/Endpoints/HealthCheckTests.cs index 97d63be..58b7232 100644 --- a/src/Clients.Admin.Bff.IntegrationTests/HealthCheckTests.cs +++ b/src/Clients.Admin.Bff.IntegrationTests/Endpoints/HealthCheckTests.cs @@ -1,11 +1,12 @@ - using System.Net; +using MadWorldNL.MantaRayPlan.Base; using Microsoft.AspNetCore.Mvc.Testing; using Shouldly; -namespace MadWorldNL.MantaRayPlan; +namespace MadWorldNL.MantaRayPlan.Endpoints; -public class HealthCheckTests(WebApplicationFactory factory) : IClassFixture> +[Collection(TestDefinitions.Default)] +public class HealthCheckTests(AdminBffFactory factory) { [Fact] public async Task Healthz_GivenEmptyRequest_ShouldBeHealthy() diff --git a/src/Clients.Admin.Bff/Admin.Bff.csproj b/src/Clients.Admin.Bff/Admin.Bff.csproj index 45f7915..e79e633 100644 --- a/src/Clients.Admin.Bff/Admin.Bff.csproj +++ b/src/Clients.Admin.Bff/Admin.Bff.csproj @@ -7,12 +7,16 @@ + + + + @@ -27,6 +31,8 @@ + + diff --git a/src/Clients.Admin.Bff/Clients.Admin.Bff.http b/src/Clients.Admin.Bff/Clients.Admin.Bff.http deleted file mode 100644 index 1f9e60f..0000000 --- a/src/Clients.Admin.Bff/Clients.Admin.Bff.http +++ /dev/null @@ -1,6 +0,0 @@ -@Clients.Admin.Bff_HostAddress = http://localhost:5173 - -GET {{Clients.Admin.Bff_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/src/Clients.Admin.Bff/Configurations/ApiSettings.cs b/src/Clients.Admin.Bff/Configurations/ApiSettings.cs new file mode 100644 index 0000000..c5044e2 --- /dev/null +++ b/src/Clients.Admin.Bff/Configurations/ApiSettings.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace MadWorldNL.MantaRayPlan.Configurations; + +public class ApiSettings +{ + public const string Key = "Api"; + + [Required] + public string Address { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/Clients.Admin.Bff/Endpoints/MessageBusEndpoints.cs b/src/Clients.Admin.Bff/Endpoints/MessageBusEndpoints.cs new file mode 100644 index 0000000..555db35 --- /dev/null +++ b/src/Clients.Admin.Bff/Endpoints/MessageBusEndpoints.cs @@ -0,0 +1,26 @@ +using Google.Protobuf.WellKnownTypes; +using MadWorldNL.MantaRayPlan.Api; +using MadWorldNL.MantaRayPlan.MessageBuses; +using Microsoft.AspNetCore.Mvc; + +namespace MadWorldNL.MantaRayPlan.Endpoints; + +public static class MessageBusEndpoints +{ + public static void AddMessageBusEndpoints(this RouteGroupBuilder endpoints) + { + var messageBusEndpoints = endpoints.MapGroup("/MessageBus"); + + messageBusEndpoints.MapGet("/Status", ([FromServices] MessageBusService.MessageBusServiceClient client) => + { + var status = client.GetStatus(new Empty()); + return new GetStatusResponse(status.Message, status.Counter); + }); + + messageBusEndpoints.MapPost("/Status", ([FromServices] MessageBusService.MessageBusServiceClient client) => + { + var response = client.PostStatus(new Empty()); + return new PostStatusResponse(response.Message); + }); + } +} \ No newline at end of file diff --git a/src/Clients.Admin.Bff/Hubs/EventHandlerService.cs b/src/Clients.Admin.Bff/Hubs/EventHandlerService.cs new file mode 100644 index 0000000..9a68fb5 --- /dev/null +++ b/src/Clients.Admin.Bff/Hubs/EventHandlerService.cs @@ -0,0 +1,28 @@ +using MadWorldNL.MantaRayPlan.Events; +using MadWorldNL.MantaRayPlan.MassTransit; +using Microsoft.AspNetCore.SignalR; + +namespace MadWorldNL.MantaRayPlan.Hubs; + +public sealed class EventHandlerService : IDisposable +{ + private readonly IHubContext _context; + + public EventHandlerService(IHubContext context) + { + _context = context; + + EventPublisher.OnMessageReceived += SendEventToClient; + } + + private void SendEventToClient(IEvent newEvent) + { + _context.Clients.All.SendAsync("NewEvent", newEvent); + } + + public void Dispose() + { + EventPublisher.OnMessageReceived -= SendEventToClient; + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/Clients.Admin.Bff/Hubs/EventsHub.cs b/src/Clients.Admin.Bff/Hubs/EventsHub.cs new file mode 100644 index 0000000..5da2226 --- /dev/null +++ b/src/Clients.Admin.Bff/Hubs/EventsHub.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.SignalR; +using SignalRSwaggerGen.Attributes; + +namespace MadWorldNL.MantaRayPlan.Hubs; + +[SignalRHub] +// The 'EventHandlerService' needs to be injected by DI but does not need to be used directly. +#pragma warning disable CS9113 // Parameter is unread. +public class EventsHub(EventHandlerService _) : Hub +#pragma warning restore CS9113 // Parameter is unread. +{ +} \ No newline at end of file diff --git a/src/Clients.Admin.Bff/Program.cs b/src/Clients.Admin.Bff/Program.cs index 3c3d8ad..1393321 100644 --- a/src/Clients.Admin.Bff/Program.cs +++ b/src/Clients.Admin.Bff/Program.cs @@ -1,24 +1,83 @@ using System.Threading.RateLimiting; -using MadWorldNL.MantaRayPlan; +using MadWorldNL.MantaRayPlan.Api; using MadWorldNL.MantaRayPlan.Configurations; +using MadWorldNL.MantaRayPlan.Endpoints; +using MadWorldNL.MantaRayPlan.Events; +using MadWorldNL.MantaRayPlan.Hubs; +using MadWorldNL.MantaRayPlan.MassTransit; using MadWorldNL.MantaRayPlan.OpenTelemetry; +using MassTransit; using Microsoft.AspNetCore.RateLimiting; -using OpenTelemetry; -using OpenTelemetry.Exporter; -using OpenTelemetry.Logs; + +const string corsName = "DefaultCors"; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddCors(options => +{ + options.AddPolicy(name: corsName, + policy => + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.AllowAnyOrigin(); + }); +}); + var openTelemetryConfig = builder.Configuration.GetSection(OpenTelemetryConfig.Key).Get() ?? new OpenTelemetryConfig(); builder.AddDefaultOpenTelemetry(openTelemetryConfig); +builder.Services.AddSignalR(); +builder.Services.AddSingleton(); + builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + options.AddSignalRSwaggerGen(); +}); builder.Services.AddHealthChecks(); +builder.Services.AddMassTransit(x => +{ + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer>(); + + x.UsingRabbitMq((context,cfg) => + { + var messageBusSettings = builder.Configuration.GetSection(MessageBusSettings.Key).Get()!; + + cfg.Host(messageBusSettings.Host, messageBusSettings.Port, "/", h => { + h.Username(messageBusSettings.Username); + h.Password(messageBusSettings.Password); + }); + + var appName = typeof(Program).Assembly.GetName().Name!; + cfg.ReceiveEndpoint(EventPusherConsumer.GetQueueName(appName, nameof(MessageBusStatusEvent)), + e => + { + e.ConfigureConsumer>(context); + }); + + cfg.ConfigureEndpoints(context); + }); +}); + +var apiSettingsSection = builder.Configuration.GetSection(ApiSettings.Key); +builder.Services.AddOptions() + .Bind(apiSettingsSection) + .ValidateDataAnnotations() + .ValidateOnStart(); +var apiSettings = apiSettingsSection.Get(); + +builder.Services.AddGrpcClient(options => +{ + options.Address = new Uri(apiSettings!.Address); +}); + builder.Services.AddRateLimiter(rl => rl .AddFixedWindowLimiter(policyName: RateLimiterConfig.DefaultName, options => { @@ -30,6 +89,7 @@ var app = builder.Build(); +app.UseCors(corsName); app.UseRateLimiter(); // Security Headers @@ -48,31 +108,16 @@ app.UseSwagger(); app.UseSwaggerUI(); -app.MapHealthChecks("/healthz"); +app.MapHub("/Events"); -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; +app.MapHealthChecks("/healthz"); -app.MapGet("/weatherforecast", (ILogger logger) => - { - logger.LogInformation("Retrieve weather forecast"); - - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; - }) - .WithName("GetWeatherForecast") +var defaultEndpoints = app.MapGroup("/") .WithOpenApi() .RequireRateLimiting(RateLimiterConfig.DefaultName); +defaultEndpoints.AddMessageBusEndpoints(); + await app.RunAsync(); public abstract partial class Program { } \ No newline at end of file diff --git a/src/Clients.Admin.Bff/WeatherForecast.cs b/src/Clients.Admin.Bff/WeatherForecast.cs deleted file mode 100644 index ef88bad..0000000 --- a/src/Clients.Admin.Bff/WeatherForecast.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MadWorldNL.MantaRayPlan; - -public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} \ No newline at end of file diff --git a/src/Clients.Admin.Bff/appsettings.Development.json b/src/Clients.Admin.Bff/appsettings.Development.json index 8cccbee..4829981 100644 --- a/src/Clients.Admin.Bff/appsettings.Development.json +++ b/src/Clients.Admin.Bff/appsettings.Development.json @@ -1,4 +1,7 @@ { + "Api": { + "Address": "https://localhost:7132" + }, "Logging": { "LogLevel": { "Default": "Information", @@ -9,5 +12,11 @@ "Application": "Admin.Bff", "LoggerEndpoint": "http://localhost:5341", "LoggerApiKey": "" + }, + "MessageBus": { + "Host": "localhost", + "Port": 5672, + "Username": "development", + "Password": "Secret1234" } } diff --git a/src/Clients.Admin.Bff/appsettings.json b/src/Clients.Admin.Bff/appsettings.json index 8873a5f..7d60c8d 100644 --- a/src/Clients.Admin.Bff/appsettings.json +++ b/src/Clients.Admin.Bff/appsettings.json @@ -1,5 +1,8 @@ { "AllowedHosts": "*", + "Api": { + "Address": "https://localhost:7132" + }, "Logging": { "LogLevel": { "Default": "Information", @@ -10,5 +13,11 @@ "Application": "Admin.Bff", "LoggerEndpoint": "http://localhost:5341", "LoggerApiKey": "" + }, + "MessageBus": { + "Host": "localhost", + "Port": 5672, + "Username": "development", + "Password": "Secret1234" } } diff --git a/src/Clients.Admin.Web/Admin.Web.csproj b/src/Clients.Admin.Web/Admin.Web.csproj index 31b6d38..6530570 100644 --- a/src/Clients.Admin.Web/Admin.Web.csproj +++ b/src/Clients.Admin.Web/Admin.Web.csproj @@ -10,6 +10,8 @@ + + @@ -20,6 +22,8 @@ + + diff --git a/src/Clients.Admin.Web/Configurations/ApiSettings.cs b/src/Clients.Admin.Web/Configurations/ApiSettings.cs new file mode 100644 index 0000000..1b9a5eb --- /dev/null +++ b/src/Clients.Admin.Web/Configurations/ApiSettings.cs @@ -0,0 +1,8 @@ +namespace MadWorldNL.MantaRayPlan.Web.Configurations; + +public class ApiSettings +{ + public const string Key = "Api"; + + public string Address { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Clients.Admin.Web/Configurations/ApiTypes.cs b/src/Clients.Admin.Web/Configurations/ApiTypes.cs new file mode 100644 index 0000000..770b053 --- /dev/null +++ b/src/Clients.Admin.Web/Configurations/ApiTypes.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.MantaRayPlan.Web.Configurations; + +public static class ApiTypes +{ + public const string AdminBff = nameof(AdminBff); +} \ No newline at end of file diff --git a/src/Clients.Admin.Web/Pages/MessageBuses/MessageBus.razor b/src/Clients.Admin.Web/Pages/MessageBuses/MessageBus.razor new file mode 100644 index 0000000..b16bbef --- /dev/null +++ b/src/Clients.Admin.Web/Pages/MessageBuses/MessageBus.razor @@ -0,0 +1,46 @@ +@using MadWorldNL.MantaRayPlan.Events +@using MadWorldNL.MantaRayPlan.Web.Services.Events +@using MadWorldNL.MantaRayPlan.Web.Services.MessageBuses +@page "/MessageBus" + +@inject IMessageBusService MessageBusService; +@inject EventsService EventsService + +

Message Bus Test Page

+@if (_isLoaded) +{ +

status counter: @_counter

+

+ +

+} + +@code { + private bool _isLoaded; + private int _counter; + + protected override async Task OnInitializedAsync() + { + var status = await MessageBusService.GetStatusAsync(); + _counter = status.counter; + _isLoaded = true; + + EventsService.EventReceived += NewEventReceived; + + await EventsService.StartAsync(); + await base.OnInitializedAsync(); + } + + private void NewEventReceived(MessageBusStatusEvent @event) + { + _counter = @event.Count; + StateHasChanged(); + } + + private async Task UpdateStatus() + { + await MessageBusService.PostNewStatusAsync(); + } +} \ No newline at end of file diff --git a/src/Clients.Admin.Web/Program.cs b/src/Clients.Admin.Web/Program.cs index 45f4973..5d3fad4 100644 --- a/src/Clients.Admin.Web/Program.cs +++ b/src/Clients.Admin.Web/Program.cs @@ -1,11 +1,27 @@ using MadWorldNL.MantaRayPlan.Web; +using MadWorldNL.MantaRayPlan.Web.Configurations; +using MadWorldNL.MantaRayPlan.Web.Services.Events; +using MadWorldNL.MantaRayPlan.Web.Services.MessageBuses; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.Options; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); +builder.Services + .AddOptions() + .Configure(builder.Configuration.GetSection(ApiSettings.Key).Bind); + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddHttpClient(ApiTypes.AdminBff, (serviceProvider, client) => + { + var apiUrlsOption = serviceProvider.GetService>()!; + client.BaseAddress = new Uri(apiUrlsOption.Value.Address); + }); + +builder.Services.AddScoped(); +builder.Services.AddSingleton(); await builder.Build().RunAsync(); \ No newline at end of file diff --git a/src/Clients.Admin.Web/Services/Events/EventsService.cs b/src/Clients.Admin.Web/Services/Events/EventsService.cs new file mode 100644 index 0000000..2a5d4da --- /dev/null +++ b/src/Clients.Admin.Web/Services/Events/EventsService.cs @@ -0,0 +1,45 @@ +using MadWorldNL.MantaRayPlan.Events; +using MadWorldNL.MantaRayPlan.Web.Configurations; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Options; + +namespace MadWorldNL.MantaRayPlan.Web.Services.Events; + +public class EventsService : IAsyncDisposable +{ + private bool _isStarted; + private readonly HubConnection _hubConnection; + + public event Action? EventReceived; + + public EventsService(IOptions settings) + { + _hubConnection = new HubConnectionBuilder() + .WithUrl($"{settings.Value.Address}Events") + .Build(); + + _hubConnection.On("NewEvent", (@event) => + { + EventReceived?.Invoke(@event); + }); + } + + public Task StartAsync() + { + if (_isStarted) + { + return Task.CompletedTask; + } + + _isStarted = true; + return _hubConnection.StartAsync(); + } + + public async ValueTask DisposeAsync() + { + await _hubConnection.StopAsync(); + await _hubConnection.DisposeAsync(); + + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/Clients.Admin.Web/Services/MessageBuses/IMessageBusService.cs b/src/Clients.Admin.Web/Services/MessageBuses/IMessageBusService.cs new file mode 100644 index 0000000..17778fc --- /dev/null +++ b/src/Clients.Admin.Web/Services/MessageBuses/IMessageBusService.cs @@ -0,0 +1,9 @@ +using MadWorldNL.MantaRayPlan.MessageBuses; + +namespace MadWorldNL.MantaRayPlan.Web.Services.MessageBuses; + +public interface IMessageBusService +{ + Task GetStatusAsync(); + Task PostNewStatusAsync(); +} \ No newline at end of file diff --git a/src/Clients.Admin.Web/Services/MessageBuses/MessageBusService.cs b/src/Clients.Admin.Web/Services/MessageBuses/MessageBusService.cs new file mode 100644 index 0000000..02b5563 --- /dev/null +++ b/src/Clients.Admin.Web/Services/MessageBuses/MessageBusService.cs @@ -0,0 +1,31 @@ +using System.Net.Http.Json; +using MadWorldNL.MantaRayPlan.MessageBuses; +using MadWorldNL.MantaRayPlan.Web.Configurations; + +namespace MadWorldNL.MantaRayPlan.Web.Services.MessageBuses; + +public class MessageBusService(IHttpClientFactory clientFactory) : IMessageBusService +{ + private const string EndpointSingular = "MessageBus"; + + private readonly HttpClient _client = clientFactory.CreateClient(ApiTypes.AdminBff); + + public async Task GetStatusAsync() + { + const int emptyCounter = -1; + + return await _client.GetFromJsonAsync($"{EndpointSingular}/Status") ?? new GetStatusResponse("No response", emptyCounter); + } + + public async Task PostNewStatusAsync() + { + var response = await _client.PostAsJsonAsync($"{EndpointSingular}/Status", "{}"); + + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync() ?? new PostStatusResponse("No Response"); + } + + return new PostStatusResponse("IsFailedStatusCode"); + } +} \ No newline at end of file diff --git a/src/Clients.Admin.Web/wwwroot/appsettings.json b/src/Clients.Admin.Web/wwwroot/appsettings.json new file mode 100644 index 0000000..23d6c3c --- /dev/null +++ b/src/Clients.Admin.Web/wwwroot/appsettings.json @@ -0,0 +1,5 @@ +{ + "Api": { + "Address": "https://localhost:7284/" + } +} \ No newline at end of file diff --git a/src/Clients.Viewer.Bff.IntegrationTests/Base/SharedTestCollection.cs b/src/Clients.Viewer.Bff.IntegrationTests/Base/SharedTestCollection.cs new file mode 100644 index 0000000..bd6e772 --- /dev/null +++ b/src/Clients.Viewer.Bff.IntegrationTests/Base/SharedTestCollection.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.MantaRayPlan.Base; + +[CollectionDefinition(TestDefinitions.Default)] +public class SharedTestCollection : ICollectionFixture +{ +} \ No newline at end of file diff --git a/src/Clients.Viewer.Bff.IntegrationTests/Base/ViewerBffFactory.cs b/src/Clients.Viewer.Bff.IntegrationTests/Base/ViewerBffFactory.cs new file mode 100644 index 0000000..05bf91e --- /dev/null +++ b/src/Clients.Viewer.Bff.IntegrationTests/Base/ViewerBffFactory.cs @@ -0,0 +1,61 @@ +using MassTransit; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Testcontainers.RabbitMq; + +namespace MadWorldNL.MantaRayPlan.Base; + +public class ViewerBffFactory : WebApplicationFactory, IAsyncLifetime +{ + private const string BusUser = "development"; + private const string BusPassword = "Password1234"; + + private RabbitMqContainer? _rabbitMqContainer; + + public async Task InitializeAsync() + { + _rabbitMqContainer = new RabbitMqBuilder() + .WithImage("rabbitmq:3.11") + .WithUsername(BusUser) + .WithPassword(BusPassword) + .Build(); + + await _rabbitMqContainer.StartAsync(); + } + + protected override IHost CreateHost(IHostBuilder builder) + { + var newSettings = new Dictionary + { + ["MessageBus:Host"] = _rabbitMqContainer!.Hostname, + ["MessageBus:Port"] = _rabbitMqContainer.GetMappedPublicPort(5672).ToString(), + ["MessageBus:Username"] = BusUser, + ["MessageBus:Password"] = BusPassword + }; + + builder.ConfigureHostConfiguration(config => + { + config.AddInMemoryCollection(newSettings!); + }); + + builder.ConfigureServices(services => + { + // For more info about testing message bus: + // https://masstransit.io/documentation/concepts/testing + //services.AddMassTransitTestHarness(); + }); + + return base.CreateHost(builder); + } + + public new async Task DisposeAsync() + { + if (_rabbitMqContainer is not null) + { + await _rabbitMqContainer.DisposeAsync(); + } + + await base.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/Clients.Viewer.Bff.IntegrationTests/Clients.Viewer.Bff.IntegrationTests.csproj b/src/Clients.Viewer.Bff.IntegrationTests/Clients.Viewer.Bff.IntegrationTests.csproj index 4d7dbde..1c18fb9 100644 --- a/src/Clients.Viewer.Bff.IntegrationTests/Clients.Viewer.Bff.IntegrationTests.csproj +++ b/src/Clients.Viewer.Bff.IntegrationTests/Clients.Viewer.Bff.IntegrationTests.csproj @@ -13,6 +13,7 @@ +
@@ -23,6 +24,7 @@ + diff --git a/src/Server.Controllers.Api.Grpc.IntegrationTests/HealthCheckTests.cs b/src/Clients.Viewer.Bff.IntegrationTests/Endpoints/HealthCheckTests.cs similarity index 68% rename from src/Server.Controllers.Api.Grpc.IntegrationTests/HealthCheckTests.cs rename to src/Clients.Viewer.Bff.IntegrationTests/Endpoints/HealthCheckTests.cs index 97d63be..467075a 100644 --- a/src/Server.Controllers.Api.Grpc.IntegrationTests/HealthCheckTests.cs +++ b/src/Clients.Viewer.Bff.IntegrationTests/Endpoints/HealthCheckTests.cs @@ -1,11 +1,12 @@ - using System.Net; +using MadWorldNL.MantaRayPlan.Base; using Microsoft.AspNetCore.Mvc.Testing; using Shouldly; -namespace MadWorldNL.MantaRayPlan; +namespace MadWorldNL.MantaRayPlan.Endpoints; -public class HealthCheckTests(WebApplicationFactory factory) : IClassFixture> +[Collection(TestDefinitions.Default)] +public class HealthCheckTests(ViewerBffFactory factory) { [Fact] public async Task Healthz_GivenEmptyRequest_ShouldBeHealthy() diff --git a/src/Clients.Viewer.Bff/Program.cs b/src/Clients.Viewer.Bff/Program.cs index a740158..e90b93a 100644 --- a/src/Clients.Viewer.Bff/Program.cs +++ b/src/Clients.Viewer.Bff/Program.cs @@ -4,8 +4,22 @@ using MadWorldNL.MantaRayPlan.OpenTelemetry; using Microsoft.AspNetCore.RateLimiting; +const string corsName = "DefaultCors"; + var builder = WebApplication.CreateBuilder(args); +builder.Services.AddCors(options => +{ + options.AddPolicy(name: corsName, + policy => + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.AllowAnyOrigin(); + }); +}); + + var openTelemetryConfig = builder.Configuration.GetSection(OpenTelemetryConfig.Key).Get() ?? new OpenTelemetryConfig(); @@ -27,6 +41,7 @@ var app = builder.Build(); +app.UseCors(corsName); app.UseRateLimiter(); // Security Headers diff --git a/src/Clients.Viewer.Mobile/Viewer.Mobile.csproj b/src/Clients.Viewer.Mobile/Viewer.Mobile.csproj index d2bb91f..b5e519a 100644 --- a/src/Clients.Viewer.Mobile/Viewer.Mobile.csproj +++ b/src/Clients.Viewer.Mobile/Viewer.Mobile.csproj @@ -2,7 +2,8 @@ False - net8.0-android;net8.0-ios; + net8.0-android + net8.0-ios diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 2c6f02e..b3d2b2e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,26 +1,35 @@ - + + + + + + + + + - + - - - - - + + + + + + @@ -29,8 +38,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MadWorldNL.AspNetCore/MadWorldNL.AspNetCore.csproj b/src/MadWorldNL.AspNetCore/MadWorldNL.AspNetCore.csproj index eda91ff..17dcc99 100644 --- a/src/MadWorldNL.AspNetCore/MadWorldNL.AspNetCore.csproj +++ b/src/MadWorldNL.AspNetCore/MadWorldNL.AspNetCore.csproj @@ -2,11 +2,11 @@ net8.0 - enable - enable + MadWorldNL.MantaRayPlan.AspNetCore + @@ -15,4 +15,8 @@ + + + + diff --git a/src/MadWorldNL.AspNetCore/MassTransit/EventPublisher.cs b/src/MadWorldNL.AspNetCore/MassTransit/EventPublisher.cs new file mode 100644 index 0000000..7532dbb --- /dev/null +++ b/src/MadWorldNL.AspNetCore/MassTransit/EventPublisher.cs @@ -0,0 +1,8 @@ +using MadWorldNL.MantaRayPlan.Events; + +namespace MadWorldNL.MantaRayPlan.MassTransit; + +public static class EventPublisher +{ + public static Action? OnMessageReceived { get; set; } = _ => { }; +} \ No newline at end of file diff --git a/src/MadWorldNL.AspNetCore/MassTransit/EventPusherConsumer.cs b/src/MadWorldNL.AspNetCore/MassTransit/EventPusherConsumer.cs new file mode 100644 index 0000000..f5594f7 --- /dev/null +++ b/src/MadWorldNL.AspNetCore/MassTransit/EventPusherConsumer.cs @@ -0,0 +1,21 @@ +using MadWorldNL.MantaRayPlan.Events; +using MassTransit; + +namespace MadWorldNL.MantaRayPlan.MassTransit; + +public class EventPusherConsumer : IConsumer where TMessage : class, IEvent +{ + public Task Consume(ConsumeContext context) + { + if (EventPublisher.OnMessageReceived is null) + { + return Task.CompletedTask; + } + + EventPublisher.OnMessageReceived(context.Message); + + return Task.CompletedTask; + } + + public static string GetQueueName(string backend, string messageType) => $"{backend}_{nameof(EventPusherConsumer)}_{messageType}"; +} \ No newline at end of file diff --git a/src/MadWorldNL.AspNetCore/MassTransit/MessageBusSettings.cs b/src/MadWorldNL.AspNetCore/MassTransit/MessageBusSettings.cs new file mode 100644 index 0000000..b0b3d91 --- /dev/null +++ b/src/MadWorldNL.AspNetCore/MassTransit/MessageBusSettings.cs @@ -0,0 +1,11 @@ +namespace MadWorldNL.MantaRayPlan.MassTransit; + +public class MessageBusSettings +{ + public const string Key = "MessageBus"; + + public string Host { get; init; } = string.Empty; + public ushort Port { get; init; } + public string Username { get; init; } = string.Empty; + public string Password { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/MadWorldNL.IntegrationTests/MadWorldNL.IntegrationTests.csproj b/src/MadWorldNL.IntegrationTests/MadWorldNL.IntegrationTests.csproj new file mode 100644 index 0000000..71283eb --- /dev/null +++ b/src/MadWorldNL.IntegrationTests/MadWorldNL.IntegrationTests.csproj @@ -0,0 +1,5 @@ + + + net8.0 + + diff --git a/src/MadWorldNL.IntegrationTests/TestDefinitions.cs b/src/MadWorldNL.IntegrationTests/TestDefinitions.cs new file mode 100644 index 0000000..3601892 --- /dev/null +++ b/src/MadWorldNL.IntegrationTests/TestDefinitions.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.MantaRayPlan; + +public static class TestDefinitions +{ + public const string Default = nameof(Default); +} \ No newline at end of file diff --git a/src/MadWorldNL.MantaRayPlan.sln b/src/MadWorldNL.MantaRayPlan.sln index 4be719d..e4c6f70 100644 --- a/src/MadWorldNL.MantaRayPlan.sln +++ b/src/MadWorldNL.MantaRayPlan.sln @@ -72,7 +72,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "code", "code", "{941D883C-3 ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props - docker-compose.env = docker-compose.env docker-compose.yml = docker-compose.yml Nuget.Config = Nuget.Config EndProjectSection @@ -113,6 +112,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Functions", "Server.Logic.F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.MessageBus.IntegrationTests", "Server.Controllers.Api.MessageBus.IntegrationTests\Api.MessageBus.IntegrationTests.csproj", "{78883E90-1F9F-4FA4-A1BC-573AF4AF432B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Queries", "Server.Logic.Queries\Queries.csproj", "{4ED4374B-83B4-43C5-8E48-F9C9179AF077}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClientSdk.Api.Grpc", "ClientSdk.Api.Grpc\ClientSdk.Api.Grpc.csproj", "{6F2083BE-51EF-48D2-AA85-8C6A099C3E85}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClientSdk.Admin.Bff", "ClientSdk.Admin.Bff\ClientSdk.Admin.Bff.csproj", "{B5FA9C6D-5F8A-4334-985F-4909833996AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClientSdk.Api.MessageBus", "ClientSdk.Api.MessageBus\ClientSdk.Api.MessageBus.csproj", "{F76D206C-3F80-4099-B4ED-3DAE193303FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MadWorldNL.IntegrationTests", "MadWorldNL.IntegrationTests\MadWorldNL.IntegrationTests.csproj", "{8809E42F-C1B5-4BB0-8C9E-3B7964A4F7E1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -154,6 +163,11 @@ Global {1803C1EF-6193-4CE5-A37A-16F920F2388D} = {A7B2EEF8-045E-4473-9DE2-6452F5E14CE3} {3952C3C4-FB97-4441-9608-A5B7C3BC4E53} = {A7B2EEF8-045E-4473-9DE2-6452F5E14CE3} {78883E90-1F9F-4FA4-A1BC-573AF4AF432B} = {FCC26FAC-D27A-4E9C-97AB-225E3131FA52} + {4ED4374B-83B4-43C5-8E48-F9C9179AF077} = {A7B2EEF8-045E-4473-9DE2-6452F5E14CE3} + {6F2083BE-51EF-48D2-AA85-8C6A099C3E85} = {00C31373-91B2-410A-A8B7-F66F7CD51708} + {B5FA9C6D-5F8A-4334-985F-4909833996AE} = {AD870EB2-89A9-471C-A5CB-C95FA4D917BC} + {F76D206C-3F80-4099-B4ED-3DAE193303FC} = {00C31373-91B2-410A-A8B7-F66F7CD51708} + {8809E42F-C1B5-4BB0-8C9E-3B7964A4F7E1} = {0F97374D-45DB-4F78-905D-07A96CA2057F} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {C3AFAE1F-DDC9-4DB1-931D-1B934F9C9DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -224,5 +238,25 @@ Global {78883E90-1F9F-4FA4-A1BC-573AF4AF432B}.Debug|Any CPU.Build.0 = Debug|Any CPU {78883E90-1F9F-4FA4-A1BC-573AF4AF432B}.Release|Any CPU.ActiveCfg = Release|Any CPU {78883E90-1F9F-4FA4-A1BC-573AF4AF432B}.Release|Any CPU.Build.0 = Release|Any CPU + {4ED4374B-83B4-43C5-8E48-F9C9179AF077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4ED4374B-83B4-43C5-8E48-F9C9179AF077}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4ED4374B-83B4-43C5-8E48-F9C9179AF077}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4ED4374B-83B4-43C5-8E48-F9C9179AF077}.Release|Any CPU.Build.0 = Release|Any CPU + {6F2083BE-51EF-48D2-AA85-8C6A099C3E85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F2083BE-51EF-48D2-AA85-8C6A099C3E85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F2083BE-51EF-48D2-AA85-8C6A099C3E85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F2083BE-51EF-48D2-AA85-8C6A099C3E85}.Release|Any CPU.Build.0 = Release|Any CPU + {B5FA9C6D-5F8A-4334-985F-4909833996AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5FA9C6D-5F8A-4334-985F-4909833996AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5FA9C6D-5F8A-4334-985F-4909833996AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5FA9C6D-5F8A-4334-985F-4909833996AE}.Release|Any CPU.Build.0 = Release|Any CPU + {F76D206C-3F80-4099-B4ED-3DAE193303FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F76D206C-3F80-4099-B4ED-3DAE193303FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F76D206C-3F80-4099-B4ED-3DAE193303FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F76D206C-3F80-4099-B4ED-3DAE193303FC}.Release|Any CPU.Build.0 = Release|Any CPU + {8809E42F-C1B5-4BB0-8C9E-3B7964A4F7E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8809E42F-C1B5-4BB0-8C9E-3B7964A4F7E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8809E42F-C1B5-4BB0-8C9E-3B7964A4F7E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8809E42F-C1B5-4BB0-8C9E-3B7964A4F7E1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Server.Controllers.Api.Grpc.IntegrationTests/Api.Grpc.IntegrationTests.csproj b/src/Server.Controllers.Api.Grpc.IntegrationTests/Api.Grpc.IntegrationTests.csproj index ae24c1f..4e9c264 100644 --- a/src/Server.Controllers.Api.Grpc.IntegrationTests/Api.Grpc.IntegrationTests.csproj +++ b/src/Server.Controllers.Api.Grpc.IntegrationTests/Api.Grpc.IntegrationTests.csproj @@ -13,6 +13,8 @@ + +
@@ -22,6 +24,7 @@ + diff --git a/src/Server.Controllers.Api.Grpc.IntegrationTests/Base/GrpcFactory.cs b/src/Server.Controllers.Api.Grpc.IntegrationTests/Base/GrpcFactory.cs new file mode 100644 index 0000000..b2fa01f --- /dev/null +++ b/src/Server.Controllers.Api.Grpc.IntegrationTests/Base/GrpcFactory.cs @@ -0,0 +1,89 @@ +using MadWorldNL.MantaRayPlan.Extensions; +using MassTransit; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Testcontainers.PostgreSql; +using Testcontainers.RabbitMq; + +namespace MadWorldNL.MantaRayPlan.Base; + +public class GrpcFactory : WebApplicationFactory, IAsyncLifetime +{ + private const string DbName = "MantaRayPlan"; + private const string DbUser = "postgres"; + private const string DbPassword = "Password1234!"; + + private const string BusUser = "development"; + private const string BusPassword = "Password1234"; + + private PostgreSqlContainer? _postgreSqlContainer; + private RabbitMqContainer? _rabbitMqContainer; + + public async Task InitializeAsync() + { + _postgreSqlContainer = new PostgreSqlBuilder() + .WithImage("postgres:16") + .WithDatabase(DbName) + .WithUsername(DbUser) + .WithPassword(DbPassword) + .Build(); + + _rabbitMqContainer = new RabbitMqBuilder() + .WithImage("rabbitmq:3.11") + .WithUsername(BusUser) + .WithPassword(BusPassword) + .Build(); + + await _postgreSqlContainer.StartAsync(); + await _rabbitMqContainer.StartAsync(); + } + + protected override IHost CreateHost(IHostBuilder builder) + { + var newSettings = new Dictionary + { + ["Database:Host"] = _postgreSqlContainer!.Hostname, + ["Database:Port"] = _postgreSqlContainer.GetMappedPublicPort(5432).ToString(), + ["Database:DbName"] = DbName, + ["Database:User"] = DbUser, + ["Database:Password"] = DbPassword, + ["MessageBus:Host"] = _rabbitMqContainer!.Hostname, + ["MessageBus:Port"] = _rabbitMqContainer.GetMappedPublicPort(5672).ToString(), + ["MessageBus:Username"] = BusUser, + ["MessageBus:Password"] = BusPassword + }; + + builder.ConfigureHostConfiguration(config => + { + config.AddInMemoryCollection(newSettings!); + }); + + builder.ConfigureServices(services => + { + // For more info about testing message bus: + // https://masstransit.io/documentation/concepts/testing + services.AddMassTransitTestHarness(); + }); + + var host = base.CreateHost(builder); + host.Services.MigrateDatabase(); + return host; + } + + public new async Task DisposeAsync() + { + if (_postgreSqlContainer is not null) + { + await _postgreSqlContainer.DisposeAsync(); + } + + if (_rabbitMqContainer is not null) + { + await _rabbitMqContainer.DisposeAsync(); + } + + await base.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/Server.Controllers.Api.Grpc.IntegrationTests/Base/SharedTestCollection.cs b/src/Server.Controllers.Api.Grpc.IntegrationTests/Base/SharedTestCollection.cs new file mode 100644 index 0000000..6201bbc --- /dev/null +++ b/src/Server.Controllers.Api.Grpc.IntegrationTests/Base/SharedTestCollection.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.MantaRayPlan.Base; + +[CollectionDefinition(TestDefinitions.Default)] +public class SharedTestCollection : ICollectionFixture +{ +} \ No newline at end of file diff --git a/src/Server.Controllers.Api.MessageBus.IntegrationTests/HealthCheckTests.cs b/src/Server.Controllers.Api.Grpc.IntegrationTests/Endpoints/HealthCheckTests.cs similarity index 55% rename from src/Server.Controllers.Api.MessageBus.IntegrationTests/HealthCheckTests.cs rename to src/Server.Controllers.Api.Grpc.IntegrationTests/Endpoints/HealthCheckTests.cs index 97d63be..37594c0 100644 --- a/src/Server.Controllers.Api.MessageBus.IntegrationTests/HealthCheckTests.cs +++ b/src/Server.Controllers.Api.Grpc.IntegrationTests/Endpoints/HealthCheckTests.cs @@ -1,11 +1,12 @@ - using System.Net; +using MadWorldNL.MantaRayPlan.Base; using Microsoft.AspNetCore.Mvc.Testing; using Shouldly; -namespace MadWorldNL.MantaRayPlan; +namespace MadWorldNL.MantaRayPlan.Endpoints; -public class HealthCheckTests(WebApplicationFactory factory) : IClassFixture> +[Collection(TestDefinitions.Default)] +public class HealthCheckTests(GrpcFactory factory) : IAsyncLifetime { [Fact] public async Task Healthz_GivenEmptyRequest_ShouldBeHealthy() @@ -19,4 +20,8 @@ public async Task Healthz_GivenEmptyRequest_ShouldBeHealthy() // Assert response.StatusCode.ShouldBe(HttpStatusCode.OK); } + + public Task InitializeAsync() => Task.CompletedTask; + + public Task DisposeAsync() => factory.DisposeAsync(); } \ No newline at end of file diff --git a/src/Server.Controllers.Api.Grpc/Api.Grpc.csproj b/src/Server.Controllers.Api.Grpc/Api.Grpc.csproj index 046bf93..6727934 100644 --- a/src/Server.Controllers.Api.Grpc/Api.Grpc.csproj +++ b/src/Server.Controllers.Api.Grpc/Api.Grpc.csproj @@ -6,13 +6,11 @@ Linux - - - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -34,8 +32,11 @@ + + + diff --git a/src/Server.Controllers.Api.Grpc/Program.cs b/src/Server.Controllers.Api.Grpc/Program.cs index 5ba4c3e..0b51aad 100644 --- a/src/Server.Controllers.Api.Grpc/Program.cs +++ b/src/Server.Controllers.Api.Grpc/Program.cs @@ -1,9 +1,26 @@ +using MadWorldNL.MantaRayPlan.Events; using MadWorldNL.MantaRayPlan.Extensions; +using MadWorldNL.MantaRayPlan.MassTransit; +using MadWorldNL.MantaRayPlan.MessageBuses; using MadWorldNL.MantaRayPlan.OpenTelemetry; using MadWorldNL.MantaRayPlan.Services; +using MassTransit; + +const string corsName = "DefaultCors"; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddCors(options => +{ + options.AddPolicy(name: corsName, + policy => + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.AllowAnyOrigin(); + }); +}); + var openTelemetryConfig = builder.Configuration.GetSection(OpenTelemetryConfig.Key).Get() ?? new OpenTelemetryConfig(); @@ -16,8 +33,44 @@ builder.Services.AddHealthChecks(); +builder.Services.AddMassTransit(x => +{ + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer>(); + + x.AddConsumer() + .Endpoint(e => e.Name = GetName()); + + x.AddRequestClient(new Uri(GetExchangeName())); + + x.UsingRabbitMq((context,cfg) => + { + var messageBusSettings = builder.Configuration.GetSection(MessageBusSettings.Key) + .Get()!; + + cfg.Host(messageBusSettings.Host, messageBusSettings.Port, "/", h => { + h.Username(messageBusSettings.Username); + h.Password(messageBusSettings.Password); + }); + + var appName = typeof(Program).Assembly.GetName().Name!; + cfg.ReceiveEndpoint(EventPusherConsumer.GetQueueName(appName, nameof(MessageBusStatusEvent)), + e => + { + e.ConfigureConsumer>(context); + }); + + cfg.ConfigureEndpoints(context); + + EndpointConvention.Map(new Uri(GetExchangeName())); + }); +}); + var app = builder.Build(); +app.UseCors(corsName); +app.MapGrpcService(); app.MapGrpcService(); if (app.Environment.IsDevelopment()) @@ -33,4 +86,8 @@ await app.RunAsync(); -public abstract partial class Program { } \ No newline at end of file +public abstract partial class Program +{ + private static string GetExchangeName() => $"exchange:{GetName()}"; + private static string GetName() => $"{typeof(T).Namespace}:{typeof(T).Name}"; +} \ No newline at end of file diff --git a/src/Server.Controllers.Api.Grpc/Protos/messagebus.proto b/src/Server.Controllers.Api.Grpc/Protos/messagebus.proto deleted file mode 100644 index 4b72ca3..0000000 --- a/src/Server.Controllers.Api.Grpc/Protos/messagebus.proto +++ /dev/null @@ -1,16 +0,0 @@ -syntax = "proto3"; - -option csharp_namespace = "MantaRayPlan"; - -package messagebus; - -import "google/protobuf/empty.proto"; - -service MessageBusService { - rpc GetStatus (google.protobuf.Empty) returns (MessageBusStatusReply); -} - -message MessageBusStatusReply { - string message = 1; - int32 counter = 2; -} \ No newline at end of file diff --git a/src/Server.Controllers.Api.Grpc/Services/EventServiceProxy.cs b/src/Server.Controllers.Api.Grpc/Services/EventServiceProxy.cs new file mode 100644 index 0000000..ef39088 --- /dev/null +++ b/src/Server.Controllers.Api.Grpc/Services/EventServiceProxy.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using MadWorldNL.MantaRayPlan.Api; +using MadWorldNL.MantaRayPlan.Events; +using MadWorldNL.MantaRayPlan.MassTransit; + +namespace MadWorldNL.MantaRayPlan.Services; + +public class EventServiceProxy : EventService.EventServiceBase +{ + public override Task Subscribe(Empty request, IServerStreamWriter responseStream, ServerCallContext context) + { + EventPublisher.OnMessageReceived += SendEventToClient; + + while (!context.CancellationToken.IsCancellationRequested) + { + Thread.Sleep(1000); + } + + EventPublisher.OnMessageReceived -= SendEventToClient; + + return Task.CompletedTask; + + void SendEventToClient(IEvent newEvent) + { + var newEventResponse = new newEvent() + { + Type = newEvent.GetType().Name, + Json = JsonSerializer.Serialize(newEvent, newEvent.GetType()) + }; + + responseStream.WriteAsync(newEventResponse); + } + } +} \ No newline at end of file diff --git a/src/Server.Controllers.Api.Grpc/Services/MessageBusServiceProxy.cs b/src/Server.Controllers.Api.Grpc/Services/MessageBusServiceProxy.cs index 61f1d90..c15e240 100644 --- a/src/Server.Controllers.Api.Grpc/Services/MessageBusServiceProxy.cs +++ b/src/Server.Controllers.Api.Grpc/Services/MessageBusServiceProxy.cs @@ -1,34 +1,55 @@ using Google.Protobuf.WellKnownTypes; using Grpc.Core; +using MadWorldNL.MantaRayPlan.Api; using MadWorldNL.MantaRayPlan.MessageBuses; -using MantaRayPlan; +using MassTransit; namespace MadWorldNL.MantaRayPlan.Services; -public class MessageBusServiceProxy(IMessageBusRepository messageBusRepository, ILogger logger) - : MessageBusService.MessageBusServiceBase +public class MessageBusServiceProxy( + IRequestClient getMessageBusStatusClient, + ISendEndpointProvider sendEndpointProvider, + ILogger logger) + : MessageBusService.MessageBusServiceBase { - public override async Task GetStatus(Empty request, ServerCallContext context) + public override async Task GetStatus(Empty request, ServerCallContext context) { try { - var status = await messageBusRepository.FindStatusAsync() ?? new MessageBusStatus(); - - return new MessageBusStatusReply() + var status = + await getMessageBusStatusClient.GetResponse(new MessageBusStatusQuery(), context.CancellationToken); + + return new GetMessageBusStatusReply() { - Counter = status.Count + Counter = status.Message.Count, }; } - catch (Exception ex) + catch (RequestFaultException exception) when (Array.Exists(exception.Fault?.Exceptions ?? [], ex => + ex.InnerException?.ExceptionType == + "Npgsql.NpgsqlException")) { - const string message = "Database error"; - - logger.LogError(ex, message); - - return new MessageBusStatusReply() + logger.LogError(exception, "Unable to connect with database"); + + return new GetMessageBusStatusReply() { - Message = message + Message = "Unable to connect with database" }; } + catch (Exception exception) + { + logger.LogError(exception, "Unknown error"); + + return new GetMessageBusStatusReply() + { + Message = "Unknown error" + }; + } + } + + public override async Task PostStatus(Empty request, ServerCallContext context) + { + await sendEndpointProvider.Send(new MessageBusStatusCommand("Test"), context.CancellationToken); + + return new PostMessageBusStatusReply(); } } \ No newline at end of file diff --git a/src/Server.Controllers.Api.Grpc/appsettings.Development.json b/src/Server.Controllers.Api.Grpc/appsettings.Development.json index 26cce75..2659f1b 100644 --- a/src/Server.Controllers.Api.Grpc/appsettings.Development.json +++ b/src/Server.Controllers.Api.Grpc/appsettings.Development.json @@ -16,5 +16,11 @@ "Application": "Api.Grpc", "LoggerEndpoint": "http://localhost:5341", "LoggerApiKey": "" + }, + "MessageBus": { + "Host": "localhost", + "Port": 5672, + "Username": "development", + "Password": "Secret1234" } } diff --git a/src/Server.Controllers.Api.Grpc/appsettings.json b/src/Server.Controllers.Api.Grpc/appsettings.json index f8f53df..a2e398b 100644 --- a/src/Server.Controllers.Api.Grpc/appsettings.json +++ b/src/Server.Controllers.Api.Grpc/appsettings.json @@ -22,5 +22,11 @@ "EndpointDefaults": { "Protocols": "Http2" } + }, + "MessageBus": { + "Host": "localhost", + "Port": 5672, + "Username": "development", + "Password": "Secret1234" } } diff --git a/src/Server.Controllers.Api.MessageBus.IntegrationTests/Api.MessageBus.IntegrationTests.csproj b/src/Server.Controllers.Api.MessageBus.IntegrationTests/Api.MessageBus.IntegrationTests.csproj index 6499e27..44be325 100644 --- a/src/Server.Controllers.Api.MessageBus.IntegrationTests/Api.MessageBus.IntegrationTests.csproj +++ b/src/Server.Controllers.Api.MessageBus.IntegrationTests/Api.MessageBus.IntegrationTests.csproj @@ -13,6 +13,9 @@ + + + @@ -22,6 +25,7 @@ + diff --git a/src/Server.Controllers.Api.MessageBus.IntegrationTests/Base/MessageBusFactory.cs b/src/Server.Controllers.Api.MessageBus.IntegrationTests/Base/MessageBusFactory.cs new file mode 100644 index 0000000..b8bf468 --- /dev/null +++ b/src/Server.Controllers.Api.MessageBus.IntegrationTests/Base/MessageBusFactory.cs @@ -0,0 +1,85 @@ +using MassTransit; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Testcontainers.PostgreSql; +using Testcontainers.RabbitMq; + +namespace MadWorldNL.MantaRayPlan.Base; + +public class MessageBusFactory : WebApplicationFactory, IAsyncLifetime +{ + private const string DbName = "MantaRayPlan"; + private const string DbUser = "postgres"; + private const string DbPassword = "Password1234!"; + + private const string BusUser = "development"; + private const string BusPassword = "Password1234"; + + private PostgreSqlContainer? _postgreSqlContainer; + private RabbitMqContainer? _rabbitMqContainer; + + public async Task InitializeAsync() + { + _postgreSqlContainer = new PostgreSqlBuilder() + .WithImage("postgres:16") + .WithDatabase(DbName) + .WithUsername(DbUser) + .WithPassword(DbPassword) + .Build(); + + _rabbitMqContainer = new RabbitMqBuilder() + .WithImage("rabbitmq:3.11") + .WithUsername(BusUser) + .WithPassword(BusPassword) + .Build(); + + await _postgreSqlContainer.StartAsync(); + await _rabbitMqContainer.StartAsync(); + } + + protected override IHost CreateHost(IHostBuilder builder) + { + var newSettings = new Dictionary + { + ["Database:Host"] = _postgreSqlContainer!.Hostname, + ["Database:Port"] = _postgreSqlContainer.GetMappedPublicPort(5432).ToString(), + ["Database:DbName"] = DbName, + ["Database:User"] = DbUser, + ["Database:Password"] = DbPassword, + ["MessageBus:Host"] = _rabbitMqContainer!.Hostname, + ["MessageBus:Port"] = _rabbitMqContainer.GetMappedPublicPort(5672).ToString(), + ["MessageBus:Username"] = BusUser, + ["MessageBus:Password"] = BusPassword + }; + + builder.ConfigureHostConfiguration(config => + { + config.AddInMemoryCollection(newSettings!); + }); + + builder.ConfigureServices(services => + { + // For more info about testing message bus: + // https://masstransit.io/documentation/concepts/testing + services.AddMassTransitTestHarness(); + }); + + return base.CreateHost(builder); + } + + public new async Task DisposeAsync() + { + if (_postgreSqlContainer is not null) + { + await _postgreSqlContainer.DisposeAsync(); + } + + if (_rabbitMqContainer is not null) + { + await _rabbitMqContainer.DisposeAsync(); + } + + await base.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/Server.Controllers.Api.MessageBus.IntegrationTests/Base/SharedTestCollection.cs b/src/Server.Controllers.Api.MessageBus.IntegrationTests/Base/SharedTestCollection.cs new file mode 100644 index 0000000..5ef7f2f --- /dev/null +++ b/src/Server.Controllers.Api.MessageBus.IntegrationTests/Base/SharedTestCollection.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.MantaRayPlan.Base; + +[CollectionDefinition(TestDefinitions.Default)] +public class SharedTestCollection : ICollectionFixture +{ +} \ No newline at end of file diff --git a/src/Clients.Viewer.Bff.IntegrationTests/HealthCheckTests.cs b/src/Server.Controllers.Api.MessageBus.IntegrationTests/Endpoints/HealthCheckTests.cs similarity index 51% rename from src/Clients.Viewer.Bff.IntegrationTests/HealthCheckTests.cs rename to src/Server.Controllers.Api.MessageBus.IntegrationTests/Endpoints/HealthCheckTests.cs index 97d63be..2fbf9ff 100644 --- a/src/Clients.Viewer.Bff.IntegrationTests/HealthCheckTests.cs +++ b/src/Server.Controllers.Api.MessageBus.IntegrationTests/Endpoints/HealthCheckTests.cs @@ -1,22 +1,26 @@ - using System.Net; -using Microsoft.AspNetCore.Mvc.Testing; +using MadWorldNL.MantaRayPlan.Base; using Shouldly; -namespace MadWorldNL.MantaRayPlan; +namespace MadWorldNL.MantaRayPlan.Endpoints; -public class HealthCheckTests(WebApplicationFactory factory) : IClassFixture> +[Collection(TestDefinitions.Default)] +public class HealthCheckTests(MessageBusFactory factory) : IAsyncLifetime { [Fact] public async Task Healthz_GivenEmptyRequest_ShouldBeHealthy() { // Arrange var client = factory.CreateClient(); - + // Act var response = await client.GetAsync("/healthz"); // Assert response.StatusCode.ShouldBe(HttpStatusCode.OK); } + + public Task InitializeAsync() => Task.CompletedTask; + + public Task DisposeAsync() => factory.DisposeAsync(); } \ No newline at end of file diff --git a/src/Server.Controllers.Api.MessageBus/Api.MessageBus.csproj b/src/Server.Controllers.Api.MessageBus/Api.MessageBus.csproj index 767f695..294bc1e 100644 --- a/src/Server.Controllers.Api.MessageBus/Api.MessageBus.csproj +++ b/src/Server.Controllers.Api.MessageBus/Api.MessageBus.csproj @@ -19,9 +19,12 @@ + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Server.Controllers.Api.MessageBus/Program.cs b/src/Server.Controllers.Api.MessageBus/Program.cs index 52c3e5f..aa91cfd 100644 --- a/src/Server.Controllers.Api.MessageBus/Program.cs +++ b/src/Server.Controllers.Api.MessageBus/Program.cs @@ -1,13 +1,27 @@ -// See https://aka.ms/new-console-template for more information - -using MadWorldNL.MantaRayPlan; +using MadWorldNL.MantaRayPlan; using MadWorldNL.MantaRayPlan.Extensions; +using MadWorldNL.MantaRayPlan.MassTransit; +using MadWorldNL.MantaRayPlan.MessageBuses; using MadWorldNL.MantaRayPlan.OpenTelemetry; +using MassTransit; + +const string corsName = "DefaultCors"; -var builder = WebApplication.CreateBuilder(); +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddCors(options => +{ + options.AddPolicy(name: corsName, + policy => + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.AllowAnyOrigin(); + }); +}); var openTelemetryConfig = builder.Configuration.GetSection(OpenTelemetryConfig.Key).Get() ?? - new OpenTelemetryConfig(); + new OpenTelemetryConfig(); builder.AddDefaultOpenTelemetry(openTelemetryConfig); @@ -15,6 +29,26 @@ builder.Services.AddHealthChecks(); +builder.Services.AddMassTransit(x => +{ + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer(); + + x.UsingRabbitMq((context,cfg) => + { + var messageBusSettings = builder.Configuration.GetSection(MessageBusSettings.Key) + .Get()!; + + cfg.Host(messageBusSettings.Host, messageBusSettings.Port, "/", h => { + h.Username(messageBusSettings.Username); + h.Password(messageBusSettings.Password); + }); + + cfg.ConfigureEndpoints(context); + }); +}); + var app = builder.Build(); app.MapHealthChecks("/healthz"); @@ -23,4 +57,4 @@ await app.RunAsync(); -public abstract partial class Program { } \ No newline at end of file +public abstract partial class Program; \ No newline at end of file diff --git a/src/Server.Controllers.Api.MessageBus/appsettings.Development.json b/src/Server.Controllers.Api.MessageBus/appsettings.Development.json index 25629a4..dc7be52 100644 --- a/src/Server.Controllers.Api.MessageBus/appsettings.Development.json +++ b/src/Server.Controllers.Api.MessageBus/appsettings.Development.json @@ -16,5 +16,11 @@ "Application": "Api.MessageBus", "LoggerEndpoint": "http://localhost:5341", "LoggerApiKey": "" + }, + "MessageBus": { + "Host": "localhost", + "Port": 5672, + "Username": "development", + "Password": "Secret1234" } } diff --git a/src/Server.Controllers.Api.MessageBus/appsettings.json b/src/Server.Controllers.Api.MessageBus/appsettings.json index aa2163c..534a16d 100644 --- a/src/Server.Controllers.Api.MessageBus/appsettings.json +++ b/src/Server.Controllers.Api.MessageBus/appsettings.json @@ -22,5 +22,11 @@ "EndpointDefaults": { "Protocols": "Http1" } + }, + "MessageBus": { + "Host": "localhost", + "Port": 5672, + "Username": "development", + "Password": "Secret1234" } } diff --git a/src/Server.Logic.Domain/MessageBuses/MessageBusStatus.cs b/src/Server.Logic.Domain/MessageBuses/MessageBusStatus.cs index c599f1c..fa35695 100644 --- a/src/Server.Logic.Domain/MessageBuses/MessageBusStatus.cs +++ b/src/Server.Logic.Domain/MessageBuses/MessageBusStatus.cs @@ -2,11 +2,16 @@ namespace MadWorldNL.MantaRayPlan.MessageBuses; public class MessageBusStatus { - public int Id { get; private set; } - public int Count { get; private set; } + public int Id { get; init; } + public int Count { get; set; } public void IncrementCount() { + if (Count >= int.MaxValue) + { + Count = 0; + } + Count++; } } \ No newline at end of file diff --git a/src/Server.Logic.Domain/MessageBuses/MessageBusStatusCommand.cs b/src/Server.Logic.Domain/MessageBuses/MessageBusStatusCommand.cs new file mode 100644 index 0000000..55b0e51 --- /dev/null +++ b/src/Server.Logic.Domain/MessageBuses/MessageBusStatusCommand.cs @@ -0,0 +1,3 @@ +namespace MadWorldNL.MantaRayPlan.MessageBuses; + +public record MessageBusStatusCommand(string RandomData); \ No newline at end of file diff --git a/src/Server.Logic.Domain/MessageBuses/MessageBusStatusQuery.cs b/src/Server.Logic.Domain/MessageBuses/MessageBusStatusQuery.cs new file mode 100644 index 0000000..7822c7a --- /dev/null +++ b/src/Server.Logic.Domain/MessageBuses/MessageBusStatusQuery.cs @@ -0,0 +1,3 @@ +namespace MadWorldNL.MantaRayPlan.MessageBuses; + +public record MessageBusStatusQuery(); \ No newline at end of file diff --git a/src/Server.Logic.Functions/Functions.csproj b/src/Server.Logic.Functions/Functions.csproj index 3a63532..71fc592 100644 --- a/src/Server.Logic.Functions/Functions.csproj +++ b/src/Server.Logic.Functions/Functions.csproj @@ -2,8 +2,16 @@ net8.0 - enable - enable + MadWorldNL.MantaRayPlan.Logic.Functions + + + + + + + + + diff --git a/src/Server.Logic.Functions/MessageBuses/MessageBusStatusCommandConsumer.cs b/src/Server.Logic.Functions/MessageBuses/MessageBusStatusCommandConsumer.cs new file mode 100644 index 0000000..f6cb2bf --- /dev/null +++ b/src/Server.Logic.Functions/MessageBuses/MessageBusStatusCommandConsumer.cs @@ -0,0 +1,35 @@ +using MadWorldNL.MantaRayPlan.Events; +using MassTransit; + +namespace MadWorldNL.MantaRayPlan.MessageBuses; + +public class MessageBusStatusCommandConsumer : IConsumer +{ + private readonly IMessageBusRepository _messageBusRepository; + + public MessageBusStatusCommandConsumer(IMessageBusRepository messageBusRepository) + { + _messageBusRepository = messageBusRepository; + } + + public async Task Consume(ConsumeContext context) + { + var status = await _messageBusRepository.FindStatusAsync(); + + if (status is null) + { + status = new MessageBusStatus(); + + await _messageBusRepository.CreateAsync(status); + } + + status.IncrementCount(); + + await _messageBusRepository.UpdateAsync(status); + + await context.Publish(new MessageBusStatusEvent + { + Count = status.Count + }); + } +} \ No newline at end of file diff --git a/src/Server.Logic.Queries/MessageBuses/MessageBusStatusQueryConsumer.cs b/src/Server.Logic.Queries/MessageBuses/MessageBusStatusQueryConsumer.cs new file mode 100644 index 0000000..a7fb378 --- /dev/null +++ b/src/Server.Logic.Queries/MessageBuses/MessageBusStatusQueryConsumer.cs @@ -0,0 +1,20 @@ +using MassTransit; + +namespace MadWorldNL.MantaRayPlan.MessageBuses; + +public class MessageBusStatusQueryConsumer : IConsumer +{ + private readonly IMessageBusRepository _messageBusRepository; + + public MessageBusStatusQueryConsumer(IMessageBusRepository messageBusRepository) + { + _messageBusRepository = messageBusRepository; + } + + + public async Task Consume(ConsumeContext context) + { + var messageBusStatus = await _messageBusRepository.FindStatusAsync() ?? new MessageBusStatus(); + await context.RespondAsync(messageBusStatus); + } +} \ No newline at end of file diff --git a/src/Server.Logic.Queries/Queries.csproj b/src/Server.Logic.Queries/Queries.csproj new file mode 100644 index 0000000..f6d3b42 --- /dev/null +++ b/src/Server.Logic.Queries/Queries.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + MadWorldNL.MantaRayPlan.Logic.Queries + + + + + + + + + + + diff --git a/src/docker-compose.env b/src/docker-compose.env deleted file mode 100644 index e69de29..0000000 diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 9821f9d..d3d636e 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -7,9 +7,19 @@ services: build: context: . dockerfile: Clients.Admin.Bff/Dockerfile + depends_on: + messagebus: + condition: service_healthy + environment: + - Api__Address=http://api-grpc:8080 + - OpenTelemetry__LoggerEndpoint=http://seq:5341 + - MessageBus__Host=messagebus ports: - "8080:8080" - "8081:8081" + networks: + - messagebus + - seq admin-web: image: ghcr.io/madworldnl/mantarayplan-admin-web:latest container_name: admin-web @@ -20,6 +30,9 @@ services: ports: - "8082:80" - "8083:443" + volumes: + - ./docker/Clients.Admin.Web/appsettings.json:/usr/share/nginx/html/appsettings.json + - ./docker/Clients.Admin.Web/default.conf:/etc/nginx/conf.d/default.conf api-grpc: image: ghcr.io/madworldnl/mantarayplan-api-grpc:latest container_name: api-grpc @@ -28,11 +41,21 @@ services: context: . dockerfile: Server.Controllers.Api.Grpc/Dockerfile depends_on: + messagebus: + condition: service_healthy postgres: condition: service_healthy + environment: + - Database__Host=postgres + - OpenTelemetry__LoggerEndpoint=http://seq:5341 + - MessageBus__Host=messagebus ports: - "8084:8080" - "8085:8081" + networks: + - messagebus + - postgres + - seq api-message-bus: image: ghcr.io/madworldnl/mantarayplan-api-message-bus:latest container_name: api-message-bus @@ -41,11 +64,21 @@ services: context: . dockerfile: Server.Controllers.Api.MessageBus/Dockerfile depends_on: + messagebus: + condition: service_healthy postgres: condition: service_healthy + environment: + - Database__Host=postgres + - OpenTelemetry__LoggerEndpoint=http://seq:5341 + - MessageBus__Host=messagebus ports: - "8086:8080" - "8087:8081" + networks: + - messagebus + - postgres + - seq viewer-bff: image: ghcr.io/madworldnl/mantarayplan-viewer-bff:latest container_name: viewer-bff @@ -53,9 +86,16 @@ services: build: context: . dockerfile: Clients.Viewer.Bff/Dockerfile + environment: + - Api__Address=http://api-grpc:8084 + - OpenTelemetry__LoggerEndpoint=http://seq:5341 + - MessageBus__Host=messagebus ports: - "8088:8080" - "8089:8081" + networks: + - messagebus + - seq viewer-web: image: ghcr.io/madworldnl/mantarayplan-viewer-web:latest container_name: viewer-web @@ -76,6 +116,8 @@ services: - SEQ_FIRSTRUN_ADMINPASSWORDHASH=QFCKH3NTSBQ5zmsH9DpXTB2YefIavEGJKa4SshKb11AXX8b4o4KPjuo9bd6WBfqDkpxKqzNjaOrCsE49ph369Wx84mIrksnJx5OtRcxAOjff ports: - "5341:80" + networks: + - seq volumes: - seq-data:/data postgres: @@ -111,9 +153,32 @@ services: - postgres volumes: - pgadmin-data:/var/lib/pgadmin + messagebus: + image: rabbitmq:3-management + container_name: messagebus + hostname: my-rabbit + environment: + - "RABBITMQ_DEFAULT_USER=development" + - "RABBITMQ_DEFAULT_PASS=Secret1234" + ports: + - "5672:5672" + - "15672:15672" + networks: + - messagebus + volumes: + - "rabbitmq-data:/var/lib/rabbitmq" + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 30s + timeout: 10s + retries: 5 + start_period: 10s volumes: postgres-data: pgadmin-data: + rabbitmq-data: seq-data: networks: - postgres: \ No newline at end of file + messagebus: + postgres: + seq: \ No newline at end of file diff --git a/src/docker/Clients.Admin.Web/appsettings.json b/src/docker/Clients.Admin.Web/appsettings.json new file mode 100644 index 0000000..e61d946 --- /dev/null +++ b/src/docker/Clients.Admin.Web/appsettings.json @@ -0,0 +1,5 @@ +{ + "Api": { + "Address": "http://localhost:8080/" + } +} \ No newline at end of file diff --git a/src/docker/Clients.Admin.Web/default.conf b/src/docker/Clients.Admin.Web/default.conf new file mode 100644 index 0000000..13cd2af --- /dev/null +++ b/src/docker/Clients.Admin.Web/default.conf @@ -0,0 +1,8 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html =404; + } +} \ No newline at end of file