From 72493764f19032fca1535bc9f0cba3532d2cc873 Mon Sep 17 00:00:00 2001 From: Ketan Patel Date: Thu, 16 Apr 2026 20:27:35 -0400 Subject: [PATCH 1/5] Adding configuration for k8s --- .gitignore | 3 +++ k8s/configmap.yaml | 17 ----------------- k8s/deployment.yaml | 2 -- k8s/kustomization.yaml | 1 - k8s/secret.yaml | 14 -------------- k8s/secret.yaml.example | 31 +++++++++++++++++++++++++++++++ 6 files changed, 34 insertions(+), 34 deletions(-) delete mode 100644 k8s/configmap.yaml delete mode 100644 k8s/secret.yaml create mode 100644 k8s/secret.yaml.example diff --git a/.gitignore b/.gitignore index 1d18d15..929aaa1 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,9 @@ Thumbs.db *.tmp *.temp +# Kubernetes secrets (sops-encrypted original stays local) +k8s/secret.yaml + # Melange / apko build artifacts packages/ melange.rsa diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml deleted file mode 100644 index c2324c3..0000000 --- a/k8s/configmap.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: apcupsd-client-config - namespace: apcupsd - labels: - app.kubernetes.io/name: apcupsd-client - app.kubernetes.io/part-of: apcupsd-client -data: - REDIS_URL: "redis://redis:6379/0" - LOG_LEVEL: "info" - TZ: "UTC" - ALLOW_PRIVATE_IPS: "true" - TRUST_PROXY: "true" - RATE_LIMIT_ENABLED: "true" - ADMIN_USERNAME: "admin" - SESSION_MAX_AGE_SECONDS: "86400" diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index ee31917..0c664b9 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -33,8 +33,6 @@ spec: - containerPort: 8000 protocol: TCP envFrom: - - configMapRef: - name: apcupsd-client-config - secretRef: name: apcupsd-client-secret livenessProbe: diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index 1552ade..321d22e 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -3,7 +3,6 @@ kind: Kustomization resources: - namespace.yaml - - configmap.yaml - secret.yaml - redis-statefulset.yaml - redis-service.yaml diff --git a/k8s/secret.yaml b/k8s/secret.yaml deleted file mode 100644 index 8a39968..0000000 --- a/k8s/secret.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: apcupsd-client-secret - namespace: apcupsd - labels: - app.kubernetes.io/name: apcupsd-client - app.kubernetes.io/part-of: apcupsd-client -type: Opaque -stringData: - SESSION_SECRET: "CHANGE_ME_TO_A_RANDOM_SECRET" - # Optional — uncomment and set as needed: - # SMTP_PASSWORD: "" - # ADMIN_PASSWORD_HASH: "" diff --git a/k8s/secret.yaml.example b/k8s/secret.yaml.example new file mode 100644 index 0000000..529b988 --- /dev/null +++ b/k8s/secret.yaml.example @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Secret +metadata: + name: apcupsd-client-secret + namespace: apcupsd + labels: + app.kubernetes.io/name: apcupsd-client + app.kubernetes.io/part-of: apcupsd-client +type: Opaque +stringData: + # Auth (required in production) + # Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))" + SESSION_SECRET: "" + ADMIN_USERNAME: "admin" + # Generate with: python -c "from passlib.hash import argon2; print(argon2.hash('yourpassword'))" + ADMIN_PASSWORD_HASH: "" + SESSION_MAX_AGE_SECONDS: "1209600" + TRUST_PROXY: "false" + # Redis + REDIS_URL: "redis://redis:6379/0" + # SMTP (optional) + SMTP_PASSWORD: "" + # Network + ALLOW_PRIVATE_IPS: "true" + # Observability + LOG_LEVEL: "INFO" + RATE_LIMIT_ENABLED: "true" + # Locale + TZ: "UTC" + # Legacy migration (unused unless /config is mounted) + UPS_CONFIG_PATH: "/config/ups.yaml" From 49d96722cbad87ec225b561af2b6a067ff2381c1 Mon Sep 17 00:00:00 2001 From: Ketan Patel Date: Thu, 16 Apr 2026 21:08:32 -0400 Subject: [PATCH 2/5] adding the replacement for k8s with charts --- .gitignore | 4 +-- k8s/deployment.yaml | 69 ------------------------------------ k8s/kustomization.yaml | 10 ------ k8s/namespace.yaml | 6 ---- k8s/redis-service.yaml | 17 --------- k8s/redis-statefulset.yaml | 72 -------------------------------------- k8s/secret.yaml.example | 31 ---------------- k8s/service.yaml | 17 --------- 8 files changed, 2 insertions(+), 224 deletions(-) delete mode 100644 k8s/deployment.yaml delete mode 100644 k8s/kustomization.yaml delete mode 100644 k8s/namespace.yaml delete mode 100644 k8s/redis-service.yaml delete mode 100644 k8s/redis-statefulset.yaml delete mode 100644 k8s/secret.yaml.example delete mode 100644 k8s/service.yaml diff --git a/.gitignore b/.gitignore index 929aaa1..491f824 100644 --- a/.gitignore +++ b/.gitignore @@ -52,8 +52,8 @@ Thumbs.db *.tmp *.temp -# Kubernetes secrets (sops-encrypted original stays local) -k8s/secret.yaml +# Secrets (sops-encrypted, never commit decrypted) +chart/values-secret.yaml # Melange / apko build artifacts packages/ diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml deleted file mode 100644 index 0c664b9..0000000 --- a/k8s/deployment.yaml +++ /dev/null @@ -1,69 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: apcupsd-client - namespace: apcupsd - labels: - app.kubernetes.io/name: apcupsd-client - app.kubernetes.io/part-of: apcupsd-client -spec: - replicas: 1 - strategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 0 - maxSurge: 1 - selector: - matchLabels: - app.kubernetes.io/name: apcupsd-client - template: - metadata: - labels: - app.kubernetes.io/name: apcupsd-client - spec: - securityContext: - runAsUser: 10001 - runAsGroup: 10001 - runAsNonRoot: true - fsGroup: 10001 - containers: - - name: web - image: ghcr.io/k2patel/apcupsd-client:latest - ports: - - containerPort: 8000 - protocol: TCP - envFrom: - - secretRef: - name: apcupsd-client-secret - livenessProbe: - httpGet: - path: /healthz - port: 8000 - initialDelaySeconds: 15 - periodSeconds: 30 - timeoutSeconds: 5 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /readyz - port: 8000 - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "256Mi" - cpu: "500m" - securityContext: - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - volumeMounts: - - name: tmp - mountPath: /tmp - volumes: - - name: tmp - emptyDir: {} diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml deleted file mode 100644 index 321d22e..0000000 --- a/k8s/kustomization.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - namespace.yaml - - secret.yaml - - redis-statefulset.yaml - - redis-service.yaml - - deployment.yaml - - service.yaml diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml deleted file mode 100644 index 12d715c..0000000 --- a/k8s/namespace.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: apcupsd - labels: - app.kubernetes.io/part-of: apcupsd-client diff --git a/k8s/redis-service.yaml b/k8s/redis-service.yaml deleted file mode 100644 index 8117ca6..0000000 --- a/k8s/redis-service.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: redis - namespace: apcupsd - labels: - app.kubernetes.io/name: redis - app.kubernetes.io/part-of: apcupsd-client -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: redis - ports: - - name: redis - protocol: TCP - port: 6379 - targetPort: 6379 diff --git a/k8s/redis-statefulset.yaml b/k8s/redis-statefulset.yaml deleted file mode 100644 index 7008a0e..0000000 --- a/k8s/redis-statefulset.yaml +++ /dev/null @@ -1,72 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: redis - namespace: apcupsd - labels: - app.kubernetes.io/name: redis - app.kubernetes.io/part-of: apcupsd-client -spec: - serviceName: redis - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: redis - template: - metadata: - labels: - app.kubernetes.io/name: redis - spec: - securityContext: - runAsUser: 999 - runAsGroup: 999 - fsGroup: 999 - containers: - - name: redis - image: redis:7-alpine - command: - - redis-server - - --appendonly - - "yes" - - --appendfsync - - everysec - ports: - - containerPort: 6379 - protocol: TCP - livenessProbe: - exec: - command: - - redis-cli - - ping - initialDelaySeconds: 10 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 5 - readinessProbe: - exec: - command: - - redis-cli - - ping - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - resources: - requests: - memory: "64Mi" - cpu: "50m" - limits: - memory: "128Mi" - cpu: "250m" - volumeMounts: - - name: redis-data - mountPath: /data - volumeClaimTemplates: - - metadata: - name: redis-data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi diff --git a/k8s/secret.yaml.example b/k8s/secret.yaml.example deleted file mode 100644 index 529b988..0000000 --- a/k8s/secret.yaml.example +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: apcupsd-client-secret - namespace: apcupsd - labels: - app.kubernetes.io/name: apcupsd-client - app.kubernetes.io/part-of: apcupsd-client -type: Opaque -stringData: - # Auth (required in production) - # Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))" - SESSION_SECRET: "" - ADMIN_USERNAME: "admin" - # Generate with: python -c "from passlib.hash import argon2; print(argon2.hash('yourpassword'))" - ADMIN_PASSWORD_HASH: "" - SESSION_MAX_AGE_SECONDS: "1209600" - TRUST_PROXY: "false" - # Redis - REDIS_URL: "redis://redis:6379/0" - # SMTP (optional) - SMTP_PASSWORD: "" - # Network - ALLOW_PRIVATE_IPS: "true" - # Observability - LOG_LEVEL: "INFO" - RATE_LIMIT_ENABLED: "true" - # Locale - TZ: "UTC" - # Legacy migration (unused unless /config is mounted) - UPS_CONFIG_PATH: "/config/ups.yaml" diff --git a/k8s/service.yaml b/k8s/service.yaml deleted file mode 100644 index f1c9b3a..0000000 --- a/k8s/service.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: apcupsd-client - namespace: apcupsd - labels: - app.kubernetes.io/name: apcupsd-client - app.kubernetes.io/part-of: apcupsd-client -spec: - type: LoadBalancer - selector: - app.kubernetes.io/name: apcupsd-client - ports: - - name: http - protocol: TCP - port: 10280 - targetPort: 8000 From 08438b225241edffcd8bb2ea28c8115300aa7465 Mon Sep 17 00:00:00 2001 From: Ketan Patel Date: Thu, 16 Apr 2026 21:10:03 -0400 Subject: [PATCH 3/5] Adding the helm chart --- chart/Chart.yaml | 6 +++ chart/templates/_helpers.tpl | 20 ++++++++ chart/templates/configmap.yaml | 11 +++++ chart/templates/deployment.yaml | 65 ++++++++++++++++++++++++ chart/templates/redis-service.yaml | 18 +++++++ chart/templates/redis-statefulset.yaml | 68 ++++++++++++++++++++++++++ chart/templates/secret.yaml | 12 +++++ chart/templates/service.yaml | 16 ++++++ chart/values-secret.yaml.example | 7 +++ chart/values.yaml | 56 +++++++++++++++++++++ 10 files changed, 279 insertions(+) create mode 100644 chart/Chart.yaml create mode 100644 chart/templates/_helpers.tpl create mode 100644 chart/templates/configmap.yaml create mode 100644 chart/templates/deployment.yaml create mode 100644 chart/templates/redis-service.yaml create mode 100644 chart/templates/redis-statefulset.yaml create mode 100644 chart/templates/secret.yaml create mode 100644 chart/templates/service.yaml create mode 100644 chart/values-secret.yaml.example create mode 100644 chart/values.yaml diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..13b26a7 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: apcupsd-client +description: FastAPI + Redis UPS monitoring dashboard +type: application +version: 1.0.0 +appVersion: "1.0.0" diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000..e50f847 --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,20 @@ +{{- define "app.labels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/part-of: {{ .Chart.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "app.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +{{- end -}} + +{{- define "redis.labels" -}} +app.kubernetes.io/name: redis +app.kubernetes.io/part-of: {{ .Chart.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "redis.selectorLabels" -}} +app.kubernetes.io/name: redis +{{- end -}} diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml new file mode 100644 index 0000000..8a805dc --- /dev/null +++ b/chart/templates/configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }}-config + namespace: {{ .Values.namespace }} + labels: + {{- include "app.labels" . | nindent 4 }} +data: + {{- range $key, $val := .Values.config }} + {{ $key }}: {{ $val | quote }} + {{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..ee13236 --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }} + namespace: {{ .Values.namespace }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.app.replicas }} + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + {{- include "app.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "app.selectorLabels" . | nindent 8 }} + spec: + securityContext: + runAsUser: {{ .Values.app.securityContext.runAsUser }} + runAsGroup: {{ .Values.app.securityContext.runAsGroup }} + runAsNonRoot: true + fsGroup: {{ .Values.app.securityContext.runAsGroup }} + containers: + - name: web + image: {{ .Values.app.image }} + ports: + - containerPort: {{ .Values.app.port }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ .Chart.Name }}-config + - secretRef: + name: {{ .Chart.Name }}-secret + livenessProbe: + httpGet: + path: {{ .Values.app.probes.liveness.path }} + port: {{ .Values.app.port }} + initialDelaySeconds: {{ .Values.app.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.app.probes.liveness.periodSeconds }} + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: {{ .Values.app.probes.readiness.path }} + port: {{ .Values.app.port }} + initialDelaySeconds: {{ .Values.app.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.app.probes.readiness.periodSeconds }} + timeoutSeconds: 5 + failureThreshold: 3 + resources: + {{- toYaml .Values.app.resources | nindent 12 }} + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/chart/templates/redis-service.yaml b/chart/templates/redis-service.yaml new file mode 100644 index 0000000..33e01cd --- /dev/null +++ b/chart/templates/redis-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.redis.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: {{ .Values.namespace }} + labels: + {{- include "redis.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "redis.selectorLabels" . | nindent 4 }} + ports: + - name: redis + protocol: TCP + port: {{ .Values.redis.port }} + targetPort: {{ .Values.redis.port }} +{{- end }} diff --git a/chart/templates/redis-statefulset.yaml b/chart/templates/redis-statefulset.yaml new file mode 100644 index 0000000..a98b3d1 --- /dev/null +++ b/chart/templates/redis-statefulset.yaml @@ -0,0 +1,68 @@ +{{- if .Values.redis.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis + namespace: {{ .Values.namespace }} + labels: + {{- include "redis.labels" . | nindent 4 }} +spec: + serviceName: redis + replicas: 1 + selector: + matchLabels: + {{- include "redis.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "redis.selectorLabels" . | nindent 8 }} + spec: + securityContext: + runAsUser: {{ .Values.redis.securityContext.runAsUser }} + runAsGroup: {{ .Values.redis.securityContext.runAsGroup }} + fsGroup: {{ .Values.redis.securityContext.runAsGroup }} + containers: + - name: redis + image: {{ .Values.redis.image }} + command: + - redis-server + - --appendonly + - "yes" + - --appendfsync + - everysec + ports: + - containerPort: {{ .Values.redis.port }} + protocol: TCP + livenessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 5 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + {{- toYaml .Values.redis.resources | nindent 12 }} + volumeMounts: + - name: redis-data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: redis-data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.redis.storage }} +{{- end }} diff --git a/chart/templates/secret.yaml b/chart/templates/secret.yaml new file mode 100644 index 0000000..f6b5917 --- /dev/null +++ b/chart/templates/secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Chart.Name }}-secret + namespace: {{ .Values.namespace }} + labels: + {{- include "app.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $val := .Values.secrets }} + {{ $key }}: {{ $val | quote }} + {{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000..5badff5 --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + namespace: {{ .Values.namespace }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "app.selectorLabels" . | nindent 4 }} + ports: + - name: http + protocol: TCP + port: {{ .Values.service.port }} + targetPort: {{ .Values.app.port }} diff --git a/chart/values-secret.yaml.example b/chart/values-secret.yaml.example new file mode 100644 index 0000000..fa0ac28 --- /dev/null +++ b/chart/values-secret.yaml.example @@ -0,0 +1,7 @@ +secrets: + # Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))" + SESSION_SECRET: "" + # Generate with: python -c "from passlib.hash import argon2; print(argon2.hash('yourpassword'))" + ADMIN_PASSWORD_HASH: "" + # Optional SMTP password + SMTP_PASSWORD: "" diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..d3ce8c0 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,56 @@ +namespace: apcupsd + +app: + image: ghcr.io/k2patel/apcupsd-client:latest + replicas: 1 + port: 8000 + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + securityContext: + runAsUser: 10001 + runAsGroup: 10001 + probes: + liveness: + path: /healthz + initialDelaySeconds: 15 + periodSeconds: 30 + readiness: + path: /readyz + initialDelaySeconds: 5 + periodSeconds: 10 + +service: + type: LoadBalancer + port: 80 + +redis: + enabled: true + image: redis:7-alpine + port: 6379 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "250m" + storage: 1Gi + securityContext: + runAsUser: 999 + runAsGroup: 999 + +config: + ADMIN_USERNAME: "admin" + REDIS_URL: "redis://redis:6379/0" + ALLOW_PRIVATE_IPS: "true" + TRUST_PROXY: "false" + LOG_LEVEL: "INFO" + RATE_LIMIT_ENABLED: "true" + SESSION_MAX_AGE_SECONDS: "1209600" + TZ: "America/New_York" + UPS_CONFIG_PATH: "/config/ups.yaml" From 5532eda43a7ae0ac02420040b00c93b307e1bb96 Mon Sep 17 00:00:00 2001 From: Ketan Patel Date: Thu, 16 Apr 2026 21:11:08 -0400 Subject: [PATCH 4/5] Adding the correct values --- .gitignore | 3 ++- chart/{values.yaml => values.yaml.example} | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) rename chart/{values.yaml => values.yaml.example} (97%) diff --git a/.gitignore b/.gitignore index 491f824..a95f4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,8 @@ Thumbs.db *.tmp *.temp -# Secrets (sops-encrypted, never commit decrypted) +# Helm values (contain environment-specific config) +chart/values.yaml chart/values-secret.yaml # Melange / apko build artifacts diff --git a/chart/values.yaml b/chart/values.yaml.example similarity index 97% rename from chart/values.yaml rename to chart/values.yaml.example index d3ce8c0..946921a 100644 --- a/chart/values.yaml +++ b/chart/values.yaml.example @@ -52,5 +52,5 @@ config: LOG_LEVEL: "INFO" RATE_LIMIT_ENABLED: "true" SESSION_MAX_AGE_SECONDS: "1209600" - TZ: "America/New_York" + TZ: "UTC" UPS_CONFIG_PATH: "/config/ups.yaml" From aabe0b9fd35d61e768bd383e3e74b8743e6acd1e Mon Sep 17 00:00:00 2001 From: Ketan Patel Date: Thu, 16 Apr 2026 21:11:51 -0400 Subject: [PATCH 5/5] Adding the install settings --- chart/SETUP.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++ deploy.sh | 97 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 chart/SETUP.md create mode 100755 deploy.sh diff --git a/chart/SETUP.md b/chart/SETUP.md new file mode 100644 index 0000000..09e6e1a --- /dev/null +++ b/chart/SETUP.md @@ -0,0 +1,110 @@ +# Helm Deployment Setup + +## Prerequisites + +- helm v3 +- sops +- age (for key generation) +- kubectl with cluster access + +## 1. Install helm-secrets plugin + +The plugin must register as `getter/v1` type so it works via the `secrets://` protocol handler. + +```bash +# Install helm-secrets as a getter/downloader plugin +helm plugin install https://github.com/jkroepke/helm-secrets --verify=false +``` + +Verify it registered correctly: + +```bash +helm plugin list +``` + +Expected output: + +``` +NAME VERSION TYPE ... +secrets 4.x.x getter/v1 ... +``` + +The `getter/v1` type means helm-secrets acts as a protocol handler. +Use `secrets://` prefix on encrypted value files instead of `helm secrets `: + +```bash +# Correct usage with getter/v1 +helm install my-release ./chart -f values.yaml -f secrets://values-secret.yaml + +# NOT: helm secrets install ... (that requires wrapper type, not getter) +``` + +## 2. Generate an age key + +```bash +age-keygen -o ~/.config/sops/age/keys.txt +``` + +Note the public key from the output (starts with `age1...`). + +## 3. Configure sops + +Create `.sops.yaml` in the repo root: + +```yaml +creation_rules: + - path_regex: values-secret\.yaml$ + age: age1yourpublickeyhere +``` + +## 4. Create values files + +```bash +cp chart/values.yaml.example chart/values.yaml +cp chart/values-secret.yaml.example chart/values-secret.yaml +``` + +Edit `chart/values.yaml` with your environment config (non-sensitive). + +Edit `chart/values-secret.yaml` with sensitive values: + +```bash +# Generate a session secret +python -c "import secrets; print(secrets.token_urlsafe(32))" + +# Generate an admin password hash +python -c "from passlib.hash import argon2; print(argon2.hash('yourpassword'))" +``` + +## 5. Encrypt secrets + +```bash +sops -e -i chart/values-secret.yaml +``` + +To edit later: + +```bash +sops chart/values-secret.yaml # opens in $EDITOR, re-encrypts on save +``` + +Or decrypt/encrypt in place: + +```bash +sops -d -i chart/values-secret.yaml # decrypt +# edit the file +sops -e -i chart/values-secret.yaml # re-encrypt +``` + +## 6. Deploy + +```bash +./deploy.sh install # first time +./deploy.sh upgrade # subsequent deploys +./deploy.sh diff # preview changes (requires helm-diff plugin) +./deploy.sh template # render manifests locally +./deploy.sh status # show running pods/services +./deploy.sh logs # tail app logs +./deploy.sh restart # rolling restart +./deploy.sh destroy # uninstall +``` diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..c05bd16 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy apcupsd-client via Helm with sops-encrypted secrets +# Usage: ./deploy.sh [install|upgrade|diff|destroy|status|logs|restart] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CHART_DIR="${SCRIPT_DIR}/chart" +RELEASE="apcupsd-client" +NAMESPACE="apcupsd" +VALUES="${CHART_DIR}/values.yaml" +SECRETS="secrets://${CHART_DIR}/values-secret.yaml" +ACTION="${1:-upgrade}" + +# Preflight checks +for cmd in helm sops; do + if ! command -v "$cmd" &>/dev/null; then + echo "ERROR: $cmd not found in PATH" >&2 + exit 1 + fi +done + +# Verify helm-secrets plugin is available +if ! helm plugin list | grep -q secrets; then + echo "ERROR: helm-secrets plugin not installed. Install with: helm plugin install https://github.com/jkroepke/helm-secrets" >&2 + exit 1 +fi + +# Check if secrets file exists +if [[ ! -f "${CHART_DIR}/values-secret.yaml" ]]; then + echo "ERROR: values-secret.yaml not found. Copy values-secret.yaml.example and encrypt with sops." >&2 + exit 1 +fi + +case "$ACTION" in + install) + echo "--- Installing ${RELEASE} ---" + helm install "$RELEASE" "$CHART_DIR" \ + -n "$NAMESPACE" --create-namespace \ + -f "$VALUES" \ + -f "$SECRETS" + echo "--- Install complete ---" + kubectl -n "$NAMESPACE" get pods + ;; + + upgrade) + echo "--- Upgrading ${RELEASE} ---" + helm upgrade "$RELEASE" "$CHART_DIR" \ + -n "$NAMESPACE" --create-namespace --install \ + -f "$VALUES" \ + -f "$SECRETS" + echo "--- Upgrade complete ---" + kubectl -n "$NAMESPACE" get pods + ;; + + diff) + if ! helm plugin list | grep -q diff; then + echo "ERROR: helm-diff plugin not installed. Install with: helm plugin install https://github.com/databus23/helm-diff" >&2 + exit 1 + fi + helm diff upgrade "$RELEASE" "$CHART_DIR" \ + -n "$NAMESPACE" \ + -f "$VALUES" \ + -f "$SECRETS" || true + ;; + + template) + helm template "$RELEASE" "$CHART_DIR" \ + -n "$NAMESPACE" \ + -f "$VALUES" \ + -f "$SECRETS" + ;; + + destroy) + echo "--- Uninstalling ${RELEASE} ---" + helm uninstall "$RELEASE" -n "$NAMESPACE" --ignore-not-found + echo "--- Destroyed ---" + ;; + + status) + kubectl -n "$NAMESPACE" get all + ;; + + logs) + kubectl -n "$NAMESPACE" logs -l app.kubernetes.io/name=apcupsd-client -f --tail=50 + ;; + + restart) + kubectl -n "$NAMESPACE" rollout restart deployment/apcupsd-client + kubectl -n "$NAMESPACE" rollout status deployment/apcupsd-client --timeout=120s + ;; + + *) + echo "Usage: $0 {install|upgrade|diff|template|destroy|status|logs|restart}" >&2 + exit 1 + ;; +esac