diff --git a/charts/memos/README.md b/charts/memos/README.md index 557928d76..d53e3cd56 100644 --- a/charts/memos/README.md +++ b/charts/memos/README.md @@ -20,6 +20,12 @@ The default chart topology is intentionally conservative: When `database.driver` is `mysql` or `postgres`, the chart renders `MEMOS_DSN` from a Kubernetes Secret. The data volume remains required because Memos can still store local assets and instance data outside the external database. +Ingress class rendering is optional. Set `ingress.ingressClassName: ""` to omit `spec.ingressClassName`. +When `networkPolicy.enabled=true`, ingress is restricted to the configured peers. +Set `networkPolicy.egressIsolation=true` to add baseline DNS and HTTPS egress rules without custom egress rules. DNS egress defaults to +`kube-system` pods labeled `k8s-app: kube-dns`; override `networkPolicy.dnsEgress` for clusters with different DNS labels. HTTPS egress +defaults to IPv4 and IPv6 internet destinations on port 443; override `networkPolicy.httpsEgress` to restrict webhook and integration +destinations. Setting `networkPolicy.extraEgress` also enables egress isolation and appends the supplied database, webhook, or proxy rules. ## Install diff --git a/charts/memos/templates/NOTES.txt b/charts/memos/templates/NOTES.txt index 382a6a496..0fb9401a0 100644 --- a/charts/memos/templates/NOTES.txt +++ b/charts/memos/templates/NOTES.txt @@ -1,46 +1,88 @@ {{/* SPDX-License-Identifier: Apache-2.0 */}} -Memos release: {{ .Release.Name }} -Namespace: {{ .Release.Namespace }} +Memos has been deployed. -Workload: +1. Access + +- Service DNS: http://{{ include "memos.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.service.port }} +- Port-forward: + kubectl port-forward svc/{{ include "memos.fullname" . }} 5230:{{ .Values.service.port }} -n {{ .Release.Namespace }} +- Browser URL: + http://127.0.0.1:5230 + +2. Workload + +- StatefulSet: kubectl get statefulset {{ include "memos.fullname" . }} -n {{ .Release.Namespace }} +- Rollout: kubectl rollout status statefulset/{{ include "memos.fullname" . }} -n {{ .Release.Namespace }} +- Replicas: {{ .Values.replicaCount }} +- Image: {{ include "memos.image" . }} -Service: - kubectl get svc {{ include "memos.fullname" . }} -n {{ .Release.Namespace }} +3. Runtime Configuration -Local access: - kubectl port-forward svc/{{ include "memos.fullname" . }} 5230:{{ .Values.service.port }} -n {{ .Release.Namespace }} - Open http://127.0.0.1:5230 - -Runtime configuration: - Database driver: {{ .Values.database.driver }} - Data directory: {{ .Values.persistence.mountPath }} - Public instance URL: {{ default "not set" .Values.memos.instanceUrl }} - Private webhooks: {{ .Values.memos.allowPrivateWebhooks }} - -Persistence: -{{- if .Values.persistence.enabled }} - PVC is enabled. Back up the data volume before upgrades. -{{- else if .Values.persistence.existingClaim }} - Existing PVC {{ .Values.persistence.existingClaim }} is mounted. +- Database driver: {{ .Values.database.driver }} +- Data directory: {{ .Values.persistence.mountPath }} +- Public instance URL: {{ default "not set" .Values.memos.instanceUrl }} +- Private webhooks: {{ .Values.memos.allowPrivateWebhooks }} +- Log level: {{ .Values.memos.logLevel }} + +4. Persistence + +{{- if .Values.persistence.existingClaim }} +- Existing PVC {{ .Values.persistence.existingClaim }} is mounted. +{{- else if .Values.persistence.enabled }} +- A chart-managed volumeClaimTemplate is enabled. +- PVC size: {{ .Values.persistence.size }} +- Access modes: {{ join "," .Values.persistence.accessModes }} {{- else }} - Persistence is disabled and Memos data is stored in an emptyDir. +- Persistence is disabled and Memos data is stored in emptyDir. {{- end }} +- Back up the data volume before upgrades and before database migrations. + +5. Database -Database notes: {{- if eq .Values.database.driver "sqlite" }} - SQLite is stored under {{ .Values.persistence.mountPath }}. Keep replicaCount=1 and back up the PVC. +- SQLite is stored under {{ .Values.persistence.mountPath }}. +- Keep replicaCount=1 when using SQLite. +{{- else }} +- External {{ .Values.database.driver }} is enabled through MEMOS_DSN. +- Back up both the external database and the Memos data volume. +{{- end }} + +6. Exposure + +{{- if .Values.ingress.enabled }} +- Ingress is enabled. +{{- else }} +- Ingress is disabled. +{{- end }} +{{- if .Values.gateway.enabled }} +- Gateway API HTTPRoute is enabled. {{- else }} - External {{ .Values.database.driver }} is enabled. Back up both the external database and the Memos data PVC. +- Gateway API HTTPRoute is disabled. +{{- end }} +{{- if .Values.networkPolicy.enabled }} +- NetworkPolicy is enabled for inbound Memos traffic. +{{- else }} +- NetworkPolicy is disabled. {{- end }} -Troubleshooting: +7. Troubleshooting + +- Pods: + kubectl get pod -l app.kubernetes.io/instance={{ .Release.Name }} -n {{ .Release.Namespace }} +- Logs: kubectl logs statefulset/{{ include "memos.fullname" . }} -n {{ .Release.Namespace }} +- Describe pods: kubectl describe pod -l app.kubernetes.io/instance={{ .Release.Name }} -n {{ .Release.Namespace }} +- Service endpoints: + kubectl get endpoints {{ include "memos.fullname" . }} -n {{ .Release.Namespace }} + +8. Resources -Validation: - helm test {{ .Release.Name }} -n {{ .Release.Namespace }} - make validate-chart CHART=memos +- Chart documentation: https://helmforge.dev/docs/charts/memos +- Memos Docker deployment: https://usememos.com/docs/deploy/docker +- Memos source: https://github.com/usememos/memos +- Validation command: make validate-chart CHART=memos Happy Helmforging :) diff --git a/charts/memos/templates/_helpers.tpl b/charts/memos/templates/_helpers.tpl index e64ee8ff2..245eff6c5 100644 --- a/charts/memos/templates/_helpers.tpl +++ b/charts/memos/templates/_helpers.tpl @@ -17,3 +17,24 @@ app.kubernetes.io/part-of: helmforge {{- define "memos.serviceAccountName" -}}{{- if .Values.serviceAccount.create -}}{{- default (include "memos.fullname" .) .Values.serviceAccount.name -}}{{- else -}}{{- default "default" .Values.serviceAccount.name -}}{{- end -}}{{- end -}} {{- define "memos.image" -}}{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}{{- end -}} {{- define "memos.databaseSecretName" -}}{{- default (printf "%s-database" (include "memos.fullname" .)) .Values.database.existingSecret -}}{{- end -}} + +{{- define "memos.validate" -}} +{{- if and (gt (int .Values.replicaCount) 1) (eq .Values.database.driver "sqlite") -}} +{{- fail "replicaCount > 1 requires database.driver=mysql or database.driver=postgres because SQLite cannot safely share state across Memos pods" -}} +{{- end -}} +{{- if and (ne .Values.database.driver "sqlite") (not (or .Values.database.dsn .Values.database.existingSecret)) -}} +{{- fail "database.dsn or database.existingSecret is required when database.driver is mysql or postgres" -}} +{{- end -}} +{{- if and (ne .Values.database.driver "sqlite") (not .Values.persistence.enabled) (not .Values.persistence.existingClaim) -}} +{{- fail "persistence.enabled or persistence.existingClaim is required with external databases because Memos still stores local assets in MEMOS_DATA" -}} +{{- end -}} +{{- if and (gt (int .Values.replicaCount) 1) (ne .Values.database.driver "sqlite") (not .Values.persistence.existingClaim) -}} +{{- fail "replicaCount > 1 with mysql or postgres requires persistence.existingClaim backed by shared storage because generated StatefulSet PVCs are per-pod and Memos can store local assets in MEMOS_DATA" -}} +{{- end -}} +{{- $podLabels := .Values.podLabels | default dict -}} +{{- range $key := (list "app.kubernetes.io/name" "app.kubernetes.io/instance") -}} +{{- if hasKey $podLabels $key -}} +{{- fail (printf "podLabels must not override the selector label %s" $key) -}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/charts/memos/templates/ingress.yaml b/charts/memos/templates/ingress.yaml index 17ef2db62..772850aec 100644 --- a/charts/memos/templates/ingress.yaml +++ b/charts/memos/templates/ingress.yaml @@ -11,7 +11,9 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: - ingressClassName: {{ .Values.ingress.ingressClassName | quote }} + {{- with .Values.ingress.ingressClassName }} + ingressClassName: {{ . | quote }} + {{- end }} {{- with .Values.ingress.tls }} tls: {{- toYaml . | nindent 4 }} diff --git a/charts/memos/templates/networkpolicy.yaml b/charts/memos/templates/networkpolicy.yaml index 21f3028b5..c4a1266f6 100644 --- a/charts/memos/templates/networkpolicy.yaml +++ b/charts/memos/templates/networkpolicy.yaml @@ -1,5 +1,9 @@ {{/* SPDX-License-Identifier: Apache-2.0 */}} {{- if .Values.networkPolicy.enabled }} +{{- $extraEgress := .Values.networkPolicy.extraEgress | default list }} +{{- $dnsEgress := .Values.networkPolicy.dnsEgress | default list }} +{{- $httpsEgress := .Values.networkPolicy.httpsEgress | default list }} +{{- $egressEnabled := or .Values.networkPolicy.egressIsolation (gt (len $extraEgress) 0) }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -12,6 +16,9 @@ spec: {{- include "memos.selectorLabels" . | nindent 6 }} policyTypes: - Ingress + {{- if $egressEnabled }} + - Egress + {{- end }} ingress: - from: {{- if .Values.networkPolicy.ingressFrom }} @@ -22,4 +29,26 @@ spec: ports: - protocol: TCP port: http + {{- if $egressEnabled }} + egress: + {{- if gt (len $dnsEgress) 0 }} + - to: + {{- toYaml $dnsEgress | nindent 8 }} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + {{- end }} + {{- if gt (len $httpsEgress) 0 }} + - to: + {{- toYaml $httpsEgress | nindent 8 }} + ports: + - protocol: TCP + port: 443 + {{- end }} + {{- with $extraEgress }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} {{- end }} diff --git a/charts/memos/templates/statefulset.yaml b/charts/memos/templates/statefulset.yaml index 5c3ca6fb6..aed751bff 100644 --- a/charts/memos/templates/statefulset.yaml +++ b/charts/memos/templates/statefulset.yaml @@ -14,7 +14,7 @@ spec: template: metadata: labels: - {{- include "memos.labels" . | nindent 8 }} + {{- include "memos.selectorLabels" . | nindent 8 }} {{- with .Values.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/memos/templates/validate.yaml b/charts/memos/templates/validate.yaml index 91f535706..8023f3c25 100644 --- a/charts/memos/templates/validate.yaml +++ b/charts/memos/templates/validate.yaml @@ -1,13 +1,2 @@ {{/* SPDX-License-Identifier: Apache-2.0 */}} -{{- if and (gt (int .Values.replicaCount) 1) (eq .Values.database.driver "sqlite") -}} -{{- fail "replicaCount > 1 requires database.driver=mysql or database.driver=postgres because SQLite cannot safely share state across Memos pods" -}} -{{- end -}} -{{- if and (ne .Values.database.driver "sqlite") (not (or .Values.database.dsn .Values.database.existingSecret)) -}} -{{- fail "database.dsn or database.existingSecret is required when database.driver is mysql or postgres" -}} -{{- end -}} -{{- if and (ne .Values.database.driver "sqlite") (not .Values.persistence.enabled) (not .Values.persistence.existingClaim) -}} -{{- fail "persistence.enabled or persistence.existingClaim is required with external databases because Memos still stores local assets in MEMOS_DATA" -}} -{{- end -}} -{{- if and (gt (int .Values.replicaCount) 1) (ne .Values.database.driver "sqlite") (not .Values.persistence.existingClaim) -}} -{{- fail "replicaCount > 1 with mysql or postgres requires persistence.existingClaim backed by shared storage because generated StatefulSet PVCs are per-pod and Memos can store local assets in MEMOS_DATA" -}} -{{- end -}} +{{- include "memos.validate" . -}} diff --git a/charts/memos/tests/networkpolicy_test.yaml b/charts/memos/tests/networkpolicy_test.yaml index f9af45238..d737376c0 100644 --- a/charts/memos/tests/networkpolicy_test.yaml +++ b/charts/memos/tests/networkpolicy_test.yaml @@ -14,3 +14,76 @@ tests: content: protocol: TCP port: http + - notContains: + path: spec.policyTypes + content: Egress + - notExists: + path: spec.egress + - it: enables baseline DNS and HTTPS egress isolation + set: + networkPolicy: + enabled: true + egressIsolation: true + asserts: + - contains: + path: spec.policyTypes + content: Egress + - equal: + path: spec.egress[0].to[0].namespaceSelector.matchLabels["kubernetes.io/metadata.name"] + value: kube-system + - equal: + path: spec.egress[0].to[0].podSelector.matchLabels["k8s-app"] + value: kube-dns + - equal: + path: spec.egress[1].ports[0].port + value: 443 + - equal: + path: spec.egress[1].to[0].ipBlock.cidr + value: 0.0.0.0/0 + - equal: + path: spec.egress[1].to[1].ipBlock.cidr + value: ::/0 + - it: renders custom baseline HTTPS egress peers + set: + networkPolicy: + enabled: true + egressIsolation: true + httpsEgress: + - ipBlock: + cidr: 203.0.113.0/24 + asserts: + - equal: + path: spec.egress[1].to[0].ipBlock.cidr + value: 203.0.113.0/24 + - notContains: + path: spec.egress[1].to + content: + ipBlock: + cidr: 0.0.0.0/0 + - it: appends extra egress rules after default DNS and HTTPS + set: + networkPolicy: + enabled: true + extraEgress: + - to: + - ipBlock: + cidr: 10.0.0.0/8 + ports: + - protocol: TCP + port: 5432 + asserts: + - contains: + path: spec.policyTypes + content: Egress + - equal: + path: spec.egress[0].ports[0].port + value: 53 + - equal: + path: spec.egress[0].to[0].podSelector.matchLabels["k8s-app"] + value: kube-dns + - equal: + path: spec.egress[1].ports[0].port + value: 443 + - equal: + path: spec.egress[2].ports[0].port + value: 5432 diff --git a/charts/memos/tests/templates_test.yaml b/charts/memos/tests/templates_test.yaml index c126aea77..821588f4b 100644 --- a/charts/memos/tests/templates_test.yaml +++ b/charts/memos/tests/templates_test.yaml @@ -13,6 +13,11 @@ tests: - equal: path: spec.template.spec.containers[0].image value: docker.io/neosmemo/memos:0.29.1 + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + value: memos + - notExists: + path: spec.template.metadata.labels["helm.sh/chart"] - it: renders the default ClusterIP service template: templates/service.yaml asserts: @@ -40,6 +45,17 @@ tests: - equal: path: spec.tls[0].hosts[0] value: memos.example.com + - it: omits empty ingress class name + template: templates/ingress.yaml + set: + ingress.enabled: true + ingress.ingressClassName: "" + ingress.hosts[0].host: memos.example.com + ingress.hosts[0].paths[0].path: / + ingress.hosts[0].paths[0].pathType: Prefix + asserts: + - notExists: + path: spec.ingressClassName - it: uses emptyDir when persistence is disabled template: templates/statefulset.yaml set: diff --git a/charts/memos/tests/validation_test.yaml b/charts/memos/tests/validation_test.yaml index 56926871f..d27e1b8d0 100644 --- a/charts/memos/tests/validation_test.yaml +++ b/charts/memos/tests/validation_test.yaml @@ -31,3 +31,17 @@ tests: asserts: - failedTemplate: errorMessage: "replicaCount > 1 with mysql or postgres requires persistence.existingClaim backed by shared storage because generated StatefulSet PVCs are per-pod and Memos can store local assets in MEMOS_DATA" + - it: fails when podLabels override selector labels + set: + podLabels: + app.kubernetes.io/instance: custom + asserts: + - failedTemplate: + errorMessage: "podLabels must not override the selector label app.kubernetes.io/instance" + - it: fails when podLabels override the name selector label + set: + podLabels: + app.kubernetes.io/name: custom + asserts: + - failedTemplate: + errorMessage: "podLabels must not override the selector label app.kubernetes.io/name" diff --git a/charts/memos/values.schema.json b/charts/memos/values.schema.json index a9be869bd..ab64c8e79 100644 --- a/charts/memos/values.schema.json +++ b/charts/memos/values.schema.json @@ -151,7 +151,27 @@ "type": "object", "properties": { "enabled": { "type": "boolean", "default": false }, - "ingressFrom": { "type": "array", "items": { "type": "object" } } + "ingressFrom": { "type": "array", "items": { "type": "object" } }, + "egressIsolation": { + "type": "boolean", + "default": false, + "description": "Enable baseline egress isolation without requiring custom extraEgress rules" + }, + "dnsEgress": { + "type": "array", + "description": "DNS peers allowed by baseline egress isolation", + "items": { "type": "object" } + }, + "httpsEgress": { + "type": "array", + "description": "HTTPS peers allowed by baseline egress isolation", + "items": { "type": "object" } + }, + "extraEgress": { + "type": "array", + "description": "Additional egress rules appended after the built-in DNS and HTTPS allowances", + "items": { "type": "object" } + } } } } diff --git a/charts/memos/values.yaml b/charts/memos/values.yaml index 2aeea75de..3a9c1b635 100644 --- a/charts/memos/values.yaml +++ b/charts/memos/values.yaml @@ -162,6 +162,24 @@ networkPolicy: enabled: false # -- Custom ingress peers. Empty allows ingress from any namespace. ingressFrom: [] + # -- Enable baseline egress isolation without requiring custom extraEgress rules. + egressIsolation: false + # -- DNS peers allowed by baseline egress isolation. Override for clusters with different DNS labels. + dnsEgress: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + # -- HTTPS peers allowed by baseline egress isolation. Override to restrict webhook and integration destinations. + httpsEgress: + - ipBlock: + cidr: 0.0.0.0/0 + - ipBlock: + cidr: ::/0 + # -- Additional egress rules appended after the built-in DNS and HTTPS allowances. + extraEgress: [] # -- Node selector for scheduling the pod. nodeSelector: {}