diff --git a/.claude/commands/helm-adapt.md b/.claude/commands/helm-adapt.md new file mode 100644 index 0000000..1f1c2a0 --- /dev/null +++ b/.claude/commands/helm-adapt.md @@ -0,0 +1,123 @@ +Adapt a Helm chart's values.yaml for this cluster. The user will provide the service name or path. + +## Cluster facts (always apply these) + +- **Node**: single Raspberry Pi, hostname `master`, arch `aarch64` +- **Ingress controller**: Traefik — use `ingressClassName: traefik` +- **TLS**: cert-manager with cluster issuer `letsencrypt-prod` (HTTP-01 only — no wildcard certs) +- **Domain pattern**: `.immich-ad.ovh` +- **StorageClass**: `local-storage` (no-provisioner, `WaitForFirstConsumer`) +- **Storage root**: `/storage//` +- **PV/PVC pattern**: pre-create PVs manually; StatefulSets use volumeClaimTemplates (add `claimRef`); Deployments use standalone PVCs referenced via `existingClaim` +- **Images**: prefer `arm64` or multi-arch images; replace any `amd64`-specific image tags + +## Ingress block template + +```yaml +ingress: + main: # or the chart's ingress key name + enabled: true + ingressClassName: traefik + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.entrypoints: websecure + hosts: + - host: .immich-ad.ovh + paths: + - path: / + pathType: Prefix + tls: + - secretName: -tls + hosts: + - .immich-ad.ovh +``` + +## PV template (for Deployments with existingClaim) + +```yaml +# pv-.yaml +apiVersion: v1 +kind: PersistentVolume +metadata: + name: pv- +spec: + capacity: + storage: + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /storage/ + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master +``` + +## PV template (for StatefulSets — claimRef binds to auto-created PVC) + +```yaml +# pv-.yaml +apiVersion: v1 +kind: PersistentVolume +metadata: + name: pv--data +spec: + capacity: + storage: + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /storage//data + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + claimRef: + name: data--0 # matches StatefulSet volumeClaimTemplate + namespace: +``` + +## PVC template (for Deployments) + +```yaml +# pvc-.yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pvc- + namespace: +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: + storageClassName: local-storage +``` + +## Steps to follow + +1. Read the chart's `values.yaml` (and `README.md` if present) to understand available keys. +2. Read an existing service's values file (e.g. `../vaultwarden/values.yaml`) if the chart type is similar. +3. Apply all cluster facts above: + - Set ingress to traefik + letsencrypt-prod + correct host + - Set storageClass to `local-storage` + - Set replicaCount to 1 + - Fix any amd64 image to arm64 equivalent +4. Create `pv-.yaml` in the service folder with correct path and sizes. +5. Create `pvc-.yaml` only if the workload is a Deployment (not StatefulSet). +6. Create `NOTE.md` with helm install/upgrade/delete commands, PV apply commands, and useful kubectl check/log commands — following the style of `../immich/notes.md`. diff --git a/garage/.helmignore b/garage/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/garage/.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/garage/Chart.yaml b/garage/Chart.yaml new file mode 100644 index 0000000..6c93b37 --- /dev/null +++ b/garage/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: garage +description: S3-compatible object store for small self-hosted geo-distributed deployments +type: application +version: 0.9.2 +appVersion: "v2.2.0" +home: https://garagehq.deuxfleurs.fr/ +icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg + +keywords: +- geo-distributed +- read-after-write-consistency +- s3-compatible + +sources: +- https://git.deuxfleurs.fr/Deuxfleurs/garage.git + +maintainers: [] diff --git a/garage/NOTE.md b/garage/NOTE.md new file mode 100644 index 0000000..eeca63e --- /dev/null +++ b/garage/NOTE.md @@ -0,0 +1,107 @@ +# garage + +S3-compatible object store — https://garagehq.deuxfleurs.fr/ + +Chart: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/script/helm + +## Namespace + +``` +kubectl create namespace garage +``` + +## PV / PVC + +Create the host directories first: +``` +sudo mkdir -p /storage/garage/{data,meta} +``` + +Apply the PVs (PVCs are created automatically by the StatefulSet): +``` +kubectl apply -f ./pv-garage.yaml +kubectl get pv | grep garage +``` + +## Helm + +``` +helm install -n garage garage . -f values.yaml + +helm upgrade --install garage . \ + -n garage \ + -f values.yaml + +helm delete garage -n garage +``` + +## Check + +``` +kubectl -n garage get pods,pvc,ingress +kubectl -n garage get pvc +kubectl get pv | grep garage +kubectl -n garage get svc +``` + +## Logs + +``` +kubectl -n garage logs -l app.kubernetes.io/name=garage --prefix +kubectl -n garage describe pod +``` + +## Garage CLI — layout & cluster + +After the pod is running, initialize the cluster layout (single-node): +``` +# Get the node ID +kubectl -n garage exec -it garage-0 -- /garage status + +# Assign capacity to the node (replace ) +kubectl -n garage exec -it garage-0 -- /garage layout assign -z dc1 -c 50G + +# Review and apply +kubectl -n garage exec -it garage-0 -- /garage layout show +kubectl -n garage exec -it garage-0 -- /garage layout apply --version 1 +``` + +## Garage CLI — buckets & keys + +``` +# List buckets +kubectl -n garage exec -it garage-0 -- /garage bucket list + +# Create a bucket +kubectl -n garage exec -it garage-0 -- /garage bucket create + +# List access keys +kubectl -n garage exec -it garage-0 -- /garage key list + +# Create an access key +kubectl -n garage exec -it garage-0 -- /garage key create + +# Grant key access to bucket +kubectl -n garage exec -it garage-0 -- /garage bucket allow \ + --read --write --owner --key + +# Show key credentials (access key + secret) +kubectl -n garage exec -it garage-0 -- /garage key info +``` + +## Certificate + +``` +kubectl -n garage get certificate +kubectl -n garage describe certificate garage-s3-tls +kubectl -n garage get challenges +``` + +## Show chart values + +``` +helm show values garage/garage | grep -A20 -B5 -i persistence +helm show values garage/garage | grep -A20 -B5 -i ingress +``` + +kubectl -n garage exec -it garage-0 -- /garage status \ No newline at end of file diff --git a/garage/README.md b/garage/README.md new file mode 100644 index 0000000..a16d05b --- /dev/null +++ b/garage/README.md @@ -0,0 +1,97 @@ +# garage + +![Version: 0.9.2](https://img.shields.io/badge/Version-0.9.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2.2.0](https://img.shields.io/badge/AppVersion-v2.2.0-informational?style=flat-square) + +S3-compatible object store for small self-hosted geo-distributed deployments + +**Homepage:** + +## Source Code + +* + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| commonLabels | object | `{}` | Extra labels for all resources | +| deployment.kind | string | `"StatefulSet"` | Switchable to DaemonSet | +| deployment.podManagementPolicy | string | `"OrderedReady"` | If using statefulset, allow Parallel or OrderedReady (default) | +| deployment.replicaCount | int | `3` | Number of StatefulSet replicas/garage nodes to start | +| environment | object | `{}` | | +| extraVolumeMounts | object | `{}` | | +| extraVolumes | object | `{}` | | +| fullnameOverride | string | `""` | | +| garage.blockSize | string | `"1048576"` | Defaults is 1MB An increase can result in better performance in certain scenarios https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size | +| garage.bootstrapPeers | list | `[]` | This is not required if you use the integrated kubernetes discovery | +| garage.compressionLevel | string | `"1"` | zstd compression level of stored blocks https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level | +| garage.dbEngine | string | `"lmdb"` | Can be changed for better performance on certain systems https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine | +| garage.existingConfigMap | string | `""` | if not empty string, allow using an existing ConfigMap for the garage.toml, if set, ignores garage.toml | +| garage.garageTomlString | string | `""` | String Template for the garage configuration if set, ignores above values. Values can be templated, see https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/ | +| garage.kubernetesSkipCrd | bool | `false` | Set to true if you want to use k8s discovery but install the CRDs manually outside of the helm chart, for example if you operate at namespace level without cluster resources | +| garage.replicationFactor | string | `"3"` | Default to 3 replicas, see the replication_factor section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor | +| garage.consistencyMode | string | `"consistent"` | Default to read-after-write consistency, see the consistency_mode section at https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode | +| garage.metadataAutoSnapshotInterval | string | `""` | If this value is set, Garage will automatically take a snapshot of the metadata DB file at a regular interval and save it in the metadata directory. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval | +| garage.rpcBindAddr | string | `"[::]:3901"` | | +| garage.rpcSecret | string | `""` | If not given, a random secret will be generated and stored in a Secret object | +| garage.s3.api.region | string | `"garage"` | | +| garage.s3.api.rootDomain | string | `".s3.garage.tld"` | | +| garage.s3.web.index | string | `"index.html"` | | +| garage.s3.web.rootDomain | string | `".web.garage.tld"` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"dxflrs/amd64_garage"` | default to amd64 docker image | +| image.tag | string | `""` | set the image tag, please prefer using the chart version and not this to avoid compatibility issues | +| imagePullSecrets | list | `[]` | set if you need credentials to pull your custom image | +| ingress.s3.api.annotations | object | `{}` | Rely _either_ on the className or the annotation below but not both! If you want to use the className, set className: "nginx" and replace "nginx" by an Ingress controller name, examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers). | +| ingress.s3.api.enabled | bool | `false` | | +| ingress.s3.api.hosts[0] | object | `{"host":"s3.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | garage S3 API endpoint, to be used with awscli for example | +| ingress.s3.api.hosts[1] | object | `{"host":"*.s3.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | garage S3 API endpoint, DNS style bucket access | +| ingress.s3.api.labels | object | `{}` | | +| ingress.s3.api.tls | list | `[]` | | +| ingress.s3.web.annotations | object | `{}` | Rely _either_ on the className or the annotation below but not both! If you want to use the className, set className: "nginx" and replace "nginx" by an Ingress controller name, examples [here](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers). | +| ingress.s3.web.enabled | bool | `false` | | +| ingress.s3.web.hosts[0] | object | `{"host":"*.web.garage.tld","paths":[{"path":"/","pathType":"Prefix"}]}` | wildcard website access with bucket name prefix | +| ingress.s3.web.hosts[1] | object | `{"host":"mywebpage.example.com","paths":[{"path":"/","pathType":"Prefix"}]}` | specific bucket access with FQDN bucket | +| ingress.s3.web.labels | object | `{}` | | +| ingress.s3.web.tls | list | `[]` | | +| initImage.pullPolicy | string | `"IfNotPresent"` | | +| initImage.repository | string | `"busybox"` | | +| initImage.tag | string | `"stable"` | | +| livenessProbe | object | `{}` | Specifies a livenessProbe | +| monitoring.metrics.enabled | bool | `false` | If true, a service for monitoring is created with a prometheus.io/scrape annotation | +| monitoring.metrics.serviceMonitor.enabled | bool | `false` | If true, a ServiceMonitor CRD is created for a prometheus operator https://github.com/coreos/prometheus-operator | +| monitoring.metrics.serviceMonitor.interval | string | `"15s"` | | +| monitoring.metrics.serviceMonitor.labels | object | `{}` | | +| monitoring.metrics.serviceMonitor.path | string | `"/metrics"` | | +| monitoring.metrics.serviceMonitor.relabelings | list | `[]` | | +| monitoring.metrics.serviceMonitor.scheme | string | `"http"` | | +| monitoring.metrics.serviceMonitor.scrapeTimeout | string | `"10s"` | | +| monitoring.metrics.serviceMonitor.tlsConfig | object | `{}` | | +| monitoring.tracing.sink | string | `""` | specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317` | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| persistence.data.hostPath | string | `"/var/lib/garage/data"` | | +| persistence.data.size | string | `"100Mi"` | | +| persistence.enabled | bool | `true` | | +| persistence.meta.hostPath | string | `"/var/lib/garage/meta"` | | +| persistence.meta.size | string | `"100Mi"` | | +| podAnnotations | object | `{}` | additional pod annotations | +| podSecurityContext.fsGroup | int | `1000` | | +| podSecurityContext.runAsGroup | int | `1000` | | +| podSecurityContext.runAsNonRoot | bool | `true` | | +| podSecurityContext.runAsUser | int | `1000` | | +| readinessProbe | object | `{}` | Specifies a readinessProbe | +| resources | object | `{}` | | +| securityContext.capabilities | object | `{"drop":["ALL"]}` | The default security context is heavily restricted, feel free to tune it to your requirements | +| securityContext.readOnlyRootFilesystem | bool | `true` | | +| service.s3.api.port | int | `3900` | | +| service.s3.web.port | int | `3902` | | +| service.type | string | `"ClusterIP"` | You can rely on any service to expose your cluster - ClusterIP (+ Ingress) - NodePort (+ Ingress) - LoadBalancer | +| serviceAccount.annotations | object | `{}` | Annotations to add to the service account | +| serviceAccount.create | bool | `true` | Specifies whether a service account should be created | +| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | +| tolerations | list | `[]` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/garage/kube-cluster.code-workspace b/garage/kube-cluster.code-workspace new file mode 100644 index 0000000..bab1b7f --- /dev/null +++ b/garage/kube-cluster.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": ".." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/garage/pv-garage.yaml b/garage/pv-garage.yaml new file mode 100644 index 0000000..e15f1f9 --- /dev/null +++ b/garage/pv-garage.yaml @@ -0,0 +1,51 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: pv-garage-data +spec: + capacity: + storage: 50Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /storage/garage/data + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + claimRef: + name: data-garage-0 + namespace: garage +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: pv-garage-meta +spec: + capacity: + storage: 1Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /storage/garage/meta + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + claimRef: + name: meta-garage-0 + namespace: garage diff --git a/garage/templates/_helpers.tpl b/garage/templates/_helpers.tpl new file mode 100644 index 0000000..2ffb90c --- /dev/null +++ b/garage/templates/_helpers.tpl @@ -0,0 +1,91 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "garage.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 "garage.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 the name of the rpc secret +*/}} +{{- define "garage.rpcSecretName" -}} +{{- .Values.garage.existingRpcSecret | default (printf "%s-rpc-secret" (include "garage.fullname" .)) -}} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "garage.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "garage.labels" -}} +helm.sh/chart: {{ include "garage.chart" . }} +{{ include "garage.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.commonLabels }} +{{- toYaml . | nindent 0 }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "garage.selectorLabels" -}} +app.kubernetes.io/name: {{ include "garage.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "garage.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "garage.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* + Returns given number of random Hex characters. + In practice, it generates up to 100 randAlphaNum strings + that are filtered from non-hex characters and augmented + to the resulting string that is finally trimmed down. +*/}} +{{- define "jupyterhub.randHex" -}} + {{- $result := "" }} + {{- range $i := until 100 }} + {{- if lt (len $result) . }} + {{- $rand_list := randAlphaNum . | splitList "" -}} + {{- $reduced_list := without $rand_list "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" }} + {{- $rand_string := join "" $reduced_list }} + {{- $result = print $result $rand_string -}} + {{- end }} + {{- end }} + {{- $result | trunc . }} +{{- end }} diff --git a/garage/templates/clusterrole.yaml b/garage/templates/clusterrole.yaml new file mode 100644 index 0000000..fa3e640 --- /dev/null +++ b/garage/templates/clusterrole.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manage-crds-{{ .Release.Namespace }}-{{ .Release.Name }} + labels: + {{- include "garage.labels" . | nindent 4 }} +rules: +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "create", "patch"] +- apiGroups: ["deuxfleurs.fr"] + resources: ["garagenodes"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: allow-crds-for-{{ .Release.Namespace }}-{{ .Release.Name }} + labels: + {{- include "garage.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + name: {{ include "garage.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: manage-crds-{{ .Release.Namespace }}-{{ .Release.Name }} + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/garage/templates/configmap.yaml b/garage/templates/configmap.yaml new file mode 100644 index 0000000..4fc3e15 --- /dev/null +++ b/garage/templates/configmap.yaml @@ -0,0 +1,62 @@ +{{- if not .Values.garage.existingConfigMap }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "garage.fullname" . }}-config +data: + garage.toml: |- + {{- if .Values.garage.garageTomlString }} + {{- tpl (index (index .Values.garage) "garageTomlString") $ | nindent 4 }} + {{- else }} + metadata_dir = "/mnt/meta" + data_dir = "/mnt/data" + + db_engine = "{{ .Values.garage.dbEngine }}" + + block_size = "{{ .Values.garage.blockSize }}" + + replication_factor = {{ .Values.garage.replicationFactor }} + consistency_mode = "{{ .Values.garage.consistencyMode }}" + + compression_level = {{ .Values.garage.compressionLevel }} + + {{- if .Values.garage.metadataAutoSnapshotInterval }} + metadata_auto_snapshot_interval = {{ .Values.garage.metadataAutoSnapshotInterval | quote }} + {{- end }} + + rpc_bind_addr = "{{ .Values.garage.rpcBindAddr }}" + # rpc_secret will be populated by the init container from a k8s secret object + rpc_secret = "__RPC_SECRET_REPLACE__" + + bootstrap_peers = [ + {{- range $index, $peer := .Values.garage.bootstrapPeers }} + {{- if $index}}, {{ end }}{{ $peer | quote }} + {{ end }} + ] + + {{- if .Values.garage.additionalTopLevelConfig }} + {{ .Values.garage.additionalTopLevelConfig | nindent 4 }} + {{- end }} + + [kubernetes_discovery] + namespace = "{{ .Release.Namespace }}" + service_name = "{{ include "garage.fullname" . }}" + skip_crd = {{ .Values.garage.kubernetesSkipCrd }} + + [s3_api] + s3_region = "{{ .Values.garage.s3.api.region }}" + api_bind_addr = "[::]:3900" + root_domain = "{{ .Values.garage.s3.api.rootDomain }}" + + [s3_web] + bind_addr = "[::]:3902" + root_domain = "{{ .Values.garage.s3.web.rootDomain }}" + index = "{{ .Values.garage.s3.web.index }}" + + [admin] + api_bind_addr = "[::]:3903" + {{- if .Values.monitoring.tracing.sink }} + trace_sink = "{{ .Values.monitoring.tracing.sink }}" + {{- end }} + {{- end }} +{{- end }} diff --git a/garage/templates/ingress.yaml b/garage/templates/ingress.yaml new file mode 100644 index 0000000..35225da --- /dev/null +++ b/garage/templates/ingress.yaml @@ -0,0 +1,129 @@ +{{- if .Values.ingress.s3.api.enabled -}} +{{- $fullName := include "garage.fullname" . -}} +{{- $svcPort := .Values.service.s3.api.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.s3.api.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.s3.api.annotations "kubernetes.io/ingress.class" .Values.ingress.s3.api.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }}-s3-api + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.ingress.s3.api.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ingress.s3.api.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.s3.api.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.s3.api.className }} + {{- end }} + {{- if .Values.ingress.s3.api.tls }} + tls: + {{- range .Values.ingress.s3.api.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.s3.api.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +--- +{{- if .Values.ingress.s3.web.enabled -}} +{{- $fullName := include "garage.fullname" . -}} +{{- $svcPort := .Values.service.s3.web.port -}} +{{- if and .Values.ingress.s3.web.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.s3.web.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.s3.web.annotations "kubernetes.io/ingress.class" .Values.ingress.s3.web.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }}-s3-web + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.ingress.s3.web.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ingress.s3.web.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.s3.web.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.s3.web.className }} + {{- end }} + {{- if .Values.ingress.s3.web.tls }} + tls: + {{- range .Values.ingress.s3.web.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.s3.web.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/garage/templates/secret.yaml b/garage/templates/secret.yaml new file mode 100644 index 0000000..c0c45b9 --- /dev/null +++ b/garage/templates/secret.yaml @@ -0,0 +1,16 @@ +{{- if not .Values.garage.existingRpcSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "garage.rpcSecretName" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} +type: Opaque +data: + {{/* retrieve the secret data using lookup function and when not exists, return an empty dictionary / map as result */}} + {{- $prevSecret := (lookup "v1" "Secret" .Release.Namespace (include "garage.rpcSecretName" .)) | default dict }} + {{- $prevSecretData := $prevSecret.data | default dict }} + {{- $prevRpcSecret := $prevSecretData.rpcSecret | default "" | b64dec }} + {{/* Priority is: 1. from values, 2. previous value, 3. generate random */}} + rpcSecret: {{ .Values.garage.rpcSecret | default $prevRpcSecret | default (include "jupyterhub.randHex" 64) | b64enc | quote }} +{{- end }} diff --git a/garage/templates/service-headless.yaml b/garage/templates/service-headless.yaml new file mode 100644 index 0000000..7bc9f2c --- /dev/null +++ b/garage/templates/service-headless.yaml @@ -0,0 +1,22 @@ +{{- if eq .Values.deployment.kind "StatefulSet" -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "garage.fullname" . }}-headless + labels: + {{- include "garage.labels" . | nindent 4 }} +spec: + type: ClusterIP + clusterIP: None + ports: + - port: {{ .Values.service.s3.api.port }} + targetPort: 3900 + protocol: TCP + name: s3-api + - port: {{ .Values.service.s3.web.port }} + targetPort: 3902 + protocol: TCP + name: s3-web + selector: + {{- include "garage.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/garage/templates/service.yaml b/garage/templates/service.yaml new file mode 100644 index 0000000..887c90d --- /dev/null +++ b/garage/templates/service.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "garage.fullname" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.s3.api.port }} + targetPort: 3900 + protocol: TCP + name: s3-api + - port: {{ .Values.service.s3.web.port }} + targetPort: 3902 + protocol: TCP + name: s3-web + selector: + {{- include "garage.selectorLabels" . | nindent 4 }} +{{- if .Values.monitoring.metrics.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "garage.fullname" . }}-metrics + labels: + {{- include "garage.labels" . | nindent 4 }} + annotations: + prometheus.io/scrape: "true" +spec: + type: ClusterIP + clusterIP: None + ports: + - port: 3903 + targetPort: 3903 + protocol: TCP + name: metrics + selector: + {{- include "garage.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/garage/templates/serviceaccount.yaml b/garage/templates/serviceaccount.yaml new file mode 100644 index 0000000..a0a89a3 --- /dev/null +++ b/garage/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "garage.serviceAccountName" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/garage/templates/servicemonitor.yaml b/garage/templates/servicemonitor.yaml new file mode 100644 index 0000000..6838d09 --- /dev/null +++ b/garage/templates/servicemonitor.yaml @@ -0,0 +1,44 @@ +{{- if .Values.monitoring.metrics.serviceMonitor.enabled }} +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "garage.fullname" . }} + {{- if .Values.monitoring.metrics.serviceMonitor.namespace }} + namespace: {{ tpl .Values.monitoring.metrics.serviceMonitor.namespace . }} + {{- else }} + namespace: {{ .Release.Namespace }} + {{- end }} + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.monitoring.metrics.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + - port: metrics + {{- with .Values.monitoring.metrics.serviceMonitor.interval }} + interval: {{ . }} + {{- end }} + {{- with .Values.monitoring.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + honorLabels: true + path: {{ .Values.monitoring.metrics.serviceMonitor.path }} + scheme: {{ .Values.monitoring.metrics.serviceMonitor.scheme }} + {{- with .Values.monitoring.metrics.serviceMonitor.tlsConfig }} + tlsConfig: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.monitoring.metrics.serviceMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 6 }} + {{- end }} + jobLabel: "{{ .Release.Name }}" + selector: + matchLabels: + {{- include "garage.selectorLabels" . | nindent 6 }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/garage/templates/workload.yaml b/garage/templates/workload.yaml new file mode 100644 index 0000000..81c8616 --- /dev/null +++ b/garage/templates/workload.yaml @@ -0,0 +1,154 @@ +apiVersion: apps/v1 +kind: {{ .Values.deployment.kind }} +metadata: + name: {{ include "garage.fullname" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "garage.selectorLabels" . | nindent 6 }} + {{- if eq .Values.deployment.kind "StatefulSet" }} + replicas: {{ .Values.deployment.replicaCount }} + serviceName: {{ include "garage.fullname" . }}-headless + podManagementPolicy: {{ .Values.deployment.podManagementPolicy }} + {{- end }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "garage.labels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "garage.serviceAccountName" . }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + # Copies garage.toml from configmap to temporary etc volume and replaces RPC secret placeholder + - name: {{ .Chart.Name }}-init + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: {{ .Values.initImage.pullPolicy }} + command: ["sh", "-c", "sed \"s/__RPC_SECRET_REPLACE__/$RPC_SECRET/\" /mnt/garage.toml > /mnt/etc/garage.toml"] + env: + - name: RPC_SECRET + valueFrom: + secretKeyRef: + name: {{ include "garage.rpcSecretName" . }} + key: rpcSecret + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + volumeMounts: + - name: configmap + mountPath: /mnt/garage.toml + subPath: garage.toml + - name: etc + mountPath: /mnt/etc + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 3900 + name: s3-api + - containerPort: 3902 + name: web-api + - containerPort: 3903 + name: admin + {{- with .Values.environment }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: meta + mountPath: /mnt/meta + - name: data + mountPath: /mnt/data + - name: etc + mountPath: /etc/garage.toml + subPath: garage.toml + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: configmap + configMap: + name: {{ include "garage.fullname" . }}-config + - name: etc + emptyDir: {} + {{- if .Values.persistence.enabled }} + {{- if eq .Values.deployment.kind "DaemonSet" }} + - name: meta + hostPath: + path: {{ .Values.persistence.meta.hostPath }} + type: DirectoryOrCreate + - name: data + hostPath: + path: {{ .Values.persistence.data.hostPath }} + type: DirectoryOrCreate + {{- end }} + {{- else }} + - name: meta + emptyDir: {} + - name: data + emptyDir: {} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if and .Values.persistence.enabled (eq .Values.deployment.kind "StatefulSet") }} + volumeClaimTemplates: + - metadata: + name: meta + spec: + accessModes: [ "ReadWriteOnce" ] + {{- if hasKey .Values.persistence.meta "storageClass" }} + storageClassName: {{ .Values.persistence.meta.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.meta.size | quote }} + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + {{- if hasKey .Values.persistence.data "storageClass" }} + storageClassName: {{ .Values.persistence.data.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.data.size | quote }} + {{- end }} diff --git a/garage/values.yaml b/garage/values.yaml new file mode 100644 index 0000000..6535c9e --- /dev/null +++ b/garage/values.yaml @@ -0,0 +1,250 @@ +# Default values for garage. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# -- Additional labels to add to all resources created by this chart +commonLabels: {} +# app.kubernetes.io/part-of: storage +# team: platform + +# Garage configuration. These values go to garage.toml +garage: + # -- Can be changed for better performance on certain systems + # https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine + dbEngine: "lmdb" + + # -- Defaults is 1MB + # An increase can result in better performance in certain scenarios + # https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size + blockSize: "1048576" + + # -- Single-node cluster + # https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication_factor + replicationFactor: "1" + + # -- By default, enable read-after-write consistency guarantees, see the consistency_mode section at + # https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#consistency_mode + consistencyMode: "consistent" + + # -- zstd compression level of stored blocks + # https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level + compressionLevel: "1" + + # -- If this value is set, Garage will automatically take a snapshot of the metadata DB file at a regular interval and save it in the metadata directory. + # https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#metadata_auto_snapshot_interval + metadataAutoSnapshotInterval: "" + + rpcBindAddr: "[::]:3901" + # -- If not given, a random secret will be generated and stored in a Secret object + rpcSecret: "" + # -- If you want to provide an rpcSecret within an existing k8s secret, + # specify the secret name here, and store the value under the secret key `rpcSecret` + # the default secret will not be created + existingRpcSecret: "" + # -- This is not required if you use the integrated kubernetes discovery + bootstrapPeers: [] + # -- Set to true if you want to use k8s discovery but install the CRDs manually outside + # of the helm chart, for example if you operate at namespace level without cluster resources + kubernetesSkipCrd: false + s3: + api: + region: "garage" + rootDomain: ".s3.immich-ad.ovh" + web: + rootDomain: ".web.immich-ad.ovh" + index: "index.html" + + # -- Additional configuration to append to garage.toml. Use a multi-line string for custom config. + # Example: + # additionalTopLevelConfig: |- + # data_fsync = true + additionalTopLevelConfig: "" + + # -- if not empty string, allow using an existing ConfigMap for the garage.toml, + # if set, ignores garage.toml + existingConfigMap: "" + + # -- String Template for the garage configuration + # if set, ignores above values. + # Values can be templated, + # see https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/ + garageTomlString: "" + +# Data persistence +persistence: + enabled: true + meta: + storageClass: "local-storage" + size: 1Gi + # used only for daemon sets + hostPath: /var/lib/garage/meta + data: + storageClass: "local-storage" + size: 50Gi + # used only for daemon sets + hostPath: /var/lib/garage/data + +# Deployment configuration +deployment: + # -- Switchable to DaemonSet + kind: StatefulSet + # -- Single-node cluster + replicaCount: 1 + # -- If using statefulset, allow Parallel or OrderedReady (default) + podManagementPolicy: OrderedReady + +image: + # -- arm64 image for Raspberry Pi + repository: dxflrs/arm64_garage + # -- set the image tag, please prefer using the chart version and not this + # to avoid compatibility issues + tag: "" + pullPolicy: IfNotPresent + +initImage: + repository: busybox + tag: stable + pullPolicy: IfNotPresent + +# -- set if you need credentials to pull your custom image +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: "" + +# -- additional pod annotations +podAnnotations: {} + +podSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: "OnRootMismatch" + runAsNonRoot: true + +securityContext: + # -- The default security context is heavily restricted, + # feel free to tune it to your requirements + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + +service: + # -- You can rely on any service to expose your cluster + # - ClusterIP (+ Ingress) + # - NodePort (+ Ingress) + # - LoadBalancer + type: ClusterIP + # -- Annotations to add to the service + annotations: {} + s3: + api: + port: 3900 + web: + port: 3902 + # NOTE: the admin API is excluded for now as it is not consistent across nodes + +ingress: + s3: + api: + enabled: true + className: "traefik" + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.entrypoints: websecure + labels: {} + hosts: + # -- garage S3 API endpoint, path-style access + - host: "s3.immich-ad.ovh" + paths: + - path: / + pathType: Prefix + # Virtual-hosted-style (*.s3.immich-ad.ovh) requires DNS-01 — omitted + tls: + - secretName: garage-s3-tls + hosts: + - s3.immich-ad.ovh + web: + enabled: true + className: "traefik" + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.entrypoints: websecure + labels: {} + hosts: + - host: "*.web.immich-ad.ovh" + paths: + - path: / + pathType: Prefix + tls: [] + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + +# -- Specifies a livenessProbe +# NOTE: disabled — /health returns 503 until garage layout is initialized. +# Re-enable after running: garage layout assign + garage layout apply +livenessProbe: {} + # httpGet: + # path: /health + # port: 3903 + # initialDelaySeconds: 10 + # periodSeconds: 30 +# -- Specifies a readinessProbe +readinessProbe: {} + # httpGet: + # path: /health + # port: 3903 + # initialDelaySeconds: 5 + # periodSeconds: 30 + # failureThreshold: 3 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# -- Optional priority class name to assign to the pods. +# See https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/ +priorityClassName: "" + +environment: {} + +extraVolumes: {} + +extraVolumeMounts: {} + +monitoring: + metrics: + # -- If true, a service for monitoring is created with a prometheus.io/scrape annotation + enabled: false + serviceMonitor: + # -- If true, a ServiceMonitor CRD is created for a prometheus operator + # https://github.com/coreos/prometheus-operator + enabled: false + path: /metrics + # namespace: monitoring (defaults to use the namespace this chart is deployed to) + labels: {} + interval: 15s + scheme: http + tlsConfig: {} + scrapeTimeout: 10s + relabelings: [] + tracing: + # -- specify a sink endpoint for OpenTelemetry Traces, eg. `http://localhost:4317` + sink: "" diff --git a/nextcloud/values.yaml b/nextcloud/values.yaml index 0eb16d0..c5f8ca1 100644 --- a/nextcloud/values.yaml +++ b/nextcloud/values.yaml @@ -14,7 +14,7 @@ image: repository: library/nextcloud flavor: apache # default is generated by flavor and appVersion - tag: 33.0.1-apache + tag: 33.0.2-apache pullPolicy: IfNotPresent # pullSecrets: # - myRegistrKeySecretName diff --git a/zot/NOTE.md b/zot/NOTE.md new file mode 100644 index 0000000..abe0459 --- /dev/null +++ b/zot/NOTE.md @@ -0,0 +1,69 @@ +# zot — OCI Container Registry + +Namespace: `zot` +Domain: `zot.immich-ad.ovh` + +https://zotregistry.dev/v2.1.15/install-guides/install-guide-k8s/ + +## Helm repo + +```bash +helm repo add zot https://zotregistry.dev/helm-charts +helm repo update +``` + +## Storage — create directory and PV first + +```bash +sudo mkdir -p /storage/zot/data +kubectl apply -f pv-zot.yaml +``` + +## Install / Upgrade / Delete + +```bash +# Install +helm install zot zot/zot -n zot --create-namespace -f values.yaml + +# Upgrade +helm upgrade zot zot/zot -n zot -f values.yaml + +# Delete +helm uninstall zot -n zot +``` + +## Check PV / PVC + +```bash +kubectl get pv pv-zot-data +kubectl get pvc -n zot +``` + +## Pod / Service status + +```bash +kubectl get pods -n zot +kubectl get svc -n zot +kubectl describe pod -n zot -l app.kubernetes.io/name=zot +``` + +## Logs + +```bash +kubectl logs -n zot -l app.kubernetes.io/name=zot --prefix +``` + +## Certificate + +```bash +kubectl get certificate -n zot +kubectl describe certificate zot-tls -n zot +kubectl get challenges -n zot +``` + +## Test registry access + +```bash +# Ping the API (replace with actual node IP if testing from outside) +curl https://zot.immich-ad.ovh/v2/ +``` diff --git a/zot/pv-zot.yaml b/zot/pv-zot.yaml new file mode 100644 index 0000000..2a84d4b --- /dev/null +++ b/zot/pv-zot.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: pv-zot-data +spec: + capacity: + storage: 20Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /storage/zot/data + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + claimRef: + name: data-zot-0 # matches StatefulSet volumeClaimTemplate: data--zot-0 + namespace: zot diff --git a/zot/values.yaml b/zot/values.yaml new file mode 100644 index 0000000..ae7ac87 --- /dev/null +++ b/zot/values.yaml @@ -0,0 +1,72 @@ +replicaCount: 1 + +image: + repository: ghcr.io/project-zot/zot + pullPolicy: IfNotPresent + # multi-arch image, no override needed for arm64 + +service: + type: ClusterIP + port: 5000 + +ingress: + enabled: true + className: traefik + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.entrypoints: websecure + pathtype: Prefix + hosts: + - host: zot.immich-ad.ovh + paths: + - path: / + tls: + - secretName: zot-tls + hosts: + - zot.immich-ad.ovh + +# Mount the config.json into /etc/zot +mountConfig: true +configFiles: + config.json: |- + { + "storage": { "rootDirectory": "/var/lib/registry" }, + "log": { "level": "info" }, + "extensions": {"search": {"enable": true}, "ui": {"enable": true}}, + "http": { + "address": "0.0.0.0", + "port": "5000", + "auth": { + "htpasswd": { + "path": "/secret/htpasswd" + } + }, + "accessControl": { + "repositories": { + "**": { + "anonymousPolicy": [], + "defaultPolicy": [] + } + }, + "adminPolicy": { + "users": ["admin"], + "actions": ["read", "create", "update", "delete"] + } + } + } + } +mountSecret: true +secretFiles: + htpasswd: | + admin:$2y$10$1w7mXxSIKGV7dAyqy9TgAeZINEizxuA9ln.Pi6esu7olUV7Kw9ffO +persistence: true +pvc: + create: true + name: data # PVC will be named: data-zot-zot-0 + accessModes: ["ReadWriteOnce"] + storage: 20Gi + storageClassName: local-storage + +# local-storage does not support live migration — Recreate avoids attach conflicts +strategy: + type: Recreate \ No newline at end of file