diff --git a/.github/workflows/image-to-ghcr.yml b/.github/workflows/image-to-ghcr.yml new file mode 100644 index 000000000..d997985ae --- /dev/null +++ b/.github/workflows/image-to-ghcr.yml @@ -0,0 +1,111 @@ +name: Image to GHCR + +on: + push: + branches-ignore: + - dependabot/** + +permissions: + contents: read + +jobs: + branch_meta: + runs-on: ubuntu-latest + outputs: + branch: ${{ steps.extract_branch_meta.outputs.branch }} + sha: ${{ steps.extract_branch_meta.outputs.sha }} + steps: + - name: Extract branch meta + shell: bash + id: extract_branch_meta + run: | + echo "lowercase_repo=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT + if [ "${{ github.event_name }}" == 'pull_request' ]; then + echo "branch=${{ github.event.pull_request.head.ref }}" >> $GITHUB_OUTPUT + echo "sha=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT + else + echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT + echo "sha=${{ github.sha }}" >> $GITHUB_OUTPUT + fi + + build_and_push: + runs-on: ubuntu-latest + needs: + - branch_meta + permissions: + packages: write + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Login to registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Lowercase REPO name + run: | + echo "LOWERCASE_REPO=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + - name: Docker meta Service Name + id: docker_meta_img + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ env.LOWERCASE_REPO }} + tags: | + type=ref,event=branch,enable=false,priority=600 + type=sha,enable=true,priority=600,prefix= + + - name: Test existence of Image + run: | + echo "IMAGE_EXISTS=$(docker manifest inspect ghcr.io/${{ env.LOWERCASE_REPO }}/${{needs.branch_meta.outputs.branch}}:${{ needs.branch_meta.outputs.sha }} > /dev/null && echo 1 || echo 0)" >> $GITHUB_ENV + + - name: Set up Docker Buildx + if: ${{ env.IMAGE_EXISTS == 0 }} + uses: docker/setup-buildx-action@v2 + + - name: Build and push ${{ github.repository }} + if: ${{ env.IMAGE_EXISTS == 0 }} + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + # temporarily change this to latest to make deployment + # tags: ghcr.io/${{ env.LOWERCASE_REPO }}:${{ needs.branch_meta.outputs.sha }} + tags: ghcr.io/${{ env.LOWERCASE_REPO }}/${{needs.branch_meta.outputs.branch}}:latest + labels: ${{ steps.docker_meta_img.outputs.labels }} + + # trivy-vulnerability-scanning: + # needs: + # - build_and_push + # - branch_meta + # runs-on: ubuntu-latest + # permissions: + # actions: read + # contents: read + # security-events: write + # steps: + # - name: Lowercase REPO name + # run: | + # echo "LOWERCASE_REPO=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + # - name: Run trivy vulnerability scanner + # uses: aquasecurity/trivy-action@9ab158e8597f3b310480b9a69402b419bc03dbd5 + # with: + # # image-ref: 'ghcr.io/${{ env.LOWERCASE_REPO }}:${{ needs.branch_meta.outputs.sha }}' + # image-ref: 'ghcr.io/${{ env.LOWERCASE_REPO }}:latest' + # format: 'sarif' + # output: 'trivy-results.sarif' + # severity: 'CRITICAL,HIGH' + # ignore-unfixed: true + + # - name: Upload trivy results + # if: ${{ always() }} + # uses: github/codeql-action/upload-sarif@v2 + # with: + # sarif_file: 'trivy-results.sarif' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..ea7719aac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +ARG BASE_IMAGE=node:20.5.1-alpine3.17 +FROM $BASE_IMAGE as deployment + +WORKDIR /app + +COPY tsconfig*.json ./ +COPY package*.json ./ + +RUN npm ci + +COPY src/ src/ + +RUN npm run build + +FROM $BASE_IMAGE +ENV NODE_ENV=prod + +WORKDIR /app +COPY package*.json ./ +COPY config/ ./config/ + +RUN npm ci --omit-dev + +COPY --from=deployment /app/dist/ ./dist/ + +CMD [ "node", "dist/src/server/main.js" ] \ No newline at end of file diff --git a/charts/dbildungs-iam/Chart.yaml b/charts/dbildungs-iam/Chart.yaml new file mode 100644 index 000000000..770fcc42f --- /dev/null +++ b/charts/dbildungs-iam/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: dbildungs-iam +version: 0.1.0 + +description: dBildungs-IAM +type: application diff --git a/charts/dbildungs-iam/templates/dbildungs-iam-deployment.yaml b/charts/dbildungs-iam/templates/dbildungs-iam-deployment.yaml new file mode 100644 index 000000000..dd813e94b --- /dev/null +++ b/charts/dbildungs-iam/templates/dbildungs-iam-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-deployment + labels: + app.kubernetes.io/name: dbildungs-iam +spec: + selector: + matchLabels: + layer: dbildungs-iam-backend + replicas: {{.Values.dbildungsIamReplications}} + template: + metadata: + name: dbildungs-iam + labels: + layer: dbildungs-iam-backend + spec: + containers: + - name: dbildungs-iam + image: {{.Values.dbildungsIamContainer}} + imagePullPolicy: Always + ports: + - name: web + containerPort: 8080 + env: + - name: NODE_ENV + value: {{.Values.environment}} + - name: DEPLOY_STAGE + value: {{.Values.environment}} + volumeMounts: + - mountPath: /app/config/ + name: config + readOnly: true + resources: + limits: + cpu: {{.Values.dbildungsIamCpuMax}} + memory: {{.Values.dbildungsIamMemMax}} + livenessProbe: + initialDelaySeconds: 10 + httpGet: + port: 8080 + scheme: 'HTTP' + path: '/health' + readinessProbe: + initialDelaySeconds: 10 + httpGet: + port: 8080 + scheme: 'HTTP' + path: '/health' + restartPolicy: Always + volumes: + - name: config + secret: + secretName: {{.Values.secrets.name | default (print .Release.Name "-secret")}} + \ No newline at end of file diff --git a/charts/dbildungs-iam/templates/dbildungs-iam-ingress.yaml b/charts/dbildungs-iam/templates/dbildungs-iam-ingress.yaml new file mode 100644 index 000000000..10516c393 --- /dev/null +++ b/charts/dbildungs-iam/templates/dbildungs-iam-ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{.Release.Name}}-backend + labels: + app.kubernetes.io/name: dbildungs-iam +spec: + ingressClassName: nginx + rules: + - host: {{.Values.backendHostname}} + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: dbiam + port: + number: 80 + - path: /docs + pathType: Prefix + backend: + service: + name: dbiam + port: + number: 80 \ No newline at end of file diff --git a/charts/dbildungs-iam/templates/dbildungs-iam-service.yaml b/charts/dbildungs-iam/templates/dbildungs-iam-service.yaml new file mode 100644 index 000000000..8ff5467fb --- /dev/null +++ b/charts/dbildungs-iam/templates/dbildungs-iam-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: dbiam + labels: + app.kubernetes.io/name: dbildungs-iam +spec: + selector: + layer: dbildungs-iam-backend + ports: + - protocol: TCP + name: web + port: {{.Values.dbildungsIamExternalPort}} + targetPort: web + type: ClusterIP + \ No newline at end of file diff --git a/charts/dbildungs-iam/templates/dbildungs-iam-servicemonitor.yaml b/charts/dbildungs-iam/templates/dbildungs-iam-servicemonitor.yaml new file mode 100644 index 000000000..a07140efa --- /dev/null +++ b/charts/dbildungs-iam/templates/dbildungs-iam-servicemonitor.yaml @@ -0,0 +1,14 @@ +{{if .Values.enableServiceMonitor}} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{.Release.Name}}-servicemonitor +spec: + namespaceSelector: + any: true + selector: + matchLabels: + app.kubernetes.io/name: dbildungs-iam + endpoints: + - port: web + {{end}} \ No newline at end of file diff --git a/charts/dbildungs-iam/values.yaml b/charts/dbildungs-iam/values.yaml new file mode 100644 index 000000000..13007af66 --- /dev/null +++ b/charts/dbildungs-iam/values.yaml @@ -0,0 +1,23 @@ +dbildungsIamContainer: "ghcr.io/dbildungsplattform/dbildungs-iam-server/feature/helm-integration:latest" + +dbildungsIamExternalPort: 80 +dbildungsIamCpuMax: 2 +dbildungsIamMemMax: 4G +dbildungsIamReplications: 1 +environment: prod + +backendHostname: helm.dev.spsh.dbildungsplattform.de + +configfile: + secrets: 'config/secrets.json' + dev: 'config/config.dev.json' + test: 'config/config.test.json' + prod: 'config/config.prod.json' + local: 'config/config.local.json' + +# Configuration of necessary secrets +# Name of the secrets to inject +secrets: + name: spsh-config +# If we're running inside an environment with a Prometheus-Operator installed we configure a service monitor +enableServiceMonitor: false \ No newline at end of file diff --git a/charts/keycloak-dev/Chart.yaml b/charts/keycloak-dev/Chart.yaml new file mode 100644 index 000000000..7fa09db3e --- /dev/null +++ b/charts/keycloak-dev/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: dbildungs-iam-keycloak-dev +version: 0.1.0 + +description: dBildungs-IAM Keycloak for local deployment +type: application diff --git a/charts/keycloak-dev/templates/dbildungs-iam-deployment-keycloak.yaml b/charts/keycloak-dev/templates/dbildungs-iam-deployment-keycloak.yaml new file mode 100644 index 000000000..73ff15f6c --- /dev/null +++ b/charts/keycloak-dev/templates/dbildungs-iam-deployment-keycloak.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-keycloak-deployment + labels: + app.kubernetes.io/name: dbildungs-iam +spec: + selector: + matchLabels: + layer: dbildungs-iam-keycloak + replicas: 1 + template: + metadata: + name: dbildungs-iam-keycloak + labels: + layer: dbildungs-iam-keycloak + spec: + containers: + - name: dbildungs-iam-keycloak + image: quay.io/keycloak/keycloak:22.0.3 + args: + - start-dev + imagePullPolicy: IfNotPresent + ports: + - name: web + containerPort: 8080 + env: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin + restartPolicy: Always + \ No newline at end of file diff --git a/charts/keycloak-dev/templates/dbildungs-iam-service-keycloak.yaml b/charts/keycloak-dev/templates/dbildungs-iam-service-keycloak.yaml new file mode 100644 index 000000000..a11d59c93 --- /dev/null +++ b/charts/keycloak-dev/templates/dbildungs-iam-service-keycloak.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: keycloak + labels: + app.kubernetes.io/name: dbildungs-iam +spec: + selector: + layer: dbildungs-iam-keycloak + ports: + - protocol: TCP + name: web + port: {{.Values.dbildungsIamExternalPort}}80 + targetPort: web + type: ClusterIP + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c506068a0..192e5f4b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,15 @@ "@mikro-orm/core": "^5.7.11", "@mikro-orm/nestjs": "^5.1.8", "@mikro-orm/postgresql": "^5.7.11", + "@nestjs/axios": "^3.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.2", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.0.4", + "@nestjs/terminus": "^9.0.0", "@s3pweb/keycloak-admin-client-cjs": "^22.0.1", + "axios": "^1.5.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "lodash": "^4.17.21", @@ -944,6 +947,14 @@ "npm": ">=6.14.13" } }, + "node_modules/@golevelup/nestjs-discovery": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-3.0.0.tgz", + "integrity": "sha512-ZvkXtobTKxXB1LJanP/l6Z/Fing88IMBr3uabQpU2IWjfsstjh02qYDSU2cfD6CSmNldX5ewW5Pd+SdK2lU8Sw==", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/@golevelup/ts-jest": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.3.8.tgz", @@ -1747,6 +1758,17 @@ } } }, + "node_modules/@nestjs/axios": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", + "integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.4.2.tgz", @@ -2109,6 +2131,20 @@ } } }, + "node_modules/@nestjs/terminus": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-9.0.0.tgz", + "integrity": "sha512-Yqx310ld2JwWgFFQyw0Cri+Y4yfG9gq66BLZ37vdOgZeKFS4wHdJZG6PlYeO3ztvE+vVZCKxvORLtaNa4u2bSQ==", + "dependencies": { + "check-disk-space": "3.3.0" + }, + "peerDependencies": { + "@nestjs/common": "9.x", + "@nestjs/core": "9.x", + "reflect-metadata": "0.1.x", + "rxjs": "7.x" + } + }, "node_modules/@nestjs/testing": { "version": "9.4.3", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.4.3.tgz", @@ -3394,8 +3430,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "devOptional": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -3413,8 +3448,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", - "optional": true, - "peer": true, "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -3865,6 +3898,14 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "node_modules/check-disk-space": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.3.0.tgz", + "integrity": "sha512-Hvr+Nr01xSSvuCpXvJ8oZ2iXjIu4XT3uHbw3g7F/Uiw6O5xk8c/Ot7ZGFDaTRDf2Bz8AdWA4DvpAgCJVKt8arw==", + "engines": { + "node": ">=12" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -4074,7 +4115,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "devOptional": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4383,7 +4423,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -5687,8 +5726,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "optional": true, - "peer": true, "engines": { "node": ">=4.0" }, @@ -5739,7 +5776,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "devOptional": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7982,11 +8018,11 @@ "dev": true }, "node_modules/nest-commander": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.11.1.tgz", - "integrity": "sha512-BuuuYx7EyGsfiGRiRNPVFE8ScrspDO1zfnf+nqaYv2M2VnjApXIItxesyLEyeqMO3vLECO2bbZLY9uXDoS+3Zg==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.9.0.tgz", + "integrity": "sha512-gtunG9QnorVUScmrum0OlI/p4woxWtre1SDFJo9TRS9ehjEfmdXvbS60NS/yw0lU6fD773f+IMUPX/BX/Eg11g==", "dependencies": { - "@golevelup/nestjs-discovery": "4.0.0", + "@golevelup/nestjs-discovery": "3.0.0", "commander": "11.0.0", "cosmiconfig": "8.2.0", "inquirer": "8.2.5" @@ -7997,18 +8033,6 @@ "@types/inquirer": "^8.1.3" } }, - "node_modules/nest-commander/node_modules/@golevelup/nestjs-discovery": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.0.tgz", - "integrity": "sha512-iyZLYip9rhVMR0C93vo860xmboRrD5g5F5iEOfpeblGvYSz8ymQrL9RAST7x/Fp3n+TAXSeOLzDIASt+rak68g==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@nestjs/common": "^10.x", - "@nestjs/core": "^10.x" - } - }, "node_modules/nest-commander/node_modules/commander": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", @@ -8842,9 +8866,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "optional": true, - "peer": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/pump": { "version": "3.0.0", @@ -11693,6 +11715,14 @@ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz", "integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==" }, + "@golevelup/nestjs-discovery": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-3.0.0.tgz", + "integrity": "sha512-ZvkXtobTKxXB1LJanP/l6Z/Fing88IMBr3uabQpU2IWjfsstjh02qYDSU2cfD6CSmNldX5ewW5Pd+SdK2lU8Sw==", + "requires": { + "lodash": "^4.17.15" + } + }, "@golevelup/ts-jest": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.3.8.tgz", @@ -12216,6 +12246,12 @@ "pg": "8.11.1" } }, + "@nestjs/axios": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", + "integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==", + "requires": {} + }, "@nestjs/cli": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.4.2.tgz", @@ -12429,6 +12465,14 @@ "swagger-ui-dist": "5.3.1" } }, + "@nestjs/terminus": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-9.0.0.tgz", + "integrity": "sha512-Yqx310ld2JwWgFFQyw0Cri+Y4yfG9gq66BLZ37vdOgZeKFS4wHdJZG6PlYeO3ztvE+vVZCKxvORLtaNa4u2bSQ==", + "requires": { + "check-disk-space": "3.3.0" + } + }, "@nestjs/testing": { "version": "9.4.3", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.4.3.tgz", @@ -13493,8 +13537,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "devOptional": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "available-typed-arrays": { "version": "1.0.5", @@ -13506,8 +13549,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", - "optional": true, - "peer": true, "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -13828,6 +13869,11 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "check-disk-space": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.3.0.tgz", + "integrity": "sha512-Hvr+Nr01xSSvuCpXvJ8oZ2iXjIu4XT3uHbw3g7F/Uiw6O5xk8c/Ot7ZGFDaTRDf2Bz8AdWA4DvpAgCJVKt8arw==" + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -13977,7 +14023,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "devOptional": true, "requires": { "delayed-stream": "~1.0.0" } @@ -14220,8 +14265,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "devOptional": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "depd": { "version": "2.0.0", @@ -15233,9 +15277,7 @@ "follow-redirects": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", - "optional": true, - "peer": true + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" }, "for-each": { "version": "0.3.3", @@ -15270,7 +15312,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "devOptional": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -16916,24 +16957,16 @@ "dev": true }, "nest-commander": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.11.1.tgz", - "integrity": "sha512-BuuuYx7EyGsfiGRiRNPVFE8ScrspDO1zfnf+nqaYv2M2VnjApXIItxesyLEyeqMO3vLECO2bbZLY9uXDoS+3Zg==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.9.0.tgz", + "integrity": "sha512-gtunG9QnorVUScmrum0OlI/p4woxWtre1SDFJo9TRS9ehjEfmdXvbS60NS/yw0lU6fD773f+IMUPX/BX/Eg11g==", "requires": { - "@golevelup/nestjs-discovery": "4.0.0", + "@golevelup/nestjs-discovery": "3.0.0", "commander": "11.0.0", "cosmiconfig": "8.2.0", "inquirer": "8.2.5" }, "dependencies": { - "@golevelup/nestjs-discovery": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.0.tgz", - "integrity": "sha512-iyZLYip9rhVMR0C93vo860xmboRrD5g5F5iEOfpeblGvYSz8ymQrL9RAST7x/Fp3n+TAXSeOLzDIASt+rak68g==", - "requires": { - "lodash": "^4.17.21" - } - }, "commander": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", @@ -17528,9 +17561,7 @@ "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "optional": true, - "peer": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "pump": { "version": "3.0.0", diff --git a/package.json b/package.json index 645826d5f..c5941e3f9 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,15 @@ "@mikro-orm/core": "^5.7.11", "@mikro-orm/nestjs": "^5.1.8", "@mikro-orm/postgresql": "^5.7.11", + "@nestjs/axios": "^3.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.2", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.0.4", + "@nestjs/terminus": "^9.0.0", "@s3pweb/keycloak-admin-client-cjs": "^22.0.1", + "axios": "^1.5.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "lodash": "^4.17.21", diff --git a/src/console/console.module.ts b/src/console/console.module.ts index 188c060b4..443503b5f 100644 --- a/src/console/console.module.ts +++ b/src/console/console.module.ts @@ -5,7 +5,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { defineConfig } from '@mikro-orm/postgresql'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { DbConfig, ServerConfig, loadConfigFiles, loadEnvConfig } from '../shared/config/index.js'; +import { DbConfig, loadConfigFiles, loadEnvConfig, ServerConfig } from '../shared/config/index.js'; import { mappingErrorHandler } from '../shared/error/index.js'; import { LoggingModule } from '../shared/logging/index.js'; import { DbConsole } from './db.console.js'; @@ -31,6 +31,11 @@ import { DbInitConsole } from './db-init.console.js'; dbName: config.getOrThrow('DB').DB_NAME, entities: ['./dist/**/*.entity.js'], entitiesTs: ['./src/**/*.entity.ts'], + driverOptions: { + connection: { + ssl: true, + }, + }, }); }, inject: [ConfigService], diff --git a/src/console/db-init.console.ts b/src/console/db-init.console.ts index b2f433c52..583f0ef97 100644 --- a/src/console/db-init.console.ts +++ b/src/console/db-init.console.ts @@ -19,7 +19,10 @@ export class DbInitConsole extends CommandRunner { if (!(await this.orm.getSchemaGenerator().ensureDatabase())) { await this.orm.getSchemaGenerator().createDatabase(this.configService.getOrThrow('DB').DB_NAME); } - await this.orm.getSchemaGenerator().createSchema(); + this.logger.info('Dropping Schema'); + await this.orm.getSchemaGenerator().dropSchema({ wrap: false }); + this.logger.info('Creating Schema'); + await this.orm.getSchemaGenerator().createSchema({ wrap: false }); this.logger.info('Initialized database'); } } diff --git a/src/health/health.controller.spec.ts b/src/health/health.controller.spec.ts new file mode 100644 index 000000000..318cc2ada --- /dev/null +++ b/src/health/health.controller.spec.ts @@ -0,0 +1,71 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HealthController } from './health.controller.js'; +import { + HealthCheckService, + HealthIndicatorFunction, + HealthIndicatorResult, + HttpHealthIndicator, + MikroOrmHealthIndicator, +} from '@nestjs/terminus'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { SqlEntityManager } from '@mikro-orm/postgresql'; +import { KeycloakConfig } from '../shared/config/index.js'; +import { ConfigService } from '@nestjs/config'; + +describe('HealthController', () => { + let controller: HealthController; + + let healthCheckService: DeepMocked; + let mikroOrmHealthIndicator: MikroOrmHealthIndicator; + let entityManager: SqlEntityManager; + let httpHealthIndicator: DeepMocked; + const keycloakConfig: KeycloakConfig = { + CLIENT_ID: '', + SECRET: '', + REALM_NAME: '', + BASE_URL: 'http://keycloak.test', + }; + let configService: DeepMocked; + + beforeEach(async () => { + healthCheckService = createMock(); + mikroOrmHealthIndicator = createMock(); + entityManager = createMock(); + httpHealthIndicator = createMock(); + configService = createMock(); + + configService.getOrThrow.mockReturnValue(keycloakConfig); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthController], + providers: [ + { provide: HealthCheckService, useValue: healthCheckService }, + { provide: MikroOrmHealthIndicator, useValue: mikroOrmHealthIndicator }, + { provide: SqlEntityManager, useValue: entityManager }, + { provide: HttpHealthIndicator, useValue: httpHealthIndicator }, + { provide: KeycloakConfig, useValue: keycloakConfig }, + { provide: ConfigService, useValue: configService }, + ], + }).compile(); + + controller = module.get(HealthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should Perform all health checks', async () => { + await controller.check(); + + expect(healthCheckService.check).toHaveBeenCalled(); + const indicators: HealthIndicatorFunction[] | undefined = healthCheckService.check.mock.lastCall?.[0]; + const firstIndicator: (() => PromiseLike | HealthIndicatorResult) | undefined = + indicators?.[0]; + expect(firstIndicator).toBeDefined(); + // Explanation: We get back the lambdas that the HealthCheck would call and call them + // ourselves to make sure they do the right things + await firstIndicator?.call(firstIndicator); + expect(mikroOrmHealthIndicator.pingCheck).toHaveBeenCalled(); + }); +}); diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts new file mode 100644 index 000000000..c34c56b35 --- /dev/null +++ b/src/health/health.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Get } from '@nestjs/common'; +import { + HealthCheck, + HealthCheckResult, + HealthCheckService, + HealthIndicatorResult, + MikroOrmHealthIndicator, +} from '@nestjs/terminus'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { ApiExcludeController } from '@nestjs/swagger'; + +@Controller('health') +@ApiExcludeController() +export class HealthController { + public constructor( + private health: HealthCheckService, + private mikroOrm: MikroOrmHealthIndicator, + private em: EntityManager, + ) {} + + @Get() + @HealthCheck() + public check(): Promise { + return this.health.check([ + (): Promise => + this.mikroOrm.pingCheck('database', { connection: this.em.getConnection() }), + ]); + } +} diff --git a/src/health/health.module.ts b/src/health/health.module.ts new file mode 100644 index 000000000..5bbe24f08 --- /dev/null +++ b/src/health/health.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { TerminusModule } from '@nestjs/terminus'; +import { HealthController } from './health.controller.js'; +import { HttpModule } from '@nestjs/axios'; + +@Module({ imports: [TerminusModule, HttpModule], controllers: [HealthController] }) +export class HealthModule {} diff --git a/src/server/main.ts b/src/server/main.ts index d6f3d7536..561db1e91 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -17,7 +17,11 @@ async function bootstrap(): Promise { .build(); const configService: ConfigService = app.get(ConfigService); const port: number = configService.getOrThrow('HOST').PORT; + app.setGlobalPrefix('api', { + exclude: ['health'], + }); SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, swagger)); + await app.listen(port); console.info(`\nListening on: http://127.0.0.1:${port}`); console.info(`API documentation can be found on: http://127.0.0.1:${port}/docs`); diff --git a/src/server/server.module.ts b/src/server/server.module.ts index 338256c44..cfd2deacb 100644 --- a/src/server/server.module.ts +++ b/src/server/server.module.ts @@ -8,6 +8,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { DbConfig, loadConfigFiles, loadEnvConfig, ServerConfig } from '../shared/config/index.js'; import { mappingErrorHandler } from '../shared/error/index.js'; import { PersonApiModule } from '../modules/person/person-api.module.js'; +import { HealthModule } from '../health/health.module.js'; +import { KeycloakAdministrationModule } from '../modules/keycloak-administration/keycloak-administration.module.js'; import { OrganisationApiModule } from '../modules/organisation/organisation-api.module.js'; @Module({ @@ -30,12 +32,21 @@ import { OrganisationApiModule } from '../modules/organisation/organisation-api. dbName: dbConfig.DB_NAME, entities: ['./dist/**/*.entity.js'], entitiesTs: ['./src/**/*.entity.ts'], + // Needed for HealthCheck + type: 'postgresql', + driverOptions: { + connection: { + ssl: true, + }, + }, }); }, inject: [ConfigService], }), PersonApiModule, OrganisationApiModule, + KeycloakAdministrationModule, + HealthModule, ], }) export class ServerModule {}