From f25cf886b59fcebd412535406b452c47b601cde2 Mon Sep 17 00:00:00 2001
From: David Bauer <david.bauer@open-xchange.com>
Date: Tue, 18 Apr 2023 16:59:14 +0200
Subject: [PATCH] Change: Redis Configuration

This is a breaking change.

Changelog:
- Redis is now mandatory and enabled by default
- Added Sentinel, Standalone and Cluster support
- Standalone mode with sidecar is used by default
  - If no host(s) are configured, a redis sidecar is injected into the deployment
  - No extra configuration needed
  - This is intended for development and does not scale.
- Helm values have changed, see README.md:
  - `redis.host` has been renamed to `redis.hosts` and expects a list of strings with the following pattern: <localhost:6379> (needed for sentinel and cluster modes)
  - `redis.port` has been removed, see `redis.hosts`
---
 .env.defaults                                 |  5 +-
 .gitlab-ci.yml                                |  3 -
 .gitlab/preview-prefix/values-template.yaml   |  7 --
 .gitlab/preview/Chart.yaml                    |  5 +-
 .gitlab/preview/values-template.yaml          |  6 --
 .gitlab/preview/values.yaml                   | 29 +------
 README.md                                     | 84 +++++++------------
 helm/core-ui-middleware/Chart.yaml            |  2 +-
 .../templates/deployment.yaml                 | 33 ++++++--
 helm/core-ui-middleware/values.yaml           | 10 ++-
 src/redis.js                                  | 32 +++++--
 11 files changed, 96 insertions(+), 120 deletions(-)

diff --git a/.env.defaults b/.env.defaults
index cce505c..0131a28 100644
--- a/.env.defaults
+++ b/.env.defaults
@@ -8,8 +8,9 @@ EXPOSE_API_DOCS=false
 COMPRESS_FILE_SIZE=600
 COMPRESS_FILE_TYPES=application/javascript application/json application/x-javascript application/xml application/xml+rss text/css text/html text/javascript text/plain text/xml image/svg+xml
 
-REDIS_PORT=6379
+REDIS_MODE=standalone
+REDIS_SENTINEL_MASTER_ID=mymaster
 REDIS_DB=0
 REDIS_PREFIX=ui-middleware
-REDIS_HOST=localhost
+REDIS_HOSTS=localhost:6379
 ORIGINS=*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5c6a88a..165dc3d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,9 +16,6 @@ integration tests:
   script:
     - yarn --non-interactive --no-progress -s
     - yarn integration --ci --coverage
-  variables:
-    # app specific settings
-    REDIS_HOST: redis
 
 deploy preview with prefix:
   extends: .auto-deploy-preview-chart
diff --git a/.gitlab/preview-prefix/values-template.yaml b/.gitlab/preview-prefix/values-template.yaml
index 39b8f55..5d03dcf 100644
--- a/.gitlab/preview-prefix/values-template.yaml
+++ b/.gitlab/preview-prefix/values-template.yaml
@@ -3,17 +3,10 @@ imagePullSecrets:
 
 appRoot: /appsuite/
 
-ingress:
-  enabled: false
 image:
   registry: ${CI_REGISTRY}
   repository: frontend/ui-middleware
   tag: ${TAG_NAME}
   pullPolicy: Always
 
-redis:
-  enabled: true
-  host: preview-app-redis-master.${PREVIEW_APP_NAME}.svc.cluster.local
-  prefix: ${CI_COMMIT_REF_SLUG}-${OX_COMPONENT}-prefix
-
 existingConfigMap: preview-prefix-core-ui-middleware
diff --git a/.gitlab/preview/Chart.yaml b/.gitlab/preview/Chart.yaml
index 286a220..5d0bf4e 100644
--- a/.gitlab/preview/Chart.yaml
+++ b/.gitlab/preview/Chart.yaml
@@ -30,7 +30,4 @@ dependencies:
   - name: core-ui-middleware
     version: ">=1.0.0-0"
     repository: "file://../../helm/core-ui-middleware"
-  - name: redis
-    version: ^17.9.0
-    repository: https://charts.bitnami.com/bitnami
-    condition: redis.enabled, global.redis.enabled
+
diff --git a/.gitlab/preview/values-template.yaml b/.gitlab/preview/values-template.yaml
index 673e369..c66f4c2 100644
--- a/.gitlab/preview/values-template.yaml
+++ b/.gitlab/preview/values-template.yaml
@@ -2,10 +2,6 @@ core-ui-middleware:
   image:
     repository: ${CI_REGISTRY_IMAGE}
     tag: ${TAG_NAME}
-  redis:
-    enabled: true
-    host: preview-app-redis-master.${PREVIEW_APP_NAME}.svc.cluster.local
-    prefix: ${CI_COMMIT_REF_SLUG}-${OX_COMPONENT}
 host: ${PREVIEW_APP_NAME}.dev.oxui.de
 
 appsuite:
@@ -14,8 +10,6 @@ appsuite:
       commit-ref: "${CI_COMMIT_SHORT_SHA}"
     deploymentAnnotations:
       commit-ref: "${CI_COMMIT_SHORT_SHA}"
-    redis:
-      prefix: ${CI_COMMIT_REF_SLUG}-${OX_COMPONENT}
   core-guidedtours:
     podAnnotations:
       commit-ref: "${CI_COMMIT_SHORT_SHA}"
diff --git a/.gitlab/preview/values.yaml b/.gitlab/preview/values.yaml
index 628d8ec..bcca0d2 100644
--- a/.gitlab/preview/values.yaml
+++ b/.gitlab/preview/values.yaml
@@ -5,10 +5,6 @@ global:
 core-ui-middleware:
   replicaCount: 1
   existingConfigMap: preview-core-ui-middleware
-  containerPort: 8080
-
-  ingress:
-    enabled: false
 
   defaultRegistry: ""
 
@@ -26,31 +22,10 @@ core-ui-middleware:
   resources:
     limits:
       cpu: 1
-      memory: 196Mi
+      memory: 384Mi
     requests:
       cpu: 100m
-      memory: 196Mi
-
-redis:
-  enabled: true
-  architecture: standalone
-  auth:
-    enabled: false
-  master:
-    persistence:
-      enabled: false
-  replica:
-    persistence:
-      enabled: false
-  metrics:
-    enabled: true
-  resources:
-    requests:
-      memory: 128Mi
-      cpu: 100m
-    limits:
-      memory: 256Mi
-      cpu: 250m
+      memory: 384Mi
 
 appsuite:
   core-ui-middleware:
diff --git a/README.md b/README.md
index 3dffb39..5e3a084 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# ui-middleware
+# UI Middleware
 
 > Provides the collected manifest.json of services in a cluster as well as a list of dependencies
 > for each source file. This information can be used to dynamically compile all ui components at
@@ -7,36 +7,23 @@
 
 ## Architecture
 
-The ui-middleware is, as the name already suggests, a middleware component to support the App Suite UI.
-As an installation of App Suite UI can consist of multiple projects, like the [Core App Suite UI](https://gitlab.open-xchange.com/frontend/ui), [Documents UI]([https://gitlab.open-xchange.com/documents/office-web), [OX Guard UI](https://gitlab.open-xchange.com/appsuite/guard-ui), and other custom plugins, those components need to be combined at some point to be served to the browser.
+The UI Middleware is, as the name already suggests, a middleware component to support the App Suite UI.
+As an installation of App Suite UI can consist of multiple projects, like the [Core App Suite UI](https://gitlab.open-xchange.com/frontend/ui), [Documents UI](https://gitlab.open-xchange.com/documents/office-web), [OX Guard UI](https://gitlab.open-xchange.com/appsuite/guard-ui), and other custom plugins, those components need to be combined at some point to be served to the browser.
 This is handled by the ui-middleware component, by putting it between the Ingress Controller and the containers serving the individual components that make up the App Suite UI.
 
 If you prefer a picture, this is, how such an installation can look like.
 
 ![UI Middleware architecture](./docs/ui-mw-architecture.png)
 
-## Endpoint
+## Prerequisites
 
-- `/manifests`
-- `/dependencies`
+- A Redis server is required (standalone, sentinel and cluster mode supported)
 
 ## Deployments
 
-**local**
+### Kubernetes
 
-- adjust `.env`
-- run `yarn dev`
-
-**docker**
-
-```
-docker build -t ui-middleware .
-docker run -t -i -p 8080:8080 ui-middleware
-```
-
-**kubernetes**
-
-```
+```shell
 cd helm
 helm upgrade -i -f ui-middleware/values.yaml -f values/develop.yaml ui-middleware ./ui-middleware
 ```
@@ -45,39 +32,24 @@ It is possible to horizontally scale the UI Middleware, as more clients are fetc
 
 ## Configuration
 
-**local, docker**
-
-| Parameter                | Description                       | Default  |
-|--------------------------|-----------------------------------|----------|
-| `PORT`                   | Exposed port                      | `"8080"` |
-| `CACHE_TTL`              | Vite manifest caching time        | `30000`  |
-| `LOG_LEVEL`              | Pino log level                    | `"info"` |
-| `REDIS_HOST`             | Redis host (required)             |          |
-| `REDIS_PORT`             | Redis port (optional)             | `6379`   |
-| `REDIS_DB`               | Redis DB, e.g. `"1"` (optional)   | null     |
-| `REDIS_PASSWORD`         | Redis password (optional)         | null     |
-| `COMPRESS_FILE_SIZE`     | Larger files will be gzipped      | `600`    |
-| `COMPRESS_FILE_TYPES`    | Set of compression mime types     |see values|
-| `SLOW_REQUEST_THRESHOLD` | Slow request threshold in ms      | `4000`   |
-
-
-**kubernetes**
-
-| Parameter             | Description                        | Default  |
-|-----------------------|------------------------------------|----------|
-| `port`                | Exposed port                       | `"8080"` |
-| `cacheTTL`            | Vite manifest caching time         | `30000`  |
-| `logLevel`            | Pino log level                     | `"info"` |
-| `redis.enabled`       | Global switch Redis integration    |  false   |
-| `redis.host`          | Redis host                         |          |
-| `redis.port`          | Redis port (optional)              | `6379`   |
-| `redis.db`            | Redis DB, e.g. `"1"` (optional)    | null     |
-| `redis.password`      | Redis password (optional)          | null     |
-| `compressFileSize`    | Larger files will be gzipped       | `600`    |
-| `compressFileTypes`   | Set of compression mime types      |see values|
-| `slowRequestThreshold`| Slow request threshold in ms       | `4000`   |
-
-**config map**
+| Helm Variable (values)   | Environment Variable       | Description                                  | Default              |
+|--------------------------|----------------------------|----------------------------------------------|----------------------|
+| `port`                   | `PORT`                     | Exposed port                                 | `"8080"`             |
+| `cacheTTL`               | `CACHE_TTL`                | Vite manifest caching time                   | `30000`              |
+| `logLevel`               | `LOG_LEVEL`                | Pino log level                               | `"info"`             |
+| `redis.mode`             | `REDIS_MODE`               | Redis mode (standalone, sentinel or cluster) | `"standalone"`       |
+| `redis.sentinelMasterId` | `REDIS_SENTINEL_MASTER_ID` | Name of the `sentinel` masterSet             | `"mymaster"`         |
+| `redis.hosts`            | N/A (see below)            | Redis hosts as list                          | `["localhost:6379"]` |
+| N/A (see above)          | `REDIS_HOSTS`              | Redis hosts as string                        | `"localhost:6379"`   |
+| `redis.db`               | `REDIS_DB`                 | Redis DB, e.g. `"1"`                         | null                 |
+| `redis.prefix`           | `REDIS_PREFIX`             | Redis prefix                                 | `"ui-middleware"`    |
+| `redis.password`         | `REDIS_PASSWORD`           | Redis password                               | null                 |
+| `redis.sidecar.image`    | N/A                        | Redis sidecar image                          | `"redis:latest"`     |
+| `compressFileSize`       | `COMPRESS_FILE_SIZE`       | Larger files will be gzipped                 | `600`                |
+| `compressFileTypes`      | `COMPRESS_FILE_TYPES`      | Set of compression mime types                | application/javascript application/json application/x-javascript application/xml application/xml+rss text/css text/html text/javascript text/plain text/xml image/svg+xml |
+| `slowRequestThreshold`   | `SLOW_REQUEST_THRESHOLD`   | Slow request threshold in ms                 | `4000`               |
+
+### Config Map
 
 ```yaml
 # List of urls where to find ui containers
@@ -103,6 +75,10 @@ It is mandatory to load the UI over https with a valid certificate and there are
 - The UI needs a **service worker** to function. The service worker is used for file-caching and version-mismatch detection (the "reload"-banner). **Service workers** are only loaded over https with valid certificate to prevent man-in-the-middle attacks.
 - The UI consists of many small files, because it cannot be bundled as before. To improve file transfer time, HTTP/2 is required which requires https.
 - The **service worker** also increases boot time by loading zipped chunks of files and puts them in the browser-cache on initial boot. Without https no **service worker**, without **service worker** no zipped loading.
-- To reduce the transferred file-size, the **UI-middleware** uses **brotli** encoding for every transferred file (except the index.html). The UI-middleware will not check for accept-enconding headers due to performance reasons. Modern browsers will not load brotli over insecure connections and the UI won't load without it.
+- To reduce the transferred file-size, the **UI-middleware** uses **brotli** encoding for every transferred file (except the index.html). The UI-middleware will not check for accept-encoding headers due to performance reasons. Modern browsers will not load brotli over insecure connections and the UI won't load without it.
 
 It is noteworthy that localhost is an exception to the above rules and the UI including **service workers** and preloading (but not http2) will work on localhost for development environments.
+
+## Redis default setup
+
+For setups where scaling of the UI middleware is not necessary you can use the default values of the helm chart, which will deploy a redis server as a sidecar container.
diff --git a/helm/core-ui-middleware/Chart.yaml b/helm/core-ui-middleware/Chart.yaml
index 503e899..74a553b 100644
--- a/helm/core-ui-middleware/Chart.yaml
+++ b/helm/core-ui-middleware/Chart.yaml
@@ -15,7 +15,7 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 1.6.0
+version: 2.0.0
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
diff --git a/helm/core-ui-middleware/templates/deployment.yaml b/helm/core-ui-middleware/templates/deployment.yaml
index 450d95c..0b84011 100644
--- a/helm/core-ui-middleware/templates/deployment.yaml
+++ b/helm/core-ui-middleware/templates/deployment.yaml
@@ -35,18 +35,16 @@ spec:
               value: "{{ .Values.compressFileTypes }}"
             - name: SLOW_REQUEST_THRESHOLD
               value: "{{ .Values.slowRequestThreshold }}"
-            {{- if .Values.redis.enabled }}
-            - name: REDIS_HOST
-              value: "{{ required "redis.host required" .Values.redis.host }}"
-            - name: REDIS_PORT
-              value: "{{ .Values.redis.port | default 6379 }}"
+            - name: REDIS_MODE
+              value: "{{ .Values.redis.mode }}"
+            - name: REDIS_HOSTS
+              value: "{{ .Values.redis.hosts | join "," }}"
             - name: REDIS_DB
               value: "{{ .Values.redis.db | int }}"
             - name: REDIS_PASSWORD
               value: "{{ .Values.redis.password }}"
             - name: REDIS_PREFIX
               value: "{{ .Values.redis.prefix }}"
-            {{- end }}
           ports:
             - name: http
               containerPort: {{ .Values.containerPort | default 8080 }}
@@ -73,8 +71,29 @@ spec:
           volumeMounts:
             - name: manifest-config
               mountPath: /app/config/
+        {{- if and (eq (len .Values.redis.hosts) 1) (eq (index .Values.redis.hosts 0) "localhost:6379") }}
+        - name: redis
+          image: {{ .Values.redis.sidecar.image }}
+          ports:
+            - name: redis
+              containerPort: 6379
+              protocol: TCP
+          volumeMounts:
+            - name: redis-data
+              mountPath: /data
+          resources:
+            limits:
+              memory: 96Mi
+            requests:
+              memory: 96Mi
+        {{- end }}
       volumes:
         - name: manifest-config
           configMap:
             name: {{ .Values.existingConfigMap | default (include "ox-common.names.fullname" .) }}
-
+        {{- if and (eq (len .Values.redis.hosts) 1) (eq (index .Values.redis.hosts 0) "localhost:6379") }}
+        - name: redis-data
+          emptyDir:
+            sizeLimit: 96Mi
+            medium: Memory
+        {{- end }}
diff --git a/helm/core-ui-middleware/values.yaml b/helm/core-ui-middleware/values.yaml
index 5bd638c..e7ecf74 100644
--- a/helm/core-ui-middleware/values.yaml
+++ b/helm/core-ui-middleware/values.yaml
@@ -116,9 +116,13 @@ extras:
     enabled: false
 
 redis:
-  enabled: false
-  host: ''
-  port: 6379
+  mode: 'standalone' # Other values: sentinel, cluster
+  hosts:
+    - localhost:6379
   db: 0
   password: null
   prefix: ui-middleware
+  # This is only used for development. A sidecar container is started in the pod.
+  # Note: This does not scale!
+  sidecar:
+    image: redis:latest
diff --git a/src/redis.js b/src/redis.js
index 6c78ae5..59b9adf 100644
--- a/src/redis.js
+++ b/src/redis.js
@@ -5,15 +5,35 @@ import { registerLatestVersionListener, updateVersionProcessor } from './version
 
 const commonQueueOptions = { enableReadyCheck: false, maxRetriesPerRequest: null }
 
-const createClient = (type, options = {}) => {
-  const client = new Redis({
-    host: process.env.REDIS_HOST,
-    port: Number(process.env.REDIS_PORT),
+const hosts = (process.env.REDIS_HOSTS || '').split(',').map(host => {
+  const [hostname, port] = host.split(':')
+  return { host: hostname, port: Number(port) }
+})
+
+function createClient (id, options = {}) {
+  options = {
     db: Number(process.env.REDIS_DB),
     password: process.env.REDIS_PASSWORD,
     ...options
-  })
-  client.on('ready', () => logger.info(`[Redis] Connected ${type} to redis on ${process.env.REDIS_HOST}`))
+  }
+
+  if (process.env.REDIS_MODE === 'sentinel') {
+    options = {
+      sentinels: hosts,
+      name: process.env.REDIS_SENTINEL_MASTER_ID,
+      ...options
+    }
+  } else if (process.env.REDIS_MODE === 'standalone') {
+    options = {
+      ...hosts[0],
+      ...options
+    }
+  }
+  const client = process.env.REDIS_MODE === 'cluster'
+    ? new Redis.Cluster(hosts, { redisOptions: options })
+    : new Redis(options)
+
+  client.on('ready', () => logger.info(`[Redis] Connected ${id} to redis on ${process.env.REDIS_HOSTS}`))
   client.on('error', (err) => logger.error(`[Redis] Connect error: ${err}`))
 
   return client
-- 
GitLab