diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ded70a..b9e4c5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: runs-on: ubuntu-latest outputs: rust: ${{ steps.filter.outputs.rust }} + helm: ${{ steps.filter.outputs.helm }} steps: - uses: actions/checkout@v7 - uses: dorny/paths-filter@v4 @@ -28,6 +29,9 @@ jobs: - 'Cargo.lock' - 'deny.toml' - '.github/workflows/ci.yml' + helm: + - 'chart/**' + - '.github/workflows/ci.yml' lint: name: Lint @@ -87,6 +91,44 @@ jobs: - name: Check dependencies run: cargo deny check + helm: + name: Lint and validate Helm chart + needs: changes + if: needs.changes.outputs.helm == 'true' + runs-on: ubuntu-latest + env: + # kubeconform release used to validate rendered manifests against the + # Kubernetes schemas. + KUBECONFORM_VERSION: v0.6.7 + steps: + - uses: actions/checkout@v7 + - uses: azure/setup-helm@v5 + - name: Install kubeconform + run: | + curl -sSL "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" \ + | tar -xz -C /usr/local/bin kubeconform + - name: Lint, render, and validate the chart + shell: bash + run: | + set -euo pipefail + helm lint chart + # Render under a few value combinations so the conditional templates + # (Ingress, emptyDir, ServiceMonitor, CA trust, auth secrets) are all + # exercised, then validate each render against the Kubernetes schemas. + # -strict rejects unknown fields; -ignore-missing-schemas skips CRDs + # (the ServiceMonitor). + for args in \ + "" \ + "--set ingress.enabled=true" \ + "--set persistence.enabled=false" \ + "--set serviceMonitor.enabled=true" \ + "--set caTrust.enabled=true,caTrust.configMapName=ca" \ + "--set upstreamAuth.existingSecret=up,serveToken.existingSecret=srv"; do + echo "-- helm template chart $args" + helm template release chart $args \ + | kubeconform -strict -summary -ignore-missing-schemas + done + # Spelling runs on everything, including docs. typos: name: Typos diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ee7fea..6c1f6b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,9 @@ env: # instead of drifting (the hardcoded name once lagged a rename and published under # the old name). `github.repository` is `owner/repo`, lowercase as GHCR requires. IMAGE: ghcr.io/${{ github.repository }} + # OCI namespace for the Helm chart, shared across this owner's charts. The chart is + # pushed as `/charts/git-cache-proxy`. + CHART_REPO: oci://ghcr.io/${{ github.repository_owner }}/charts jobs: release: @@ -142,3 +145,30 @@ jobs: run: cargo publish --locked env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + + # Package and push the Helm chart to GHCR as an OCI artifact, in the same run that + # cut the release, at the tag knope just created. The published GHCR package starts + # private; make it public once to allow anonymous `helm pull`. + publish-chart: + name: Publish chart + needs: release + if: needs.release.outputs.released == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + with: + ref: v${{ needs.release.outputs.version }} + - uses: azure/setup-helm@v5 + # Version the chart in lockstep with the app: package at the released version + # (overriding the static Chart.yaml version) with the matching appVersion, so a + # pulled chart deploys the image it was cut with. + - name: Package and push the chart + run: | + helm package chart -d dist \ + --version "${{ needs.release.outputs.version }}" \ + --app-version "${{ needs.release.outputs.version }}" + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin + helm push dist/git-cache-proxy-*.tgz "${{ env.CHART_REPO }}" diff --git a/README.md b/README.md index d5143aa..66c7a0b 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,17 @@ rather than a fully distroless/`FROM scratch` image. Removing that dependency (and enabling a git-free image) means moving the git plumbing in-process to a Rust library - see the roadmap below. +On Kubernetes, a Helm chart lives in [`chart/`](./chart) (single-writer +Deployment, `/healthz`+`/readyz` probes, cache PVC, optional Ingress and +Prometheus `ServiceMonitor`): + +```shell +helm install git-cache-proxy oci://ghcr.io/rolandjitsu/charts/git-cache-proxy \ + --set upstream=https://your-git-host.example.com +``` + +See the [chart README](./chart/README.md) for the full values reference. + ## Status / scope Working and end-to-end tested against both Git wire protocol versions — the @@ -237,8 +248,6 @@ rely on it. Not yet implemented, in rough priority order: -- A Helm chart for Kubernetes deployment (liveness/readiness probes, - single-writer RWO PVC, metrics scrape), shipped in-repo. - A background/scheduled refresh option (today every `info/refs` triggers an on-demand, TTL-coalesced fetch). - No external `git` binary: move the plumbing in-process to a Rust library diff --git a/chart/.helmignore b/chart/.helmignore new file mode 100644 index 0000000..6c1a0fb --- /dev/null +++ b/chart/.helmignore @@ -0,0 +1,6 @@ +.DS_Store +.git/ +.gitignore +*.tmproj +*.orig +*.bak diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..34d2f39 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: git-cache-proxy +description: Read-only caching proxy for Git - serves clones/fetches from an in-region mirror, pulling only deltas from upstream. +type: application +# Chart version. Overridden at release time with the app version (see +# release.yml); this static value is for installs straight from the git source. +version: 0.1.0 +# The git-cache-proxy release this chart deploys by default - the image tag used +# when image.tag is empty. Set to the released version at publish time. +appVersion: "0.1.8" +home: https://github.com/rolandjitsu/git-cache-proxy +sources: + - https://github.com/rolandjitsu/git-cache-proxy +keywords: + - git + - cache + - proxy + - ci + - mirror +maintainers: + - name: Roland Groza + url: https://github.com/rolandjitsu diff --git a/chart/README.md b/chart/README.md new file mode 100644 index 0000000..83b3ed4 --- /dev/null +++ b/chart/README.md @@ -0,0 +1,124 @@ +# git-cache-proxy + +A Helm chart that deploys [git-cache-proxy](https://github.com/rolandjitsu/git-cache-proxy), +a read-only caching proxy for Git. It sits between CI machines and an origin git server, +keeps a local bare mirror fresh (incremental pull from upstream), and serves clones/fetches +from that mirror - so the bulk history is served in-region and only the delta crosses the +WAN. It is strictly pull-only: it never pushes and never proactively replicates. + +## Install + +```sh +helm install git-cache-proxy oci://ghcr.io/rolandjitsu/charts/git-cache-proxy \ + --set upstream=https://your-git-host.example.com +``` + +Pin a version with `--version`, and override defaults with `-f my-values.yaml` or `--set`. +See [Configuration](#configuration). + +Clients then clone through the Service as if it were the origin: + +```sh +git clone http://git-cache-proxy..svc:8080//.git +``` + +## Upstream and auth + +`upstream` is the origin base URL (default `https://github.com`); requested repo paths are +appended to it. To cache private repos, give the proxy a read-only upstream credential as a +Secret holding the full HTTP `Authorization` header, and reference it: + +```sh +kubectl create secret generic gcp-upstream \ + --from-literal=auth-header="Authorization: Bearer $TOKEN" + +helm install git-cache-proxy ... \ + --set upstreamAuth.existingSecret=gcp-upstream +``` + +The header is injected via an env var, so the token never appears in the process argv. For +GitLab, a personal access token goes in as HTTP Basic: the header value is +`Authorization: Basic `. + +## Serving auth + +By default the proxy serves anonymously, which is intended for a network-restricted +deployment. Anyone who can reach the port can read **every mirrored repo** (the proxy holds +one upstream credential). Restrict who can reach it (private network, NetworkPolicy, mTLS at +the ingress), and/or require a client bearer token via `serveToken.existingSecret`. The +proxy speaks plain HTTP, so terminate TLS in front of it on any untrusted network. + +## Single writer (not HA) + +The chart runs a single replica with the `Recreate` strategy. The bare mirrors live on one +`ReadWriteOnce` volume and concurrent fetches are already coalesced in-process, so a second +replica would only contend for the same PVC; `Recreate` ensures the PVC detaches from the +old pod before the new one attaches. `replicas` is therefore not exposed. An evicted or lost +mirror is transparently re-cloned on the next request, so an `emptyDir` +(`persistence.enabled=false`) is a valid choice for a pure accelerator. + +## Persistence and eviction + +With `persistence.enabled` (default), the chart creates a PVC of `persistence.size` on +`persistence.storageClassName` (empty = cluster default), or mounts +`persistence.existingClaim` if set. Bound the cache with `config.cacheMaxMb`: when the total +exceeds it, least-recently-used idle mirrors are evicted in the background until back under. +Leaving it `0` (unlimited) lets the volume grow until full, so set it whenever the volume is +bounded. + +## Metrics + +The proxy exposes Prometheus metrics at `/metrics` on the Service port: per-repo request and +upstream counters, cache-size gauges, and `*_duration_seconds` fetch/serve latency +histograms. Scrape it with pod annotations (`podAnnotations`) or, with the Prometheus +Operator, set `serviceMonitor.enabled=true`. + +## Ingress + +A `ClusterIP` Service is exposed by default; reach it in-cluster or port-forward it. Set +`ingress.enabled=true` for a standard `networking.k8s.io/v1` Ingress (configurable +`className`, `host`, `annotations`, `tls`). For a non-standard controller (e.g. a Traefik +`IngressRoute` CRD), leave the Ingress off and manage the route as a separate manifest +pointing at the Service. + +## Configuration + +| Key | Default | Description | +| --- | --- | --- | +| `image.repository` | `ghcr.io/rolandjitsu/git-cache-proxy` | Image; override for a fork/mirror | +| `image.tag` | `""` | Image tag; empty uses the chart `appVersion` | +| `image.pullPolicy` | `IfNotPresent` | | +| `imagePullSecrets` | `[]` | Pull secrets for a private registry | +| `nameOverride` / `fullnameOverride` | `""` | Override the generated names | +| `upstream` | `https://github.com` | Origin git base URL; repo paths are appended | +| `upstreamAuth.existingSecret` | `""` | Secret with the full upstream `Authorization` header (empty = anonymous) | +| `upstreamAuth.key` | `auth-header` | Key in that Secret | +| `serveToken.existingSecret` | `""` | Secret with a client bearer token to require (empty = anonymous) | +| `serveToken.key` | `token` | Key in that Secret | +| `config.fetchTtlSeconds` | `10` | Skip upstream fetch if refreshed within this window (`0` = always) | +| `config.cacheMaxMb` | `0` | Cap on on-disk cache, MiB; evicts LRU idle mirrors (`0` = unlimited) | +| `config.maxConcurrentRequests` | `64` | Max concurrent in-flight requests (`0` = unlimited) | +| `config.maxDecodedBodyMb` | `512` | Cap on a decoded upload-pack request body, MiB | +| `config.logLevel` | `info` | Log filter directive | +| `config.logFormat` | `text` | `text` or `json` | +| `persistence.enabled` | `true` | Mount a PVC for the cache; `false` uses an emptyDir | +| `persistence.existingClaim` | `""` | Use this PVC instead of creating one | +| `persistence.size` | `20Gi` | Created PVC size | +| `persistence.storageClassName` | `""` | StorageClass for the PVC; empty = cluster default | +| `persistence.accessModes` | `[ReadWriteOnce]` | PVC access modes | +| `persistence.emptyDirSizeLimit` | `20Gi` | emptyDir limit when persistence is off | +| `service.type` | `ClusterIP` | Service type | +| `service.port` | `8080` | Service port and the port the container binds | +| `resources` | `{}` | Pod resource requests/limits | +| `terminationGracePeriodSeconds` | `60` | Drain window for in-flight clones on shutdown | +| `podAnnotations` | `{}` | Extra pod annotations (e.g. a metrics scraper) | +| `podSecurityContext` / `securityContext` | `{}` | Pod/container security contexts (empty = image defaults) | +| `nodeSelector` / `tolerations` / `affinity` | `{}` / `[]` / `{}` | Scheduling | +| `caTrust.enabled` | `false` | Mount a private-CA bundle and point git at it | +| `caTrust.configMapName` / `caTrust.key` | `""` / `ca-certificates.crt` | Source ConfigMap and key | +| `serviceMonitor.enabled` | `false` | Render a Prometheus Operator ServiceMonitor | +| `serviceMonitor.interval` / `.scrapeTimeout` / `.labels` | `30s` / `10s` / `{}` | ServiceMonitor config | +| `ingress.enabled` | `false` | Render a standard Ingress | +| `ingress.className` / `.host` / `.path` / `.pathType` / `.annotations` / `.tls` | see `values.yaml` | Ingress config | + +Full defaults and inline comments: [`values.yaml`](./values.yaml). diff --git a/chart/templates/NOTES.txt b/chart/templates/NOTES.txt new file mode 100644 index 0000000..cdbf8d9 --- /dev/null +++ b/chart/templates/NOTES.txt @@ -0,0 +1,30 @@ +git-cache-proxy is running as {{ include "git-cache-proxy.fullname" . }}, caching +{{ .Values.upstream }}. + +Point CI clients at it in-cluster (it speaks git smart-HTTP, so clone/fetch as if +it were the origin): + + git clone http://{{ include "git-cache-proxy.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.service.port }}//.git + +{{ if .Values.ingress.enabled -}} +Externally, via the Ingress: + + http://{{ .Values.ingress.host }}{{ .Values.ingress.path }} +{{ else -}} +No Ingress is enabled. Reach it in-cluster (above), port-forward the Service, or +set ingress.enabled=true. +{{ end }} +Health and metrics: + + kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "git-cache-proxy.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} + # then: curl localhost:{{ .Values.service.port }}/readyz and /metrics + +{{ if not .Values.upstreamAuth.existingSecret -}} +No upstream auth is set, so only public repos on {{ .Values.upstream }} will work. +Set upstreamAuth.existingSecret to a Secret holding the full Authorization header +to cache private repos. +{{ end -}} +{{ if and (eq (int .Values.config.cacheMaxMb) 0) .Values.persistence.enabled -}} +config.cacheMaxMb is 0 (unlimited): the cache volume can grow until full. Set it +to bound on-disk usage with LRU eviction. +{{ end -}} diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000..5efd267 --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,24 @@ +{{/* +Chart name, overridable via nameOverride. +*/}} +{{- define "git-cache-proxy.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* +Fully qualified app name. Truncated at 63 chars for the Kubernetes name limit. +Used as the resource name and as the stable `app` selector label - do not change +its shape, since editing a Deployment selector is a breaking in-place upgrade. +*/}} +{{- define "git-cache-proxy.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 }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..27db5dc --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,142 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: '{{ include "git-cache-proxy.fullname" . }}' + labels: + app: '{{ include "git-cache-proxy.fullname" . }}' +spec: + # One writer by design: the bare mirrors sit on a single ReadWriteOnce volume + # and concurrent fetches are already coalesced in-process, so a second replica + # would only contend for the same PVC. Recreate (not RollingUpdate) so the PVC + # detaches from the old pod before the new one attaches. This is not HA. + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: '{{ include "git-cache-proxy.fullname" . }}' + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + app: '{{ include "git-cache-proxy.fullname" . }}' + spec: + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: git-cache-proxy + image: '{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}' + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + env: + - name: GITCACHEPROXY_BIND + value: '0.0.0.0:{{ .Values.service.port }}' + - name: GITCACHEPROXY_UPSTREAM + value: {{ .Values.upstream | quote }} + - name: GITCACHEPROXY_CACHE_ROOT + value: /var/cache/git-cache-proxy + - name: GITCACHEPROXY_FETCH_TTL_SECONDS + value: {{ .Values.config.fetchTtlSeconds | quote }} + - name: GITCACHEPROXY_CACHE_MAX_MB + value: {{ .Values.config.cacheMaxMb | quote }} + - name: GITCACHEPROXY_MAX_CONCURRENT_REQUESTS + value: {{ .Values.config.maxConcurrentRequests | quote }} + - name: GITCACHEPROXY_MAX_DECODED_BODY_MB + value: {{ .Values.config.maxDecodedBodyMb | quote }} + - name: GITCACHEPROXY_LOG + value: {{ .Values.config.logLevel | quote }} + - name: GITCACHEPROXY_LOG_FORMAT + value: {{ .Values.config.logFormat | quote }} + {{- if .Values.upstreamAuth.existingSecret }} + - name: GITCACHEPROXY_UPSTREAM_AUTH_HEADER + valueFrom: + secretKeyRef: + name: {{ .Values.upstreamAuth.existingSecret | quote }} + key: {{ .Values.upstreamAuth.key | quote }} + {{- end }} + {{- if .Values.serveToken.existingSecret }} + - name: GITCACHEPROXY_SERVE_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.serveToken.existingSecret | quote }} + key: {{ .Values.serveToken.key | quote }} + {{- end }} + {{- if .Values.caTrust.enabled }} + # Validate upstream TLS against the CA bundle mounted below. + - name: GIT_SSL_CAINFO + value: /etc/ssl/certs/ca-certificates.crt + {{- end }} + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + # Readiness checks the cache root is writable, so a detached or read-only + # volume surfaces as NotReady instead of a flood of upstream 502s. + readinessProbe: + httpGet: + path: /readyz + port: http + initialDelaySeconds: 5 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: cache + mountPath: /var/cache/git-cache-proxy + {{- if .Values.caTrust.enabled }} + - name: ca-trust + mountPath: /etc/ssl/certs/ca-certificates.crt + subPath: ca-certificates.crt + readOnly: true + {{- end }} + volumes: + - name: cache + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "git-cache-proxy.fullname" .) }} + {{- else }} + emptyDir: + sizeLimit: {{ .Values.persistence.emptyDirSizeLimit }} + {{- end }} + {{- if .Values.caTrust.enabled }} + - name: ca-trust + configMap: + name: {{ .Values.caTrust.configMapName | quote }} + items: + - key: {{ .Values.caTrust.key | quote }} + path: ca-certificates.crt + {{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 0000000..fbfe189 --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,31 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: '{{ include "git-cache-proxy.fullname" . }}' + labels: + app: '{{ include "git-cache-proxy.fullname" . }}' + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . | quote }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: {{ .Values.ingress.path }} + pathType: {{ .Values.ingress.pathType }} + backend: + service: + name: '{{ include "git-cache-proxy.fullname" . }}' + port: + number: {{ .Values.service.port }} +{{- end }} diff --git a/chart/templates/pvc.yaml b/chart/templates/pvc.yaml new file mode 100644 index 0000000..6761d10 --- /dev/null +++ b/chart/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: '{{ include "git-cache-proxy.fullname" . }}' + labels: + app: '{{ include "git-cache-proxy.fullname" . }}' +spec: + accessModes: + {{- toYaml .Values.persistence.accessModes | nindent 4 }} + {{- with .Values.persistence.storageClassName }} + storageClassName: {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000..0d9f92a --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: '{{ include "git-cache-proxy.fullname" . }}' + labels: + app: '{{ include "git-cache-proxy.fullname" . }}' +spec: + type: {{ .Values.service.type }} + selector: + app: '{{ include "git-cache-proxy.fullname" . }}' + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/chart/templates/servicemonitor.yaml b/chart/templates/servicemonitor.yaml new file mode 100644 index 0000000..cc0def5 --- /dev/null +++ b/chart/templates/servicemonitor.yaml @@ -0,0 +1,20 @@ +{{- if .Values.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: '{{ include "git-cache-proxy.fullname" . }}' + labels: + app: '{{ include "git-cache-proxy.fullname" . }}' + {{- with .Values.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + app: '{{ include "git-cache-proxy.fullname" . }}' + endpoints: + - port: http + path: /metrics + interval: {{ .Values.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} +{{- end }} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..04ac639 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,138 @@ +# git-cache-proxy container image. Defaults to the published image; override +# repository/tag to point at a fork or mirror. An empty tag uses the chart +# appVersion, so the chart and the image it deploys stay in lockstep. +image: + repository: ghcr.io/rolandjitsu/git-cache-proxy + tag: "" + pullPolicy: IfNotPresent +# Pull secrets for a private registry. Empty for the public default image. +imagePullSecrets: [] + +nameOverride: "" +fullnameOverride: "" + +# Upstream git server base URL. Requested repo paths are appended to it, so +# `https://github.com` serves `github.com//.git`. Point it at your +# own GitHub/GitLab/Gitea host to cache that. +upstream: https://github.com + +# Optional auth for upstream clone/fetch. The proxy reads a full HTTP header from +# GITCACHEPROXY_UPSTREAM_AUTH_HEADER (injected via env so the token never lands in +# argv). Supply it from a Secret holding the full header value, for example +# `Authorization: Bearer ` or `Authorization: Basic ` (GitLab: the +# base64 of `oauth2:`). Leave existingSecret empty to contact upstream +# anonymously - public repos only. +upstreamAuth: + existingSecret: "" + # Key in the Secret whose value is the full Authorization header line. + key: auth-header + +# Optional bearer token clients must present (Authorization: Bearer ), +# sourced from a Secret. Empty serves anonymously; only safe on a +# network-restricted deployment (the token grants access to every mirrored repo - +# see the chart README). +serveToken: + existingSecret: "" + key: token + +# Proxy configuration (each maps to a GITCACHEPROXY_* env var / flag). +config: + # Skip the upstream fetch on info/refs if the mirror was refreshed within this + # many seconds; coalesces bursts of clones for one repo. 0 = always fetch. + fetchTtlSeconds: 10 + # Cap on total on-disk mirror cache, in MiB; evicts least-recently-used idle + # mirrors when exceeded. 0 = unlimited (no eviction). Set this whenever the + # cache volume is bounded. + cacheMaxMb: 0 + # Max concurrent in-flight requests; excess queue. Bounds the upstream + # clone/fetch a burst can trigger. 0 = unlimited. + maxConcurrentRequests: 64 + # Cap on a decoded upload-pack request body, in MiB (bounds memory, defuses a + # gzip bomb). Caps only the negotiation request, never the streamed packfile. + maxDecodedBodyMb: 512 + # Log filter directive, e.g. `info` or `git_cache_proxy=debug,tower=warn`. + logLevel: info + # Log output: `text` (human) or `json` (for log shippers). + logFormat: text + +# On-disk cache holding the bare mirrors. +persistence: + # true mounts a PersistentVolumeClaim so mirrors survive restarts; false uses an + # emptyDir (mirrors are lost on restart, then transparently re-cloned - fine for + # a pure accelerator). + enabled: true + # Mount this existing PVC instead of creating one. Empty creates one named after + # the release. + existingClaim: "" + size: 20Gi + # StorageClass for the created PVC. Empty uses the cluster default class. + storageClassName: "" + # ReadWriteOnce matches the single-writer design (one replica, see the README). + accessModes: + - ReadWriteOnce + # Size limit for the emptyDir when persistence is disabled. + emptyDirSizeLimit: 20Gi + +service: + type: ClusterIP + # Serves both the git smart-HTTP endpoints and /healthz, /readyz, /metrics. Also + # the port the container binds to. + port: 8080 + +# Pod resource requests/limits. Empty by default; size to your repos and traffic. +resources: {} + # requests: + # cpu: 250m + # memory: 256Mi + # limits: + # memory: 1Gi + +# Seconds Kubernetes waits for in-flight clones/fetches to drain on shutdown. The +# binary handles SIGTERM (stops accepting new requests, finishes in-flight ones); +# a large clone can take a while, so keep this generous. +terminationGracePeriodSeconds: 60 + +# Extra annotations on the pod, e.g. to opt a plain Prometheus scraper into +# /metrics on the service port. (For the Prometheus Operator use serviceMonitor.) +podAnnotations: {} + +# Pod- and container-level security contexts. Empty uses the image defaults (it +# runs as root and owns the cache dir). To harden, set a non-root runAsUser and a +# matching podSecurityContext.fsGroup so the cache volume stays writable. +podSecurityContext: {} +securityContext: {} + +nodeSelector: {} +tolerations: [] +affinity: {} + +# Validate upstream TLS against a private CA. When enabled, mounts `key` from the +# named ConfigMap over the system CA bundle and points git at it. Off by default +# (public CAs). +caTrust: + enabled: false + configMapName: "" + key: ca-certificates.crt + +# Prometheus Operator ServiceMonitor for /metrics. Off by default; use +# podAnnotations for a plain scraper instead. +serviceMonitor: + enabled: false + interval: 30s + scrapeTimeout: 10s + # Extra labels, e.g. to match your Prometheus `serviceMonitorSelector`. + labels: {} + +# Optional standard networking.k8s.io/v1 Ingress in front of the Service. Off by +# default (reach the ClusterIP in-cluster or port-forward it). For a non-standard +# controller (e.g. a Traefik IngressRoute CRD), leave this off and manage the +# route as a separate manifest. +ingress: + enabled: false + className: "" + annotations: {} + host: git-cache.example.com + path: / + pathType: Prefix + # List of { secretName, hosts: [] } for TLS. + tls: []