diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..93f1361 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5282728..ba8e64f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,132 +1,149 @@ name: CI on: - push: - branches: [develop, master, next, beta, alpha] - pull_request: - branches: [develop, master, next, beta, alpha] + push: + branches: [develop, master, next, beta, alpha] + pull_request: + branches: [develop, master, next, beta, alpha] permissions: - packages: write - contents: write - issues: write - pull-requests: write + packages: write + contents: write + issues: write + pull-requests: write env: - PRIMARY_NODE_VERSION: 20 + PRIMARY_NODE_VERSION: 20 jobs: - install: - name: Checkout and Install - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Install - uses: ./.github/actions/install - with: - node-version: ${{ env.PRIMARY_NODE_VERSION }} - - lint: - name: Lint - needs: [build] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install - uses: ./.github/actions/install - with: - node-version: ${{ env.PRIMARY_NODE_VERSION }} - - - name: Build - uses: ./.github/actions/build - - - name: Lint - run: | - npm run lint - - build: - name: Build - needs: [install] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Install - uses: ./.github/actions/install - with: - node-version: ${{ env.PRIMARY_NODE_VERSION }} - - name: Build - uses: ./.github/actions/build - - tests: - name: Test - needs: [build] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install - uses: ./.github/actions/install - with: - node-version: ${{ env.PRIMARY_NODE_VERSION }} - - - name: Build - uses: ./.github/actions/build - - - name: Run tests - run: | - npm run test - - release: - name: Release - needs: [lint, tests] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install - uses: ./.github/actions/install - with: - node-version: ${{ env.PRIMARY_NODE_VERSION }} - - - name: Build - uses: ./.github/actions/build - - #- name: Release - #env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - # - # run: npx semantic-release - - coverage: - name: Coverage - needs: [release] - runs-on: ubuntu-latest - if: ${{ github.ref_name == 'master' }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install - uses: ./.github/actions/install - with: - node-version: ${{ env.PRIMARY_NODE_VERSION }} - - - name: Build - uses: ./.github/actions/build - - - name: Coverage - run: | - npm run test:coverage - - #- name: Upload report - # uses: codecov/codecov-action@v3.1.4 - # with: - # token: ${{ secrets.codecov }} - # directory: ./coverage/ + install: + name: Checkout and Install + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install + uses: ./.github/actions/install + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + lint: + name: Lint + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install + uses: ./.github/actions/install + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + - name: Build + uses: ./.github/actions/build + + - name: Lint + run: | + npm run lint + + build: + name: Build + needs: [install] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install + uses: ./.github/actions/install + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + - name: Build + uses: ./.github/actions/build + + tests: + name: Test + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install + uses: ./.github/actions/install + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + - name: Build + uses: ./.github/actions/build + + - name: Run tests + run: | + npm run test + + release: + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + name: Release + needs: [lint, tests] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install + uses: ./.github/actions/install + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + - name: Build + uses: ./.github/actions/build + + - name: GitHub Docker Registry Login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU for multi platform builds + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Prepare Artifact Versioning Information + id: prep-artifact-info + run: | + echo ::set-output name=repository::$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]') + echo ::set-output name=version::${GITHUB_REF#refs/tags/v} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/${{ steps.prep-artifact-info.outputs.repository }}:latest + ghcr.io/${{ steps.prep-artifact-info.outputs.repository }}:${{ steps.prep-artifact-info.outputs.version }} + + coverage: + name: Coverage + needs: [release] + runs-on: ubuntu-latest + if: ${{ github.ref_name == 'master' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install + uses: ./.github/actions/install + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + - name: Build + uses: ./.github/actions/build + + - name: Coverage + run: | + npm run test:coverage diff --git a/.gitignore b/.gitignore index 9d6b18c..2d64771 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ db.sqlite .env cache .swc + +k8s/tmp** diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3ec1d20 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20.11-alpine3.19@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c +RUN mkdir -p /home/node/node-message-broker/node_modules && chown -R node:node /home/node/node-message-broker +WORKDIR /home/node/node-message-broker +COPY --chown=node:node package*.json . + +USER node + +RUN npm install + +COPY --chown=node:node ./dist . + +EXPOSE 3000 + +CMD [ "node", "index.js" ] diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..33f4dd5 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,59 @@ +# k8s + +> [!CAUTION] +> Files found in this directory should be used with caution and NOT in a production environment! They are mainly for showcasing purposes. So, adjust them as necessary before applying them to your cluster. + +This directory contains: +- a deployment script (`deploy-to-minikube.sh`) +- k8s manifest files + +## Prerequisites + +### minikube + +Make sure the following `minikube` addons are enabled before using this deployment solution: + +- ingress +- registry +- storage-provisioner + +You can enable addons using the following command: + +```shell +minikube addons enable +``` + +For further information, see: [minikube addon docs](https://minikube.sigs.k8s.io/docs/commands/addons/). + +### build artifact + +Since the script builds a new Docker image on the fly before using it in the deployment, make sure that the application has been built using the following command in the root directory of the project: + +```shell +npm run build +``` + + +## Usage + +The script will install a single message broker instance to an already existing `minikube` cluster. In order to use it make sure the following environment variables are set: + +| ENV VAR | DESCRIPTION | +|---------|-------------| +| AUTH_JWKS_URL | URL to obtain JWKS from. Using keycloak this has the pattern `/realms//protocol/openid-connect/certs`. | +| HUB_AUTH_ROBOT_ID | ID of the robot account to be used. Needs to exist on the central side (hub) at `https://auth.privateaim.net/`. | +| ROBOT_SECRET | Associated secret of the robot account. | +| NODE_MESSAGE_BROKER_HOST | Host to be used for the message broker. It will be accessible under `message-broker..nip.io`. | +| NAMESPACE | Namespace to be used within the minikube cluster. | + +Set the following optional environment variables for further configuration: + +| ENV VAR | DESCRIPTION | +|---------|-------------| +| HUB_BASE_URL | Base URL of the central side (hub). Defaults to `https://api.privateaim.net`. | +| HUB_AUTH_BASE_URL | Base URL of the central side's (hub) auth provider. Defaults to `https://auth.privateaim.net`. | + +After that simply call the script with: +```shell +./deploy-to-minikube +``` diff --git a/k8s/deploy-to-minikube.sh b/k8s/deploy-to-minikube.sh new file mode 100755 index 0000000..fdc4bc3 --- /dev/null +++ b/k8s/deploy-to-minikube.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Mandatory environment variables: +# +# - AUTH_JWKS_URL +# - HUB_AUTH_ROBOT_ID +# - ROBOT_SECRET +# - NODE_MESSAGE_BROKER_HOST +# - NAMESPACE + +# Optional environment variables: +# +# - HUB_AUTH_BASE_URL +# - HUB_BASE_URL + +BASE_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit 1 ; pwd -P )" + +if [[ -z "${AUTH_JWKS_URL}" || -z "${HUB_AUTH_ROBOT_ID}" || -z "${ROBOT_SECRET}" || -z "${NODE_MESSAGE_BROKER_HOST}" || -z "${NAMESPACE}" ]]; then + echo "One or more mandatory environment variables are not set!" + echo "Mandatory environment variables are:" + echo "" + echo " - AUTH_JWKS_URL" + echo " - HUB_AUTH_ROBOT_ID" + echo " - ROBOT_SECRET" + echo " - NODE_MESSAGE_BROKER_HOST" + echo " - NAMESPACE" + exit 1 +fi + +checkSuccessOrFailWithCode() { + if [ $1 -ne 0 ]; then + echo "FAILED" + exit $2 + else + echo "OK" + fi +} + +echo -n "Creating temporary working directory..." +WORK_DIR=`mktemp -d -p "${BASE_DIR}"` +checkSuccessOrFailWithCode $? 2 + +echo -n "Copying k8s manifest files..." +for f in "${BASE_DIR}"/manifests/*.yml; do + cp "${f}" "${WORK_DIR}" +done +checkSuccessOrFailWithCode $? 3 + +echo -n "Preparing broker deployment..." +sed -i -e "s##${AUTH_JWKS_URL}#" \ + -e "s##${HUB_AUTH_ROBOT_ID}#" \ + -e "s##${HUB_AUTH_BASE_URL:-"https://auth.privateaim.net"}#" \ + -e "s##${HUB_BASE_URL:-"https://api.privateaim.net"}#" \ + "${WORK_DIR}/broker-deployment.yml" +checkSuccessOrFailWithCode $? 4 + +echo -n "Preparing hub auth secret..." +sed -i -e "s##$(echo -n ${ROBOT_SECRET} | base64)#" \ + "${WORK_DIR}/hub-auth-secret.yml" +checkSuccessOrFailWithCode $? 5 + +echo -n "Preparing ingress..." +sed -i -e "s##${NODE_MESSAGE_BROKER_HOST}#" \ + "${WORK_DIR}/ingress.yml" +checkSuccessOrFailWithCode $? 6 + + +echo -n "Deleting previous image..." +minikube image rm docker.io/flame/node-message-broker:latest >/dev/null 2>&1 +checkSuccessOrFailWithCode $? 7 + +echo -n "Creating Docker image..." +minikube image build -t docker.io/flame/node-message-broker:latest "${BASE_DIR}/.." >/dev/null 2>&1 +checkSuccessOrFailWithCode $? 8 + +echo -n "Applying manifest files..." +# TODO: make namespace adjustable!!! +minikube kubectl -- --namespace "${NAMESPACE}" apply -f "${WORK_DIR}/hub-auth-secret.yml" \ + -f "${WORK_DIR}/broker-db-service.yml" \ + -f "${WORK_DIR}/broker-db-statefulset.yml" \ + -f "${WORK_DIR}/broker-service.yml" \ + -f "${WORK_DIR}/broker-deployment.yml" \ + -f "${WORK_DIR}/ingress.yml" >/dev/null 2>&1 +checkSuccessOrFailWithCode $? 9 + +echo -n "Deleting temporary working directory..." +rm -Rf "${WORK_DIR}" +checkSuccessOrFailWithCode $? 10 diff --git a/k8s/manifests/broker-db-service.yml b/k8s/manifests/broker-db-service.yml new file mode 100644 index 0000000..bfcdb85 --- /dev/null +++ b/k8s/manifests/broker-db-service.yml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: node-message-broker-db +spec: + selector: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: database + app.kubernetes.io/part-of: flame + ports: + - port: 27017 + targetPort: 27017 diff --git a/k8s/manifests/broker-db-statefulset.yml b/k8s/manifests/broker-db-statefulset.yml new file mode 100644 index 0000000..e899d2f --- /dev/null +++ b/k8s/manifests/broker-db-statefulset.yml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: node-message-broker-db + labels: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: database + app.kubernetes.io/part-of: flame +spec: + serviceName: "node-message-broker-db" + updateStrategy: + rollingUpdate: + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: database + app.kubernetes.io/part-of: flame + replicas: 1 + template: + metadata: + labels: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: database + app.kubernetes.io/part-of: flame + spec: + restartPolicy: "Always" + containers: + - name: node-message-broker-db + image: mongo:7.0.5@sha256:fcde2d71bf00b592c9cabab1d7d01defde37d69b3d788c53c3bc7431b6b15de8 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + ports: + - containerPort: 27017 + env: + - name: MONGO_INITDB_DATABASE + value: "message-broker" + - name: TZ + value: "Europe/Berlin" + resources: + requests: + memory: "256Mi" + limits: + memory: "1Gi" + cpu: "1" + readinessProbe: + exec: + command: + - mongosh + - --eval + - 'db.runCommand("ping").ok' + - --quiet + failureThreshold: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 15 + initialDelaySeconds: 30 + livenessProbe: + exec: + command: + - mongosh + - --eval + - 'db.runCommand("ping").ok' + - --quiet + failureThreshold: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 15 + + volumeMounts: + - name: storage + mountPath: /data/db + volumeClaimTemplates: + - metadata: + name: storage + spec: + accessModes: ["ReadWriteMany"] + # We are not using this to make use of the default storage class + # This however, should be changed in the future. + # storageClassName: + resources: + requests: + storage: 100Mi diff --git a/k8s/manifests/broker-deployment.yml b/k8s/manifests/broker-deployment.yml new file mode 100644 index 0000000..6c9bed7 --- /dev/null +++ b/k8s/manifests/broker-deployment.yml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: node-message-broker + labels: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: server + app.kubernetes.io/part-of: flame +spec: + revisionHistoryLimit: 3 + selector: + matchLabels: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: server + app.kubernetes.io/part-of: flame + template: + metadata: + labels: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: server + app.kubernetes.io/part-of: flame + spec: + restartPolicy: "Always" + containers: + - name: node-message-broker + image: docker.io/flame/node-message-broker:latest + imagePullPolicy: "IfNotPresent" + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + ports: + - containerPort: 8080 + env: + - name: SERVER_PORT + value: "8080" + - name: AUTH_JWKS_URL + value: + - name: MONGO_DB_URL + value: "mongodb://node-message-broker-db:27017" + - name: MONGO_DB_NAME + value: "message-broker" + - name: HUB_BASE_URL + value: + - name: HUB_AUTH_BASE_URL + value: + - name: HUB_AUTH_ROBOT_ID + value: + - name: HUB_AUTH_ROBOT_SECRET + valueFrom: + secretKeyRef: + name: hub-auth + key: robot-secret + # DO NOT USE THIS IN PRODUCTION!!! This is just for internal testing purposes. + - name: NODE_TLS_REJECT_UNAUTHORIZED + value: "0" + resources: + requests: + memory: "256Mi" + limits: + memory: "512Mi" + cpu: "500m" + readinessProbe: + httpGet: + path: "/health" + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 5 + timeoutSeconds: 10 + livenessProbe: + httpGet: + path: "/health" + port: 8080 + periodSeconds: 10 + timeoutSeconds: 10 diff --git a/k8s/manifests/broker-service.yml b/k8s/manifests/broker-service.yml new file mode 100644 index 0000000..0737dcf --- /dev/null +++ b/k8s/manifests/broker-service.yml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: node-message-broker +spec: + selector: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: server + app.kubernetes.io/part-of: flame + ports: + - port: 80 + targetPort: 8080 diff --git a/k8s/manifests/hub-auth-secret.yml b/k8s/manifests/hub-auth-secret.yml new file mode 100644 index 0000000..5d9d8a4 --- /dev/null +++ b/k8s/manifests/hub-auth-secret.yml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: hub-auth +data: + robot-secret: diff --git a/k8s/manifests/ingress.yml b/k8s/manifests/ingress.yml new file mode 100644 index 0000000..08296a7 --- /dev/null +++ b/k8s/manifests/ingress.yml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: node-message-broker + labels: + app.kubernetes.io/name: node-message-broker + app.kubernetes.io/component: server + app.kubernetes.io/part-of: flame +spec: + rules: + - host: message-broker..nip.io + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: node-message-broker + port: + number: 80