From b89935c334e000368e42c853a9db33da3569ca66 Mon Sep 17 00:00:00 2001 From: Wayne Beaton Date: Tue, 22 Jul 2025 19:39:59 -0400 Subject: [PATCH 01/50] Add email contact information. --- website/src/about.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/about.tsx b/website/src/about.tsx index 428635d7e..470565b48 100644 --- a/website/src/about.tsx +++ b/website/src/about.tsx @@ -42,6 +42,12 @@ const About = () => { Contact Us + + To report malicious extensions, extensions with known vulnerabilities, or other urgent matters, connect with us via + marketplace@eclipse-foundation.org. + For non-urgent matters, including managing namespaces or questions about publishing extensions, please visit the + wiki + We use Slack for instant messaging and general communication, use this link to join our Slack workspace. From 11a7de1fa2a4f48a5c5e6f71ecddb47ba238267e Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 8 Jul 2025 11:29:13 +0300 Subject: [PATCH 02/50] Test Redis cluster on staging --- .gitignore | 1 + Dockerfile | 6 +- charts/openvsx/Chart.lock | 9 ++ charts/openvsx/Chart.yaml | 9 +- charts/openvsx/templates/configmap.yaml | 65 ---------- charts/openvsx/templates/deployment.yaml | 33 +---- charts/openvsx/templates/grafana-alloy.yaml | 99 ++++++++++++++ charts/openvsx/templates/service-monitor.yaml | 21 +++ charts/openvsx/templates/service.yaml | 4 + charts/openvsx/values-staging.yaml | 96 +++++++++++++- configuration/application.yml | 13 +- configuration/ehcache.xml | 121 ------------------ 12 files changed, 249 insertions(+), 228 deletions(-) create mode 100644 charts/openvsx/Chart.lock delete mode 100644 charts/openvsx/templates/configmap.yaml create mode 100644 charts/openvsx/templates/grafana-alloy.yaml create mode 100644 charts/openvsx/templates/service-monitor.yaml delete mode 100644 configuration/ehcache.xml diff --git a/.gitignore b/.gitignore index eaabf8fab..0e09140f7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /generated-staging/ /jsonnet-generated-production/ /jsonnet-generated-staging/ +/charts/openvsx/charts diff --git a/Dockerfile b/Dockerfile index 86935a796..21746f27b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=v0.27.0 +ARG SERVER_VERSION=5dd4aa28 # Builder image to compile the website FROM ubuntu AS builder @@ -27,13 +27,13 @@ RUN /usr/bin/yarn --cwd website \ && /usr/bin/yarn --cwd website build # Main image derived from openvsx-server -FROM ghcr.io/eclipse/openvsx-server:${SERVER_VERSION} +# FROM ghcr.io/eclipse/openvsx-server:${SERVER_VERSION} +FROM docker.io/amvanbaren/openvsx-server:${SERVER_VERSION} ARG SERVER_VERSION COPY --from=builder --chown=openvsx:openvsx /workdir/website/static/ BOOT-INF/classes/static/ COPY --from=builder --chown=openvsx:openvsx /workdir/configuration/application.yml config/ COPY --from=builder --chown=openvsx:openvsx /workdir/configuration/logback-spring.xml BOOT-INF/classes/ -COPY --from=builder --chown=openvsx:openvsx /workdir/configuration/ehcache.xml BOOT-INF/classes/ # Replace version placeholder with arg value RUN sed -i "s//$SERVER_VERSION/g" config/application.yml \ No newline at end of file diff --git a/charts/openvsx/Chart.lock b/charts/openvsx/Chart.lock new file mode 100644 index 000000000..c269ee46f --- /dev/null +++ b/charts/openvsx/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: redis-cluster + repository: https://charts.bitnami.com/bitnami + version: 12.0.12 +- name: alloy + repository: https://grafana.github.io/helm-charts + version: 1.1.2 +digest: sha256:e9f71eed4e319e79393873684f56db74c9db3408604c934755aecbd0ff9a457e +generated: "2025-08-04T14:57:58.074328516+03:00" diff --git a/charts/openvsx/Chart.yaml b/charts/openvsx/Chart.yaml index ed99222dd..c85a61e56 100644 --- a/charts/openvsx/Chart.yaml +++ b/charts/openvsx/Chart.yaml @@ -2,5 +2,12 @@ apiVersion: v2 name: openvsx description: A Helm chart for Kubernetes type: application -version: 0.1.0 +version: 0.2.0 appVersion: "1.16.0" +dependencies: + - name: redis-cluster + version: 12.0.12 + repository: https://charts.bitnami.com/bitnami + - name: alloy + version: 1.1.2 + repository: https://grafana.github.io/helm-charts \ No newline at end of file diff --git a/charts/openvsx/templates/configmap.yaml b/charts/openvsx/templates/configmap.yaml deleted file mode 100644 index 77f3b0200..000000000 --- a/charts/openvsx/templates/configmap.yaml +++ /dev/null @@ -1,65 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - labels: - app: {{ .Values.name }} - environment: {{ .Values.environment }} - name: grafana-agent-configmap-{{ .Values.environment }} - namespace: {{ .Values.namespace }} -data: - agent.yml: | - server: - log_level: debug - integrations: - prometheus_remote_write: - - basic_auth: - password: ${PROMETHEUS_PASSWORD} - username: ${PROMETHEUS_USERNAME} - url: ${PROMETHEUS_URL} - agent: - enabled: true - relabel_configs: - - action: replace - source_labels: - - agent_hostname - target_label: instance - - action: replace - target_label: job - replacement: integrations/agent-check - metric_relabel_configs: - - action: keep - regex: (prometheus_target_sync_length_seconds_sum|prometheus_target_scrapes_.*|prometheus_target_interval.*|prometheus_sd_discovered_targets|agent_build.*|agent_wal_samples_appended_total|process_start_time_seconds) - source_labels: - - __name__ - metrics: - configs: - - name: integrations - remote_write: - - basic_auth: - password: ${PROMETHEUS_PASSWORD} - username: ${PROMETHEUS_USERNAME} - url: ${PROMETHEUS_URL} - scrape_configs: - - job_name: integrations/spring-boot - static_configs: - - targets: ['localhost:8081'] - metrics_path: /actuator/prometheus - metric_relabel_configs: - - source_labels: [exported_instance] - target_label: instance - - regex: ^(exported_instance|deployment_environment|service_instance_id|service_name)$ - action: labeldrop - global: - scrape_interval: 60s - traces: - configs: - - name: default - remote_write: - - endpoint: ${TEMPO_URL} - basic_auth: - username: ${TEMPO_USERNAME} - password: ${TEMPO_PASSWORD} - receivers: - zipkin: - endpoint: localhost:9411 - parse_string_tags: false diff --git a/charts/openvsx/templates/deployment.yaml b/charts/openvsx/templates/deployment.yaml index c9bdaa4d4..4f0a68a37 100644 --- a/charts/openvsx/templates/deployment.yaml +++ b/charts/openvsx/templates/deployment.yaml @@ -77,29 +77,7 @@ spec: path: /actuator/health/readiness port: http-management periodSeconds: 10 - - name: grafana-agent - image: docker.io/grafana/agent:v0.39.1 - command: - - /bin/grafana-agent - args: - - --config.file=$(CONFIG_FILE_PATH) - - --metrics.wal-directory=$(DATA_FILE_PATH) - - --config.expand-env=true - env: - - name: CONFIG_FILE_PATH - value: /etc/grafana-agent/agent.yml - - name: DATA_FILE_PATH - value: /etc/grafana-agent/data - - name: ENVNAME - value: {{ .Values.environment }} - envFrom: - - secretRef: - name: grafana-cloud-secret-{{ .Values.environment }} - volumeMounts: - - name: grafana-agent-config-volume - mountPath: /etc/grafana-agent - - name: grafana-agent-data-volume - mountPath: /etc/grafana-agent/data + initialDelaySeconds: 30 initContainers: - command: - sh @@ -131,15 +109,6 @@ spec: defaultMode: 420 optional: false secretName: elasticsearch-{{ .Values.environment }}-es-http-certs-internal - - name: grafana-agent-config-volume - configMap: - name: grafana-agent-configmap-{{ .Values.environment }} - items: - - key: agent.yml - path: agent.yml - - name: grafana-agent-data-volume - emptyDir: - medium: Memory - name: truststore-with-elasticsearch-certs emptyDir: medium: Memory diff --git a/charts/openvsx/templates/grafana-alloy.yaml b/charts/openvsx/templates/grafana-alloy.yaml new file mode 100644 index 000000000..95cb4eb9f --- /dev/null +++ b/charts/openvsx/templates/grafana-alloy.yaml @@ -0,0 +1,99 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: {{ .Values.name }} + environment: {{ .Values.environment }} + name: {{ .Values.alloy.alloy.configMap.name }} + namespace: {{ .Values.namespace }} +data: + {{ .Values.alloy.alloy.configMap.key }}: | + logging { + level = "debug" + format = "logfmt" + } + + prometheus.operator.servicemonitors "spring_boot" { + namespaces = ["{{ .Values.namespace }}"] + selector { + match_labels = { + app = "{{ .Values.name }}", + environment = "{{ .Values.environment }}", + } + } + + forward_to = [prometheus.relabel.spring_boot_relabel.receiver] + } + + prometheus.relabel "spring_boot_relabel" { + rule { + source_labels = ["exported_instance"] + target_label = "instance" + action = "replace" + } + + rule { + regex = "^(exported_instance|deployment_environment|service_instance_id|service_name)$" + action = "labeldrop" + } + + forward_to = [prometheus.remote_write.default.receiver] + } + + prometheus.operator.servicemonitors "redis" { + namespaces = ["{{ index .Values "redis-cluster" "namespaceOverride" }}"] + selector { + match_labels = { + app = "{{ index .Values "redis-cluster" "commonLabels" "app" }}", + environment = "{{ index .Values "redis-cluster" "commonLabels" "environment" }}", + } + } + + forward_to = [prometheus.remote_write.default.receiver] + } + + prometheus.remote_write "default" { + endpoint { + name = "hosted-prometheus" + url = sys.env("PROMETHEUS_URL") + + basic_auth { + username = sys.env("PROMETHEUS_USERNAME") + password = sys.env("PROMETHEUS_PASSWORD") + } + } + } + + otelcol.receiver.zipkin "default" { + endpoint = "0.0.0.0:9411" + output { + traces = [otelcol.processor.memory_limiter.default.input] + } + } + + otelcol.processor.memory_limiter "default" { + check_interval = "1s" + limit = "400MiB" + spike_limit = "80MiB" + output { + traces = [otelcol.processor.batch.default.input] + } + } + + otelcol.processor.batch "default" { + output { + traces = [otelcol.exporter.otlp.tempo.input] + } + } + + otelcol.auth.basic "tempo_auth" { + username = sys.env("TEMPO_USERNAME") + password = sys.env("TEMPO_PASSWORD") + } + + otelcol.exporter.otlp "tempo" { + client { + endpoint = sys.env("TEMPO_URL") + auth = otelcol.auth.basic.tempo_auth.handler + } + } diff --git a/charts/openvsx/templates/service-monitor.yaml b/charts/openvsx/templates/service-monitor.yaml new file mode 100644 index 000000000..14a5d8196 --- /dev/null +++ b/charts/openvsx/templates/service-monitor.yaml @@ -0,0 +1,21 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + app: {{ .Values.name }} + environment: {{ .Values.environment }} + name: spring-boot-monitor-{{ .Values.environment }} + namespace: {{ .Values.namespace }} +spec: + selector: + matchLabels: + app: {{ .Values.name }} + environment: {{ .Values.environment }} + type: website + namespaceSelector: + matchNames: + - {{ .Values.namespace }} + endpoints: + - port: http + path: /actuator/prometheus + interval: 15s \ No newline at end of file diff --git a/charts/openvsx/templates/service.yaml b/charts/openvsx/templates/service.yaml index 68ebaba65..3672f75ca 100644 --- a/charts/openvsx/templates/service.yaml +++ b/charts/openvsx/templates/service.yaml @@ -13,6 +13,10 @@ spec: port: {{ .Values.service.port }} protocol: TCP targetPort: {{ .Values.service.targetPort }} + - name: management + port: 8081 + protocol: TCP + targetPort: 8081 selector: app: {{ .Values.name }} environment: {{ .Values.environment }} diff --git a/charts/openvsx/values-staging.yaml b/charts/openvsx/values-staging.yaml index a671a788e..d7e5f1f75 100644 --- a/charts/openvsx/values-staging.yaml +++ b/charts/openvsx/values-staging.yaml @@ -1,8 +1,8 @@ # Default values for openvsx staging. -name: open-vsx-org -environment: staging -namespace: open-vsx-org +name: &name open-vsx-org +environment: &environment staging +namespace: &namespace open-vsx-org host: staging.open-vsx.org replicaCount: 1 @@ -45,3 +45,93 @@ es: memory: 2Gi storage_class: cephfs-new +# redis +redis-cluster: + cluster: + nodes: 6 + existingSecret: redis-secret-staging + existingSecretPasswordKey: REDIS_REPLICA_PASSWORD + persistence: + enabled: true + size: 128Mi # 1 Gi for production + storageClass: "cephfs-new" + accessModes: + - ReadWriteOnce + metrics: + enabled: true + serviceMonitor: + enabled: true + redis: + configmap: | + maxmemory 64mb + maxmemory-policy allkeys-lru + appendonly yes + appendfsync everysec + save 900 1 300 10 60 10000 + tcp-keepalive 60 + timeout 300 + aclfile /opt/openvsx/redis/custom.acl + extraVolumes: + - name: redis-secret-staging + secret: + secretName: redis-secret-staging + - name: redis-config + emptyDir: {} + extraVolumeMounts: + - name: redis-secret-staging + mountPath: /etc/redis-users + readOnly: true + - name: redis-config + mountPath: /opt/openvsx/redis + initContainers: + - name: generate-acl + image: busybox + imagePullPolicy: Always + command: + - sh + - -c + - | + echo -e "user $(cat /etc/redis-users/REDIS_REPLICA_USERNAME) on >$(cat /etc/redis-users/REDIS_REPLICA_PASSWORD) ~* +psync +replconf +ping +cluster +info\nuser $(cat /etc/redis-users/REDIS_OPENVSX_USERNAME) on >$(cat /etc/redis-users/REDIS_OPENVSX_PASSWORD) ~* +@all -@admin -@dangerous -@pubsub -@slow -function +client +cluster +keys +eval +set" > /opt/openvsx/redis/custom.acl + volumeMounts: + - name: redis-secret-staging + mountPath: /etc/redis-users + - name: redis-config + mountPath: /opt/openvsx/redis + extraEnvVarsSecret: redis-secret-staging + resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" + fullnameOverride: redis-cluster-staging + namespaceOverride: *namespace + commonLabels: + app: redis-cluster + environment: *environment + +# grafana alloy +alloy: + alloy: + configMap: + create: false + name: "grafana-alloy-configmap-staging" + key: "config.river" + envFrom: + - secretRef: + name: grafana-cloud-secret-staging + extraPorts: + - name: zipkin + port: 9411 + targetPort: 9411 + stabilityLevel: experimental + crds: + create: true + controller: + type: "daemonset" + podLabels: + app: *name + environment: *environment + fullnameOverride: grafana-alloy-staging + namespaceOverride: *namespace diff --git a/configuration/application.yml b/configuration/application.yml index 80c06ad42..a5c8c01ee 100644 --- a/configuration/application.yml +++ b/configuration/application.yml @@ -27,9 +27,12 @@ spring: name: openvsx-server config: import: file:${DEPLOYMENT_CONFIG} - cache: - jcache: - config: classpath:ehcache.xml + data: + redis: + cluster: + nodes: 'redis-cluster-staging-0.redis-cluster-staging-headless:6379,redis-cluster-staging-1.redis-cluster-staging-headless:6379,redis-cluster-staging-2.redis-cluster-staging-headless:6379,redis-cluster-staging-3.redis-cluster-staging-headless:6379,redis-cluster-staging-4.redis-cluster-staging-headless:6379,redis-cluster-staging-5.redis-cluster-staging-headless:6379' + username: openvsx + password: openvsx datasource: hikari: maximum-pool-size: 10 @@ -78,6 +81,9 @@ management: include: - health - prometheus + zipkin: + tracing: + endpoint: http://grafana-alloy-staging:9411/api/v2/spans tracing: sampling: probability: 0.01 @@ -105,6 +111,7 @@ org: allow-anonymous-data-usage: false bucket4j: enabled: true + cache-to-use: redis-cluster-jedis filters: - cache-name: buckets url: '/api/-/(namespace/create|publish)' diff --git a/configuration/ehcache.xml b/configuration/ehcache.xml deleted file mode 100644 index 75b4757d8..000000000 --- a/configuration/ehcache.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - 1 - 1 - 2 - - - - - 3600 - - - 1024 - 32 - 128 - - - - - 3600 - - - 1024 - - - - - 10800 - - - 8192 - 256 - 1024 - - - - - 10800 - - - 4096 - 256 - 1024 - - - - - 3600 - - - 1024 - 32 - 128 - - - - - 3600 - - - 1 - 2 - 8 - - - - - 1 - - - 1 - 2 - 8 - - - - - 1 - - - - org.eclipse.openvsx.cache.ExpiredFileListener - ASYNCHRONOUS - UNORDERED - EXPIRED - EVICTED - REMOVED - UPDATED - - - - 500 - - - - - 2 - - - - org.eclipse.openvsx.cache.ExpiredFileListener - ASYNCHRONOUS - UNORDERED - EXPIRED - EVICTED - REMOVED - UPDATED - - - - 200 - - - \ No newline at end of file From 7d9ca7a2e509348203d9d71ae9c73b21260b968c Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 5 Aug 2025 10:30:48 +0300 Subject: [PATCH 03/50] Update staging image.tag --- charts/openvsx/values-staging.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/openvsx/values-staging.yaml b/charts/openvsx/values-staging.yaml index d7e5f1f75..783c6cdbf 100644 --- a/charts/openvsx/values-staging.yaml +++ b/charts/openvsx/values-staging.yaml @@ -12,7 +12,7 @@ image: repository: ghcr.io/eclipsefdn/openvsx-website pullPolicy: Always # Overrides the image tag whose default is the chart appVersion. - tag: 06c8bea-430 + tag: 6d777ca-570 website: jvmArgs: -Dspring.datasource.hikari.maximum-pool-size=5 -Xms512M -Xmx1536M -XX:+AlwaysPreTouch -XX:+HeapDumpOnOutOfMemoryError -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:+DisableExplicitGC -XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -Dlog4j2.formatMsgNoLookups=true -Dlog4j.formatMsgNoLookups=true From 70e7cc5acd242efdb541e22820523ae386caab4b Mon Sep 17 00:00:00 2001 From: John Kellerman Date: Mon, 18 Aug 2025 14:01:51 -0400 Subject: [PATCH 04/50] Update page-settings.tsx Changing report abuse link to 'mailto:openvsx@eclipse-foundation.org' --- website/src/page-settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/page-settings.tsx b/website/src/page-settings.tsx index 8407f9cb6..9ee21eaed 100644 --- a/website/src/page-settings.tsx +++ b/website/src/page-settings.tsx @@ -168,7 +168,7 @@ export default function createPageSettings(theme: Theme, prefersDarkMode: boolea const reportAbuseText = encodeURIComponent(''); const extensionURL = encodeURIComponent(`${location.protocol}//${location.hostname}/extension/${extension.namespace}/${extension.name}`); return Report Abuse ; From 05c5ec72a4c1dfba638d5a04be6ecb067a1f882a Mon Sep 17 00:00:00 2001 From: John Kellerman Date: Mon, 18 Aug 2025 14:20:29 -0400 Subject: [PATCH 05/50] Update about.tsx updating email link on about page to openvsx@eclipse-foundation.org for reporting abuse --- website/src/about.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/about.tsx b/website/src/about.tsx index 470565b48..3b2785f27 100644 --- a/website/src/about.tsx +++ b/website/src/about.tsx @@ -44,8 +44,8 @@ const About = () => { Contact Us To report malicious extensions, extensions with known vulnerabilities, or other urgent matters, connect with us via - marketplace@eclipse-foundation.org. - For non-urgent matters, including managing namespaces or questions about publishing extensions, please visit the + openvsx@eclipse-foundation.org. + For non-urgent questions, including managing namespaces or publishing extensions, please visit our wiki From 197fedc7dde027b512639fae2ad4ec12a9140d14 Mon Sep 17 00:00:00 2001 From: John Kellerman Date: Tue, 19 Aug 2025 10:14:28 -0400 Subject: [PATCH 06/50] Update about.tsx Fixing formatting issues --- website/src/about.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/about.tsx b/website/src/about.tsx index 3b2785f27..08522d1ca 100644 --- a/website/src/about.tsx +++ b/website/src/about.tsx @@ -43,10 +43,10 @@ const About = () => { Contact Us - To report malicious extensions, extensions with known vulnerabilities, or other urgent matters, connect with us via - openvsx@eclipse-foundation.org. - For non-urgent questions, including managing namespaces or publishing extensions, please visit our - wiki + To report malicious extensions, extensions with known vulnerabilities, or other urgent matters, connect with us + at openvsx@eclipse-foundation.org. + For non-urgent questions, including managing namespaces or publishing extensions, please visit + our wiki We use Slack for instant messaging and general communication, From c0377d63e1962e8ae96bdb423cf26c0bb1f47f25 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Wed, 20 Aug 2025 20:24:26 +0300 Subject: [PATCH 07/50] Add dot at end of line --- website/src/about.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/about.tsx b/website/src/about.tsx index 08522d1ca..95be4c528 100644 --- a/website/src/about.tsx +++ b/website/src/about.tsx @@ -46,7 +46,7 @@ const About = () => { To report malicious extensions, extensions with known vulnerabilities, or other urgent matters, connect with us at openvsx@eclipse-foundation.org. For non-urgent questions, including managing namespaces or publishing extensions, please visit - our wiki + our wiki. We use Slack for instant messaging and general communication, From 96645fed934ee5dcf7edcd3137fe90ed5a67dd24 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Sat, 9 Aug 2025 12:22:09 +0300 Subject: [PATCH 08/50] Redis cluster operator --- charts/openvsx/Chart.lock | 7 +- charts/openvsx/Chart.yaml | 3 - charts/openvsx/crds/redis-cluster.yaml | 61 +++ charts/openvsx/templates/grafana-alloy.yaml | 19 +- .../redis-cluster-operator/deployment.yaml | 39 ++ .../redis-cluster-operator/rbac.yaml | 83 ++++ charts/openvsx/templates/redis-cluster.yaml | 17 + charts/openvsx/values-staging.yaml | 79 +--- docker/redis-cluster-operator/Dockerfile | 9 + .../redis-cluster-operator/src/redis_cli.py | 299 ++++++++++++++ .../src/redis_cluster_operator.py | 365 ++++++++++++++++++ .../src/templates/configmap.yaml | 22 ++ .../src/templates/service.yaml | 16 + .../src/templates/statefulset.yaml | 147 +++++++ docker/redis-cluster-operator/src/utils.py | 14 + 15 files changed, 1103 insertions(+), 77 deletions(-) create mode 100644 charts/openvsx/crds/redis-cluster.yaml create mode 100644 charts/openvsx/templates/redis-cluster-operator/deployment.yaml create mode 100644 charts/openvsx/templates/redis-cluster-operator/rbac.yaml create mode 100644 charts/openvsx/templates/redis-cluster.yaml create mode 100644 docker/redis-cluster-operator/Dockerfile create mode 100644 docker/redis-cluster-operator/src/redis_cli.py create mode 100644 docker/redis-cluster-operator/src/redis_cluster_operator.py create mode 100644 docker/redis-cluster-operator/src/templates/configmap.yaml create mode 100644 docker/redis-cluster-operator/src/templates/service.yaml create mode 100644 docker/redis-cluster-operator/src/templates/statefulset.yaml create mode 100644 docker/redis-cluster-operator/src/utils.py diff --git a/charts/openvsx/Chart.lock b/charts/openvsx/Chart.lock index c269ee46f..d3dcdc1a0 100644 --- a/charts/openvsx/Chart.lock +++ b/charts/openvsx/Chart.lock @@ -1,9 +1,6 @@ dependencies: -- name: redis-cluster - repository: https://charts.bitnami.com/bitnami - version: 12.0.12 - name: alloy repository: https://grafana.github.io/helm-charts version: 1.1.2 -digest: sha256:e9f71eed4e319e79393873684f56db74c9db3408604c934755aecbd0ff9a457e -generated: "2025-08-04T14:57:58.074328516+03:00" +digest: sha256:66403884b7f293e86e2a61d0d822fd0878a6b4a64e5e88f181b93022bc4f9bcd +generated: "2025-08-20T12:51:18.346537659+03:00" diff --git a/charts/openvsx/Chart.yaml b/charts/openvsx/Chart.yaml index c85a61e56..37f2b46a7 100644 --- a/charts/openvsx/Chart.yaml +++ b/charts/openvsx/Chart.yaml @@ -5,9 +5,6 @@ type: application version: 0.2.0 appVersion: "1.16.0" dependencies: - - name: redis-cluster - version: 12.0.12 - repository: https://charts.bitnami.com/bitnami - name: alloy version: 1.1.2 repository: https://grafana.github.io/helm-charts \ No newline at end of file diff --git a/charts/openvsx/crds/redis-cluster.yaml b/charts/openvsx/crds/redis-cluster.yaml new file mode 100644 index 000000000..10d3fec73 --- /dev/null +++ b/charts/openvsx/crds/redis-cluster.yaml @@ -0,0 +1,61 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: redisclusters.open-vsx.org +spec: + scope: Namespaced + group: open-vsx.org + names: + kind: RedisCluster + plural: redisclusters + singular: rediscluster + shortNames: + - rcl + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + replicas: + type: integer + minimum: 6 + maxmemory: + type: string + image: + type: string + imagePullPolicy: + type: string + resources: + type: object + properties: + requests: + type: object + properties: + memory: + type: string + cpu: + type: string + limits: + type: object + properties: + memory: + type: string + cpu: + type: string + persistence: + type: object + properties: + storageGi: + type: number + format: int32 + storageClass: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true diff --git a/charts/openvsx/templates/grafana-alloy.yaml b/charts/openvsx/templates/grafana-alloy.yaml index 95cb4eb9f..7fc4bff7c 100644 --- a/charts/openvsx/templates/grafana-alloy.yaml +++ b/charts/openvsx/templates/grafana-alloy.yaml @@ -39,19 +39,22 @@ data: forward_to = [prometheus.remote_write.default.receiver] } + {{- $name := .Values.redis.name }} + {{- $environment := .Values.environment }} + {{- range $index := until (int .Values.redis.replicas) }} - prometheus.operator.servicemonitors "redis" { - namespaces = ["{{ index .Values "redis-cluster" "namespaceOverride" }}"] - selector { - match_labels = { - app = "{{ index .Values "redis-cluster" "commonLabels" "app" }}", - environment = "{{ index .Values "redis-cluster" "commonLabels" "environment" }}", - } - } + prometheus.exporter.redis "redis_metrics_{{ $index }}" { + redis_addr = "{{ $name }}-{{ $environment }}-{{ $index }}.{{ $name }}-{{ $environment }}-service:6379" + redis_user = sys.env("REDIS_METRICS_USERNAME") + redis_password = sys.env("REDIS_METRICS_PASSWORD") + } + prometheus.scrape "redis_scrape_{{ $index }}" { + targets = prometheus.exporter.redis.redis_metrics_{{ $index }}.targets forward_to = [prometheus.remote_write.default.receiver] } + {{- end }} prometheus.remote_write "default" { endpoint { name = "hosted-prometheus" diff --git a/charts/openvsx/templates/redis-cluster-operator/deployment.yaml b/charts/openvsx/templates/redis-cluster-operator/deployment.yaml new file mode 100644 index 000000000..ba89e7031 --- /dev/null +++ b/charts/openvsx/templates/redis-cluster-operator/deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: {{ .Values.name }} + environment: {{ .Values.environment }} + name: redis-cluster-operator-{{ .Values.environment }} + namespace: {{ .Values.namespace }} +spec: + selector: + matchLabels: + app: {{ .Values.name }} + environment: {{ .Values.environment }} + component: redis-cluster-operator + template: + metadata: + labels: + app: {{ .Values.name }} + environment: {{ .Values.environment }} + component: redis-cluster-operator + spec: + serviceAccountName: {{ .Values.redis.serviceAccountName }} + containers: + - name: redis-cluster-operator + image: "amvanbaren/redis-cluster-operator" + imagePullPolicy: Always + env: + - name: NAMESPACE + value: {{ .Values.namespace }} + - name: REDIS_USERNAME + valueFrom: + secretKeyRef: + name: redis-secret-{{ .Values.environment }} + key: REDIS_CLI_USERNAME + - name: REDISCLI_AUTH + valueFrom: + secretKeyRef: + name: redis-secret-{{ .Values.environment }} + key: REDIS_CLI_PASSWORD \ No newline at end of file diff --git a/charts/openvsx/templates/redis-cluster-operator/rbac.yaml b/charts/openvsx/templates/redis-cluster-operator/rbac.yaml new file mode 100644 index 000000000..07e607df5 --- /dev/null +++ b/charts/openvsx/templates/redis-cluster-operator/rbac.yaml @@ -0,0 +1,83 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.redis.serviceAccountName }} + namespace: {{ .Values.namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: redis-cluster-operator-role-cluster +rules: + # Kopf: runtime observation of namespaces & CRDs (addition/deletion). + - apiGroups: [apiextensions.k8s.io] + resources: [customresourcedefinitions] + verbs: [list, watch] + - apiGroups: ["storage.k8s.io"] + resources: [storageclasses] + verbs: [get] + - apiGroups: [""] + resources: [namespaces] + verbs: [list, watch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: redis-cluster-operator-role-binding-cluster +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: redis-cluster-operator-role-cluster +subjects: + - kind: ServiceAccount + name: {{ .Values.redis.serviceAccountName }} + namespace: {{ .Values.namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: {{ .Values.namespace }} + name: redis-cluster-operator-role +rules: + # Kopf: knowing which other operators are running (i.e. peering). + - apiGroups: [open-vsx.org] + resources: [kopfpeerings] + verbs: [list, watch, patch, get] + # Kopf: posting the events about the handlers progress/errors. + - apiGroups: [""] + resources: [events] + verbs: [create] + # App: watching & handling for the custom resource. + - apiGroups: [open-vsx.org] + resources: [redisclusters] + verbs: [list, watch, patch] + # App: resources it produces and manipulates. + - apiGroups: [""] + resources: [configmaps, services] + verbs: [create] + - apiGroups: ["apps"] + resources: [statefulsets] + verbs: [create, patch] + - apiGroups: [""] + resources: [secrets] + verbs: [get] + - apiGroups: [""] + resources: [pods] + verbs: [watch, delete] + - apiGroups: [""] + resources: [persistentvolumeclaims] + verbs: [patch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + namespace: {{ .Values.namespace }} + name: redis-cluster-operator-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: redis-cluster-operator-role +subjects: + - kind: ServiceAccount + name: {{ .Values.redis.serviceAccountName }} + namespace: {{ .Values.namespace }} diff --git a/charts/openvsx/templates/redis-cluster.yaml b/charts/openvsx/templates/redis-cluster.yaml new file mode 100644 index 000000000..66468950a --- /dev/null +++ b/charts/openvsx/templates/redis-cluster.yaml @@ -0,0 +1,17 @@ +apiVersion: open-vsx.org/v1 +kind: RedisCluster +metadata: + labels: + app: {{ .Values.name }} + environment: {{ .Values.environment }} + name: {{ .Values.redis.name }} + namespace: {{ .Values.namespace }} +spec: + image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag}}" + imagePullPolicy: {{ .Values.redis.image.pullPolicy }} + maxmemory: {{ .Values.redis.maxmemory }} + replicas: {{ .Values.redis.replicas }} + resources: + {{- toYaml .Values.redis.resources | nindent 4 }} + persistence: + {{- toYaml .Values.redis.persistence | nindent 4 }} diff --git a/charts/openvsx/values-staging.yaml b/charts/openvsx/values-staging.yaml index 783c6cdbf..9daa6bf72 100644 --- a/charts/openvsx/values-staging.yaml +++ b/charts/openvsx/values-staging.yaml @@ -46,70 +46,25 @@ es: storage_class: cephfs-new # redis -redis-cluster: - cluster: - nodes: 6 - existingSecret: redis-secret-staging - existingSecretPasswordKey: REDIS_REPLICA_PASSWORD - persistence: - enabled: true - size: 128Mi # 1 Gi for production - storageClass: "cephfs-new" - accessModes: - - ReadWriteOnce - metrics: - enabled: true - serviceMonitor: - enabled: true - redis: - configmap: | - maxmemory 64mb - maxmemory-policy allkeys-lru - appendonly yes - appendfsync everysec - save 900 1 300 10 60 10000 - tcp-keepalive 60 - timeout 300 - aclfile /opt/openvsx/redis/custom.acl - extraVolumes: - - name: redis-secret-staging - secret: - secretName: redis-secret-staging - - name: redis-config - emptyDir: {} - extraVolumeMounts: - - name: redis-secret-staging - mountPath: /etc/redis-users - readOnly: true - - name: redis-config - mountPath: /opt/openvsx/redis - initContainers: - - name: generate-acl - image: busybox - imagePullPolicy: Always - command: - - sh - - -c - - | - echo -e "user $(cat /etc/redis-users/REDIS_REPLICA_USERNAME) on >$(cat /etc/redis-users/REDIS_REPLICA_PASSWORD) ~* +psync +replconf +ping +cluster +info\nuser $(cat /etc/redis-users/REDIS_OPENVSX_USERNAME) on >$(cat /etc/redis-users/REDIS_OPENVSX_PASSWORD) ~* +@all -@admin -@dangerous -@pubsub -@slow -function +client +cluster +keys +eval +set" > /opt/openvsx/redis/custom.acl - volumeMounts: - - name: redis-secret-staging - mountPath: /etc/redis-users - - name: redis-config - mountPath: /opt/openvsx/redis - extraEnvVarsSecret: redis-secret-staging +redis: + name: redis + replicas: 6 + image: + repository: redis + tag: "7.2.10" + pullPolicy: IfNotPresent + maxmemory: "256mb" resources: requests: - memory: "64Mi" - cpu: "250m" - limits: memory: "128Mi" - cpu: "500m" - fullnameOverride: redis-cluster-staging - namespaceOverride: *namespace - commonLabels: - app: redis-cluster - environment: *environment + cpu: "50m" + limits: + memory: "512Mi" + cpu: "100m" + persistence: + storageGi: 1 + storageClass: standard + serviceAccountName: redis-cluster-operator # grafana alloy alloy: @@ -121,6 +76,8 @@ alloy: envFrom: - secretRef: name: grafana-cloud-secret-staging + - secretRef: + name: redis-secret-staging extraPorts: - name: zipkin port: 9411 diff --git a/docker/redis-cluster-operator/Dockerfile b/docker/redis-cluster-operator/Dockerfile new file mode 100644 index 000000000..c4ebcbda4 --- /dev/null +++ b/docker/redis-cluster-operator/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3 + +RUN apt-get update && apt-get install -y redis-tools +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir kopf kubernetes pyyaml + +COPY ./src ./ +ENV NAMESPACE=open-vsx-org +CMD ["sh", "-c", "kopf run -n $NAMESPACE ./redis_cluster_operator.py --verbose"] diff --git a/docker/redis-cluster-operator/src/redis_cli.py b/docker/redis-cluster-operator/src/redis_cli.py new file mode 100644 index 000000000..be99a854c --- /dev/null +++ b/docker/redis-cluster-operator/src/redis_cli.py @@ -0,0 +1,299 @@ +# ****************************************************************************** +# Copyright (c) 2025 Precies. Software OU and others +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +# ****************************************************************************** +import os +import asyncio +import socket +import subprocess +from utils import pod_host, pod_name + +def redis_cli(args, logger): + try: + user = os.getenv("REDIS_USERNAME") + cmd = f"redis-cli --user {user} {args}" + logger.debug(cmd) + result = subprocess.run( + cmd, + shell=True, + capture_output=True, + text=True, + check=True + ) + + output = result.stdout.strip() + # logger.debug(output) + return output + except subprocess.CalledProcessError as e: + error = e.stderr.strip() + logger.error(error) + logger.error(e.stdout.strip()) + return error + except FileNotFoundError: + error = "redis-cli not found. Is it installed and in your PATH?" + logger.error(error) + return error + +def redis_cli_cmd(command, logger, host, port=6379): + return redis_cli(f"-h {host} -p {str(port)} {command}", logger) + +async def wait_until_redis_node_reachable(index, name, service_name, logger): + pname = pod_name(name, index) + phost = pod_host(pname, service_name) + logger.info(f"Waiting for Redis node '{pname}' to be reachable...") + reachable = False + while not reachable: + reachable = redis_cli_cmd("PING", logger, phost) == "PONG" + if not reachable: + await asyncio.sleep(2) + + logger.info(f"{pname} is reachable.") + +async def wait_until_node_is_known(name, index, service_name, logger): + self_host = pod_host(pod_name(name, index), service_name) + nodes = redis_cli_cmd("CLUSTER NODES", logger, self_host).splitlines() + myself = next((node for node in nodes if self_host in node), None).split() + search = f"{myself[0]} {myself[1]}" + logger.debug(f"Searching for '{search}'") + + found = 0 + while found < index: + found = 0 + for i in range(0, index): + other_host = pod_host(pod_name(name, i), service_name) + nodes = redis_cli_cmd("CLUSTER NODES", logger, other_host).splitlines() + other = next((node for node in nodes if node.startswith(search)), None) + if other is not None: + found += 1 + logger.debug(f"Found {found}/{index}") + if found < index: + await asyncio.sleep(2) + + return myself[0] + +async def wait_until_node_deleted(name, index, service_name, expected_nodes, logger): + host_name = pod_host(pod_name(name, index), service_name) + actual_nodes = -1 + while actual_nodes != expected_nodes: + nodes = redis_cli_cmd("CLUSTER NODES", logger, host_name).splitlines() + actual_nodes = len(nodes) + logger.debug(f"Node deleted? expected: {expected_nodes}, actual: {actual_nodes}") + if actual_nodes != expected_nodes: + await asyncio.sleep(2) + +async def wait_until_redis_reshard_complete(reshard_host, name, service_name, total, logger): + complete_count = 0 + reshard_search = f"{reshard_host}:6379" + logger.info("Waiting for Redis reshard command to complete...") + while complete_count < total: + complete_count = 0 + for i in range(0, total): + host_name = pod_host(pod_name(name, i), service_name) + + line_index = -1 + lines = await redis_cluster_check(host_name, logger) + for i in range(0, len(lines)): + if lines[i].endswith(reshard_search): + line_index = i + 1 + break + + if line_index != -1: + line = lines[line_index].strip() + logger.debug(f"LINE: {line}") + if line.startswith("slots: (0 slots)"): + complete_count += 1 + logger.info(f"Reshard complete for {reshard_host}") + if complete_count < total: + await asyncio.sleep(2) + +async def wait_until_redis_node_added(name, index, service_name, logger): + for i in range(0, index + 1): + pname = pod_name(name, i) + logger.info(f"Waiting for Redis node '{pname}' to be added...") + new_node = True + while new_node: + new_node = is_cluster_unhealthy(pod_host(pname, service_name), logger) + if new_node: + await asyncio.sleep(2) + +async def wait_until_redis_cluster_healthy(pod_host, logger): + unhealthy = True + logger.info("Waiting for Redis cluster to stabilize...") + while unhealthy: + unhealthy = is_cluster_unhealthy(pod_host, logger) + if unhealthy: + await asyncio.sleep(2) + +async def redis_cluster_create(replicas, name, service_name, logger): + hosts = "" + for i in range(0, replicas): + await wait_until_redis_node_reachable(i, name, service_name, logger) + hosts += f"{pod_host(pod_name(name, i), service_name)}:6379 " + + redis_cli(f"--cluster create {hosts}--cluster-replicas 1 --cluster-yes", logger) + +def is_new_redis_node(host_name, logger): + nodes = redis_cli_cmd("CLUSTER NODES", logger, host_name).splitlines() + return len(nodes) == 1 and " myself,master " in nodes[0] + +def get_first_master_without_replica(host_name, myself, logger): + nodes = redis_cli_cmd("CLUSTER NODES", logger, host_name).splitlines() + masters = [node.split()[0] for node in nodes if "master " in node] + masters = [master for master in masters if master != myself] + replica_search = "slave " + replicas = [] + for node in nodes: + if replica_search in node: + start = node.index(replica_search) + len(replica_search) + end = node.index(" ", start) + replicas.append(node[start:end]) + + logger.debug(f"MASTERS: [{', '.join(masters)}]") + logger.debug(f"REPLICAS: [{', '.join(replicas)}]") + master = next((master for master in masters if master not in replicas), None) + logger.debug(f"Found master: {master}, myself: {myself}") + return master + +def is_cluster_unhealthy(host_name, logger): + checks = redis_cli(rf"--cluster check {host_name}:6379 | grep -E '\[OK\] All nodes agree about slots configuration.|\[OK\] All 16384 slots covered.|\[WARNING\]'", logger).splitlines() + return len(checks) != 2 or any(check.startswith('[WARNING]') for check in checks) + +def rebalance_redis_cluster(ip_addr, logger): + retries = 0 + rebalanced = False + while not rebalanced and retries < 3: + output = redis_cli(f"--cluster rebalance {ip_addr}:6379 --cluster-use-empty-masters", logger) + if " ERR " in output: + redis_cli(f"--cluster fix {ip_addr}:6379", logger) + retries += 1 + else: + rebalanced = True + +async def redis_cluster_check(host_name, logger): + valid = False + cluster_check_output = "" + while not valid: + cluster_check_output = redis_cli(f"--cluster check {host_name}:6379", logger) + all_agree = "[OK] All nodes agree about slots configuration." in cluster_check_output + all_covered = "[OK] All 16384 slots covered." in cluster_check_output + no_warnings = "[WARNING]" not in cluster_check_output + valid = all_agree and all_covered and no_warnings + if not valid: + await asyncio.sleep(2) + + return cluster_check_output.splitlines() + +async def get_redis_master_node(host_name, logger): + node_slots = 0 + node_id = redis_cli_cmd("CLUSTER MYID", logger, host_name) + node_search = f"M: {node_id} {host_name}:6379" + line_index = -1 + lines = await redis_cluster_check(host_name, logger) + for i in range(0, len(lines)): + logger.debug(f"LINE: '{lines[i]}'") + logger.debug(f"SEARCH: '{node_search}'") + if lines[i] == node_search: + line_index = i + 1 + break + + if line_index != -1: + line = lines[line_index] + start_search = "] (" + start_index = line.index(start_search) + len(start_search) + end_search = " slots) master" + end_index = len(line) - len(end_search) + node_slots = int(line[start_index:end_index]) + + return node_id, node_slots + +async def find_redis_master_node_to_reshard_to(name, service_name, start, limit, logger): + cluster_host = pod_host(pod_name(name, 0), service_name) + lines = await redis_cluster_check(cluster_host, logger) + for i in range(start, limit): + master_host = pod_host(pod_name(name, i), service_name) + start_search = "M: " + end_search = f" {master_host}:6379" + for line in lines: + if line.startswith(start_search) and line.endswith(end_search): + return line[len(start_search):len(line) - len(end_search)] + +async def create_redis_cluster(replicas, name, service_name, logger): + logger.info("Creating Redis cluster...") + if not is_new_redis_node(pod_host(pod_name(name, replicas - 1), service_name), logger): + logger.info("Cluster already exists, skipping creation") + return + + await redis_cluster_create(replicas, name, service_name, logger) + logger.info("Created Redis cluster") + +async def remove_redis_cluster_nodes(current_nodes, new_nodes, name, service_name, logger): + remove_count = current_nodes - new_nodes + logger.info(f"Removing {remove_count} nodes from Redis cluster...") + + cluster_master_index = 0 + for i in range(0, remove_count): + pod_index = current_nodes - i - 1 + node_host = pod_host(pod_name(name, pod_index), service_name) + node_id, node_slots = await get_redis_master_node(node_host, logger) + is_master = node_slots > 0 + if is_master: + node_ip = socket.gethostbyname(node_host) + cluster_to = await find_redis_master_node_to_reshard_to(name, service_name, cluster_master_index, new_nodes, logger) + redis_cli(f"--cluster reshard {node_ip}:6379 --cluster-from {node_id} --cluster-to {cluster_to} --cluster-slots {str(node_slots)} --cluster-yes", logger) + await wait_until_redis_reshard_complete(node_host, name, service_name, current_nodes, logger) + cluster_master_index += 1 + + did_reshard = cluster_master_index > 0 + if did_reshard: + for i in range(0, current_nodes): + host_name = pod_host(pod_name(name, i), service_name) + await wait_until_redis_cluster_healthy(host_name, logger) + + cluster_host = pod_host(pod_name(name, 0), service_name) + cluster_ip = socket.gethostbyname(cluster_host) + rebalance_redis_cluster(cluster_ip, logger) + for i in range(0, current_nodes): + host_name = pod_host(pod_name(name, i), service_name) + await wait_until_redis_cluster_healthy(host_name, logger) + + for i in range(0, remove_count): + pod_index = current_nodes - i - 1 + node_host = pod_host(pod_name(name, pod_index), service_name) + node_ip = socket.gethostbyname(node_host) + node_id = redis_cli_cmd("CLUSTER MYID", logger, node_host) + redis_cli(f"--cluster del-node {node_ip}:6379 {node_id}", logger) + redis_cli_cmd("FLUSHDB", logger, node_host) + for x in range(0, pod_index): + host_name = pod_host(pod_name(name, x), service_name) + await wait_until_redis_cluster_healthy(host_name, logger) + for x in range(0, pod_index): + await wait_until_node_deleted(name, x, service_name, pod_index, logger) + +async def add_redis_cluster_nodes(current_nodes, new_nodes, name, service_name, logger): + logger.info(f"Adding {new_nodes - current_nodes} nodes to Redis cluster...") + for i in range(current_nodes, new_nodes): + await wait_until_redis_node_reachable(i, name, service_name, logger) + pname = pod_name(name, i) + phost = pod_host(pname, service_name) + if not is_new_redis_node(phost, logger): + logger.info(f"Pod {pname} is known Redis node, skipping") + continue + + cluster_host = pod_host(pod_name(name, 0), service_name) + new_ip = socket.gethostbyname(phost) + ip = socket.gethostbyname(cluster_host) + redis_cli(f"--cluster add-node {new_ip}:6379 {ip}:6379", logger) + await wait_until_redis_node_added(name, i, service_name, logger) + myself = await wait_until_node_is_known(name, i, service_name, logger) + master_node_id = get_first_master_without_replica(cluster_host, myself, logger) + if master_node_id is not None: + redis_cli_cmd(f"CLUSTER REPLICATE {master_node_id}", logger, phost) + logger.info(f"Added replica {pname} to Redis cluster") + else: + rebalance_redis_cluster(new_ip, logger) + logger.info(f"Added master {pname} to Redis cluster") \ No newline at end of file diff --git a/docker/redis-cluster-operator/src/redis_cluster_operator.py b/docker/redis-cluster-operator/src/redis_cluster_operator.py new file mode 100644 index 000000000..9101468e9 --- /dev/null +++ b/docker/redis-cluster-operator/src/redis_cluster_operator.py @@ -0,0 +1,365 @@ +# ****************************************************************************** +# Copyright (c) 2025 Precies. Software OU and others +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +# ****************************************************************************** +import asyncio +import os +import socket +import subprocess +import yaml +import kopf +import kubernetes +from kubernetes.client.rest import ApiException +from redis_cli import ( + redis_cli, + redis_cli_cmd, + is_new_redis_node, + redis_cluster_create, + is_cluster_unhealthy, + wait_until_redis_node_reachable, + wait_until_node_is_known, + get_first_master_without_replica, + rebalance_redis_cluster, + redis_cluster_check, + get_redis_master_node, + wait_until_node_deleted, + create_redis_cluster, + add_redis_cluster_nodes, + remove_redis_cluster_nodes +) +from utils import pod_host, pod_name + +def create_config_data(name, namespace, labels): + path = os.path.join(os.path.dirname(__file__), 'templates', 'configmap.yaml') + with open(path, "rt") as f: + tmpl = f.read() + text = tmpl.format(name=name, namespace=namespace, labels=labels) + data = yaml.safe_load(text) + kopf.adopt(data) + return data + +def create_service_data(name, sts_name, namespace, labels): + path = os.path.join(os.path.dirname(__file__), 'templates', 'service.yaml') + with open(path, "rt") as f: + tmpl = f.read() + text = tmpl.format(name=name, sts_name=sts_name, namespace=namespace, labels=labels) + data = yaml.safe_load(text) + kopf.adopt(data) + return data + +def create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, maxmemory): + path = os.path.join(os.path.dirname(__file__), 'templates', 'statefulset.yaml') + with open(path, "rt") as f: + tmpl = f.read() + text = tmpl.format( + name=name, + namespace=namespace, + labels=labels, spec=spec, + service_name=service_name, + secret_name=secret_name, + config_name=config_name, + pvc_name=pvc_name, + maxmemory=maxmemory + ) + data = yaml.safe_load(text) + kopf.adopt(data) + return data + +def statefulset_patch_replicas(name, namespace, replicas, apps_api): + apps_api.patch_namespaced_stateful_set( + name=name, + namespace=namespace, + body={ + "spec": { + "replicas": replicas + } + } + ) + +def statefulset_patch_container(name, namespace, container_name, key, value, apps_api): + apps_api.patch_namespaced_stateful_set( + name=name, + namespace=namespace, + body={ + "spec": { + "template": { + "spec":{ + "containers":[{ + "name": container_name, + key: value + }] + } + } + } + } + ) + +def statefulset_patch_container_image(name, namespace, container_name, image, apps_api): + statefulset_patch_container(name, namespace, container_name, "image", image, apps_api) + +def statefulset_patch_container_image_pull_policy(name, namespace, container_name, image_pull_policy, apps_api): + statefulset_patch_container(name, namespace, container_name, "imagePullPolicy", image_pull_policy, apps_api) + +def statefulset_patch_container_resources(name, namespace, container_name, resources, apps_api): + statefulset_patch_container(name, namespace, container_name, "resources", resources, apps_api) + +def statefulset_patch_container_maxmemory(name, namespace, container_name, maxmemory, apps_api): + env = [{"name": "MAXMEMORY", "value": maxmemory}] + statefulset_patch_container(name, namespace, container_name, "env", env, apps_api) + +def pvc_patch_storage(name, namespace, storage, core_api): + core_api.patch_namespaced_persistent_volume_claim( + name=name, + namespace=namespace, + body={ + "spec": { + "resources": { + "requests":{ + "storage": f"{storage}Gi" + } + } + } + } + ) + +def validate_spec(spec): + replicas = spec.get('replicas') + validate_replicas(replicas) + +def validate_replicas(replicas): + if replicas < 6: + raise kopf.PermanentError(f"Replicas must be at least 6. Got {replicas!r}.") + +def validate_host(name, service_name): + host_name = pod_host(name, service_name) + char_count = len(host_name) + if char_count > 46: + raise kopf.PermanentError(f"Host name must be 46 chars or less. Got {char_count}.") + +def validate_secret(secret_name, namespace, core_api): + try: + secret_obj = core_api.read_namespaced_secret(name=secret_name, namespace=namespace) + secret_data = secret_obj.data + keys = [ + 'REDIS_CLI_PASSWORD', + 'REDIS_CLI_USERNAME', + 'REDIS_METRICS_PASSWORD', + 'REDIS_METRICS_USERNAME', + 'REDIS_OPENVSX_PASSWORD', + 'REDIS_OPENVSX_USERNAME', + 'REDIS_REPLICA_PASSWORD', + 'REDIS_REPLICA_USERNAME' + ] + for key in keys: + if key not in secret_data: + raise kopf.PermanentError(f"Secret {secret_name} must have {key}.") + except ApiException as e: + raise kopf.TemporaryError(f"Failed to read secret {secret_name}.") from e + +def wait_until_pod_ready(index, name, namespace, core_api, logger): + pname = pod_name(name, index) + logger.info(f"Waiting for pod '{pname}' to be ready...") + + w = kubernetes.watch.Watch() + for event in w.stream(core_api.list_namespaced_pod, namespace=namespace): + pod = event['object'] + name = pod.metadata.name + ready = False + if pname == name: + conditions = pod.status.conditions or [] + ready = any(cond.type == "Ready" and cond.status == "True" for cond in conditions) + + logger.debug(f"Event: {event['type']} - Pod: {name} - Ready: {ready}") + if ready: + break + + logger.info(f"{pname} is ready.") + +def wait_until_pod_deleted(index, name, namespace, core_api, logger): + pname = pod_name(name, index) + logger.info(f"Waiting for pod '{pname}' to be deleted...") + + w = kubernetes.watch.Watch() + for event in w.stream(core_api.list_namespaced_pod, namespace=namespace): + pod = event['object'] + name = pod.metadata.name + logger.debug(f"Event: {event['type']} - Pod: {name}") + if pname == name and event['type'] == "DELETED": + break + + logger.info(f"{pname} deleted.") + +async def wait_until_pvc_capacity_increased(name, namespace, storage_gi, core_api, logger): + logger.info(f"Waiting for {name} capacity to be increased") + storage = f"{storage_gi}Gi" + capacity_increased = False + while not capacity_increased: + status = core_api.read_namespaced_persistent_volume_claim_status(name, namespace) + capacity_increased = status.capacity['storage'] == storage and any(cond.type == "FileSystemResizePending" for cond in status.conditions) + if not capacity_increased: + await asyncio.sleep(2) + + logger.info(f"{name} capacity increased") + +@kopf.on.create('redisclusters') +async def create_fn(spec, name, namespace, labels, logger, **kwargs): + validate_spec(spec) + + name = f"{name}-{labels['environment']}" + config_name = f"{name}-config" + service_name = f"{name}-service" + validate_host(name, service_name) + + secret_name = f"redis-secret-{labels['environment']}" + core_api = kubernetes.client.CoreV1Api() + validate_secret(secret_name, namespace, core_api) + + cfg_data = create_config_data(config_name, namespace, labels) + core_api.create_namespaced_config_map(namespace=namespace, body=cfg_data) + logger.info("ConfigMap child is created") + + srv_data = create_service_data(service_name, name, namespace, labels) + core_api.create_namespaced_service(namespace=namespace, body=srv_data) + logger.info("Service child is created") + + pvc_name = "redis-data" + sts_data = create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, spec['maxmemory']) + apps_api = kubernetes.client.AppsV1Api() + obj = apps_api.create_namespaced_stateful_set(namespace=namespace, body=sts_data) + logger.info("StatefulSet child is created") + + return { + 'sts-name': obj.metadata.name, + 'srv-name': service_name, + 'cntr-name': obj.spec.template.spec.containers[0].name, + 'pvc-name': pvc_name, + 'cfg-name': config_name + } + +@kopf.on.field('redisclusters', field='spec.replicas') +async def on_replicas_change(old, new, status, namespace, logger, **kwargs): + validate_replicas(new) + + name = status['create_fn']['sts-name'] + service_name = status['create_fn']['srv-name'] + core_api = kubernetes.client.CoreV1Api() + apps_api = kubernetes.client.AppsV1Api() + if old is None: + wait_until_pod_ready(new - 1, name, namespace, core_api, logger) + await create_redis_cluster(new, name, service_name, logger) + elif new > old: + statefulset_patch_replicas(name, namespace, new, apps_api) + for i in range(old, new): + wait_until_pod_ready(i, name, namespace, core_api, logger) + + await add_redis_cluster_nodes(old, new, name, service_name, logger) + elif new < old: + await remove_redis_cluster_nodes(old, new, name, service_name, logger) + statefulset_patch_replicas(name, namespace, new, apps_api) + +@kopf.on.field('redisclusters', field='spec.maxmemory') +async def on_maxmemory_change(spec, old, new, status, namespace, logger, **kwargs): + if old is None: + return + + logger.info(f"Maxmemory changed from {old} to {new}") + + name = status['create_fn']['sts-name'] + service_name = status['create_fn']['srv-name'] + container_name = status['create_fn']['cntr-name'] + apps_api = kubernetes.client.AppsV1Api() + statefulset_patch_container_maxmemory(name, namespace, container_name, new, apps_api) + + replicas = spec.get('replicas') + core_api = kubernetes.client.CoreV1Api() + for index in range(replicas - 1, -1, -1): + core_api.delete_namespaced_pod(pod_name(name, index), namespace) + wait_until_pod_deleted(index, name, namespace, core_api, logger) + wait_until_pod_ready(index, name, namespace, core_api, logger) + await wait_until_redis_node_reachable(index, name, service_name, logger) + +@kopf.on.field('redisclusters', field='spec.image') +def on_image_change(old, new, status, namespace, logger, **kwargs): + if old is None: + return + + logger.info(f"Image changed from {old} to {new}") + + name = status['create_fn']['sts-name'] + container_name = status['create_fn']['cntr-name'] + apps_api = kubernetes.client.AppsV1Api() + statefulset_patch_container_image(name, namespace, container_name, new, apps_api) + +@kopf.on.field('redisclusters', field='spec.imagePullPolicy') +def on_image_pull_policy_change(old, new, status, namespace, logger, **kwargs): + if old is None: + return + + logger.info(f"Image pull policy changed from {old} to {new}") + + name = status['create_fn']['sts-name'] + container_name = status['create_fn']['cntr-name'] + apps_api = kubernetes.client.AppsV1Api() + statefulset_patch_container_image_pull_policy(name, namespace, container_name, new, apps_api) + +@kopf.on.field('redisclusters', field='spec.resources') +def on_resources_change(old, new, status, namespace, logger, **kwargs): + if old is None: + return + + logger.info(f"Resources changed from {old} to {new}") + + name = status['create_fn']['sts-name'] + container_name = status['create_fn']['cntr-name'] + apps_api = kubernetes.client.AppsV1Api() + statefulset_patch_container_resources(name, namespace, container_name, new, apps_api) + +@kopf.on.field('redisclusters', field='spec.persistence.storageGi') +async def on_persistence_storage_change(spec, old, new, status, namespace, logger, **kwargs): + if old is None: + return + + logger.info(f"Storage changed from {old} to {new}") + if new < old: + raise kopf.PermanentError("New storage size must be greater than old storage size.") + + storage_api = kubernetes.client.StorageV1Api() + storage_class_name = spec.get('persistence').get('storageClass') + storage_class = storage_api.read_storage_class(name=storage_class_name) + if not storage_class.allow_volume_expansion: + raise kopf.PermanentError(f"Storage class '{storage_class_name}' does not allow volume expansion.") + + apps_api = kubernetes.client.AppsV1Api() + core_api = kubernetes.client.CoreV1Api() + replicas = spec.get('replicas') + name = status['create_fn']['sts-name'] + pvc_name = status['create_fn']['pvc-name'] + statefulset_patch_replicas(name, namespace, 0, apps_api) + for index in range(replicas - 1, -1, -1): + wait_until_pod_deleted(index, name, namespace, core_api, logger) + + exception = None + try: + for index in range(0, replicas): + pvc = f"{pvc_name}-{name}-{index}" + pvc_patch_storage(pvc, namespace, new, core_api) + await wait_until_pvc_capacity_increased(pvc, namespace, new, core_api, logger) + except ApiException as e: + exception = e + + statefulset_patch_replicas(name, namespace, replicas, apps_api) + if exception is not None: + raise kopf.TemporaryError("Failed to increase persistent volume capacity") from exception + +@kopf.on.field('redisclusters', field='spec.persistence.storageClass', annotations={'update-storage-class-unsupported': kopf.ABSENT}) +def update_fn(patch, old, new, **kwargs): + if old is None: + return + + patch.metadata.annotations['update-storage-class-unsupported'] = 'yes' + raise kopf.PermanentError(f"Redis operator is unable to migrate from '{old}' to '{new}' storageClassName.") diff --git a/docker/redis-cluster-operator/src/templates/configmap.yaml b/docker/redis-cluster-operator/src/templates/configmap.yaml new file mode 100644 index 000000000..95cd1226d --- /dev/null +++ b/docker/redis-cluster-operator/src/templates/configmap.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: "{labels[app]}" + environment: "{labels[environment]}" + name: "{name}" + namespace: "{namespace}" +data: + redis.conf: | + cluster-enabled yes + cluster-config-file nodes.conf + cluster-node-timeout 5000 + maxmemory-policy allkeys-lru + dir /etc/redis/data + appendonly yes + appendfsync everysec + auto-aof-rewrite-percentage 100 + auto-aof-rewrite-min-size 64mb + save 900 1 300 10 60 10000 + tcp-keepalive 60 + timeout 300 diff --git a/docker/redis-cluster-operator/src/templates/service.yaml b/docker/redis-cluster-operator/src/templates/service.yaml new file mode 100644 index 000000000..17f8d8374 --- /dev/null +++ b/docker/redis-cluster-operator/src/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{name}" + namespace: "{namespace}" +spec: + clusterIP: None + ports: + - port: 6379 + name: redis + - port: 16379 + name: cluster + selector: + app: "{labels[app]}" + component: "{sts_name}" + environment: "{labels[environment]}" \ No newline at end of file diff --git a/docker/redis-cluster-operator/src/templates/statefulset.yaml b/docker/redis-cluster-operator/src/templates/statefulset.yaml new file mode 100644 index 000000000..e0a063946 --- /dev/null +++ b/docker/redis-cluster-operator/src/templates/statefulset.yaml @@ -0,0 +1,147 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app: "{labels[app]}" + environment: "{labels[environment]}" + name: "{name}" + namespace: "{namespace}" +spec: + serviceName: "{service_name}" + replicas: {spec[replicas]} + selector: + matchLabels: + app: "{labels[app]}" + component: "{name}" + environment: "{labels[environment]}" + persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Retain + template: + metadata: + labels: + app: "{labels[app]}" + component: "{name}" + environment: "{labels[environment]}" + spec: + containers: + - name: "redis-{labels[environment]}" + image: "{spec[image]}" + imagePullPolicy: "{spec[imagePullPolicy]}" + ports: + - containerPort: 6379 + - containerPort: 16379 + env: + - name: MAXMEMORY + value: {maxmemory} + - name: USERNAME + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_REPLICA_USERNAME + - name: PASSWORD + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_REPLICA_PASSWORD + command: ["sh", "-c"] + args: + - > + redis-server /etc/redis/redis.conf + --aclfile /etc/redis-acl/users.acl + --masteruser "$USERNAME" + --masterauth "$PASSWORD" + --maxmemory "$MAXMEMORY" + --cluster-announce-ip "$HOSTNAME.{service_name}" + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + resources: + {spec[resources]} + volumeMounts: + - name: config + mountPath: /etc/redis/redis.conf + subPath: redis.conf + - name: redis-data + mountPath: /etc/redis/data + - name: redis-acl + mountPath: /etc/redis-acl + initContainers: + - name: generate-acl + image: busybox + imagePullPolicy: Always + env: + - name: REPLICA_USERNAME + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_REPLICA_USERNAME + - name: REPLICA_PASSWORD + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_REPLICA_PASSWORD + - name: OPENVSX_USERNAME + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_OPENVSX_USERNAME + - name: OPENVSX_PASSWORD + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_OPENVSX_PASSWORD + - name: CLI_USERNAME + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_CLI_USERNAME + - name: CLI_PASSWORD + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_CLI_PASSWORD + - name: METRICS_USERNAME + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_METRICS_USERNAME + - name: METRICS_PASSWORD + valueFrom: + secretKeyRef: + name: "{secret_name}" + key: REDIS_METRICS_PASSWORD + command: ["sh", "-c"] + args: + - | + echo "user $REPLICA_USERNAME on >$REPLICA_PASSWORD ~* +psync +replconf +ping" > /etc/redis-acl/users.acl + echo "user $CLI_USERNAME on >$CLI_PASSWORD ~* +ping +cluster +info +dbsize" >> /etc/redis-acl/users.acl + echo "user $METRICS_USERNAME on >$METRICS_PASSWORD ~* +client +ping +info +config|get +cluster|info +cluster|slots +slowlog +latency +memory +select +get +scan +xinfo +type +pfcount +strlen +llen +scard +zcard +hlen +xlen +eval allkeys" >> /etc/redis-acl/users.acl + echo "user $OPENVSX_USERNAME on >$OPENVSX_PASSWORD ~* +@all -@admin -@dangerous -@pubsub -@slow -function +client +cluster +keys +eval +set +psetex" >> /etc/redis-acl/users.acl + volumeMounts: + - name: redis-acl + mountPath: /etc/redis-acl + volumes: + - name: config + configMap: + name: "{config_name}" + - name: redis-acl + emptyDir: {{}} + volumeClaimTemplates: + - metadata: + labels: + app: "{labels[app]}" + environment: "{labels[environment]}" + name: {pvc_name} + spec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: "{spec[persistence][storageGi]}Gi" + storageClassName: "{spec[persistence][storageClass]}" diff --git a/docker/redis-cluster-operator/src/utils.py b/docker/redis-cluster-operator/src/utils.py new file mode 100644 index 000000000..7f815dd19 --- /dev/null +++ b/docker/redis-cluster-operator/src/utils.py @@ -0,0 +1,14 @@ +# ****************************************************************************** +# Copyright (c) 2025 Precies. Software OU and others +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +# ****************************************************************************** +def pod_name(name, index): + return f"{name}-{index}" + +def pod_host(name, service_name): + return f"{name}.{service_name}" \ No newline at end of file From 07e48713a4f0438d7ebf42813f55d846304786fb Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Fri, 29 Aug 2025 21:02:40 +0300 Subject: [PATCH 09/50] CephFS Redis storage class --- charts/openvsx/values-staging.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/openvsx/values-staging.yaml b/charts/openvsx/values-staging.yaml index 9daa6bf72..0d2f84a1a 100644 --- a/charts/openvsx/values-staging.yaml +++ b/charts/openvsx/values-staging.yaml @@ -63,7 +63,7 @@ redis: cpu: "100m" persistence: storageGi: 1 - storageClass: standard + storageClass: cephfs-new serviceAccountName: redis-cluster-operator # grafana alloy From 256f710932ac428018f21eca31735110982131f1 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Wed, 3 Sep 2025 16:03:01 +0300 Subject: [PATCH 10/50] Add extra permissions to RBAC --- charts/openvsx/templates/redis-cluster-operator/rbac.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/charts/openvsx/templates/redis-cluster-operator/rbac.yaml b/charts/openvsx/templates/redis-cluster-operator/rbac.yaml index 07e607df5..d13d5ce35 100644 --- a/charts/openvsx/templates/redis-cluster-operator/rbac.yaml +++ b/charts/openvsx/templates/redis-cluster-operator/rbac.yaml @@ -51,10 +51,13 @@ rules: - apiGroups: [open-vsx.org] resources: [redisclusters] verbs: [list, watch, patch] + - apiGroups: [open-vsx.org] + resources: [redisclusters/finalizers] + verbs: [update, patch] # App: resources it produces and manipulates. - apiGroups: [""] resources: [configmaps, services] - verbs: [create] + verbs: [get, list, create, patch, update] - apiGroups: ["apps"] resources: [statefulsets] verbs: [create, patch] From a1c5078bfcddf518f99e62ca07ef651096319f4d Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Wed, 3 Sep 2025 17:56:40 +0300 Subject: [PATCH 11/50] Add Redis and Grafana production values --- charts/openvsx/templates/grafana-alloy.yaml | 2 +- charts/openvsx/values.yaml | 48 +++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/charts/openvsx/templates/grafana-alloy.yaml b/charts/openvsx/templates/grafana-alloy.yaml index 7fc4bff7c..7eaad4350 100644 --- a/charts/openvsx/templates/grafana-alloy.yaml +++ b/charts/openvsx/templates/grafana-alloy.yaml @@ -9,7 +9,7 @@ metadata: data: {{ .Values.alloy.alloy.configMap.key }}: | logging { - level = "debug" + level = "info" format = "logfmt" } diff --git a/charts/openvsx/values.yaml b/charts/openvsx/values.yaml index e3f02f205..8e29b79f9 100644 --- a/charts/openvsx/values.yaml +++ b/charts/openvsx/values.yaml @@ -44,3 +44,51 @@ es: cpu: 1 memory: 8Gi storage_class: cephfs-new + +# redis +redis: + name: redis + replicas: 6 + image: + repository: redis + tag: "7.2.10" + pullPolicy: IfNotPresent + maxmemory: "896mb" + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1536Mi" + cpu: "500m" + persistence: + storageGi: 5 + storageClass: cephfs-new + serviceAccountName: redis-cluster-operator + +# grafana alloy +alloy: + alloy: + configMap: + create: false + name: "grafana-alloy-configmap-production" + key: "config.river" + envFrom: + - secretRef: + name: grafana-cloud-secret-production + - secretRef: + name: redis-secret-production + extraPorts: + - name: zipkin + port: 9411 + targetPort: 9411 + stabilityLevel: experimental + crds: + create: true + controller: + type: "daemonset" + podLabels: + app: *name + environment: *environment + fullnameOverride: grafana-alloy-production + namespaceOverride: *namespace \ No newline at end of file From cd9220ddc2f4a87743d9b0ae3994bc56fa96efe5 Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Thu, 4 Sep 2025 15:29:51 +0200 Subject: [PATCH 12/50] Increase maxSkew to 2 --- charts/openvsx/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/openvsx/templates/deployment.yaml b/charts/openvsx/templates/deployment.yaml index 4f0a68a37..b2e0bc91f 100644 --- a/charts/openvsx/templates/deployment.yaml +++ b/charts/openvsx/templates/deployment.yaml @@ -95,7 +95,7 @@ spec: matchLabels: app: open-vsx-org environment: {{ .Values.environment }} - maxSkew: 1 + maxSkew: 2 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule volumes: From 1928dd13bbc81ae517a65ebecb52cce7f889b9bf Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Thu, 4 Sep 2025 15:30:35 +0200 Subject: [PATCH 13/50] Adjust redis cluster nodes names --- configuration/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/application.yml b/configuration/application.yml index a5c8c01ee..c5a6c587d 100644 --- a/configuration/application.yml +++ b/configuration/application.yml @@ -30,7 +30,7 @@ spring: data: redis: cluster: - nodes: 'redis-cluster-staging-0.redis-cluster-staging-headless:6379,redis-cluster-staging-1.redis-cluster-staging-headless:6379,redis-cluster-staging-2.redis-cluster-staging-headless:6379,redis-cluster-staging-3.redis-cluster-staging-headless:6379,redis-cluster-staging-4.redis-cluster-staging-headless:6379,redis-cluster-staging-5.redis-cluster-staging-headless:6379' + nodes: 'redis-staging-0.redis-staging-service:6379,redis-staging-1.redis-staging-service:6379,redis-staging-2.redis-staging-service:6379,redis-staging-3.redis-staging-service:6379,redis-staging-4.redis-staging-service:6379,redis-staging-5.redis-staging-service:6379' username: openvsx password: openvsx datasource: From cdb75a6b4643661df6558cb3bead3d48d19980db Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Thu, 4 Sep 2025 15:31:20 +0200 Subject: [PATCH 14/50] Add application user required deletion ACLs --- docker/redis-cluster-operator/src/templates/statefulset.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/redis-cluster-operator/src/templates/statefulset.yaml b/docker/redis-cluster-operator/src/templates/statefulset.yaml index e0a063946..2247a3f36 100644 --- a/docker/redis-cluster-operator/src/templates/statefulset.yaml +++ b/docker/redis-cluster-operator/src/templates/statefulset.yaml @@ -122,7 +122,7 @@ spec: echo "user $REPLICA_USERNAME on >$REPLICA_PASSWORD ~* +psync +replconf +ping" > /etc/redis-acl/users.acl echo "user $CLI_USERNAME on >$CLI_PASSWORD ~* +ping +cluster +info +dbsize" >> /etc/redis-acl/users.acl echo "user $METRICS_USERNAME on >$METRICS_PASSWORD ~* +client +ping +info +config|get +cluster|info +cluster|slots +slowlog +latency +memory +select +get +scan +xinfo +type +pfcount +strlen +llen +scard +zcard +hlen +xlen +eval allkeys" >> /etc/redis-acl/users.acl - echo "user $OPENVSX_USERNAME on >$OPENVSX_PASSWORD ~* +@all -@admin -@dangerous -@pubsub -@slow -function +client +cluster +keys +eval +set +psetex" >> /etc/redis-acl/users.acl + echo "user $OPENVSX_USERNAME on >$OPENVSX_PASSWORD ~* +@all -@admin -@dangerous -@pubsub -@slow -function +client +cluster +keys +eval +set +psetex +del +unlink" >> /etc/redis-acl/users.acl volumeMounts: - name: redis-acl mountPath: /etc/redis-acl From 89993633fefb601bd5b82d41a1cb72a2d7056a14 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 4 Sep 2025 18:32:54 +0300 Subject: [PATCH 15/50] Wait until reachable before checking is new node --- docker/redis-cluster-operator/src/redis_cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/redis-cluster-operator/src/redis_cli.py b/docker/redis-cluster-operator/src/redis_cli.py index be99a854c..5079956bd 100644 --- a/docker/redis-cluster-operator/src/redis_cli.py +++ b/docker/redis-cluster-operator/src/redis_cli.py @@ -132,7 +132,6 @@ async def wait_until_redis_cluster_healthy(pod_host, logger): async def redis_cluster_create(replicas, name, service_name, logger): hosts = "" for i in range(0, replicas): - await wait_until_redis_node_reachable(i, name, service_name, logger) hosts += f"{pod_host(pod_name(name, i), service_name)}:6379 " redis_cli(f"--cluster create {hosts}--cluster-replicas 1 --cluster-yes", logger) @@ -224,6 +223,9 @@ async def find_redis_master_node_to_reshard_to(name, service_name, start, limit, async def create_redis_cluster(replicas, name, service_name, logger): logger.info("Creating Redis cluster...") + for i in range(0, replicas): + await wait_until_redis_node_reachable(i, name, service_name, logger) + if not is_new_redis_node(pod_host(pod_name(name, replicas - 1), service_name), logger): logger.info("Cluster already exists, skipping creation") return From bf60705af8129a1b61ae809e2627cfa1a1ff2d14 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 4 Sep 2025 18:54:18 +0300 Subject: [PATCH 16/50] Add environment to pvc_name --- docker/redis-cluster-operator/src/redis_cluster_operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/redis-cluster-operator/src/redis_cluster_operator.py b/docker/redis-cluster-operator/src/redis_cluster_operator.py index 9101468e9..fb6e88eec 100644 --- a/docker/redis-cluster-operator/src/redis_cluster_operator.py +++ b/docker/redis-cluster-operator/src/redis_cluster_operator.py @@ -212,6 +212,7 @@ async def create_fn(spec, name, namespace, labels, logger, **kwargs): name = f"{name}-{labels['environment']}" config_name = f"{name}-config" + pvc_name = f"{name}-data" service_name = f"{name}-service" validate_host(name, service_name) @@ -227,7 +228,6 @@ async def create_fn(spec, name, namespace, labels, logger, **kwargs): core_api.create_namespaced_service(namespace=namespace, body=srv_data) logger.info("Service child is created") - pvc_name = "redis-data" sts_data = create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, spec['maxmemory']) apps_api = kubernetes.client.AppsV1Api() obj = apps_api.create_namespaced_stateful_set(namespace=namespace, body=sts_data) From 58de20ad7553ff35577a7b49fabd2fb92a3fc7bf Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Fri, 5 Sep 2025 13:21:02 +0300 Subject: [PATCH 17/50] Template add variable names --- .../src/redis_cluster_operator.py | 6 ++++-- .../src/templates/statefulset.yaml | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docker/redis-cluster-operator/src/redis_cluster_operator.py b/docker/redis-cluster-operator/src/redis_cluster_operator.py index fb6e88eec..0c979be2c 100644 --- a/docker/redis-cluster-operator/src/redis_cluster_operator.py +++ b/docker/redis-cluster-operator/src/redis_cluster_operator.py @@ -52,7 +52,7 @@ def create_service_data(name, sts_name, namespace, labels): kopf.adopt(data) return data -def create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, maxmemory): +def create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, acl_name, maxmemory): path = os.path.join(os.path.dirname(__file__), 'templates', 'statefulset.yaml') with open(path, "rt") as f: tmpl = f.read() @@ -64,6 +64,7 @@ def create_statefulset_data(spec, name, namespace, labels, service_name, secret_ secret_name=secret_name, config_name=config_name, pvc_name=pvc_name, + acl_name=acl_name, maxmemory=maxmemory ) data = yaml.safe_load(text) @@ -214,6 +215,7 @@ async def create_fn(spec, name, namespace, labels, logger, **kwargs): config_name = f"{name}-config" pvc_name = f"{name}-data" service_name = f"{name}-service" + acl_name = f"{name}-acl" validate_host(name, service_name) secret_name = f"redis-secret-{labels['environment']}" @@ -228,7 +230,7 @@ async def create_fn(spec, name, namespace, labels, logger, **kwargs): core_api.create_namespaced_service(namespace=namespace, body=srv_data) logger.info("Service child is created") - sts_data = create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, spec['maxmemory']) + sts_data = create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, acl_name, spec['maxmemory']) apps_api = kubernetes.client.AppsV1Api() obj = apps_api.create_namespaced_stateful_set(namespace=namespace, body=sts_data) logger.info("StatefulSet child is created") diff --git a/docker/redis-cluster-operator/src/templates/statefulset.yaml b/docker/redis-cluster-operator/src/templates/statefulset.yaml index 2247a3f36..f04b5abdd 100644 --- a/docker/redis-cluster-operator/src/templates/statefulset.yaml +++ b/docker/redis-cluster-operator/src/templates/statefulset.yaml @@ -67,9 +67,9 @@ spec: - name: config mountPath: /etc/redis/redis.conf subPath: redis.conf - - name: redis-data + - name: "{pvc_name}" mountPath: /etc/redis/data - - name: redis-acl + - name: "{acl_name}" mountPath: /etc/redis-acl initContainers: - name: generate-acl @@ -124,20 +124,20 @@ spec: echo "user $METRICS_USERNAME on >$METRICS_PASSWORD ~* +client +ping +info +config|get +cluster|info +cluster|slots +slowlog +latency +memory +select +get +scan +xinfo +type +pfcount +strlen +llen +scard +zcard +hlen +xlen +eval allkeys" >> /etc/redis-acl/users.acl echo "user $OPENVSX_USERNAME on >$OPENVSX_PASSWORD ~* +@all -@admin -@dangerous -@pubsub -@slow -function +client +cluster +keys +eval +set +psetex +del +unlink" >> /etc/redis-acl/users.acl volumeMounts: - - name: redis-acl + - name: "{acl_name}" mountPath: /etc/redis-acl volumes: - name: config configMap: name: "{config_name}" - - name: redis-acl + - name: "{acl_name}" emptyDir: {{}} volumeClaimTemplates: - metadata: labels: app: "{labels[app]}" environment: "{labels[environment]}" - name: {pvc_name} + name: "{pvc_name}" spec: accessModes: - "ReadWriteOnce" From 6941bb72b155dd7ace8dfaa6312c25bd6820aa33 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Fri, 5 Sep 2025 14:26:30 +0300 Subject: [PATCH 18/50] Add yaml anchors --- charts/openvsx/values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/openvsx/values.yaml b/charts/openvsx/values.yaml index 8e29b79f9..c4f71a5a4 100644 --- a/charts/openvsx/values.yaml +++ b/charts/openvsx/values.yaml @@ -1,8 +1,8 @@ # Default values for openvsx. -name: open-vsx-org -environment: production -namespace: open-vsx-org +name: &name open-vsx-org +environment: &environment production +namespace: &namespace open-vsx-org host: open-vsx.org replicaCount: 6 From 88c8d5c4c31242333caaa0e95283f124ab1fe5c5 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 8 Sep 2025 10:39:34 +0200 Subject: [PATCH 19/50] Disable claim-namespace.yml The workflow is compromised as people can claim namespaces even though the claim is invalid. Disable it for now to avoid further damage. --- .github/workflows/claim-namespace.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claim-namespace.yml b/.github/workflows/claim-namespace.yml index 15bcbcc53..60301a911 100644 --- a/.github/workflows/claim-namespace.yml +++ b/.github/workflows/claim-namespace.yml @@ -1,7 +1,9 @@ name: Claim Namespace on: - issues: - types: [opened, edited, labeled] + # alibi value to not show the workflow as broken + workflow_dispatch: +# issues: +# types: [opened, edited, labeled] concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true @@ -44,7 +46,7 @@ jobs: name: Namespace API request uses: JamesIves/fetch-api-data-action@v2 with: - endpoint: https://open-vsx.org/api/${{steps.get_namespace.outputs.namespace}} + endpoint: https://open-vsx.org/api/${o{steps.get_namespace.outputs.namespace}} configuration: '{ "method": "GET" }' - id: namespace_not_found_should_close if: ${{ failure() && steps.get_namespace.outputs.namespace != null }} @@ -119,4 +121,4 @@ jobs: GH_REPO: ${{ github.repository }} NUMBER: ${{ github.event.issue.number }} LABELS: granted - ASSIGNEE: tfroment \ No newline at end of file + ASSIGNEE: tfroment From 746392714169462f848d5de260b81b8304b8b1e8 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Mon, 8 Sep 2025 13:02:30 +0300 Subject: [PATCH 20/50] Improve Grafana configuration --- charts/openvsx/templates/grafana-alloy.yaml | 1 + charts/openvsx/templates/service-monitor.yaml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/charts/openvsx/templates/grafana-alloy.yaml b/charts/openvsx/templates/grafana-alloy.yaml index 7eaad4350..c135eee24 100644 --- a/charts/openvsx/templates/grafana-alloy.yaml +++ b/charts/openvsx/templates/grafana-alloy.yaml @@ -51,6 +51,7 @@ data: prometheus.scrape "redis_scrape_{{ $index }}" { targets = prometheus.exporter.redis.redis_metrics_{{ $index }}.targets + scrape_interval = "600s" forward_to = [prometheus.remote_write.default.receiver] } diff --git a/charts/openvsx/templates/service-monitor.yaml b/charts/openvsx/templates/service-monitor.yaml index 14a5d8196..38060ebcc 100644 --- a/charts/openvsx/templates/service-monitor.yaml +++ b/charts/openvsx/templates/service-monitor.yaml @@ -16,6 +16,6 @@ spec: matchNames: - {{ .Values.namespace }} endpoints: - - port: http + - port: management path: /actuator/prometheus - interval: 15s \ No newline at end of file + interval: 60s \ No newline at end of file From f4d61c9d8792c9c6a78f04d13165998e5eb22dd0 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 8 Sep 2025 12:55:04 +0200 Subject: [PATCH 21/50] Apply suggestions from code review --- .github/workflows/claim-namespace.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claim-namespace.yml b/.github/workflows/claim-namespace.yml index 60301a911..785bf7009 100644 --- a/.github/workflows/claim-namespace.yml +++ b/.github/workflows/claim-namespace.yml @@ -46,7 +46,7 @@ jobs: name: Namespace API request uses: JamesIves/fetch-api-data-action@v2 with: - endpoint: https://open-vsx.org/api/${o{steps.get_namespace.outputs.namespace}} + endpoint: https://open-vsx.org/api/${{steps.get_namespace.outputs.namespace}} configuration: '{ "method": "GET" }' - id: namespace_not_found_should_close if: ${{ failure() && steps.get_namespace.outputs.namespace != null }} From b15525ad838cb86a03f889669222e1d7aa41742f Mon Sep 17 00:00:00 2001 From: John Kellerman Date: Mon, 8 Sep 2025 15:32:00 -0400 Subject: [PATCH 22/50] Update about.tsx Providing a better link to join Open VSX Slack space. --- website/src/about.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/about.tsx b/website/src/about.tsx index 95be4c528..195b11ee9 100644 --- a/website/src/about.tsx +++ b/website/src/about.tsx @@ -50,7 +50,7 @@ const About = () => { We use Slack for instant messaging and general communication, - use this link to join our Slack workspace. + use this link to join our Slack workspace. ; From 851305b0bdcccfc2ce13c5bfafe9852b850351a1 Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Fri, 12 Sep 2025 15:51:44 +0200 Subject: [PATCH 23/50] Move redis-cluster-operator to external project --- .../redis-cluster-operator/deployment.yaml | 4 +- docker/redis-cluster-operator/Dockerfile | 9 - .../redis-cluster-operator/src/redis_cli.py | 301 -------------- .../src/redis_cluster_operator.py | 367 ------------------ .../src/templates/configmap.yaml | 22 -- .../src/templates/service.yaml | 16 - .../src/templates/statefulset.yaml | 147 ------- docker/redis-cluster-operator/src/utils.py | 14 - 8 files changed, 2 insertions(+), 878 deletions(-) delete mode 100644 docker/redis-cluster-operator/Dockerfile delete mode 100644 docker/redis-cluster-operator/src/redis_cli.py delete mode 100644 docker/redis-cluster-operator/src/redis_cluster_operator.py delete mode 100644 docker/redis-cluster-operator/src/templates/configmap.yaml delete mode 100644 docker/redis-cluster-operator/src/templates/service.yaml delete mode 100644 docker/redis-cluster-operator/src/templates/statefulset.yaml delete mode 100644 docker/redis-cluster-operator/src/utils.py diff --git a/charts/openvsx/templates/redis-cluster-operator/deployment.yaml b/charts/openvsx/templates/redis-cluster-operator/deployment.yaml index ba89e7031..0e64f5a64 100644 --- a/charts/openvsx/templates/redis-cluster-operator/deployment.yaml +++ b/charts/openvsx/templates/redis-cluster-operator/deployment.yaml @@ -22,7 +22,7 @@ spec: serviceAccountName: {{ .Values.redis.serviceAccountName }} containers: - name: redis-cluster-operator - image: "amvanbaren/redis-cluster-operator" + image: "ghcr.io/eclipsefdn/redis-cluster-operator" imagePullPolicy: Always env: - name: NAMESPACE @@ -36,4 +36,4 @@ spec: valueFrom: secretKeyRef: name: redis-secret-{{ .Values.environment }} - key: REDIS_CLI_PASSWORD \ No newline at end of file + key: REDIS_CLI_PASSWORD diff --git a/docker/redis-cluster-operator/Dockerfile b/docker/redis-cluster-operator/Dockerfile deleted file mode 100644 index c4ebcbda4..000000000 --- a/docker/redis-cluster-operator/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3 - -RUN apt-get update && apt-get install -y redis-tools -RUN pip install --no-cache-dir --upgrade pip \ - && pip install --no-cache-dir kopf kubernetes pyyaml - -COPY ./src ./ -ENV NAMESPACE=open-vsx-org -CMD ["sh", "-c", "kopf run -n $NAMESPACE ./redis_cluster_operator.py --verbose"] diff --git a/docker/redis-cluster-operator/src/redis_cli.py b/docker/redis-cluster-operator/src/redis_cli.py deleted file mode 100644 index 5079956bd..000000000 --- a/docker/redis-cluster-operator/src/redis_cli.py +++ /dev/null @@ -1,301 +0,0 @@ -# ****************************************************************************** -# Copyright (c) 2025 Precies. Software OU and others -# -# This program and the accompanying materials are made available under the -# terms of the Eclipse Public License v. 2.0 which is available at -# http://www.eclipse.org/legal/epl-2.0. -# -# SPDX-License-Identifier: EPL-2.0 -# ****************************************************************************** -import os -import asyncio -import socket -import subprocess -from utils import pod_host, pod_name - -def redis_cli(args, logger): - try: - user = os.getenv("REDIS_USERNAME") - cmd = f"redis-cli --user {user} {args}" - logger.debug(cmd) - result = subprocess.run( - cmd, - shell=True, - capture_output=True, - text=True, - check=True - ) - - output = result.stdout.strip() - # logger.debug(output) - return output - except subprocess.CalledProcessError as e: - error = e.stderr.strip() - logger.error(error) - logger.error(e.stdout.strip()) - return error - except FileNotFoundError: - error = "redis-cli not found. Is it installed and in your PATH?" - logger.error(error) - return error - -def redis_cli_cmd(command, logger, host, port=6379): - return redis_cli(f"-h {host} -p {str(port)} {command}", logger) - -async def wait_until_redis_node_reachable(index, name, service_name, logger): - pname = pod_name(name, index) - phost = pod_host(pname, service_name) - logger.info(f"Waiting for Redis node '{pname}' to be reachable...") - reachable = False - while not reachable: - reachable = redis_cli_cmd("PING", logger, phost) == "PONG" - if not reachable: - await asyncio.sleep(2) - - logger.info(f"{pname} is reachable.") - -async def wait_until_node_is_known(name, index, service_name, logger): - self_host = pod_host(pod_name(name, index), service_name) - nodes = redis_cli_cmd("CLUSTER NODES", logger, self_host).splitlines() - myself = next((node for node in nodes if self_host in node), None).split() - search = f"{myself[0]} {myself[1]}" - logger.debug(f"Searching for '{search}'") - - found = 0 - while found < index: - found = 0 - for i in range(0, index): - other_host = pod_host(pod_name(name, i), service_name) - nodes = redis_cli_cmd("CLUSTER NODES", logger, other_host).splitlines() - other = next((node for node in nodes if node.startswith(search)), None) - if other is not None: - found += 1 - logger.debug(f"Found {found}/{index}") - if found < index: - await asyncio.sleep(2) - - return myself[0] - -async def wait_until_node_deleted(name, index, service_name, expected_nodes, logger): - host_name = pod_host(pod_name(name, index), service_name) - actual_nodes = -1 - while actual_nodes != expected_nodes: - nodes = redis_cli_cmd("CLUSTER NODES", logger, host_name).splitlines() - actual_nodes = len(nodes) - logger.debug(f"Node deleted? expected: {expected_nodes}, actual: {actual_nodes}") - if actual_nodes != expected_nodes: - await asyncio.sleep(2) - -async def wait_until_redis_reshard_complete(reshard_host, name, service_name, total, logger): - complete_count = 0 - reshard_search = f"{reshard_host}:6379" - logger.info("Waiting for Redis reshard command to complete...") - while complete_count < total: - complete_count = 0 - for i in range(0, total): - host_name = pod_host(pod_name(name, i), service_name) - - line_index = -1 - lines = await redis_cluster_check(host_name, logger) - for i in range(0, len(lines)): - if lines[i].endswith(reshard_search): - line_index = i + 1 - break - - if line_index != -1: - line = lines[line_index].strip() - logger.debug(f"LINE: {line}") - if line.startswith("slots: (0 slots)"): - complete_count += 1 - logger.info(f"Reshard complete for {reshard_host}") - if complete_count < total: - await asyncio.sleep(2) - -async def wait_until_redis_node_added(name, index, service_name, logger): - for i in range(0, index + 1): - pname = pod_name(name, i) - logger.info(f"Waiting for Redis node '{pname}' to be added...") - new_node = True - while new_node: - new_node = is_cluster_unhealthy(pod_host(pname, service_name), logger) - if new_node: - await asyncio.sleep(2) - -async def wait_until_redis_cluster_healthy(pod_host, logger): - unhealthy = True - logger.info("Waiting for Redis cluster to stabilize...") - while unhealthy: - unhealthy = is_cluster_unhealthy(pod_host, logger) - if unhealthy: - await asyncio.sleep(2) - -async def redis_cluster_create(replicas, name, service_name, logger): - hosts = "" - for i in range(0, replicas): - hosts += f"{pod_host(pod_name(name, i), service_name)}:6379 " - - redis_cli(f"--cluster create {hosts}--cluster-replicas 1 --cluster-yes", logger) - -def is_new_redis_node(host_name, logger): - nodes = redis_cli_cmd("CLUSTER NODES", logger, host_name).splitlines() - return len(nodes) == 1 and " myself,master " in nodes[0] - -def get_first_master_without_replica(host_name, myself, logger): - nodes = redis_cli_cmd("CLUSTER NODES", logger, host_name).splitlines() - masters = [node.split()[0] for node in nodes if "master " in node] - masters = [master for master in masters if master != myself] - replica_search = "slave " - replicas = [] - for node in nodes: - if replica_search in node: - start = node.index(replica_search) + len(replica_search) - end = node.index(" ", start) - replicas.append(node[start:end]) - - logger.debug(f"MASTERS: [{', '.join(masters)}]") - logger.debug(f"REPLICAS: [{', '.join(replicas)}]") - master = next((master for master in masters if master not in replicas), None) - logger.debug(f"Found master: {master}, myself: {myself}") - return master - -def is_cluster_unhealthy(host_name, logger): - checks = redis_cli(rf"--cluster check {host_name}:6379 | grep -E '\[OK\] All nodes agree about slots configuration.|\[OK\] All 16384 slots covered.|\[WARNING\]'", logger).splitlines() - return len(checks) != 2 or any(check.startswith('[WARNING]') for check in checks) - -def rebalance_redis_cluster(ip_addr, logger): - retries = 0 - rebalanced = False - while not rebalanced and retries < 3: - output = redis_cli(f"--cluster rebalance {ip_addr}:6379 --cluster-use-empty-masters", logger) - if " ERR " in output: - redis_cli(f"--cluster fix {ip_addr}:6379", logger) - retries += 1 - else: - rebalanced = True - -async def redis_cluster_check(host_name, logger): - valid = False - cluster_check_output = "" - while not valid: - cluster_check_output = redis_cli(f"--cluster check {host_name}:6379", logger) - all_agree = "[OK] All nodes agree about slots configuration." in cluster_check_output - all_covered = "[OK] All 16384 slots covered." in cluster_check_output - no_warnings = "[WARNING]" not in cluster_check_output - valid = all_agree and all_covered and no_warnings - if not valid: - await asyncio.sleep(2) - - return cluster_check_output.splitlines() - -async def get_redis_master_node(host_name, logger): - node_slots = 0 - node_id = redis_cli_cmd("CLUSTER MYID", logger, host_name) - node_search = f"M: {node_id} {host_name}:6379" - line_index = -1 - lines = await redis_cluster_check(host_name, logger) - for i in range(0, len(lines)): - logger.debug(f"LINE: '{lines[i]}'") - logger.debug(f"SEARCH: '{node_search}'") - if lines[i] == node_search: - line_index = i + 1 - break - - if line_index != -1: - line = lines[line_index] - start_search = "] (" - start_index = line.index(start_search) + len(start_search) - end_search = " slots) master" - end_index = len(line) - len(end_search) - node_slots = int(line[start_index:end_index]) - - return node_id, node_slots - -async def find_redis_master_node_to_reshard_to(name, service_name, start, limit, logger): - cluster_host = pod_host(pod_name(name, 0), service_name) - lines = await redis_cluster_check(cluster_host, logger) - for i in range(start, limit): - master_host = pod_host(pod_name(name, i), service_name) - start_search = "M: " - end_search = f" {master_host}:6379" - for line in lines: - if line.startswith(start_search) and line.endswith(end_search): - return line[len(start_search):len(line) - len(end_search)] - -async def create_redis_cluster(replicas, name, service_name, logger): - logger.info("Creating Redis cluster...") - for i in range(0, replicas): - await wait_until_redis_node_reachable(i, name, service_name, logger) - - if not is_new_redis_node(pod_host(pod_name(name, replicas - 1), service_name), logger): - logger.info("Cluster already exists, skipping creation") - return - - await redis_cluster_create(replicas, name, service_name, logger) - logger.info("Created Redis cluster") - -async def remove_redis_cluster_nodes(current_nodes, new_nodes, name, service_name, logger): - remove_count = current_nodes - new_nodes - logger.info(f"Removing {remove_count} nodes from Redis cluster...") - - cluster_master_index = 0 - for i in range(0, remove_count): - pod_index = current_nodes - i - 1 - node_host = pod_host(pod_name(name, pod_index), service_name) - node_id, node_slots = await get_redis_master_node(node_host, logger) - is_master = node_slots > 0 - if is_master: - node_ip = socket.gethostbyname(node_host) - cluster_to = await find_redis_master_node_to_reshard_to(name, service_name, cluster_master_index, new_nodes, logger) - redis_cli(f"--cluster reshard {node_ip}:6379 --cluster-from {node_id} --cluster-to {cluster_to} --cluster-slots {str(node_slots)} --cluster-yes", logger) - await wait_until_redis_reshard_complete(node_host, name, service_name, current_nodes, logger) - cluster_master_index += 1 - - did_reshard = cluster_master_index > 0 - if did_reshard: - for i in range(0, current_nodes): - host_name = pod_host(pod_name(name, i), service_name) - await wait_until_redis_cluster_healthy(host_name, logger) - - cluster_host = pod_host(pod_name(name, 0), service_name) - cluster_ip = socket.gethostbyname(cluster_host) - rebalance_redis_cluster(cluster_ip, logger) - for i in range(0, current_nodes): - host_name = pod_host(pod_name(name, i), service_name) - await wait_until_redis_cluster_healthy(host_name, logger) - - for i in range(0, remove_count): - pod_index = current_nodes - i - 1 - node_host = pod_host(pod_name(name, pod_index), service_name) - node_ip = socket.gethostbyname(node_host) - node_id = redis_cli_cmd("CLUSTER MYID", logger, node_host) - redis_cli(f"--cluster del-node {node_ip}:6379 {node_id}", logger) - redis_cli_cmd("FLUSHDB", logger, node_host) - for x in range(0, pod_index): - host_name = pod_host(pod_name(name, x), service_name) - await wait_until_redis_cluster_healthy(host_name, logger) - for x in range(0, pod_index): - await wait_until_node_deleted(name, x, service_name, pod_index, logger) - -async def add_redis_cluster_nodes(current_nodes, new_nodes, name, service_name, logger): - logger.info(f"Adding {new_nodes - current_nodes} nodes to Redis cluster...") - for i in range(current_nodes, new_nodes): - await wait_until_redis_node_reachable(i, name, service_name, logger) - pname = pod_name(name, i) - phost = pod_host(pname, service_name) - if not is_new_redis_node(phost, logger): - logger.info(f"Pod {pname} is known Redis node, skipping") - continue - - cluster_host = pod_host(pod_name(name, 0), service_name) - new_ip = socket.gethostbyname(phost) - ip = socket.gethostbyname(cluster_host) - redis_cli(f"--cluster add-node {new_ip}:6379 {ip}:6379", logger) - await wait_until_redis_node_added(name, i, service_name, logger) - myself = await wait_until_node_is_known(name, i, service_name, logger) - master_node_id = get_first_master_without_replica(cluster_host, myself, logger) - if master_node_id is not None: - redis_cli_cmd(f"CLUSTER REPLICATE {master_node_id}", logger, phost) - logger.info(f"Added replica {pname} to Redis cluster") - else: - rebalance_redis_cluster(new_ip, logger) - logger.info(f"Added master {pname} to Redis cluster") \ No newline at end of file diff --git a/docker/redis-cluster-operator/src/redis_cluster_operator.py b/docker/redis-cluster-operator/src/redis_cluster_operator.py deleted file mode 100644 index 0c979be2c..000000000 --- a/docker/redis-cluster-operator/src/redis_cluster_operator.py +++ /dev/null @@ -1,367 +0,0 @@ -# ****************************************************************************** -# Copyright (c) 2025 Precies. Software OU and others -# -# This program and the accompanying materials are made available under the -# terms of the Eclipse Public License v. 2.0 which is available at -# http://www.eclipse.org/legal/epl-2.0. -# -# SPDX-License-Identifier: EPL-2.0 -# ****************************************************************************** -import asyncio -import os -import socket -import subprocess -import yaml -import kopf -import kubernetes -from kubernetes.client.rest import ApiException -from redis_cli import ( - redis_cli, - redis_cli_cmd, - is_new_redis_node, - redis_cluster_create, - is_cluster_unhealthy, - wait_until_redis_node_reachable, - wait_until_node_is_known, - get_first_master_without_replica, - rebalance_redis_cluster, - redis_cluster_check, - get_redis_master_node, - wait_until_node_deleted, - create_redis_cluster, - add_redis_cluster_nodes, - remove_redis_cluster_nodes -) -from utils import pod_host, pod_name - -def create_config_data(name, namespace, labels): - path = os.path.join(os.path.dirname(__file__), 'templates', 'configmap.yaml') - with open(path, "rt") as f: - tmpl = f.read() - text = tmpl.format(name=name, namespace=namespace, labels=labels) - data = yaml.safe_load(text) - kopf.adopt(data) - return data - -def create_service_data(name, sts_name, namespace, labels): - path = os.path.join(os.path.dirname(__file__), 'templates', 'service.yaml') - with open(path, "rt") as f: - tmpl = f.read() - text = tmpl.format(name=name, sts_name=sts_name, namespace=namespace, labels=labels) - data = yaml.safe_load(text) - kopf.adopt(data) - return data - -def create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, acl_name, maxmemory): - path = os.path.join(os.path.dirname(__file__), 'templates', 'statefulset.yaml') - with open(path, "rt") as f: - tmpl = f.read() - text = tmpl.format( - name=name, - namespace=namespace, - labels=labels, spec=spec, - service_name=service_name, - secret_name=secret_name, - config_name=config_name, - pvc_name=pvc_name, - acl_name=acl_name, - maxmemory=maxmemory - ) - data = yaml.safe_load(text) - kopf.adopt(data) - return data - -def statefulset_patch_replicas(name, namespace, replicas, apps_api): - apps_api.patch_namespaced_stateful_set( - name=name, - namespace=namespace, - body={ - "spec": { - "replicas": replicas - } - } - ) - -def statefulset_patch_container(name, namespace, container_name, key, value, apps_api): - apps_api.patch_namespaced_stateful_set( - name=name, - namespace=namespace, - body={ - "spec": { - "template": { - "spec":{ - "containers":[{ - "name": container_name, - key: value - }] - } - } - } - } - ) - -def statefulset_patch_container_image(name, namespace, container_name, image, apps_api): - statefulset_patch_container(name, namespace, container_name, "image", image, apps_api) - -def statefulset_patch_container_image_pull_policy(name, namespace, container_name, image_pull_policy, apps_api): - statefulset_patch_container(name, namespace, container_name, "imagePullPolicy", image_pull_policy, apps_api) - -def statefulset_patch_container_resources(name, namespace, container_name, resources, apps_api): - statefulset_patch_container(name, namespace, container_name, "resources", resources, apps_api) - -def statefulset_patch_container_maxmemory(name, namespace, container_name, maxmemory, apps_api): - env = [{"name": "MAXMEMORY", "value": maxmemory}] - statefulset_patch_container(name, namespace, container_name, "env", env, apps_api) - -def pvc_patch_storage(name, namespace, storage, core_api): - core_api.patch_namespaced_persistent_volume_claim( - name=name, - namespace=namespace, - body={ - "spec": { - "resources": { - "requests":{ - "storage": f"{storage}Gi" - } - } - } - } - ) - -def validate_spec(spec): - replicas = spec.get('replicas') - validate_replicas(replicas) - -def validate_replicas(replicas): - if replicas < 6: - raise kopf.PermanentError(f"Replicas must be at least 6. Got {replicas!r}.") - -def validate_host(name, service_name): - host_name = pod_host(name, service_name) - char_count = len(host_name) - if char_count > 46: - raise kopf.PermanentError(f"Host name must be 46 chars or less. Got {char_count}.") - -def validate_secret(secret_name, namespace, core_api): - try: - secret_obj = core_api.read_namespaced_secret(name=secret_name, namespace=namespace) - secret_data = secret_obj.data - keys = [ - 'REDIS_CLI_PASSWORD', - 'REDIS_CLI_USERNAME', - 'REDIS_METRICS_PASSWORD', - 'REDIS_METRICS_USERNAME', - 'REDIS_OPENVSX_PASSWORD', - 'REDIS_OPENVSX_USERNAME', - 'REDIS_REPLICA_PASSWORD', - 'REDIS_REPLICA_USERNAME' - ] - for key in keys: - if key not in secret_data: - raise kopf.PermanentError(f"Secret {secret_name} must have {key}.") - except ApiException as e: - raise kopf.TemporaryError(f"Failed to read secret {secret_name}.") from e - -def wait_until_pod_ready(index, name, namespace, core_api, logger): - pname = pod_name(name, index) - logger.info(f"Waiting for pod '{pname}' to be ready...") - - w = kubernetes.watch.Watch() - for event in w.stream(core_api.list_namespaced_pod, namespace=namespace): - pod = event['object'] - name = pod.metadata.name - ready = False - if pname == name: - conditions = pod.status.conditions or [] - ready = any(cond.type == "Ready" and cond.status == "True" for cond in conditions) - - logger.debug(f"Event: {event['type']} - Pod: {name} - Ready: {ready}") - if ready: - break - - logger.info(f"{pname} is ready.") - -def wait_until_pod_deleted(index, name, namespace, core_api, logger): - pname = pod_name(name, index) - logger.info(f"Waiting for pod '{pname}' to be deleted...") - - w = kubernetes.watch.Watch() - for event in w.stream(core_api.list_namespaced_pod, namespace=namespace): - pod = event['object'] - name = pod.metadata.name - logger.debug(f"Event: {event['type']} - Pod: {name}") - if pname == name and event['type'] == "DELETED": - break - - logger.info(f"{pname} deleted.") - -async def wait_until_pvc_capacity_increased(name, namespace, storage_gi, core_api, logger): - logger.info(f"Waiting for {name} capacity to be increased") - storage = f"{storage_gi}Gi" - capacity_increased = False - while not capacity_increased: - status = core_api.read_namespaced_persistent_volume_claim_status(name, namespace) - capacity_increased = status.capacity['storage'] == storage and any(cond.type == "FileSystemResizePending" for cond in status.conditions) - if not capacity_increased: - await asyncio.sleep(2) - - logger.info(f"{name} capacity increased") - -@kopf.on.create('redisclusters') -async def create_fn(spec, name, namespace, labels, logger, **kwargs): - validate_spec(spec) - - name = f"{name}-{labels['environment']}" - config_name = f"{name}-config" - pvc_name = f"{name}-data" - service_name = f"{name}-service" - acl_name = f"{name}-acl" - validate_host(name, service_name) - - secret_name = f"redis-secret-{labels['environment']}" - core_api = kubernetes.client.CoreV1Api() - validate_secret(secret_name, namespace, core_api) - - cfg_data = create_config_data(config_name, namespace, labels) - core_api.create_namespaced_config_map(namespace=namespace, body=cfg_data) - logger.info("ConfigMap child is created") - - srv_data = create_service_data(service_name, name, namespace, labels) - core_api.create_namespaced_service(namespace=namespace, body=srv_data) - logger.info("Service child is created") - - sts_data = create_statefulset_data(spec, name, namespace, labels, service_name, secret_name, config_name, pvc_name, acl_name, spec['maxmemory']) - apps_api = kubernetes.client.AppsV1Api() - obj = apps_api.create_namespaced_stateful_set(namespace=namespace, body=sts_data) - logger.info("StatefulSet child is created") - - return { - 'sts-name': obj.metadata.name, - 'srv-name': service_name, - 'cntr-name': obj.spec.template.spec.containers[0].name, - 'pvc-name': pvc_name, - 'cfg-name': config_name - } - -@kopf.on.field('redisclusters', field='spec.replicas') -async def on_replicas_change(old, new, status, namespace, logger, **kwargs): - validate_replicas(new) - - name = status['create_fn']['sts-name'] - service_name = status['create_fn']['srv-name'] - core_api = kubernetes.client.CoreV1Api() - apps_api = kubernetes.client.AppsV1Api() - if old is None: - wait_until_pod_ready(new - 1, name, namespace, core_api, logger) - await create_redis_cluster(new, name, service_name, logger) - elif new > old: - statefulset_patch_replicas(name, namespace, new, apps_api) - for i in range(old, new): - wait_until_pod_ready(i, name, namespace, core_api, logger) - - await add_redis_cluster_nodes(old, new, name, service_name, logger) - elif new < old: - await remove_redis_cluster_nodes(old, new, name, service_name, logger) - statefulset_patch_replicas(name, namespace, new, apps_api) - -@kopf.on.field('redisclusters', field='spec.maxmemory') -async def on_maxmemory_change(spec, old, new, status, namespace, logger, **kwargs): - if old is None: - return - - logger.info(f"Maxmemory changed from {old} to {new}") - - name = status['create_fn']['sts-name'] - service_name = status['create_fn']['srv-name'] - container_name = status['create_fn']['cntr-name'] - apps_api = kubernetes.client.AppsV1Api() - statefulset_patch_container_maxmemory(name, namespace, container_name, new, apps_api) - - replicas = spec.get('replicas') - core_api = kubernetes.client.CoreV1Api() - for index in range(replicas - 1, -1, -1): - core_api.delete_namespaced_pod(pod_name(name, index), namespace) - wait_until_pod_deleted(index, name, namespace, core_api, logger) - wait_until_pod_ready(index, name, namespace, core_api, logger) - await wait_until_redis_node_reachable(index, name, service_name, logger) - -@kopf.on.field('redisclusters', field='spec.image') -def on_image_change(old, new, status, namespace, logger, **kwargs): - if old is None: - return - - logger.info(f"Image changed from {old} to {new}") - - name = status['create_fn']['sts-name'] - container_name = status['create_fn']['cntr-name'] - apps_api = kubernetes.client.AppsV1Api() - statefulset_patch_container_image(name, namespace, container_name, new, apps_api) - -@kopf.on.field('redisclusters', field='spec.imagePullPolicy') -def on_image_pull_policy_change(old, new, status, namespace, logger, **kwargs): - if old is None: - return - - logger.info(f"Image pull policy changed from {old} to {new}") - - name = status['create_fn']['sts-name'] - container_name = status['create_fn']['cntr-name'] - apps_api = kubernetes.client.AppsV1Api() - statefulset_patch_container_image_pull_policy(name, namespace, container_name, new, apps_api) - -@kopf.on.field('redisclusters', field='spec.resources') -def on_resources_change(old, new, status, namespace, logger, **kwargs): - if old is None: - return - - logger.info(f"Resources changed from {old} to {new}") - - name = status['create_fn']['sts-name'] - container_name = status['create_fn']['cntr-name'] - apps_api = kubernetes.client.AppsV1Api() - statefulset_patch_container_resources(name, namespace, container_name, new, apps_api) - -@kopf.on.field('redisclusters', field='spec.persistence.storageGi') -async def on_persistence_storage_change(spec, old, new, status, namespace, logger, **kwargs): - if old is None: - return - - logger.info(f"Storage changed from {old} to {new}") - if new < old: - raise kopf.PermanentError("New storage size must be greater than old storage size.") - - storage_api = kubernetes.client.StorageV1Api() - storage_class_name = spec.get('persistence').get('storageClass') - storage_class = storage_api.read_storage_class(name=storage_class_name) - if not storage_class.allow_volume_expansion: - raise kopf.PermanentError(f"Storage class '{storage_class_name}' does not allow volume expansion.") - - apps_api = kubernetes.client.AppsV1Api() - core_api = kubernetes.client.CoreV1Api() - replicas = spec.get('replicas') - name = status['create_fn']['sts-name'] - pvc_name = status['create_fn']['pvc-name'] - statefulset_patch_replicas(name, namespace, 0, apps_api) - for index in range(replicas - 1, -1, -1): - wait_until_pod_deleted(index, name, namespace, core_api, logger) - - exception = None - try: - for index in range(0, replicas): - pvc = f"{pvc_name}-{name}-{index}" - pvc_patch_storage(pvc, namespace, new, core_api) - await wait_until_pvc_capacity_increased(pvc, namespace, new, core_api, logger) - except ApiException as e: - exception = e - - statefulset_patch_replicas(name, namespace, replicas, apps_api) - if exception is not None: - raise kopf.TemporaryError("Failed to increase persistent volume capacity") from exception - -@kopf.on.field('redisclusters', field='spec.persistence.storageClass', annotations={'update-storage-class-unsupported': kopf.ABSENT}) -def update_fn(patch, old, new, **kwargs): - if old is None: - return - - patch.metadata.annotations['update-storage-class-unsupported'] = 'yes' - raise kopf.PermanentError(f"Redis operator is unable to migrate from '{old}' to '{new}' storageClassName.") diff --git a/docker/redis-cluster-operator/src/templates/configmap.yaml b/docker/redis-cluster-operator/src/templates/configmap.yaml deleted file mode 100644 index 95cd1226d..000000000 --- a/docker/redis-cluster-operator/src/templates/configmap.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - labels: - app: "{labels[app]}" - environment: "{labels[environment]}" - name: "{name}" - namespace: "{namespace}" -data: - redis.conf: | - cluster-enabled yes - cluster-config-file nodes.conf - cluster-node-timeout 5000 - maxmemory-policy allkeys-lru - dir /etc/redis/data - appendonly yes - appendfsync everysec - auto-aof-rewrite-percentage 100 - auto-aof-rewrite-min-size 64mb - save 900 1 300 10 60 10000 - tcp-keepalive 60 - timeout 300 diff --git a/docker/redis-cluster-operator/src/templates/service.yaml b/docker/redis-cluster-operator/src/templates/service.yaml deleted file mode 100644 index 17f8d8374..000000000 --- a/docker/redis-cluster-operator/src/templates/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: "{name}" - namespace: "{namespace}" -spec: - clusterIP: None - ports: - - port: 6379 - name: redis - - port: 16379 - name: cluster - selector: - app: "{labels[app]}" - component: "{sts_name}" - environment: "{labels[environment]}" \ No newline at end of file diff --git a/docker/redis-cluster-operator/src/templates/statefulset.yaml b/docker/redis-cluster-operator/src/templates/statefulset.yaml deleted file mode 100644 index f04b5abdd..000000000 --- a/docker/redis-cluster-operator/src/templates/statefulset.yaml +++ /dev/null @@ -1,147 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - labels: - app: "{labels[app]}" - environment: "{labels[environment]}" - name: "{name}" - namespace: "{namespace}" -spec: - serviceName: "{service_name}" - replicas: {spec[replicas]} - selector: - matchLabels: - app: "{labels[app]}" - component: "{name}" - environment: "{labels[environment]}" - persistentVolumeClaimRetentionPolicy: - whenDeleted: Retain - whenScaled: Retain - template: - metadata: - labels: - app: "{labels[app]}" - component: "{name}" - environment: "{labels[environment]}" - spec: - containers: - - name: "redis-{labels[environment]}" - image: "{spec[image]}" - imagePullPolicy: "{spec[imagePullPolicy]}" - ports: - - containerPort: 6379 - - containerPort: 16379 - env: - - name: MAXMEMORY - value: {maxmemory} - - name: USERNAME - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_REPLICA_USERNAME - - name: PASSWORD - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_REPLICA_PASSWORD - command: ["sh", "-c"] - args: - - > - redis-server /etc/redis/redis.conf - --aclfile /etc/redis-acl/users.acl - --masteruser "$USERNAME" - --masterauth "$PASSWORD" - --maxmemory "$MAXMEMORY" - --cluster-announce-ip "$HOSTNAME.{service_name}" - readinessProbe: - tcpSocket: - port: 6379 - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - resources: - {spec[resources]} - volumeMounts: - - name: config - mountPath: /etc/redis/redis.conf - subPath: redis.conf - - name: "{pvc_name}" - mountPath: /etc/redis/data - - name: "{acl_name}" - mountPath: /etc/redis-acl - initContainers: - - name: generate-acl - image: busybox - imagePullPolicy: Always - env: - - name: REPLICA_USERNAME - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_REPLICA_USERNAME - - name: REPLICA_PASSWORD - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_REPLICA_PASSWORD - - name: OPENVSX_USERNAME - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_OPENVSX_USERNAME - - name: OPENVSX_PASSWORD - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_OPENVSX_PASSWORD - - name: CLI_USERNAME - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_CLI_USERNAME - - name: CLI_PASSWORD - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_CLI_PASSWORD - - name: METRICS_USERNAME - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_METRICS_USERNAME - - name: METRICS_PASSWORD - valueFrom: - secretKeyRef: - name: "{secret_name}" - key: REDIS_METRICS_PASSWORD - command: ["sh", "-c"] - args: - - | - echo "user $REPLICA_USERNAME on >$REPLICA_PASSWORD ~* +psync +replconf +ping" > /etc/redis-acl/users.acl - echo "user $CLI_USERNAME on >$CLI_PASSWORD ~* +ping +cluster +info +dbsize" >> /etc/redis-acl/users.acl - echo "user $METRICS_USERNAME on >$METRICS_PASSWORD ~* +client +ping +info +config|get +cluster|info +cluster|slots +slowlog +latency +memory +select +get +scan +xinfo +type +pfcount +strlen +llen +scard +zcard +hlen +xlen +eval allkeys" >> /etc/redis-acl/users.acl - echo "user $OPENVSX_USERNAME on >$OPENVSX_PASSWORD ~* +@all -@admin -@dangerous -@pubsub -@slow -function +client +cluster +keys +eval +set +psetex +del +unlink" >> /etc/redis-acl/users.acl - volumeMounts: - - name: "{acl_name}" - mountPath: /etc/redis-acl - volumes: - - name: config - configMap: - name: "{config_name}" - - name: "{acl_name}" - emptyDir: {{}} - volumeClaimTemplates: - - metadata: - labels: - app: "{labels[app]}" - environment: "{labels[environment]}" - name: "{pvc_name}" - spec: - accessModes: - - "ReadWriteOnce" - resources: - requests: - storage: "{spec[persistence][storageGi]}Gi" - storageClassName: "{spec[persistence][storageClass]}" diff --git a/docker/redis-cluster-operator/src/utils.py b/docker/redis-cluster-operator/src/utils.py deleted file mode 100644 index 7f815dd19..000000000 --- a/docker/redis-cluster-operator/src/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -# ****************************************************************************** -# Copyright (c) 2025 Precies. Software OU and others -# -# This program and the accompanying materials are made available under the -# terms of the Eclipse Public License v. 2.0 which is available at -# http://www.eclipse.org/legal/epl-2.0. -# -# SPDX-License-Identifier: EPL-2.0 -# ****************************************************************************** -def pod_name(name, index): - return f"{name}-{index}" - -def pod_host(name, service_name): - return f"{name}.{service_name}" \ No newline at end of file From 5e7e6b58256d76e858c2ad4e16f2f07327fe4b3b Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Mon, 15 Sep 2025 15:10:26 +0300 Subject: [PATCH 24/50] test c2ec92e6 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 21746f27b..ab7b312f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=5dd4aa28 +ARG SERVER_VERSION=c2ec92e6 # Builder image to compile the website FROM ubuntu AS builder From eda9c13881eb53c2dcadf8be5ee01cc4a98d0e80 Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Mon, 15 Sep 2025 15:21:59 +0200 Subject: [PATCH 25/50] Add missing dependency updates --- kubernetes/helm-deploy.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index 46b129da4..2a15d561a 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -56,5 +56,7 @@ else action="install" fi +helm dependency update "${ROOT_DIR}/charts" +helm dependency build "${ROOT_DIR}/charts" helm "${action}" "${release_name}" "${ROOT_DIR}/charts/${chart_name}" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" - \ No newline at end of file + From 8043f8a6868080f82f712a17ab71d80a5f72edd7 Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Wed, 17 Sep 2025 17:02:48 +0200 Subject: [PATCH 26/50] Add missing label to service required by serviceMonitor --- charts/openvsx/templates/service.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/openvsx/templates/service.yaml b/charts/openvsx/templates/service.yaml index 3672f75ca..ae2c2bf16 100644 --- a/charts/openvsx/templates/service.yaml +++ b/charts/openvsx/templates/service.yaml @@ -4,6 +4,7 @@ metadata: labels: app: {{ .Values.name }} environment: {{ .Values.environment }} + type: website name: {{ .Values.name }}-{{ .Values.environment }} namespace: {{ .Values.namespace }} spec: From 315fa457059fc43540e47461826ec32252b01e61 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 18 Sep 2025 12:09:22 +0300 Subject: [PATCH 27/50] test 71c36b47 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ab7b312f4..fb6909c3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=c2ec92e6 +ARG SERVER_VERSION=71c36b47 # Builder image to compile the website FROM ubuntu AS builder From a1ec8d8aa81606ef971a104ce5a39dab0c73f035 Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Thu, 18 Sep 2025 17:32:00 +0200 Subject: [PATCH 28/50] Fix helm dependency path --- kubernetes/helm-deploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index 2a15d561a..26690e58a 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -56,7 +56,7 @@ else action="install" fi -helm dependency update "${ROOT_DIR}/charts" -helm dependency build "${ROOT_DIR}/charts" +helm dependency update "${ROOT_DIR}/charts/openvsx" +helm dependency build "${ROOT_DIR}/charts/openvsx" helm "${action}" "${release_name}" "${ROOT_DIR}/charts/${chart_name}" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" From 71fea1032ba7877813bde52d766fa2355da56b8a Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Mon, 22 Sep 2025 09:15:13 +0200 Subject: [PATCH 29/50] Fix helm path and remove dependency update --- kubernetes/helm-deploy.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index 26690e58a..bd4f60d44 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -56,7 +56,5 @@ else action="install" fi -helm dependency update "${ROOT_DIR}/charts/openvsx" -helm dependency build "${ROOT_DIR}/charts/openvsx" -helm "${action}" "${release_name}" "${ROOT_DIR}/charts/${chart_name}" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" +helm "${action}" "${release_name}" "${ROOT_DIR}/charts/openvsx" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" From ae02c056400cb78f4dc15d51ae9c054cf076e0c7 Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Mon, 22 Sep 2025 09:46:37 +0200 Subject: [PATCH 30/50] Reorganise helm deploy --- kubernetes/helm-deploy.sh | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index bd4f60d44..f13f74f96 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -48,13 +48,6 @@ else exit 1 fi -if helm list -n "${namespace}" | grep "${release_name}" > /dev/null; then - echo "Found installed Helm chart for release name '${release_name}'. Upgrading..." - action="upgrade" -else - echo "Found no installed Helm chart for release name '${release_name}'. Installing..." - action="install" -fi +chmod 600 "${KUBECONFIG}" -helm "${action}" "${release_name}" "${ROOT_DIR}/charts/openvsx" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" - +helm upgrade --install "${release_name}" "${ROOT_DIR}/charts/openvsx" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" From 92e46282183456b3bbf0f777168440dbf4bf2707 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 23 Sep 2025 14:32:27 +0300 Subject: [PATCH 31/50] test b871d61c --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fb6909c3c..70cf4ca7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=71c36b47 +ARG SERVER_VERSION=b871d61c # Builder image to compile the website FROM ubuntu AS builder From f7d607c9ac9e8376b9dc0e368fa18ef08d9690d8 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 23 Sep 2025 15:02:13 +0300 Subject: [PATCH 32/50] test b1ccf631 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 70cf4ca7b..660f99c66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=b871d61c +ARG SERVER_VERSION=b1ccf631 # Builder image to compile the website FROM ubuntu AS builder From a072fca40bb9d0f579d5cbe2c7052a90cdfb7a31 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 23 Sep 2025 15:39:45 +0300 Subject: [PATCH 33/50] build helm dependencies --- kubernetes/helm-deploy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index f13f74f96..e3456f30b 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -50,4 +50,5 @@ fi chmod 600 "${KUBECONFIG}" +helm dependency build "${ROOT_DIR}/charts/openvsx" helm upgrade --install "${release_name}" "${ROOT_DIR}/charts/openvsx" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" From 41d26f0f337a94fe4ce1885d257778887de7ab1f Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 23 Sep 2025 16:00:40 +0300 Subject: [PATCH 34/50] helm repo add --- kubernetes/helm-deploy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index e3456f30b..622c73480 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -50,5 +50,6 @@ fi chmod 600 "${KUBECONFIG}" +helm repo add https://grafana.github.io/helm-charts helm dependency build "${ROOT_DIR}/charts/openvsx" helm upgrade --install "${release_name}" "${ROOT_DIR}/charts/openvsx" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" From 80573e935cb221ab54d709490953d84fce70b1a6 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 23 Sep 2025 16:12:44 +0300 Subject: [PATCH 35/50] add repo name --- kubernetes/helm-deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index 622c73480..ed1ff1757 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -50,6 +50,6 @@ fi chmod 600 "${KUBECONFIG}" -helm repo add https://grafana.github.io/helm-charts +helm repo add grafana https://grafana.github.io/helm-charts helm dependency build "${ROOT_DIR}/charts/openvsx" helm upgrade --install "${release_name}" "${ROOT_DIR}/charts/openvsx" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" From a1f7b1a4e195992a3e14eab2432074d198c8a0bd Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 23 Sep 2025 16:41:18 +0300 Subject: [PATCH 36/50] change default helm directories --- kubernetes/helm-deploy.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index ed1ff1757..c7f8cf821 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -50,6 +50,14 @@ fi chmod 600 "${KUBECONFIG}" +HELM_CACHE_HOME="${ROOT_DIR}/.helm/cache" +HELM_CONFIG_HOME="${ROOT_DIR}/.helm/config" +HELM_DATA_HOME="${ROOT_DIR}/.helm/data" + +mkdir -p "${HELM_CACHE_HOME}" +mkdir -p "${HELM_CONFIG_HOME}" +mkdir -p "${HELM_DATA_HOME}" + helm repo add grafana https://grafana.github.io/helm-charts helm dependency build "${ROOT_DIR}/charts/openvsx" helm upgrade --install "${release_name}" "${ROOT_DIR}/charts/openvsx" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" From f699353313320d41301a02e2b978d665c6755857 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 23 Sep 2025 16:50:36 +0300 Subject: [PATCH 37/50] Add old HELM_HOME variable --- kubernetes/helm-deploy.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index c7f8cf821..43a6283c8 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -50,14 +50,17 @@ fi chmod 600 "${KUBECONFIG}" +HELM_HOME="${ROOT_DIR}/.helm" HELM_CACHE_HOME="${ROOT_DIR}/.helm/cache" HELM_CONFIG_HOME="${ROOT_DIR}/.helm/config" HELM_DATA_HOME="${ROOT_DIR}/.helm/data" -mkdir -p "${HELM_CACHE_HOME}" -mkdir -p "${HELM_CONFIG_HOME}" -mkdir -p "${HELM_DATA_HOME}" +mkdir "${HELM_HOME}" +mkdir "${HELM_CACHE_HOME}" +mkdir "${HELM_CONFIG_HOME}" +mkdir "${HELM_DATA_HOME}" +helm version helm repo add grafana https://grafana.github.io/helm-charts helm dependency build "${ROOT_DIR}/charts/openvsx" helm upgrade --install "${release_name}" "${ROOT_DIR}/charts/openvsx" -f "${values_file}" --set image.tag="${image_tag}" --namespace "${namespace}" From ff78bda62632d27df5f19201e34409923e9588b7 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 23 Sep 2025 16:58:41 +0300 Subject: [PATCH 38/50] export variables --- kubernetes/helm-deploy.sh | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/kubernetes/helm-deploy.sh b/kubernetes/helm-deploy.sh index 43a6283c8..bda6e5b50 100755 --- a/kubernetes/helm-deploy.sh +++ b/kubernetes/helm-deploy.sh @@ -50,15 +50,13 @@ fi chmod 600 "${KUBECONFIG}" -HELM_HOME="${ROOT_DIR}/.helm" -HELM_CACHE_HOME="${ROOT_DIR}/.helm/cache" -HELM_CONFIG_HOME="${ROOT_DIR}/.helm/config" -HELM_DATA_HOME="${ROOT_DIR}/.helm/data" +export HELM_CACHE_HOME="${ROOT_DIR}/.helm/cache" +export HELM_CONFIG_HOME="${ROOT_DIR}/.helm/config" +export HELM_DATA_HOME="${ROOT_DIR}/.helm/data" -mkdir "${HELM_HOME}" -mkdir "${HELM_CACHE_HOME}" -mkdir "${HELM_CONFIG_HOME}" -mkdir "${HELM_DATA_HOME}" +mkdir -p "${HELM_CACHE_HOME}" +mkdir -p "${HELM_CONFIG_HOME}" +mkdir -p "${HELM_DATA_HOME}" helm version helm repo add grafana https://grafana.github.io/helm-charts From fbf2eb56e95d6115b29fc3a62b52d3540f4da172 Mon Sep 17 00:00:00 2001 From: John Kellerman Date: Tue, 23 Sep 2025 14:52:55 -0400 Subject: [PATCH 39/50] Update about.tsx Fixing link to request to join Slack space. Previous one expired. From be25bb8596ef05b475bfb683fb044e32f6e22731 Mon Sep 17 00:00:00 2001 From: John Kellerman Date: Tue, 23 Sep 2025 14:54:23 -0400 Subject: [PATCH 40/50] Update about.tsx Fixing slack link. Previous one had expired. From 782bcf43e3bc8319a2b8c5ccb9338e6fa2779d7e Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Wed, 24 Sep 2025 11:53:08 +0200 Subject: [PATCH 41/50] Add clusterrole for EF jiro ci-bot --- kubernetes/README.md | 9 ++++++++- kubernetes/clusterroles.yaml | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 kubernetes/clusterroles.yaml diff --git a/kubernetes/README.md b/kubernetes/README.md index b5e8832c1..72247ae2c 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -13,7 +13,14 @@ Where `` can be de4f2c Where `` can be de4f2c +## Preparing for EF JIRO specific environment +Since EF [JIRO](https://foundation.eclipse.org/ci/infra/job/open-vsx.org) runs with specific user, `clusterroles.yaml` has been added to allow jenkins to deploy environment. Resources need to be added with: + +```bash +kubectl apply -f clusterroles.yaml +``` + ## Dependencies * bash 4 -* [Helm](https://https://helm.sh/) \ No newline at end of file +* [Helm](https://https://helm.sh/) diff --git a/kubernetes/clusterroles.yaml b/kubernetes/clusterroles.yaml new file mode 100644 index 000000000..037256fc2 --- /dev/null +++ b/kubernetes/clusterroles.yaml @@ -0,0 +1,33 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ci-bot-manage-cluster-rbac +rules: + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles","clusterrolebindings"] + verbs: ["get","list","watch"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles","rolebindings"] + verbs: ["get","list","watch"] + - apiGroups: ["open-vsx.org"] + resources: ["redisclusters"] + resourceNames: ["redis"] + verbs: ["get","list","watch","update","patch"] + - apiGroups: ["monitoring.coreos.com"] + resources: ["servicemonitors"] + resourceNames: ["spring-boot-monitor-staging"] + verbs: ["get","list","watch","update","patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ci-bot-manage-cluster-rbac +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ci-bot-manage-cluster-rbac +subjects: + - kind: ServiceAccount + name: ci-bot + namespace: foundation-internal-infra + From 1c2a54fd90e553346ccd693702277dc857187044 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 25 Sep 2025 12:37:16 +0300 Subject: [PATCH 42/50] test cc14a668 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 660f99c66..066d9bcfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=b1ccf631 +ARG SERVER_VERSION=cc14a668 # Builder image to compile the website FROM ubuntu AS builder From d427f42110eb42623aa4b1601eaac1b674e4ad5d Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 25 Sep 2025 17:54:37 +0300 Subject: [PATCH 43/50] test 4ab10e5b --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 066d9bcfa..ec53c2d43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=cc14a668 +ARG SERVER_VERSION=4ab10e5b # Builder image to compile the website FROM ubuntu AS builder From c7c1ded0a684f70829da9e53e9571815d73913df Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 25 Sep 2025 22:04:34 +0300 Subject: [PATCH 44/50] Test v0.28.0-rc.1 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ec53c2d43..ef9f4e400 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=4ab10e5b +ARG SERVER_VERSION=v0.28-rc.1 # Builder image to compile the website FROM ubuntu AS builder From cc02c520e0520f82fe8e48b560bc897db5c3f14d Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Wed, 17 Sep 2025 08:51:20 +0300 Subject: [PATCH 45/50] Configure token-prefix --- configuration/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/configuration/application.yml b/configuration/application.yml index c5a6c587d..905580fe6 100644 --- a/configuration/application.yml +++ b/configuration/application.yml @@ -165,6 +165,7 @@ bucket4j: time: 1 unit: seconds ovsx: + token-prefix: ovsxp_ storage: primary-service: azure-blob webui: From 5eec8568ded39ed16bbdba24dfed4e351997ed44 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 25 Sep 2025 22:40:14 +0300 Subject: [PATCH 46/50] configure allowed versions --- configuration/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/configuration/application.yml b/configuration/application.yml index 905580fe6..2904c29ff 100644 --- a/configuration/application.yml +++ b/configuration/application.yml @@ -174,6 +174,7 @@ ovsx: base-url: https://api.eclipse.org/ publisher-agreement: version: 1 + allowed-versions: "1,1.0,1.1" publishing: require-license: true elasticsearch: From 67d839a42a6ba76d86c622296fc6dd2f538ce6bd Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 30 Sep 2025 12:17:39 +0300 Subject: [PATCH 47/50] external cache config --- configuration/application.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/configuration/application.yml b/configuration/application.yml index 2904c29ff..97adf836d 100644 --- a/configuration/application.yml +++ b/configuration/application.yml @@ -195,3 +195,27 @@ ovsx: key-pair: create registry: version: + caching: + files-extension: + tti: PT1H + max-size: 200 + files-webresource: + tti: PT1H + max-size: 500 + files-browse: + tti: PT1H + max-size: 100 + average-review-rating: + ttl: P3D + namespace-details-json: + ttl: PT1H + database-search: + ttl:PT1H + extension-json: + ttl: PT1H + latest-extension-version: + ttl: PT1H + sitemap: + ttl: PT1H + malicious-extensions: + ttl: P3D \ No newline at end of file From f74f3509e149d4200596861d02ea9d4826d712b5 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 30 Sep 2025 14:00:02 +0300 Subject: [PATCH 48/50] Remove test config --- charts/openvsx/values-staging.yaml | 1 - charts/openvsx/values.yaml | 1 - configuration/application.yml | 9 --------- kubernetes/clusterroles.yaml | 2 +- 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/charts/openvsx/values-staging.yaml b/charts/openvsx/values-staging.yaml index 0d2f84a1a..50eec1666 100644 --- a/charts/openvsx/values-staging.yaml +++ b/charts/openvsx/values-staging.yaml @@ -82,7 +82,6 @@ alloy: - name: zipkin port: 9411 targetPort: 9411 - stabilityLevel: experimental crds: create: true controller: diff --git a/charts/openvsx/values.yaml b/charts/openvsx/values.yaml index c4f71a5a4..dd06567ab 100644 --- a/charts/openvsx/values.yaml +++ b/charts/openvsx/values.yaml @@ -82,7 +82,6 @@ alloy: - name: zipkin port: 9411 targetPort: 9411 - stabilityLevel: experimental crds: create: true controller: diff --git a/configuration/application.yml b/configuration/application.yml index 97adf836d..20ebe9ad1 100644 --- a/configuration/application.yml +++ b/configuration/application.yml @@ -27,12 +27,6 @@ spring: name: openvsx-server config: import: file:${DEPLOYMENT_CONFIG} - data: - redis: - cluster: - nodes: 'redis-staging-0.redis-staging-service:6379,redis-staging-1.redis-staging-service:6379,redis-staging-2.redis-staging-service:6379,redis-staging-3.redis-staging-service:6379,redis-staging-4.redis-staging-service:6379,redis-staging-5.redis-staging-service:6379' - username: openvsx - password: openvsx datasource: hikari: maximum-pool-size: 10 @@ -81,9 +75,6 @@ management: include: - health - prometheus - zipkin: - tracing: - endpoint: http://grafana-alloy-staging:9411/api/v2/spans tracing: sampling: probability: 0.01 diff --git a/kubernetes/clusterroles.yaml b/kubernetes/clusterroles.yaml index 037256fc2..ca16f82a3 100644 --- a/kubernetes/clusterroles.yaml +++ b/kubernetes/clusterroles.yaml @@ -15,7 +15,7 @@ rules: verbs: ["get","list","watch","update","patch"] - apiGroups: ["monitoring.coreos.com"] resources: ["servicemonitors"] - resourceNames: ["spring-boot-monitor-staging"] + resourceNames: ["spring-boot-monitor-staging", "spring-boot-monitor-production"] verbs: ["get","list","watch","update","patch"] --- apiVersion: rbac.authorization.k8s.io/v1 From c53d47876abd3a2e9dd0021e768dbd1a8a4e692f Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Tue, 30 Sep 2025 13:31:48 +0300 Subject: [PATCH 49/50] release v0.28.0 --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index ef9f4e400..11a12825e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG SERVER_VERSION=v0.28-rc.1 +ARG SERVER_VERSION=v0.28.0 # Builder image to compile the website FROM ubuntu AS builder @@ -27,8 +27,7 @@ RUN /usr/bin/yarn --cwd website \ && /usr/bin/yarn --cwd website build # Main image derived from openvsx-server -# FROM ghcr.io/eclipse/openvsx-server:${SERVER_VERSION} -FROM docker.io/amvanbaren/openvsx-server:${SERVER_VERSION} +FROM ghcr.io/eclipse/openvsx-server:${SERVER_VERSION} ARG SERVER_VERSION COPY --from=builder --chown=openvsx:openvsx /workdir/website/static/ BOOT-INF/classes/static/ From e82c39490a7c86f1d48d6d1439d0030d2e350a49 Mon Sep 17 00:00:00 2001 From: Pawel Stankiewicz Date: Wed, 1 Oct 2025 15:29:15 +0200 Subject: [PATCH 50/50] Add alloy nodeSelector for daemonst --- charts/openvsx/values-staging.yaml | 2 ++ charts/openvsx/values.yaml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/charts/openvsx/values-staging.yaml b/charts/openvsx/values-staging.yaml index 50eec1666..6ee199548 100644 --- a/charts/openvsx/values-staging.yaml +++ b/charts/openvsx/values-staging.yaml @@ -89,5 +89,7 @@ alloy: podLabels: app: *name environment: *environment + nodeSelector: + openvsx-staging-alloy: "true" fullnameOverride: grafana-alloy-staging namespaceOverride: *namespace diff --git a/charts/openvsx/values.yaml b/charts/openvsx/values.yaml index dd06567ab..10fb774e4 100644 --- a/charts/openvsx/values.yaml +++ b/charts/openvsx/values.yaml @@ -89,5 +89,7 @@ alloy: podLabels: app: *name environment: *environment + nodeSelector: + openvsx-production-alloy: "true" fullnameOverride: grafana-alloy-production - namespaceOverride: *namespace \ No newline at end of file + namespaceOverride: *namespace