diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..1b529cb --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,23 @@ +name: Lint Chart + +on: + pull_request: + +env: + CHART_NAME: garage + +jobs: + lint: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + - name: Set up Helm + uses: azure/setup-helm@v5 + with: + version: 4.2.3 + # admin.token is a required value; a dummy is passed so all templates + # render during linting instead of being skipped with a warning. + - name: Lint Helm chart + run: helm lint ${{ env.CHART_NAME }} --strict --set admin.token=ci-lint-only + shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a981f74 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release Package + +on: + workflow_dispatch: + inputs: + version: + description: "Version (eg: x.x.x)" + type: string + required: true + +env: + CHART_NAME: garage + OCI_IMAGE: oci://ghcr.io/${{ github.repository }} + +jobs: + build-and-publish: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + with: + token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} + - uses: aboutbits/github-actions-base/git-setup@v2 + - name: Set up Helm + uses: azure/setup-helm@v5 + with: + version: 4.2.3 + - name: Set version of Chart.yaml file + run: yq -i '.version = "${{ github.event.inputs.version }}"' ${{ env.CHART_NAME }}/Chart.yaml + shell: bash + - name: Package Helm chart + run: helm package ${{ env.CHART_NAME }} + shell: bash + - name: Push to OCI registry + run: | + helm registry login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} + helm push ${{ env.CHART_NAME }}-${{ github.event.inputs.version }}.tgz ${{ env.OCI_IMAGE }} + shell: bash + - uses: aboutbits/github-actions-base/git-commit-and-push-all@v2 + with: + message: '${{ github.event.inputs.version }}' + - uses: aboutbits/github-actions-base/git-create-or-update-tag@v2 + with: + tag-name: 'v${{ github.event.inputs.version }}' + - uses: aboutbits/github-actions-base/github-create-release@v2 + with: + tag-name: 'v${{ github.event.inputs.version }}' + release-notes-generation: 'true' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0da2a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +*.iml +*.tgz +.DS_Store 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..ea31583 --- /dev/null +++ b/garage/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: garage +description: AboutBits single-node Garage (S3-compatible object storage) Helm chart +type: application +version: "0.1.0" +appVersion: "v2.3.0" +keywords: + - garage + - s3 + - object-storage +home: https://garagehq.deuxfleurs.fr +sources: + - https://git.deuxfleurs.fr/Deuxfleurs/garage + - https://github.com/deuxfleurs-org/garage diff --git a/garage/templates/NOTES.txt b/garage/templates/NOTES.txt new file mode 100644 index 0000000..724d361 --- /dev/null +++ b/garage/templates/NOTES.txt @@ -0,0 +1,53 @@ +Garage single-node deployment "{{ include "garage.fullname" . }}" is starting up. + +{{- if .Values.bootstrap.enabled }} + +It bootstraps itself on first boot via `garage server --single-node --default-bucket` +(no manual layout/bucket setup needed). This is a one-time bootstrap: adding a +second bucket or rotating the default key later means calling the Admin API +directly, not editing values.yaml. + +Bucket: {{ .Values.bootstrap.defaultBucket }} +Access key: (value of bootstrap.accessKey in your values) +Secret key: (value of bootstrap.secretKey in your values) +{{- else }} + +No bucket/key is auto-created (bootstrap.enabled=false). Create buckets and +keys yourself via the Admin API or the `garage` CLI, e.g.: + kubectl -n {{ .Release.Namespace }} exec -it {{ include "garage.fullname" . }}-0 -- /garage layout assign -z dc1 -c 1G + kubectl -n {{ .Release.Namespace }} exec -it {{ include "garage.fullname" . }}-0 -- /garage layout apply --version 1 + kubectl -n {{ .Release.Namespace }} exec -it {{ include "garage.fullname" . }}-0 -- /garage bucket create + kubectl -n {{ .Release.Namespace }} exec -it {{ include "garage.fullname" . }}-0 -- /garage key create + kubectl -n {{ .Release.Namespace }} exec -it {{ include "garage.fullname" . }}-0 -- /garage bucket allow --read --write --owner --key +{{- end }} + +S3 endpoint (from inside the cluster): + http://{{ include "garage.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.s3.port }} + +Region: {{ .Values.s3.region }} + +{{- if .Values.ingress.enabled }} + +External S3 endpoint (via Ingress): +{{- range .Values.ingress.hosts }} + {{- $host := .host }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host }}{{ .path }} + {{- end }} +{{- end }} +{{- end }} +{{- if .Values.httpRoute.enabled }} + +External S3 endpoint (via Gateway API HTTPRoute): +{{- if .Values.httpRoute.hostnames }} +{{- range .Values.httpRoute.hostnames }} + https://{{ . }}/ +{{- end }} +{{- else }} + (matches all hostnames on the referenced Gateway) +{{- end }} +{{- end }} + +Check status: + kubectl -n {{ .Release.Namespace }} exec -it {{ include "garage.fullname" . }}-0 -- /garage status + kubectl -n {{ .Release.Namespace }} exec -it {{ include "garage.fullname" . }}-0 -- /garage bucket list diff --git a/garage/templates/_helpers.tpl b/garage/templates/_helpers.tpl new file mode 100644 index 0000000..be4ed0e --- /dev/null +++ b/garage/templates/_helpers.tpl @@ -0,0 +1,82 @@ +{{/* +Chart name and fullname +*/}} +{{- define "garage.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- 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 }} + +{{- define "garage.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- 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 }} +{{- end }} + +{{- define "garage.selectorLabels" -}} +app.kubernetes.io/name: {{ include "garage.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Name of the ServiceAccount to use. +*/}} +{{- define "garage.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- $baseName := .Values.serviceAccount.name | default (printf "sa-%s" (include "garage.fullname" .)) -}} +{{- $baseName | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Names for the ingress / HTTPRoute objects (prefixed for readability, overridable +via .Values..name). These name the routing object only — the backend still +targets the Service (garage.fullname). +*/}} +{{- define "garage.ingressName" -}} +{{- $baseName := .Values.ingress.name | default (printf "ingress-%s" (include "garage.fullname" .)) -}} +{{- $baseName | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "garage.httpRouteName" -}} +{{- $baseName := .Values.httpRoute.name | default (printf "httproute-%s" (include "garage.fullname" .)) -}} +{{- $baseName | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Auto-generated RPC secret (inter-node clustering auth; loopback-only on a single +node, so it is never consumed externally). `lookup` reuses the previously stored +value on `helm upgrade` to avoid a needless Secret diff — this is churn +avoidance, not a correctness requirement: the node's identity and cluster layout +live in the metadata volume, independent of this secret, so regenerating it does +not lose data or orphan the layout. The admin_token, by contrast, is a required, +operator-provided value (see admin.token) so it is known up front. +*/}} +{{- define "garage.rpcSecret" -}} +{{- $existing := (lookup "v1" "Secret" .Release.Namespace (include "garage.fullname" .)) -}} +{{- if $existing -}} +{{- index $existing.data "rpc_secret" | b64dec -}} +{{- else -}} +{{- sha256sum (printf "%s-rpc-%s" .Release.Name (randAlphaNum 32)) -}} +{{- end -}} +{{- end }} diff --git a/garage/templates/httproute.yaml b/garage/templates/httproute.yaml new file mode 100644 index 0000000..b82b28f --- /dev/null +++ b/garage/templates/httproute.yaml @@ -0,0 +1,35 @@ +{{- if and .Values.ingress.enabled .Values.httpRoute.enabled -}} +{{- fail "Enable only one of ingress.enabled or httpRoute.enabled, not both" -}} +{{- end -}} +{{- if .Values.httpRoute.enabled -}} +{{- if not .Values.httpRoute.parentRefs -}} +{{- fail "httpRoute.parentRefs is required when httpRoute.enabled=true" -}} +{{- end -}} +{{- $fullName := include "garage.fullname" . -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "garage.httpRouteName" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.httpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- toYaml .Values.httpRoute.parentRefs | nindent 4 }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + - matches: + {{- toYaml .Values.httpRoute.matches | nindent 8 }} + backendRefs: + - name: {{ $fullName }} + port: {{ .Values.service.s3.port }} +{{- end }} diff --git a/garage/templates/ingress.yaml b/garage/templates/ingress.yaml new file mode 100644 index 0000000..7a77d86 --- /dev/null +++ b/garage/templates/ingress.yaml @@ -0,0 +1,46 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "garage.fullname" . -}} +{{- $svcPort := .Values.service.s3.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "garage.ingressName" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} + {{- with .Values.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/garage/templates/secret.yaml b/garage/templates/secret.yaml new file mode 100644 index 0000000..17b032b --- /dev/null +++ b/garage/templates/secret.yaml @@ -0,0 +1,50 @@ +{{- $rpcSecret := include "garage.rpcSecret" . -}} +{{- $adminToken := required "admin.token is required" .Values.admin.token -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "garage.fullname" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} +type: Opaque +stringData: + rpc_secret: {{ $rpcSecret | quote }} + admin_token: {{ $adminToken | quote }} + {{- if .Values.bootstrap.enabled }} + default_access_key: {{ required "bootstrap.accessKey is required when bootstrap.enabled=true (e.g. GK + 16 hex chars)" .Values.bootstrap.accessKey | quote }} + default_secret_key: {{ required "bootstrap.secretKey is required when bootstrap.enabled=true" .Values.bootstrap.secretKey | quote }} + {{- end }} + garage.toml: | + metadata_dir = "/var/lib/garage/meta" + data_dir = "/var/lib/garage/data" + db_engine = {{ .Values.garage.dbEngine | quote }} + block_size = {{ .Values.garage.blockSize | quote }} + {{- if eq (toString .Values.garage.compressionLevel) "none" }} + compression_level = "none" + {{- else }} + compression_level = {{ .Values.garage.compressionLevel | int }} + {{- end }} + + replication_factor = 1 + + {{- if .Values.garage.metadataAutoSnapshotInterval }} + metadata_auto_snapshot_interval = {{ .Values.garage.metadataAutoSnapshotInterval | quote }} + {{- end }} + + rpc_bind_addr = "[::]:3901" + rpc_public_addr = "127.0.0.1:3901" + rpc_secret = {{ $rpcSecret | quote }} + + {{- if .Values.garage.additionalTopLevelConfig }} + {{ .Values.garage.additionalTopLevelConfig | nindent 4 }} + {{- end }} + + [s3_api] + s3_region = {{ .Values.s3.region | quote }} + api_bind_addr = "[::]:{{ .Values.service.s3.port }}" + root_domain = {{ .Values.s3.rootDomain | quote }} + + + [admin] + api_bind_addr = "[::]:{{ .Values.admin.port }}" + admin_token = {{ $adminToken | quote }} diff --git a/garage/templates/service.yaml b/garage/templates/service.yaml new file mode 100644 index 0000000..03c070e --- /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: + # Not headless: single-node Garage has no peers to discover via pod DNS, so + # this plain ClusterIP Service is used both as the StatefulSet's + # `serviceName` and as the address clients connect to for the S3 API. + type: {{ .Values.service.type }} + selector: + {{- include "garage.selectorLabels" . | nindent 4 }} + ports: + - name: s3 + port: {{ .Values.service.s3.port }} + targetPort: s3 + - name: admin + port: {{ .Values.admin.port }} + targetPort: admin +{{- 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: + - name: metrics + port: {{ .Values.admin.port }} + targetPort: admin + 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..0879449 --- /dev/null +++ b/garage/templates/servicemonitor.yaml @@ -0,0 +1,39 @@ +{{- if and .Values.monitoring.metrics.enabled .Values.monitoring.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "garage.fullname" . }} + namespace: {{ .Release.Namespace }} + 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 8 }} + {{- end }} + {{- with .Values.monitoring.metrics.serviceMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 8 }} + {{- end }} + jobLabel: "{{ .Release.Name }}" + selector: + matchLabels: + {{- include "garage.selectorLabels" . | nindent 6 }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} +{{- end }} diff --git a/garage/templates/statefulset.yaml b/garage/templates/statefulset.yaml new file mode 100644 index 0000000..dcd1239 --- /dev/null +++ b/garage/templates/statefulset.yaml @@ -0,0 +1,131 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "garage.fullname" . }} + labels: + {{- include "garage.labels" . | nindent 4 }} +spec: + serviceName: {{ include "garage.fullname" . }} + replicas: 1 + podManagementPolicy: OrderedReady + selector: + matchLabels: + {{- include "garage.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "garage.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "garage.serviceAccountName" . }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: garage + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /garage + - server + - --single-node + {{- if .Values.bootstrap.enabled }} + - --default-bucket + {{- end }} + {{- if or .Values.bootstrap.enabled .Values.environment }} + env: + {{- if .Values.bootstrap.enabled }} + - name: GARAGE_DEFAULT_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "garage.fullname" . }} + key: default_access_key + - name: GARAGE_DEFAULT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "garage.fullname" . }} + key: default_secret_key + - name: GARAGE_DEFAULT_BUCKET + value: {{ .Values.bootstrap.defaultBucket | quote }} + {{- end }} + {{- with .Values.environment }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + ports: + - name: s3 + containerPort: {{ .Values.service.s3.port }} + - name: rpc + containerPort: 3901 + - name: admin + containerPort: {{ .Values.admin.port }} + volumeMounts: + - name: config + mountPath: /etc/garage.toml + subPath: garage.toml + readOnly: true + - name: meta + mountPath: /var/lib/garage/meta + - name: data + mountPath: /var/lib/garage/data + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + livenessProbe: + {{- toYaml .Values.probes.liveness | nindent 12 }} + readinessProbe: + {{- toYaml .Values.probes.readiness | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: config + secret: + secretName: {{ include "garage.fullname" . }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumeClaimTemplates: + - metadata: + name: meta + spec: + accessModes: + {{- toYaml .Values.persistence.meta.accessModes | nindent 10 }} + storageClassName: {{ .Values.persistence.meta.storageClassName }} + resources: + requests: + storage: {{ .Values.persistence.meta.size }} + - metadata: + name: data + spec: + accessModes: + {{- toYaml .Values.persistence.data.accessModes | nindent 10 }} + storageClassName: {{ .Values.persistence.data.storageClassName }} + resources: + requests: + storage: {{ .Values.persistence.data.size }} diff --git a/garage/values.reference.yaml b/garage/values.reference.yaml new file mode 100644 index 0000000..3c79bbc --- /dev/null +++ b/garage/values.reference.yaml @@ -0,0 +1,261 @@ +# Reference values for the garage chart. +# +# This chart deploys a single-node Garage (S3-compatible object store): one +# StatefulSet replica, replication_factor=1, no node discovery/clustering. It +# keeps the upstream chart's security hardening and optional Prometheus +# monitoring, and adds an optional first-boot bucket + access key bootstrap. + +# -- Override the name of the chart +nameOverride: "" +# -- Override the full name of the chart +fullnameOverride: "" + +# -- Container image configuration +image: + # -- Repository of the container image + repository: dxflrs/garage + # -- Tag of the container image + tag: v2.3.0 + # -- Image pull policy. Accepted values: Always, Never, IfNotPresent + pullPolicy: IfNotPresent + +# -- Image pull secrets for private registries +imagePullSecrets: [] + +# -- Garage configuration. These values are rendered into garage.toml. +garage: + # -- Metadata database engine. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine + dbEngine: lmdb + # -- Block size in bytes (default 1MB). https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#block_size + blockSize: "1048576" + # -- zstd compression level of stored blocks (integer 1-19), or "none" to + # disable. https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#compression_level + compressionLevel: "1" + # -- If set, Garage snapshots the metadata DB at this interval into the metadata dir. + metadataAutoSnapshotInterval: "" + # -- Additional lines appended verbatim to garage.toml. + additionalTopLevelConfig: "" + +# -- S3 API settings. Garage only speaks plain HTTP; terminate TLS at the +# ingress/gateway if exposing it outside the cluster. +s3: + # -- S3 region name Garage responds to. Arbitrary, but the client must sign + # with the same value (e.g. AWS_DEFAULT_REGION=garage) or requests fail with + # AuthorizationHeaderMalformed. Change only if a tool hardcodes a region. + region: garage + # -- Domain suffix (with leading dot) for virtual-hosted-style bucket access + # (.). Only relevant when exposing Garage externally with + # bucket-name-in-hostname URLs; requires wildcard DNS and a matching ingress + # host. Unused for in-cluster / path-style clients (AWS_S3_FORCE_PATH_STYLE=true). + rootDomain: ".s3.garage" + +# -- Admin API: bucket/key management, health checks, and Prometheus metrics. +# Always enabled — the /health probes and monitoring below depend on it. +# Kept ClusterIP-only; never exposed via ingress. +admin: + # -- Port the admin API listens on inside the container. + port: 3903 + # -- Bearer token protecting the admin API. Required. + # Set your own (any sufficiently random string) so it is known up front for + # managing buckets/keys via the admin API. Prefer --set / Terraform over + # committing it to git. + token: "" + +# -- Optional bootstrap of a single default bucket + access key on first boot, +# via `garage server --default-bucket`. Disabled by default: most deployments +# provision buckets/keys out-of-band (Admin API, Terraform, ...). This is a +# one-time first-boot action; later buckets/keys or key rotation go through the +# Admin API, not values. +bootstrap: + # -- Enable first-boot bucket + key creation. + enabled: false + # -- Name of the single bucket auto-created on first boot. + defaultBucket: default-bucket + # -- Access key ID for the default bucket. Required when bootstrap.enabled=true. + # Must start with "GK" by Garage convention (GK + 16 hex chars). + accessKey: "" + # -- Secret access key for the default bucket. Required when bootstrap.enabled=true. + secretKey: "" + +# -- Service configuration (ClusterIP by default; this is the in-cluster S3 endpoint). +service: + # -- Kubernetes Service type. + type: ClusterIP + s3: + # -- Port the S3 API is exposed on. + port: 3900 + # -- Extra annotations for the Service. + annotations: {} + +# -- Classic Ingress for the S3 API (networking.k8s.io/v1; requires an ingress +# controller). Only needed to reach Garage from OUTSIDE the cluster. In-cluster +# clients use the Service directly. Enable AT MOST ONE of ingress / httpRoute. +ingress: + # -- Enable the Ingress. + enabled: false + # -- Name of the Ingress object. Defaults to ingress- when empty. + name: "" + # -- Ingress class name. + className: "" + # -- Ingress annotations. + annotations: {} + # cert-manager.io/cluster-issuer: letsencrypt + # -- Extra labels for the Ingress. + labels: {} + # -- Ingress hosts configuration. + hosts: + - host: s3.example.com + paths: + - path: / + pathType: ImplementationSpecific + # -- TLS configuration. + tls: [] + # - secretName: garage-tls + # hosts: + # - s3.example.com + +# -- Gateway API HTTPRoute for the S3 API (gateway.networking.k8s.io/v1) — the +# modern alternative to ingress. Attaches to an existing Gateway via parentRefs +# (the Gateway itself is shared infra managed outside this chart). Requires the +# Gateway API CRDs installed. Enable AT MOST ONE of ingress / httpRoute. +httpRoute: + # -- Enable the HTTPRoute. + enabled: false + # -- Name of the HTTPRoute object. Defaults to httproute- when empty. + name: "" + # -- Gateway(s) this route attaches to. Required when httpRoute.enabled=true. + parentRefs: [] + # - name: my-gateway + # namespace: gateway-system + # sectionName: https + # -- Hostnames this route matches. Empty = match all hostnames on the Gateway. + hostnames: [] + # - s3.example.com + # -- HTTPRoute annotations. + annotations: {} + # -- Extra labels for the HTTPRoute. + labels: {} + # -- Path matches for the route. + matches: + - path: + type: PathPrefix + value: / + +# -- Persistent storage. Single-node with replication_factor=1: there is no +# in-cluster replica, so durability comes from the underlying volumes and their +# backups. Make sure both the meta and data volumes are covered by the node's +# backup/snapshot schedule (e.g. daily disk snapshots). +persistence: + # -- Metadata volume (LMDB database, indexes). + meta: + storageClassName: local-path + size: 2Gi + accessModes: + - ReadWriteOnce + # -- Data volume (object blocks). + data: + storageClassName: local-path + size: 20Gi + accessModes: + - ReadWriteOnce + +# -- Resource requests and limits. Starting point for a small single-node +# deployment; tune to your workload. LMDB benefits from memory for its page +# cache. No CPU limit by default to avoid throttling under bursty S3 traffic. +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + +# -- Node selector for pod scheduling. +nodeSelector: {} +# -- Tolerations for pod scheduling. +tolerations: [] +# -- Affinity rules for pod scheduling. +affinity: {} + +# -- Optional priority class name to assign to the pod. +priorityClassName: "" + +# -- Extra annotations for the pod. +podAnnotations: {} +# -- Extra labels for the pod. +podLabels: {} + +# -- Pod-level security context. +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + +# -- Container-level hardening. If a custom image needs to write outside the +# mounted meta/data dirs, add a writable volume via extraVolumes/extraVolumeMounts +# rather than loosening this. +securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + +# -- Liveness/readiness probes. Default to the admin API's /health endpoint +# (200 only when the node can serve requests), served by the admin API. +# If you disable the admin API, switch these to a tcpSocket check on the `s3` port. +probes: + liveness: + httpGet: + path: /health + port: admin + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 6 + readiness: + httpGet: + path: /health + port: admin + initialDelaySeconds: 5 + periodSeconds: 5 + +# -- Extra environment variables for the garage container. +environment: {} + +# -- Extra volumes, e.g. for custom TLS trust or a writable scratch dir. +extraVolumes: [] +# -- Extra volume mounts matching extraVolumes. +extraVolumeMounts: [] + +# -- Service account configuration. Not required for single-node operation (no +# Kubernetes API access needed); off by default. +serviceAccount: + # -- Create a ServiceAccount for the pod. + create: false + # -- Annotations for the ServiceAccount. + annotations: {} + # -- Name of the ServiceAccount (generated from the fullname template if empty). + name: "" + +# -- Prometheus monitoring (scrapes the admin API port). +monitoring: + metrics: + # -- Expose the admin API's Prometheus metrics on a separate headless Service. + enabled: false + serviceMonitor: + # -- Create a ServiceMonitor CRD for a prometheus-operator setup. + enabled: false + # -- Metrics path to scrape. + path: /metrics + # -- Extra labels for the ServiceMonitor. + labels: {} + # -- Scrape interval. + interval: 15s + # -- Scrape scheme. + scheme: http + # -- TLS config for scraping. + tlsConfig: {} + # -- Scrape timeout. + scrapeTimeout: 10s + # -- Metric relabeling rules. + relabelings: [] diff --git a/garage/values.yaml b/garage/values.yaml new file mode 100644 index 0000000..64b3ec7 --- /dev/null +++ b/garage/values.yaml @@ -0,0 +1,140 @@ +nameOverride: "" +fullnameOverride: "" + +image: + repository: dxflrs/garage + tag: v2.3.0 + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +garage: + dbEngine: lmdb + blockSize: "1048576" + compressionLevel: "1" + metadataAutoSnapshotInterval: "" + additionalTopLevelConfig: "" + +s3: + region: garage + rootDomain: ".s3.garage" + +admin: + port: 3903 + token: "" + +bootstrap: + enabled: false + defaultBucket: default-bucket + accessKey: "" + secretKey: "" + +service: + type: ClusterIP + s3: + port: 3900 + annotations: {} + +ingress: + enabled: false + name: "" + className: "" + annotations: {} + labels: {} + hosts: + - host: s3.example.com + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + +httpRoute: + enabled: false + name: "" + parentRefs: [] + hostnames: [] + annotations: {} + labels: {} + matches: + - path: + type: PathPrefix + value: / + +persistence: + meta: + storageClassName: local-path + size: 2Gi + accessModes: + - ReadWriteOnce + data: + storageClassName: local-path + size: 20Gi + accessModes: + - ReadWriteOnce + +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + +nodeSelector: {} +tolerations: [] +affinity: {} + +priorityClassName: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + +securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + +probes: + liveness: + httpGet: + path: /health + port: admin + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 6 + readiness: + httpGet: + path: /health + port: admin + initialDelaySeconds: 5 + periodSeconds: 5 + +environment: {} + +extraVolumes: [] +extraVolumeMounts: [] + +serviceAccount: + create: false + annotations: {} + name: "" + +monitoring: + metrics: + enabled: false + serviceMonitor: + enabled: false + path: /metrics + labels: {} + interval: 15s + scheme: http + tlsConfig: {} + scrapeTimeout: 10s + relabelings: [] diff --git a/license.md b/license.md new file mode 100644 index 0000000..21586f5 --- /dev/null +++ b/license.md @@ -0,0 +1,7 @@ +Copyright About Bits GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/readme.md b/readme.md index 2ced57e..56721ad 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,43 @@ # Garage S3 Helm Chart + +Helm chart to deploy a single-node [Garage](https://garagehq.deuxfleurs.fr) +(S3-compatible object storage) to Kubernetes. It runs one node with +`replication_factor=1` (no clustering/node discovery), keeps upstream security +hardening and optional Prometheus monitoring, and can optionally bootstrap a +bucket + access key on first boot. + +## Configuration + +The default configuration for this Helm chart is defined in the [values.yaml](./garage/values.yaml) file. +For detailed descriptions of all the configurable parameters, refer to the provided [values.reference.yaml](./garage/values.reference.yaml) file. +This file serves as the reference for all available settings and their default values. Do not use this for deployment. + +## Manual Installation + +You can pull and install the Helm chart directly from the OCI registry. + +### Install Using OCI Registry + +```bash +helm install oci://ghcr.io/aboutbits/helm-garage/garage --version --values values-.yaml --namespace +``` + +## Build & Publish + +To build and publish the chart, visit the GitHub Actions page of the repository and trigger the workflow "Release Package" manually. + +## Information + +About Bits is a company based in South Tyrol, Italy. You can find more information about us on [our website](https://aboutbits.it). + +### Support + +For support, please contact [info@aboutbits.it](mailto:info@aboutbits.it). + +### Credits + +- [All Contributors](../../contributors) + +### License + +The MIT License (MIT). Please see the [license file](license.md) for more information.