diff --git a/charts/mok/.helmignore b/charts/mok/.helmignore new file mode 100644 index 0000000000000000000000000000000000000000..4430ae9aed0b3f188786c249b97d8e7dba87c594 --- /dev/null +++ b/charts/mok/.helmignore @@ -0,0 +1,25 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# helm-unittest +tests/ diff --git a/charts/mok/Chart.yaml b/charts/mok/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..83e6f456f627e85bad5b0899caf6db22ffdc3561 --- /dev/null +++ b/charts/mok/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +name: mok +description: | + Mail on Kubernetes (MoK) is a project to deploy a functional mailserver that runs without a database server on Kubernetes, taking advantage of configmaps and secret. +type: application +version: 0.1.0 +sources: + - https://de.postfix.org/ftpmirror/index.html + - https://github.com/dovecot/core +maintainers: + - name: Sheogorath + url: https://shivering-isles.com/contribute diff --git a/charts/mok/Makefile b/charts/mok/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..d996e967de7fa83d3b34bbb062c7fffd8cb940f8 --- /dev/null +++ b/charts/mok/Makefile @@ -0,0 +1,10 @@ +lint: + koolbox helm lint + +test: lint helm-unittest + +helm-unittest: + podman run -ti --rm -v $$(pwd):/apps docker.io/quintush/helm-unittest:3.7.1-0.2.8 -3 . + +docs: + koolbox helm-docs diff --git a/charts/mok/README.md b/charts/mok/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1c51b608edb4c52174224dfdf580d914aec6fa29 --- /dev/null +++ b/charts/mok/README.md @@ -0,0 +1,88 @@ +# mok + +  + +Mail on Kubernetes (MoK) is a project to deploy a functional mailserver that runs without a database server on Kubernetes, taking advantage of configmaps and secret. + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| Sheogorath | | <https://shivering-isles.com/contribute> | + +## Source Code + +* <https://de.postfix.org/ftpmirror/index.html> +* <https://github.com/dovecot/core> + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| domains | object | `{}` | list of configured domains and users. See values.yaml for details. | +| dovecot.affinity | object | `{}` | | +| dovecot.image.pullPolicy | string | `"IfNotPresent"` | | +| dovecot.image.repository | string | `"quay.io/shivering-isles/dovecot"` | dovecot container image | +| dovecot.image.tag | string | `"0.1.0"` | Overrides the image tag whose default is "latest" | +| dovecot.imagePullSecrets | list | `[]` | pull secret to access the afore defined image | +| dovecot.nodeSelector | object | `{}` | | +| dovecot.podAnnotations | object | `{}` | | +| dovecot.podSecurityContext | object | `{}` | | +| dovecot.replicaCount | int | `1` | Number of Dovecot pods. **Important**: With the current configuration, it's not recommended to scale beyond 1 | +| dovecot.resources.limits.cpu | string | `"500m"` | | +| dovecot.resources.limits.memory | string | `"512Mi"` | | +| dovecot.resources.requests.cpu | string | `"100m"` | | +| dovecot.resources.requests.memory | string | `"128Mi"` | | +| dovecot.securityContext.allowPrivilegeEscalation | bool | `false` | | +| dovecot.securityContext.capabilities.add[0] | string | `"SYS_CHROOT"` | required to setup chroot for dovecot https://wiki.dovecot.org/HowTo/Rootless | +| dovecot.securityContext.capabilities.add[1] | string | `"CHOWN"` | required to set up file structure | +| dovecot.securityContext.capabilities.add[2] | string | `"CAP_NET_BIND_SERVICE"` | required to bind privileged ports in the container, such as 993, 143, 24, etc. | +| dovecot.securityContext.capabilities.add[3] | string | `"SETUID"` | required to drop privileges with dovecot process | +| dovecot.securityContext.capabilities.add[4] | string | `"SETGID"` | required to drop privileges with dovecot process | +| dovecot.securityContext.capabilities.drop[0] | string | `"ALL"` | required to drop privileges by default | +| dovecot.securityContext.runAsNonRoot | bool | `false` | | +| dovecot.service.internal.type | string | `"ClusterIP"` | type of the public endpoint for lmtp, metrics, authentication | +| dovecot.service.public.type | string | `"LoadBalancer"` | type of the public endpoint for pop3, imap, and sieve **Note**: It's configured to share the IP with postfix in case of metallb | +| dovecot.tls.secretName | string | `"nil"` | secret holding the TLS keys for dovecot. **Required** | +| dovecot.tolerations | list | `[]` | | +| dovecot.volumes.vmail.accessModes | list | `["ReadWriteMany"]` | Volume access mode, using ReadWriteMany in order to prepare setup with dovcecot director | +| dovecot.volumes.vmail.resources.requests.storage | string | `"5Gi"` | | +| dovecot.volumes.vmail.volumeMode | string | `"Filesystem"` | | +| fullnameOverride | string | `""` | | +| nameOverride | string | `""` | | +| postfix.affinity | object | `{}` | | +| postfix.image.pullPolicy | string | `"IfNotPresent"` | | +| postfix.image.repository | string | `"quay.io/shivering-isles/postfix"` | postfix container image | +| postfix.image.tag | string | `"0.1.0"` | Overrides the image tag whose default is "latest" | +| postfix.imagePullSecrets | list | `[]` | | +| postfix.nodeSelector | object | `{}` | | +| postfix.podAnnotations | object | `{}` | | +| postfix.podSecurityContext | object | `{}` | | +| postfix.replicaCount | int | `1` | Number of postfix pods. | +| postfix.resources.limits.cpu | string | `"500m"` | | +| postfix.resources.limits.memory | string | `"512Mi"` | | +| postfix.resources.requests.cpu | string | `"100m"` | | +| postfix.resources.requests.memory | string | `"128Mi"` | | +| postfix.securityContext.allowPrivilegeEscalation | bool | `false` | prevent any process in the container to regain capabilities once dropped | +| postfix.securityContext.capabilities.add[0] | string | `"SYS_CHROOT"` | required to setup chroot with postfix | +| postfix.securityContext.capabilities.add[1] | string | `"CHOWN"` | required to adjust ownership of files using supervisord | +| postfix.securityContext.capabilities.add[2] | string | `"CAP_NET_BIND_SERVICE"` | required to bind privileged ports like 25, 465, 587 | +| postfix.securityContext.capabilities.add[3] | string | `"SETUID"` | required to change user id as supervisord as well as postfix | +| postfix.securityContext.capabilities.add[4] | string | `"SETGID"` | required to change group id as supervisord as well as postfix | +| postfix.securityContext.capabilities.add[5] | string | `"FOWNER"` | required to set up the chroot directory on startup | +| postfix.securityContext.capabilities.drop[0] | string | `"ALL"` | getting rid of all capabilities since we already have too many | +| postfix.securityContext.runAsNonRoot | bool | `false` | | +| postfix.service.public.type | string | `"LoadBalancer"` | type of the public endpoint for smtp, submission, and submissions. **Note**: It's configured to share the IP with dovecot in case of metallb | +| postfix.tls.secretName | string | `"nil"` | secret holding the TLS keys for postfix. **Required** | +| postfix.tolerations | list | `[]` | | +| postfix.volumes.spool.accessModes[0] | string | `"ReadWriteOnce"` | | +| postfix.volumes.spool.resources.requests.storage | string | `"1Gi"` | | +| relay.relayHosts | object | `{}` | relay hosts used as part of the deployment | +| relay.saslPasswords | object | `{}` | passwords for the relay hosts | +| relay.tlsPolicies | string | `""` | tls policy in postfix https://www.postfix.org/TLS_README.html#client_tls_policy | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `""` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.10.0](https://github.com/norwoodj/helm-docs/releases/v1.10.0) diff --git a/charts/mok/templates/_helpers.tpl b/charts/mok/templates/_helpers.tpl new file mode 100644 index 0000000000000000000000000000000000000000..11c0252b0d4d15ed608aeb1470de48ae74ce3d09 --- /dev/null +++ b/charts/mok/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "mok.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "mok.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "mok.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "mok.labels" -}} +helm.sh/chart: {{ include "mok.chart" . }} +{{ include "mok.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "mok.selectorLabels" -}} +app.kubernetes.io/name: {{ include "mok.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "mok.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "mok.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/mok/templates/dovecot.yaml b/charts/mok/templates/dovecot.yaml new file mode 100644 index 0000000000000000000000000000000000000000..30d4c0ce33217e2cff8c21d308732b5c0970fc94 --- /dev/null +++ b/charts/mok/templates/dovecot.yaml @@ -0,0 +1,140 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + metallb.universe.tf/allow-shared-ip: "{{ include "mok.fullname" . }}-mail" + name: {{ include "mok.fullname" . }}-dovecot + labels: + {{- include "mok.labels" . | nindent 4 }} + app.kubernetes.io/component: dovecot +spec: + ports: + - port: 110 + name: pop3 + protocol: TCP + - port: 143 + name: imap4 + protocol: TCP + - port: 993 + name: imaps + protocol: TCP + - port: 995 + name: pop3s + protocol: TCP + - port: 4190 + name: sieve + protocol: TCP + selector: + {{- include "mok.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: dovecot + type: {{ .Values.dovecot.service.public.type }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mok.fullname" . }}-dovecot-internal + labels: + {{- include "mok.labels" . | nindent 4 }} + app.kubernetes.io/component: dovecot +spec: + ports: + - port: 24 + name: lmtp + - port: 9090 + name: metrics + - port: 12345 + name: auth + selector: + {{- include "mok.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: dovecot + type: {{ .Values.dovecot.service.internal.type }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + {{- include "mok.labels" . | nindent 4 }} + app.kubernetes.io/component: dovecot + name: {{ include "mok.fullname" . }}-dovecot +spec: + selector: + matchLabels: + {{- include "mok.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: dovecot + replicas: {{ .Values.postfix.replicaCount }} + strategy: + type: Recreate + template: + metadata: + labels: + {{- include "mok.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: dovecot + {{- with .Values.dovecot.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.dovecot.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "mok.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.dovecot.podSecurityContext | nindent 8 }} + terminationGracePeriodSeconds: 300 + containers: + - name: dovecot + image: "{{ .Values.dovecot.image.repository }}:{{.Values.dovecot.image.tag | default "latest" }}" + imagePullPolicy: {{ .Values.dovecot.image.pullPolicy }} + ports: + - containerPort: 24 + name: lmtp + - containerPort: 110 + name: pop3 + - containerPort: 143 + name: imap4 + - containerPort: 993 + name: imaps + - containerPort: 995 + name: pop3s + - containerPort: 4190 + name: sieve + - containerPort: 9090 + name: metrics + - containerPort: 12345 + name: auth + resources: + {{- toYaml .Values.dovecot.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.dovecot.securityContext | nindent 12 }} + volumeMounts: + - name: vmail + mountPath: /srv/mail/mailboxes/ + - name: users + mountPath: "/srv/passdb/" + readOnly: true + - name: tls + mountPath: "/srv/tls/" + readOnly: true + {{- with .Values.dovecot.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.dovecot.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.dovecot.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: vmail + persistentVolumeClaim: + claimName: {{ include "mok.fullname" . }}-dovecot-vmail + - name: users + secret: + secretName: {{ include "mok.fullname" . }}-dovecot-users + - name: tls + secret: + secretName: {{ required "TLS secret for dovecot is required" .Values.postfix.tls.secretName }} diff --git a/charts/mok/templates/persistentvolumeclaim.yaml b/charts/mok/templates/persistentvolumeclaim.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8d8f5f67d119fa60afda0b1783286034ae9ebf10 --- /dev/null +++ b/charts/mok/templates/persistentvolumeclaim.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "mok.fullname" . }}-dovecot-vmail + labels: + {{- include "mok.labels" . | nindent 4 }} + app.kubernetes.io/component: dovecot +spec: + {{- toYaml .Values.dovecot.volumes.vmail | nindent 2 }} diff --git a/charts/mok/templates/postfix.yaml b/charts/mok/templates/postfix.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3fb47c26b7ab978af41842f36c672aaf8e57b996 --- /dev/null +++ b/charts/mok/templates/postfix.yaml @@ -0,0 +1,153 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mok.fullname" . }}-postfix + labels: + {{- include "mok.labels" . | nindent 4 }} + app.kubernetes.io/component: postfix + annotations: + metallb.universe.tf/allow-shared-ip: "{{ include "mok.fullname" . }}-mail" +spec: + ports: + - port: 25 + name: smtp + protocol: TCP + - port: 465 + name: submissions + protocol: TCP + - port: 587 + name: submission + protocol: TCP + selector: + {{- include "mok.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: postfix + type: {{ .Values.postfix.service.public.type }} +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "mok.fullname" . }}-postfix + labels: + {{- include "mok.labels" . | nindent 4 }} + app.kubernetes.io/component: postfix +spec: + replicas: {{ .Values.postfix.replicaCount }} + serviceName: {{ include "mok.fullname" . }}-postfix-statefulset + selector: + matchLabels: + {{- include "mok.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: postfix + volumeClaimTemplates: + - metadata: + name: spool + spec: + {{- toYaml .Values.postfix.volumes.spool | nindent 8 }} + {{- if semverCompare ">=1.23.0" .Capabilities.KubeVersion.Version }} + persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Delete + {{- end }} + template: + metadata: + {{- with .Values.postfix.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "mok.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: postfix + spec: + {{- with .Values.postfix.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "mok.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.postfix.podSecurityContext | nindent 8 }} + containers: + - name: postfix + image: {{ .Values.postfix.image.repository }}:{{.Values.postfix.image.tag | default "latest" }} + imagePullPolicy: {{ .Values.postfix.image.pullPolicy }} + lifecycle: + preStop: + exec: + # flush all emails before shutting down + command: + - postqueue + - -f + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 60 + timeoutSeconds: 5 + exec: + command: + - sh + - -c + - >- + printf "EHLO healthcheck\n" | nc 127.0.0.1 587 | + grep -qE "^220.*ESMTP Postfix" + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 1 + exec: + command: + - sh + - -c + - >- + ps axf | fgrep -v grep | fgrep -q "supervisord" && + ps axf | fgrep -v grep | fgrep -q "/usr/libexec/postfix/master" + startupProbe: + initialDelaySeconds: 2 + periodSeconds: 5 + failureThreshold: 12 + exec: + command: + - sh + - -c + - >- + ps axf | fgrep -v grep | fgrep -q "supervisord" && + ps axf | fgrep -v grep | fgrep -q "/usr/libexec/postfix/master" + ports: + - containerPort: 25 + name: smtp + - containerPort: 465 + name: submissions + - containerPort: 587 + name: submission + resources: + {{- toYaml .Values.postfix.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.postfix.securityContext | nindent 12 }} + volumeMounts: + - name: spool + mountPath: /var/spool/postfix/ + - name: cache + mountPath: "/srv/tmp" + - name: maps + mountPath: "/srv/virtual" + readOnly: true + - name: tls + mountPath: "/srv/tls" + readOnly: true + {{- with .Values.postfix.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.postfix.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.postfix.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: maps + secret: + secretName: {{ include "mok.fullname" . }}-postfix-maps + - name: tls + secret: + secretName: {{ required "TLS secret for postfix is required" .Values.postfix.tls.secretName }} + - name: cache + emptyDir: {} diff --git a/charts/mok/templates/secret.yaml b/charts/mok/templates/secret.yaml new file mode 100644 index 0000000000000000000000000000000000000000..019525340579b726ce53dfac045fa6883f98c63f --- /dev/null +++ b/charts/mok/templates/secret.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "mok.fullname" . }}-dovecot-users + labels: + {{- include "mok.labels" . | nindent 4 }} + app.kubernetes.io/component: dovecot +type: Opaque +stringData: + passwd: | + {{- range $domain,$config := .Values.domains }} + {{- range $config.users }} + {{ .name }}@{{ $domain }}:{{.passwordHash}} + {{- end }} + {{- end }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "mok.fullname" . }}-postfix-maps + labels: + {{- include "mok.labels" . | nindent 4 }} + app.kubernetes.io/component: postfix +type: Opaque +stringData: + aliases: | + {{- range $domain,$config := .Values.domains }} + {{- range $config.users }} + {{- $username := .name }} + {{- range .aliases }} + {{ . }} {{ $username }}@{{ $domain }} + {{- end }} + {{- end }} + {{- end }} + domains: | + {{- $domainList := list }} + {{- range $domain,$config := .Values.domains }} + {{- $domainList = (append $domainList $domain | uniq) }} + {{- range $config.users }} + {{- $username := .name }} + {{- range .aliases }} + {{- $domainList = (append $domainList (regexFind "@.*" .) | uniq) }} + {{- end }} + {{- end }} + {{- end }} + + {{- range $domainList }} + {{ trimPrefix "@" . }} OK + {{- end }} + mailboxes: | + {{- range $domain,$config := .Values.domains }} + {{- range $config.users }} + {{ .name }}@{{ $domain }} OK + {{- end }} + {{- end }} + relayhosts: | + {{- range $domain,$relay := .Values.relay.relayHosts }} + {{ $domain }} {{ $relay }} + {{- end }} + sender-login-maps: | + {{- range $domain,$config := .Values.domains }} + {{- range $config.users }} + {{- $username := .name }} + {{ $username }}@{{ $domain }} {{ $username }}@{{ $domain }} + {{- range .aliases }} + {{ . }} {{ $username }}@{{ $domain }} + {{- end }} + {{- end }} + {{- end }} + sasl_passwd: | + {{- range $relay,$password := .Values.relay.saslPasswords }} + {{ $relay }} {{ $password }} + {{- end }} + tls-policies: | + {{- .Values.relay.tlsPolicies | nindent 4 }} diff --git a/charts/mok/templates/serviceaccount.yaml b/charts/mok/templates/serviceaccount.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e06dd08c02751c1a126ab626b38d6d8735144510 --- /dev/null +++ b/charts/mok/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "mok.serviceAccountName" . }} + labels: + {{- include "mok.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/mok/tests/__snapshot__/domains_test.yaml.snap b/charts/mok/tests/__snapshot__/domains_test.yaml.snap new file mode 100644 index 0000000000000000000000000000000000000000..809373a2361c2b600e28b42470d67fa6105ac9b1 --- /dev/null +++ b/charts/mok/tests/__snapshot__/domains_test.yaml.snap @@ -0,0 +1,45 @@ +keeps stays the same: + 1: | + apiVersion: v1 + kind: Secret + metadata: + labels: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-dovecot-users + stringData: + passwd: | + john@example.com:{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg + type: Opaque + 2: | + apiVersion: v1 + kind: Secret + metadata: + labels: + app.kubernetes.io/component: postfix + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-postfix-maps + stringData: + aliases: | + steve@example.net john@example.com + @example.info john@example.com + domains: | + example.com OK + example.net OK + example.info OK + mailboxes: | + john@example.com OK + relayhosts: "" + sasl_passwd: "" + sender-login-maps: | + john@example.com john@example.com + steve@example.net john@example.com + @example.info john@example.com + tls-policies: "" + type: Opaque diff --git a/charts/mok/tests/__snapshot__/dovecot_test.yaml.snap b/charts/mok/tests/__snapshot__/dovecot_test.yaml.snap new file mode 100644 index 0000000000000000000000000000000000000000..a0353e1bfd732c120c983a12b3f3438859db4a0f --- /dev/null +++ b/charts/mok/tests/__snapshot__/dovecot_test.yaml.snap @@ -0,0 +1,167 @@ +should match snapshot: + 1: | + apiVersion: v1 + kind: Service + metadata: + annotations: + metallb.universe.tf/allow-shared-ip: RELEASE-NAME-mok-mail + labels: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-dovecot + spec: + ports: + - name: pop3 + port: 110 + protocol: TCP + - name: imap4 + port: 143 + protocol: TCP + - name: imaps + port: 993 + protocol: TCP + - name: pop3s + port: 995 + protocol: TCP + - name: sieve + port: 4190 + protocol: TCP + selector: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: mok + type: LoadBalancer + 2: | + apiVersion: v1 + kind: Service + metadata: + labels: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-dovecot-internal + spec: + ports: + - name: lmtp + port: 24 + - name: metrics + port: 9090 + - name: auth + port: 12345 + selector: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: mok + type: ClusterIP + 3: | + apiVersion: apps/v1 + kind: Deployment + metadata: + labels: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-dovecot + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: mok + strategy: + type: Recreate + template: + metadata: + labels: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: mok + spec: + containers: + - image: quay.io/shivering-isles/dovecot:0.1.0 + imagePullPolicy: IfNotPresent + name: dovecot + ports: + - containerPort: 24 + name: lmtp + - containerPort: 110 + name: pop3 + - containerPort: 143 + name: imap4 + - containerPort: 993 + name: imaps + - containerPort: 995 + name: pop3s + - containerPort: 4190 + name: sieve + - containerPort: 9090 + name: metrics + - containerPort: 12345 + name: auth + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - SYS_CHROOT + - CHOWN + - CAP_NET_BIND_SERVICE + - SETUID + - SETGID + drop: + - ALL + runAsNonRoot: false + volumeMounts: + - mountPath: /srv/mail/mailboxes/ + name: vmail + - mountPath: /srv/passdb/ + name: users + readOnly: true + - mountPath: /srv/tls/ + name: tls + readOnly: true + securityContext: {} + serviceAccountName: RELEASE-NAME-mok + terminationGracePeriodSeconds: 300 + volumes: + - name: vmail + persistentVolumeClaim: + claimName: RELEASE-NAME-mok-dovecot-vmail + - name: users + secret: + secretName: RELEASE-NAME-mok-dovecot-users + - name: tls + secret: + secretName: nil + 4: | + apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + labels: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-dovecot-vmail + spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + volumeMode: Filesystem diff --git a/charts/mok/tests/__snapshot__/postfix_test.yaml.snap b/charts/mok/tests/__snapshot__/postfix_test.yaml.snap new file mode 100644 index 0000000000000000000000000000000000000000..92f369083e758ed712c9221e64ff666530381e1f --- /dev/null +++ b/charts/mok/tests/__snapshot__/postfix_test.yaml.snap @@ -0,0 +1,154 @@ +should match snapshot: + 1: | + apiVersion: v1 + kind: Service + metadata: + annotations: + metallb.universe.tf/allow-shared-ip: RELEASE-NAME-mok-mail + labels: + app.kubernetes.io/component: postfix + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-postfix + spec: + ports: + - name: smtp + port: 25 + protocol: TCP + - name: submissions + port: 465 + protocol: TCP + - name: submission + port: 587 + protocol: TCP + selector: + app.kubernetes.io/component: postfix + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: mok + type: LoadBalancer + 2: | + apiVersion: apps/v1 + kind: StatefulSet + metadata: + labels: + app.kubernetes.io/component: postfix + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-postfix + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: postfix + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: mok + serviceName: RELEASE-NAME-mok-postfix-statefulset + template: + metadata: + labels: + app.kubernetes.io/component: postfix + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: mok + spec: + containers: + - image: quay.io/shivering-isles/postfix:0.1.0 + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - postqueue + - -f + livenessProbe: + exec: + command: + - sh + - -c + - ps axf | fgrep -v grep | fgrep -q "supervisord" && ps axf | fgrep -v + grep | fgrep -q "/usr/libexec/postfix/master" + failureThreshold: 1 + initialDelaySeconds: 5 + periodSeconds: 5 + name: postfix + ports: + - containerPort: 25 + name: smtp + - containerPort: 465 + name: submissions + - containerPort: 587 + name: submission + readinessProbe: + exec: + command: + - sh + - -c + - printf "EHLO healthcheck\n" | nc 127.0.0.1 587 | grep -qE "^220.*ESMTP + Postfix" + initialDelaySeconds: 5 + periodSeconds: 60 + timeoutSeconds: 5 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - SYS_CHROOT + - CHOWN + - CAP_NET_BIND_SERVICE + - SETUID + - SETGID + - FOWNER + drop: + - ALL + runAsNonRoot: false + startupProbe: + exec: + command: + - sh + - -c + - ps axf | fgrep -v grep | fgrep -q "supervisord" && ps axf | fgrep -v + grep | fgrep -q "/usr/libexec/postfix/master" + failureThreshold: 12 + initialDelaySeconds: 2 + periodSeconds: 5 + volumeMounts: + - mountPath: /var/spool/postfix/ + name: spool + - mountPath: /srv/tmp + name: cache + - mountPath: /srv/virtual + name: maps + readOnly: true + - mountPath: /srv/tls + name: tls + readOnly: true + securityContext: {} + serviceAccountName: RELEASE-NAME-mok + volumes: + - name: maps + secret: + secretName: RELEASE-NAME-mok-postfix-maps + - name: tls + secret: + secretName: example-tls + - emptyDir: {} + name: cache + volumeClaimTemplates: + - metadata: + name: spool + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/charts/mok/tests/__snapshot__/relay_test.yaml.snap b/charts/mok/tests/__snapshot__/relay_test.yaml.snap new file mode 100644 index 0000000000000000000000000000000000000000..66c75fa96ffe27fbc340dbd31c5e90fca77ef46f --- /dev/null +++ b/charts/mok/tests/__snapshot__/relay_test.yaml.snap @@ -0,0 +1,38 @@ +keeps stays the same: + 1: | + apiVersion: v1 + kind: Secret + metadata: + labels: + app.kubernetes.io/component: dovecot + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-dovecot-users + stringData: + passwd: "" + type: Opaque + 2: | + apiVersion: v1 + kind: Secret + metadata: + labels: + app.kubernetes.io/component: postfix + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: mok + helm.sh/chart: mok-0.1.0 + name: RELEASE-NAME-mok-postfix-maps + stringData: + aliases: "" + domains: "" + mailboxes: "" + relayhosts: | + @example.com [mail.example.org]:587 + sasl_passwd: | + [mail.example.org]:587 somesecretpassword + sender-login-maps: "" + tls-policies: | + example.edu none + type: Opaque diff --git a/charts/mok/tests/domains_test.yaml b/charts/mok/tests/domains_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..43cff2e70341e7acad27c6e4aa166fb7030582f5 --- /dev/null +++ b/charts/mok/tests/domains_test.yaml @@ -0,0 +1,57 @@ +suite: Domain configuration +templates: + - secret.yaml +tests: + - it: can configure domains and users + set: + domains: + "example.com": + users: + - name: john + passwordHash: "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg" + aliases: + - "steve@example.net" + - "@example.info" + asserts: + - equal: + path: stringData.aliases + value: | + steve@example.net john@example.com + @example.info john@example.com + documentIndex: 1 + - equal: + path: stringData.domains + value: | + example.com OK + example.net OK + example.info OK + documentIndex: 1 + - equal: + path: stringData.mailboxes + value: | + john@example.com OK + documentIndex: 1 + - equal: + path: stringData.sender-login-maps + value: | + john@example.com john@example.com + steve@example.net john@example.com + @example.info john@example.com + documentIndex: 1 + - equal: + path: stringData.passwd + value: | + john@example.com:{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg + documentIndex: 0 + - it: keeps stays the same + set: + domains: + "example.com": + users: + - name: john + passwordHash: "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg" + aliases: + - "steve@example.net" + - "@example.info" + asserts: + - matchSnapshot: {} diff --git a/charts/mok/tests/dovecot_test.yaml b/charts/mok/tests/dovecot_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bcd2d8452f768d49a908daf0faaff60c25c1e9a2 --- /dev/null +++ b/charts/mok/tests/dovecot_test.yaml @@ -0,0 +1,85 @@ +suite: Dovecot +templates: + - dovecot.yaml + - persistentvolumeclaim.yaml +tests: + - it: should match snapshot + set: + dovecot: + tls: + secretName: example-tls + asserts: + - matchSnapshot: {} + - it: has a public service + set: + dovecot: + tls: + secretName: example-tls + release: + name: "test-suite" + asserts: + - isKind: + of: Service + documentIndex: 0 + template: dovecot.yaml + - equal: + path: metadata.name + value: test-suite-mok-dovecot + documentIndex: 0 + template: dovecot.yaml + - it: has lmtp port + set: + dovecot: + tls: + secretName: example-tls + asserts: + - contains: + path: spec.ports + content: + port: 24 + name: lmtp + documentIndex: 1 + template: dovecot.yaml + - contains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 24 + name: lmtp + documentIndex: 2 + template: dovecot.yaml + - it: has pop3 & pop3s port + set: + dovecot: + tls: + secretName: example-tls + asserts: + - contains: + path: spec.ports + content: + port: 110 + name: pop3 + protocol: TCP + documentIndex: 0 + template: dovecot.yaml + - contains: + path: spec.ports + content: + port: 995 + name: pop3s + protocol: TCP + documentIndex: 0 + template: dovecot.yaml + - contains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 110 + name: pop3 + documentIndex: 2 + template: dovecot.yaml + - contains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 995 + name: pop3s + documentIndex: 2 + template: dovecot.yaml diff --git a/charts/mok/tests/helmlabels_test.yaml b/charts/mok/tests/helmlabels_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d2322d97837cef4093a451ef77638663a9b949cf --- /dev/null +++ b/charts/mok/tests/helmlabels_test.yaml @@ -0,0 +1,30 @@ +suite: Kubernetes recommendations +templates: + - dovecot.yaml + - persistentvolumeclaim.yaml + - postfix.yaml + - secret.yaml + - serviceaccount.yaml +tests: + - it: should have the kubernetes recommended labels + set: + dovecot: + tls: + secretName: example-tls + postfix: + tls: + secretName: example-tls + release: + name: "test-suite" + chart: + version: 1.2.3 + asserts: + - equal: + path: metadata.labels.[app.kubernetes.io/instance] + value: "test-suite" + - equal: + path: metadata.labels.[app.kubernetes.io/managed-by] + value: "Helm" + - equal: + path: metadata.labels.[app.kubernetes.io/name] + value: "mok" diff --git a/charts/mok/tests/postfix_test.yaml b/charts/mok/tests/postfix_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3e38ee45c1a00fdbc4a47142e09c105e76ca1914 --- /dev/null +++ b/charts/mok/tests/postfix_test.yaml @@ -0,0 +1,45 @@ +suite: Postfix +templates: + - postfix.yaml +tests: + - it: should match snapshot + set: + postfix: + tls: + secretName: example-tls + asserts: + - matchSnapshot: {} + - it: has a public service + set: + postfix: + tls: + secretName: example-tls + release: + name: "test-suite" + asserts: + - isKind: + of: Service + documentIndex: 0 + - equal: + path: metadata.name + value: test-suite-mok-postfix + documentIndex: 0 + - it: has smtp port + set: + postfix: + tls: + secretName: example-tls + asserts: + - contains: + path: spec.ports + content: + port: 25 + name: smtp + protocol: TCP + documentIndex: 0 + - contains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 25 + name: smtp + documentIndex: 1 diff --git a/charts/mok/tests/relay_test.yaml b/charts/mok/tests/relay_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c46334066c1706a19e5146a1ae63167adb7aee86 --- /dev/null +++ b/charts/mok/tests/relay_test.yaml @@ -0,0 +1,48 @@ +suite: Relay configuration +templates: + - secret.yaml +tests: + - it: can configure relay hosts + set: + relay: + relayHosts: + "@example.com": "[mail.example.org]:587" + asserts: + - equal: + path: stringData.relayhosts + value: | + @example.com [mail.example.org]:587 + documentIndex: 1 + - it: can configure sasl + set: + relay: + saslPasswords: + "[mail.example.org]:587": somesecretpassword + asserts: + - equal: + path: stringData.sasl_passwd + value: | + [mail.example.org]:587 somesecretpassword + documentIndex: 1 + - it: can configure tls policies + set: + relay: + tlsPolicies: | + example.edu none + asserts: + - equal: + path: stringData.tls-policies + value: | + example.edu none + documentIndex: 1 + - it: keeps stays the same + set: + relay: + relayHosts: + "@example.com": "[mail.example.org]:587" + saslPasswords: + "[mail.example.org]:587": somesecretpassword + tlsPolicies: | + example.edu none + asserts: + - matchSnapshot: {} diff --git a/charts/mok/values.yaml b/charts/mok/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5f66282d3c1322e2a18b659ef216aad90045f149 --- /dev/null +++ b/charts/mok/values.yaml @@ -0,0 +1,184 @@ +# Default values for mok. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +nameOverride: "" +fullnameOverride: "" + +# -- list of configured domains and users. See values.yaml for details. +domains: {} + # "example.com": + # users: + # - name: john + # # -- pregenerated password hash. Generate it using `podman run --rm -it --entrypoint=doveadm registry.shivering-isles.com/sheogorath/dovecot:latest pw -s ARGON2ID` + # passwordHash: "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg" + # aliases: + # - "steve@example.net" + # - "@example.info" +relay: + # -- tls policy in postfix https://www.postfix.org/TLS_README.html#client_tls_policy + tlsPolicies: | + # example.edu none + # example.mil may + # example.gov encrypt ciphers=high + # example.com verify match=hostname:dot-nexthop ciphers=high + # example.net secure + # .example.net secure match=.example.net:example.net + # [mail.example.org]:587 secure match=nexthop + # example.info may protocols=>=TLSv1 ciphers=medium exclude=3DES + + # -- relay hosts used as part of the deployment + relayHosts: {} + # "@example.com": "[mail.example.org]:587" + # -- passwords for the relay hosts + saslPasswords: {} + # "[mail.example.org]:587": somesecretpassword + + +postfix: + # -- Number of postfix pods. + replicaCount: 1 + + image: + # -- postfix container image + repository: quay.io/shivering-isles/postfix + pullPolicy: IfNotPresent + # -- Overrides the image tag whose default is "latest" + tag: "0.1.0" + + imagePullSecrets: [] + + podAnnotations: {} + + podSecurityContext: {} + + securityContext: + # -- prevent any process in the container to regain capabilities once dropped + allowPrivilegeEscalation: false + capabilities: + add: + # -- required to setup chroot with postfix + - SYS_CHROOT + # -- required to adjust ownership of files using supervisord + - CHOWN + # -- required to bind privileged ports like 25, 465, 587 + - CAP_NET_BIND_SERVICE + # -- required to change user id as supervisord as well as postfix + - SETUID + # -- required to change group id as supervisord as well as postfix + - SETGID + # -- required to set up the chroot directory on startup + - FOWNER + drop: + # -- getting rid of all capabilities since we already have too many + - ALL + runAsNonRoot: false + + service: + public: + # -- type of the public endpoint for smtp, submission, and submissions. **Note**: It's configured to share the IP with dovecot in case of metallb + type: LoadBalancer + + tls: + # -- secret holding the TLS keys for postfix. **Required** + secretName: nil + + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + + volumes: + spool: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: 1Gi + + nodeSelector: {} + tolerations: [] + affinity: {} + +dovecot: + # -- Number of Dovecot pods. **Important**: With the current configuration, it's not recommended to scale beyond 1 + replicaCount: 1 + + image: + # -- dovecot container image + repository: quay.io/shivering-isles/dovecot + pullPolicy: IfNotPresent + # -- Overrides the image tag whose default is "latest" + tag: "0.1.0" + + # -- pull secret to access the afore defined image + imagePullSecrets: [] + + podAnnotations: {} + + podSecurityContext: {} + # fsGroup: 2000 + + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + # -- required to setup chroot for dovecot https://wiki.dovecot.org/HowTo/Rootless + - SYS_CHROOT + # -- required to set up file structure + - CHOWN + # -- required to bind privileged ports in the container, such as 993, 143, 24, etc. + - CAP_NET_BIND_SERVICE + # -- required to drop privileges with dovecot process + - SETUID + # -- required to drop privileges with dovecot process + - SETGID + drop: + # -- required to drop privileges by default + - ALL + runAsNonRoot: false + + service: + public: + # -- type of the public endpoint for pop3, imap, and sieve **Note**: It's configured to share the IP with postfix in case of metallb + type: LoadBalancer + internal: + # -- type of the public endpoint for lmtp, metrics, authentication + type: ClusterIP + + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + tls: + # -- secret holding the TLS keys for dovecot. **Required** + secretName: nil + + volumes: + vmail: + # -- Volume access mode, using ReadWriteMany in order to prepare setup with dovcecot director + accessModes: + - ReadWriteMany + volumeMode: Filesystem + resources: + requests: + storage: 5Gi + + nodeSelector: {} + tolerations: [] + affinity: {} + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 633281235d2b2e8e61d8f7de0d18501dd1a2d6b9..883a8c534d3617265e4784d4cb3e3ef502b1c057 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -28,6 +28,9 @@ - [Nextcloud]() - [Registry]() +# Helm Charts +- [MoK](charts/mok.md) + --- # Links diff --git a/docs/src/charts/mok.md b/docs/src/charts/mok.md new file mode 120000 index 0000000000000000000000000000000000000000..6b605c8bed8dc0edd1743d73ab55cb66c4a56d87 --- /dev/null +++ b/docs/src/charts/mok.md @@ -0,0 +1 @@ +../../../charts/mok/README.md \ No newline at end of file