From 388692b660b7adfaff92466af68ccacf8976992c Mon Sep 17 00:00:00 2001 From: lucasdc6 Date: Thu, 23 May 2024 16:53:48 -0300 Subject: [PATCH] Added mimir-provisioning chart --- .github/workflows/release.yml | 32 +++++ charts/mimir-provisioning/.helmignore | 23 ++++ charts/mimir-provisioning/Chart.yaml | 5 + charts/mimir-provisioning/README.md | 68 ++++++++++ .../templates/_cronjob .tpl | 16 +++ .../mimir-provisioning/templates/_helpers.tpl | 69 ++++++++++ charts/mimir-provisioning/templates/_job.tpl | 104 +++++++++++++++ .../templates/provisioning-cm.yaml | 17 +++ .../templates/provisioning-workload.yaml | 7 + charts/mimir-provisioning/templates/role.yaml | 9 ++ .../templates/rolebinding.yaml | 15 +++ .../templates/script-cm.yaml | 6 + .../templates/serviceaccount.yaml | 12 ++ charts/mimir-provisioning/values.yaml | 120 ++++++++++++++++++ 14 files changed, 503 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 charts/mimir-provisioning/.helmignore create mode 100644 charts/mimir-provisioning/Chart.yaml create mode 100644 charts/mimir-provisioning/README.md create mode 100644 charts/mimir-provisioning/templates/_cronjob .tpl create mode 100644 charts/mimir-provisioning/templates/_helpers.tpl create mode 100644 charts/mimir-provisioning/templates/_job.tpl create mode 100644 charts/mimir-provisioning/templates/provisioning-cm.yaml create mode 100644 charts/mimir-provisioning/templates/provisioning-workload.yaml create mode 100644 charts/mimir-provisioning/templates/role.yaml create mode 100644 charts/mimir-provisioning/templates/rolebinding.yaml create mode 100644 charts/mimir-provisioning/templates/script-cm.yaml create mode 100644 charts/mimir-provisioning/templates/serviceaccount.yaml create mode 100644 charts/mimir-provisioning/values.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c35b2c9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release Charts + +on: + push: + branches: + - main + +jobs: + release: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v4 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file diff --git a/charts/mimir-provisioning/.helmignore b/charts/mimir-provisioning/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/mimir-provisioning/.helmignore @@ -0,0 +1,23 @@ +# 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/ diff --git a/charts/mimir-provisioning/Chart.yaml b/charts/mimir-provisioning/Chart.yaml new file mode 100644 index 0000000..7928734 --- /dev/null +++ b/charts/mimir-provisioning/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: mimir-provisioning +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 diff --git a/charts/mimir-provisioning/README.md b/charts/mimir-provisioning/README.md new file mode 100644 index 0000000..189415d --- /dev/null +++ b/charts/mimir-provisioning/README.md @@ -0,0 +1,68 @@ +# Chart de aprovisionamiento de mimir + +Este chart se utilizara para tareas de aprovisionamiento de mimir (como por +ejemplo la carga de rules). + +En lineas generales funciona de la siguiente manera: + +- Genera un conjunto de configmaps que se configuran mediante la clave + `provision`. + +- Una vez que se instala el chart se crea un job con dos contenedores: + + - Estos configmaps luego son levantados por un contenedor de + [k8s-sidecar-container](https://github.com/kiwigrid/k8s-sidecar) quien los + persiste en un volumen local en forma de archivos con path + `/tmp//` + + - Finalmente este volumen es utilizado por un contenedor de `mimirtool` quien + itera sobre las carpetas y archivos aprovisionamdo mimir en base al tenant y + el archivo. + +## Values + +| nombre | tipo | default | descripcion | +| --- | --- | --- | --- | +| `mode` | `String` | `"job"` | Modo de depligue del aprovisionador. Posibles valores `job` y `cronjob` | +| `cronjob.schedule` | `String` | `"*/5 * * * *"` | Expresión para el cronjob | +| `global.provisioner.mimirtoolCommand` | `String` | `rules sync` | Subcomando que se le envia a mimirtool. Por defecto realizara un sync de las rules | +| `global.provisioner.mimirtoolArgs` | `String` | `/tmp/$tenant/$file` | Argumentos que se le pasan al comando de mimirtool | +| `imagePullSecrets` | `Array` | `[]` | Nombre de secreto con credenciales para bajar las imagenes si se utilizacen repositorios privados. | +| `nameOverride` | `String` | `""` | | +| `fullnameOverride` | `String` | `""` | | +| `serviceAccount.create` | `Bool` | `true` | Especifica si se debe crear una sa | +| `serviceAccount.annotations` | `Object` | `{}` | Anotaciones para la sa | +| `serviceAccoint.name` | `String` | `""` | Nombre de la sa que se utiliza con el despliegue. Si es vacio se utiliza el nombre del release | +| `podAnnotations` | `Object` | `{}` | Anotaciones para el pod que se despliega | +| `podSecurityContext` | `Object` | `{}` | Security context a nivel pod | +| `nodeSelector` | `Object` | `{}` | Nodeselector para schedulear el pod | +| `tolerations` | `Array` | `[]` | Tolerations para el pod | +| `affinity` | `Object` | `{}` | Affinity para schedulear el pod | +| `backoffLimit` | `Int` | `3` | Cantidad de veces que se ejecuta el pod en caso de fallos | +| `provisioner.image.repository` | `String` | `grafana/mimirtool` | Repositorio donde se obtiene la imagen de mimirtool | +| `provisioner.image.tag` | `String` | `2.10.5` | Tag de la imagen de mimirtool | +| `provisioner.image.pullPolicy` | `String` | `IfNotPresent` | Polituca para bajar la imagen del provisioner | +| `provisioner.securityContext` | `Object` | `{}` | Security context para el contenedor de mimirtool | +| `provisioner.resources` | `Object` | `{}` | Requests y Limits para el contenedor de mimirtool | +| `provisioner.mimirAddress` | `String` | `http://mimir-nginx.mimir.svc` | Url de la api de mimir. Por defecto se configura para utilizar un mimir instalado en el mismo namespace que este chart. | +| `provisioner.script` | `String` | `Ver values.yaml` | Script que se invoca en el contenedor de mimirtool. Por defecto itera en una estructura de archivos `/tmp//` aplicando el `mimirtoolCommand` con el archivo y el tenant infiriendolos del path. | +| `sidecar.image.repository` | `String` | `ghcr.io/kiwigrid/k8s-sidecar` | Repositorio donde se obtiene la imagen de mimirtool | +| `sidecar.image.tag` | `String` | `1.25.3` | Tag de la imagen de mimirtool | +| `sidecar.image.pullPolicy` | `String` | `IfNotPresent` | Polituca para bajar la imagen del sidecar | +| `sidecar.securityContext` | `Object` | `{}` | Security context para el contenedor de sidecar | +| `sidecar.resources` | `Object` | `{}` | Requests y Limits para el contenedor de sidecar | +| `sidecar.extraEnvs` | `Array` | `[]` | Objeto de variables extra para pasar al contenedor del sidecar. Respetan el formato de variables de ambiente de k8s (name,value) | +| `sidecar.resourceLabel` | `String` | `provisioning` | Label que se aplica a los configmaps y a traves del cual el sidecar los identifica para montar como archivos. | +| `sidecar.resourceType` | `String` | `both` | Que tipo de recursos mira el sidecar. Por defecto seran configmaps y secrets | +| `sidecar.behaviour` | `String` | `LIST` | Indica en que modo se ejecuta el sidecar. `LIST` chequea los recursos y termina, `WATCH` queda en loop esperando cambios. | +| `provision` | `Object` | `{}` | Objeto que define tenant y archivos para `aprovisiona`r mimir | +| `provision.tenant` | `Array` | `""` | Define los archivos que se utilizan para aprovisionar un tenant en mimir | +| `provision.tenant.file` | `Object` | `""` | Define un archivo para aprovisionar un tenant. El mismo cuenta con `name` (nombre con que se montara el archivo en el pod de aprovisionamiento) y `content` (contenido de dicho archivo) | + +## Acerca de provision + +Dado que mimir es multitenant, es importante que el objeto provision tenga como +claves nombres de tenants validos ya que a partir de estos es que se terminan +montando los archivos e infiriendo contra que tenant aplicar el +aprovisionamiento. + diff --git a/charts/mimir-provisioning/templates/_cronjob .tpl b/charts/mimir-provisioning/templates/_cronjob .tpl new file mode 100644 index 0000000..5a90c2b --- /dev/null +++ b/charts/mimir-provisioning/templates/_cronjob .tpl @@ -0,0 +1,16 @@ +{{/* +Expand the cronjob definition. +*/}} +{{- define "mimir-provisioning.cronjob" -}} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "mimir-provisioning.fullname" . }} + labels: + {{- include "mimir-provisioning.labels" . | nindent 4 }} +spec: + concurrencyPolicy: Forbid + schedule: {{ .Values.cronjob.schedule }} + jobTemplate: + spec: {{- include "mimir-provisioning.jobSpec" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/charts/mimir-provisioning/templates/_helpers.tpl b/charts/mimir-provisioning/templates/_helpers.tpl new file mode 100644 index 0000000..0ad0b5c --- /dev/null +++ b/charts/mimir-provisioning/templates/_helpers.tpl @@ -0,0 +1,69 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "mimir-provisioning.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 "mimir-provisioning.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 "mimir-provisioning.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "mimir-provisioning.labels" -}} +helm.sh/chart: {{ include "mimir-provisioning.chart" . }} +{{ include "mimir-provisioning.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "mimir-provisioning.selectorLabels" -}} +app.kubernetes.io/name: {{ include "mimir-provisioning.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "mimir-provisioning.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "mimir-provisioning.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create the name of the provision script configmap +*/}} +{{- define "mimir-provisioning.provisionConfigmapName" }} +{{- printf "%s-%s" (include "mimir-provisioning.fullname" .) "script" | trunc 63 | trimSuffix "-" }} +{{- end }} diff --git a/charts/mimir-provisioning/templates/_job.tpl b/charts/mimir-provisioning/templates/_job.tpl new file mode 100644 index 0000000..6069b14 --- /dev/null +++ b/charts/mimir-provisioning/templates/_job.tpl @@ -0,0 +1,104 @@ +{{/* +Expand the job definition. +*/}} +{{- define "mimir-provisioning.job" -}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "mimir-provisioning.fullname" . }} + labels: + {{- include "mimir-provisioning.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install, post-upgrade + "helm.sh/hook-weight": "-5" +spec: + {{- include "mimir-provisioning.jobSpec" . | nindent 2 }} +{{- end }} + +{{/* +Expand the job spec definition. +*/}} +{{- define "mimir-provisioning.jobSpec" -}} +backoffLimit: {{ .Values.backoffLimit }} +template: + metadata: + name: {{ printf "%s-%s" (include "mimir-provisioning.fullname" .) "provisioning" | trunc 63 | trimSuffix "-" }} + labels: + {{- include "mimir-provisioning.labels" . | nindent 4 }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "mimir-provisioning.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + restartPolicy: OnFailure + initContainers: + - name: file-provisioner + image: {{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag }} + securityContext: + {{- toYaml .Values.sidecar.securityContext | nindent 12 }} + imagePullPolicy: {{ .Values.sidecar.image.pullPolicy }} + volumeMounts: + - name: provisioning + mountPath: /tmp/ + env: + - name: LABEL + value: {{ .Values.sidecar.resourceLabel }} + - name: FOLDER + value: /tmp + - name: RESOURCE + value: {{ .Values.sidecar.resourceType }} + - name: METHOD + value: {{ .Values.sidecar.behaviour }} + {{- if .Values.sidecar.extraEnvs }} + {{ .Values.sidecar.extraEnvs | toYaml | indent 12 | trim }} + {{- end }} + resources: + {{- toYaml .Values.sidecar.resources | nindent 12 }} + containers: + - name: mimirtool + image: {{ .Values.provisioner.image.repository }}:{{ .Values.provisioner.image.tag }} + securityContext: + {{- toYaml .Values.provisioner.securityContext | nindent 12 }} + imagePullPolicy: {{ .Values.provisioner.image.pullPolicy }} + env: + - name: MIMIR_ADDRESS + value: {{ .Values.provisioner.mimirAddress }} + - name: MIMIR_TLS_INSECURE_SKIP_VERIFY + value: {{ .Values.provisioner.mimirTLSInsecureSkipVerify | ternary "1" "0" | quote }} + volumeMounts: + - name: provisioning + mountPath: /tmp/ + - name: provision-script + mountPath: /script/mimirtool.sh + subPath: mimirtool.sh + command: + - /script/mimirtool.sh + resources: + {{- toYaml .Values.provisioner.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: provisioning + emptyDir: {} + - name: provision-script + configMap: + name: {{ include "mimir-provisioning.provisionConfigmapName" . }} + defaultMode: 0777 +{{- end }} \ No newline at end of file diff --git a/charts/mimir-provisioning/templates/provisioning-cm.yaml b/charts/mimir-provisioning/templates/provisioning-cm.yaml new file mode 100644 index 0000000..2b67574 --- /dev/null +++ b/charts/mimir-provisioning/templates/provisioning-cm.yaml @@ -0,0 +1,17 @@ +{{- $resourceLabel:=.Values.sidecar.resourceLabel -}} +{{ range $tenant,$files:=.Values.provision }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "%s-%s" (include "mimir-provisioning.fullname" $) $tenant | trunc 63 | trimSuffix "-" }} + annotations: + k8s-sidecar-target-directory: {{ $tenant | quote }} + labels: + {{ $resourceLabel | quote }}: "" +data: + {{ range $file:=$files -}} + {{ required (printf "El campo name de los objetos files es requerido. Revise .Values.provisioning.%s" $tenant) $file.name }}: | + {{ (required (printf "El campo content de los objetos files es requerido. Revise .Values.provisioning.%s" $tenant) $file.content) | indent 4 | trim }} + {{ end -}} +{{ end }} diff --git a/charts/mimir-provisioning/templates/provisioning-workload.yaml b/charts/mimir-provisioning/templates/provisioning-workload.yaml new file mode 100644 index 0000000..8a9d8ac --- /dev/null +++ b/charts/mimir-provisioning/templates/provisioning-workload.yaml @@ -0,0 +1,7 @@ +{{- if eq .Values.mode "cronjob"}} +{{- include "mimir-provisioning.cronjob" . }} +{{- else if eq .Values.mode "job" }} +{{- include "mimir-provisioning.job" . }} +{{- else }} +{{- fail "Invalid mode. It should be one of 'cronjob' or 'job'" }} +{{- end }} \ No newline at end of file diff --git a/charts/mimir-provisioning/templates/role.yaml b/charts/mimir-provisioning/templates/role.yaml new file mode 100644 index 0000000..0e67533 --- /dev/null +++ b/charts/mimir-provisioning/templates/role.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "mimir-provisioning.fullname" . }} +rules: + - apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "watch", "list"] diff --git a/charts/mimir-provisioning/templates/rolebinding.yaml b/charts/mimir-provisioning/templates/rolebinding.yaml new file mode 100644 index 0000000..9663a3e --- /dev/null +++ b/charts/mimir-provisioning/templates/rolebinding.yaml @@ -0,0 +1,15 @@ +{{- if .Values.serviceAccount.create }} +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "mimir-provisioning.fullname" . }} +roleRef: + kind: Role + name: {{ include "mimir-provisioning.fullname" . }} + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "mimir-provisioning.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/mimir-provisioning/templates/script-cm.yaml b/charts/mimir-provisioning/templates/script-cm.yaml new file mode 100644 index 0000000..b1387ad --- /dev/null +++ b/charts/mimir-provisioning/templates/script-cm.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "mimir-provisioning.provisionConfigmapName" . }} +data: + mimirtool.sh: {{- tpl .Values.provisioner.script . | toYaml | indent 4 -}} diff --git a/charts/mimir-provisioning/templates/serviceaccount.yaml b/charts/mimir-provisioning/templates/serviceaccount.yaml new file mode 100644 index 0000000..864f4e0 --- /dev/null +++ b/charts/mimir-provisioning/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "mimir-provisioning.serviceAccountName" . }} + labels: + {{- include "mimir-provisioning.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/mimir-provisioning/values.yaml b/charts/mimir-provisioning/values.yaml new file mode 100644 index 0000000..a619b0b --- /dev/null +++ b/charts/mimir-provisioning/values.yaml @@ -0,0 +1,120 @@ +# +mode: job + +cronjob: + schedule: "*/5 * * * *" + +global: + provisioner: + mimirtoolCommand: "rules sync" + mimirtoolArgs: "/tmp/$tenant/$file" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +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: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +backoffLimit: 3 + +provisioner: + image: + repository: grafana/mimirtool + pullPolicy: IfNotPresent + tag: "2.10.5" + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + mimirAddress: http://mimir-nginx.mimir.svc + mimirTLSInsecureSkipVerify: true + + script: | + #!/bin/sh + for tenant in $(ls /tmp/); do + echo "Found tenant $tenant" + for file in $(ls /tmp/$tenant); do + echo "found file $file" + MIMIR_TENANT_ID=$tenant mimirtool {{.Values.global.provisioner.mimirtoolCommand}} {{.Values.global.provisioner.mimirtoolArgs}} + done + done + +sidecar: + image: + repository: ghcr.io/kiwigrid/k8s-sidecar + pullPolicy: IfNotPresent + tag: "1.25.3" + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + extraEnvs: {} + + resourceLabel: provisioning + resourceType: both + behaviour: LIST + + +provision: {} +#provision: +# sampletenant: +# - name: sampletenant.yaml +# content: | +# a: valid +# config: yaml +# - name: sampletenant.tpl +# content: | +# {{ define "hello.world" }} +# hello world +# {{ end }} +# anothertenant: +# - name: anothertenant.yaml +# content: +# hi: you