diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..749ddaa --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,58 @@ +name: Publish Docker Image + +on: + push: + branches: [main] + tags: ["v*"] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + publish: + name: Build & push multi-arch image + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml new file mode 100644 index 0000000..3ed6d1f --- /dev/null +++ b/.github/workflows/helm.yml @@ -0,0 +1,46 @@ +name: Helm Chart + +on: + push: + branches: [main] + paths: ["deploy/helm/**"] + pull_request: + branches: [main] + paths: ["deploy/helm/**"] + +jobs: + lint: + name: Lint & template + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + + - uses: azure/setup-helm@v4 + + - name: helm lint + run: helm lint deploy/helm/gitmirrorcache --set config.objectStore.s3.bucket=ci-bucket + + - name: helm template (s3 defaults) + run: | + helm template git-cache deploy/helm/gitmirrorcache \ + --set config.objectStore.s3.bucket=ci-bucket \ + --set aws.region=us-west-2 \ + --set ingress.enabled=true + + - name: helm template (local store, no persistence) + run: | + helm template git-cache deploy/helm/gitmirrorcache \ + --set config.objectStore.kind=local \ + --set persistence.enabled=false \ + --set compaction.enabled=false + + - name: helm template (configFile set; CronJob must stay env-configured) + run: | + out=$(helm template git-cache deploy/helm/gitmirrorcache \ + --set config.objectStore.s3.bucket=ci-bucket \ + --set-string configFile='cache_root = "/cache"') + echo "$out" | grep -q 'name: GIT_CACHE_CONFIG' || { echo "server missing GIT_CACHE_CONFIG"; exit 1; } + cronjob=$(echo "$out" | awk '/^kind: CronJob$/{f=1} /^---$/{f=0} f') + echo "$cronjob" | grep -q 'name: GIT_CACHE_CONFIG' && { echo "CronJob must not set GIT_CACHE_CONFIG"; exit 1; } + echo "ok" diff --git a/deploy/helm/gitmirrorcache/Chart.yaml b/deploy/helm/gitmirrorcache/Chart.yaml new file mode 100644 index 0000000..42c1d7d --- /dev/null +++ b/deploy/helm/gitmirrorcache/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: gitmirrorcache +description: Read-through Git mirroring and caching service backed by S3-compatible object storage. +type: application +version: 0.1.0 +appVersion: "latest" +home: https://github.com/0lut/gitmirrorcache +sources: + - https://github.com/0lut/gitmirrorcache +keywords: + - git + - cache + - mirror + - s3 diff --git a/deploy/helm/gitmirrorcache/README.md b/deploy/helm/gitmirrorcache/README.md new file mode 100644 index 0000000..59811c1 --- /dev/null +++ b/deploy/helm/gitmirrorcache/README.md @@ -0,0 +1,91 @@ +# gitmirrorcache Helm Chart + +Deploys the gitmirrorcache read-through Git caching service to Kubernetes. + +The server runs as a StatefulSet with a persistent volume mounted at `/cache` +(the hot cache). An S3-compatible object store remains the durable source of +truth, so the cache volume is disposable: losing it only forces rehydration. +An hourly CronJob runs `git-cache compact --all`, mirroring the EventBridge +compaction rule from the AWS deployment. + +## Install + +```sh +helm install git-cache deploy/helm/gitmirrorcache \ + --set config.objectStore.s3.bucket=my-git-cache-bucket \ + --set aws.region=us-west-2 +``` + +## S3 credentials + +Prefer workload identity over static keys: + +```yaml +serviceAccount: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/gitmirrorcache +``` + +Or reference an existing Secret with `AWS_ACCESS_KEY_ID` / +`AWS_SECRET_ACCESS_KEY` keys: + +```yaml +aws: + region: us-west-2 + existingSecret: git-cache-aws +``` + +For S3-compatible stores (MinIO, Cloudflare R2, ...), set +`config.objectStore.s3.endpoint`. + +## Upstream authentication + +To warm and serve private repositories, point `upstreamAuth.existingSecret` +at a Secret containing a token (key `token` by default): + +```yaml +upstreamAuth: + existingSecret: git-cache-github-token +``` + +## Key values + +| Value | Default | Description | +| --- | --- | --- | +| `image.repository` | `ghcr.io/0lut/gitmirrorcache` | Container image | +| `config.objectStore.kind` | `s3` | `s3` or `local` (testing only) | +| `config.objectStore.s3.bucket` | – | Required for `s3` | +| `config.allowedUpstreamHosts` | `[github.com]` | Upstream allowlist | +| `config.gitRemote.enabled` | `true` | Serve `/git/{host}/{owner}/{repo}.git` | +| `config.disk.quotaBytes` | 100 GiB | Hot-cache disk quota | +| `persistence.size` | `100Gi` | PVC size (keep ≥ disk quota) | +| `persistence.enabled` | `true` | Use a PVC; `false` falls back to emptyDir | +| `compaction.enabled` | `true` | Hourly `git-cache compact --all` CronJob | +| `configFile` | `""` | Optional full TOML config (see `config/production.example.toml`) | +| `config.extraEnv` | `[]` | Extra `GIT_CACHE_*` env vars | + +See `values.yaml` for the full list. + +## Using the cache + +```sh +kubectl port-forward svc/git-cache-gitmirrorcache 8080:80 +curl http://localhost:8080/healthz +git clone http://localhost:8080/git/github.com//.git +``` + +## Sizing + +Serving clones spawns `git pack-objects` children, which are CPU- and +memory-intensive (bounded by `config.maxConcurrentGitProcesses`, default 64). +The chart defaults to requests of 2 vCPU / 4 GiB with an 8 GiB memory limit — +treat that as the floor. The production ECS deployment runs 8 vCPU / 24 GiB; +scale CPU with concurrent clone traffic and tune +`config.maxConcurrentGitProcesses` to match the allocation. + +## Scaling + +Each replica keeps its own hot cache on its own PVC. Replicas coordinate only +through the object store; compaction/publish uses conditional PUTs on the +generation head, so multiple replicas and the compaction CronJob are safe to +run concurrently. diff --git a/deploy/helm/gitmirrorcache/templates/NOTES.txt b/deploy/helm/gitmirrorcache/templates/NOTES.txt new file mode 100644 index 0000000..e6c38d1 --- /dev/null +++ b/deploy/helm/gitmirrorcache/templates/NOTES.txt @@ -0,0 +1,20 @@ +gitmirrorcache has been deployed. + +Service endpoint (in-cluster): + http://{{ include "gitmirrorcache.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.service.port }} + +Quick checks: + kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "gitmirrorcache.fullname" . }} 8080:{{ .Values.service.port }} + curl http://localhost:8080/healthz + +{{- if .Values.config.gitRemote.enabled }} + +Clone through the cache: + git clone http://localhost:8080/git/github.com//.git +{{- end }} +{{- if and (eq .Values.config.objectStore.kind "s3") (not .Values.aws.existingSecret) }} + +NOTE: objectStore.kind is "s3" and no aws.existingSecret was provided. +Make sure pods can reach S3 via IRSA/pod identity (serviceAccount.annotations) +or node instance roles. +{{- end }} diff --git a/deploy/helm/gitmirrorcache/templates/_helpers.tpl b/deploy/helm/gitmirrorcache/templates/_helpers.tpl new file mode 100644 index 0000000..b9734fd --- /dev/null +++ b/deploy/helm/gitmirrorcache/templates/_helpers.tpl @@ -0,0 +1,155 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "gitmirrorcache.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "gitmirrorcache.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 }} + +{{/* +Chart label. +*/}} +{{- define "gitmirrorcache.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels. +*/}} +{{- define "gitmirrorcache.labels" -}} +helm.sh/chart: {{ include "gitmirrorcache.chart" . }} +{{ include "gitmirrorcache.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels. +*/}} +{{- define "gitmirrorcache.selectorLabels" -}} +app.kubernetes.io/name: {{ include "gitmirrorcache.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Service account name. +*/}} +{{- define "gitmirrorcache.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "gitmirrorcache.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Image reference. +*/}} +{{- define "gitmirrorcache.image" -}} +{{- printf "%s:%s" .Values.image.repository (default .Chart.AppVersion .Values.image.tag) }} +{{- end }} + +{{/* +Shared GIT_CACHE_* environment variables used by both the server and the +compaction CronJob. Takes a dict: "ctx" (the root context) and an optional +"cacheRoot" override so the CronJob can use a scratch directory. GIT_CACHE_CONFIG +is deliberately not set here: when it is set the application reads the entire +config from the TOML file and ignores env vars, so only the server (which +mounts the ConfigMap) opts into it. +*/}} +{{- define "gitmirrorcache.env" -}} +{{- $ctx := .ctx -}} +{{- $cacheRoot := default $ctx.Values.config.cacheRoot .cacheRoot -}} +{{- with $ctx }} +- name: GIT_CACHE_BIND_ADDR + value: {{ .Values.config.bindAddr | quote }} +- name: GIT_CACHE_ROOT + value: {{ $cacheRoot | quote }} +- name: GIT_CACHE_GIT_BINARY + value: {{ .Values.config.gitBinary | quote }} +- name: GIT_CACHE_GIT_TIMEOUT_SECONDS + value: {{ .Values.config.gitTimeoutSeconds | int64 | quote }} +- name: GIT_CACHE_MAX_GIT_OUTPUT_BYTES + value: {{ .Values.config.maxGitOutputBytes | int64 | quote }} +- name: GIT_CACHE_RATE_LIMIT_PER_MINUTE + value: {{ .Values.config.rateLimitPerMinute | int64 | quote }} +- name: GIT_CACHE_ALLOWED_UPSTREAM_HOSTS + value: {{ join "," .Values.config.allowedUpstreamHosts | quote }} +- name: GIT_CACHE_MAX_CONCURRENT_GIT_PROCESSES + value: {{ .Values.config.maxConcurrentGitProcesses | int64 | quote }} +- name: GIT_CACHE_DISK_QUOTA_BYTES + value: {{ .Values.config.disk.quotaBytes | int64 | quote }} +- name: GIT_CACHE_DISK_MIN_FREE_BYTES + value: {{ .Values.config.disk.minFreeBytes | int64 | quote }} +- name: GIT_CACHE_GIT_REMOTE_ENABLED + value: {{ .Values.config.gitRemote.enabled | quote }} +- name: GIT_CACHE_COMPACTION_CHAIN_DEPTH_THRESHOLD + value: {{ .Values.config.compaction.chainDepthThreshold | int64 | quote }} +- name: GIT_CACHE_COMPACTION_INLINE + value: {{ .Values.config.compaction.inline | quote }} +- name: RUST_LOG + value: {{ .Values.config.logLevel | quote }} +{{- if eq .Values.config.objectStore.kind "s3" }} +- name: GIT_CACHE_OBJECT_STORE_KIND + value: "s3" +- name: GIT_CACHE_S3_BUCKET + value: {{ required "config.objectStore.s3.bucket is required when objectStore.kind is s3" .Values.config.objectStore.s3.bucket | quote }} +- name: GIT_CACHE_S3_PREFIX + value: {{ .Values.config.objectStore.s3.prefix | quote }} +{{- if .Values.config.objectStore.s3.endpoint }} +- name: GIT_CACHE_S3_ENDPOINT + value: {{ .Values.config.objectStore.s3.endpoint | quote }} +{{- end }} +{{- else }} +- name: GIT_CACHE_OBJECT_STORE_KIND + value: "local" +- name: GIT_CACHE_OBJECT_STORE_ROOT + value: {{ .Values.config.objectStore.local.root | quote }} +{{- end }} +{{- if .Values.aws.region }} +- name: AWS_REGION + value: {{ .Values.aws.region | quote }} +{{- end }} +{{- if .Values.aws.existingSecret }} +- name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Values.aws.existingSecret }} + key: AWS_ACCESS_KEY_ID +- name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.aws.existingSecret }} + key: AWS_SECRET_ACCESS_KEY +{{- end }} +{{- if .Values.upstreamAuth.existingSecret }} +- name: GIT_CACHE_UPSTREAM_AUTH_TOKEN_ENV + value: {{ .Values.upstreamAuth.tokenEnv | quote }} +- name: {{ .Values.upstreamAuth.tokenEnv }} + valueFrom: + secretKeyRef: + name: {{ .Values.upstreamAuth.existingSecret }} + key: {{ .Values.upstreamAuth.secretKey }} +{{- end }} +{{- with .Values.config.extraEnv }} +{{ toYaml . }} +{{- end }} +{{- end }} +{{- end }} diff --git a/deploy/helm/gitmirrorcache/templates/configmap.yaml b/deploy/helm/gitmirrorcache/templates/configmap.yaml new file mode 100644 index 0000000..eb0deee --- /dev/null +++ b/deploy/helm/gitmirrorcache/templates/configmap.yaml @@ -0,0 +1,11 @@ +{{- if .Values.configFile -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gitmirrorcache.fullname" . }} + labels: + {{- include "gitmirrorcache.labels" . | nindent 4 }} +data: + config.toml: | + {{- .Values.configFile | nindent 4 }} +{{- end }} diff --git a/deploy/helm/gitmirrorcache/templates/cronjob-compaction.yaml b/deploy/helm/gitmirrorcache/templates/cronjob-compaction.yaml new file mode 100644 index 0000000..80eed14 --- /dev/null +++ b/deploy/helm/gitmirrorcache/templates/cronjob-compaction.yaml @@ -0,0 +1,53 @@ +{{- if .Values.compaction.enabled -}} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "gitmirrorcache.fullname" . }}-compaction + labels: + {{- include "gitmirrorcache.labels" . | nindent 4 }} +spec: + schedule: {{ .Values.compaction.schedule | quote }} + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: {{ .Values.compaction.backoffLimit }} + template: + metadata: + labels: + {{- include "gitmirrorcache.selectorLabels" . | nindent 12 }} + app.kubernetes.io/component: compaction + spec: + restartPolicy: {{ .Values.compaction.restartPolicy }} + serviceAccountName: {{ include "gitmirrorcache.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 12 }} + {{- end }} + containers: + - name: compaction + image: {{ include "gitmirrorcache.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 16 }} + command: ["/usr/local/bin/git-cache", "compact", "--all"] + # The CLI is configured purely via env vars (no GIT_CACHE_CONFIG + # here: a TOML config would override the scratch cache root). + # Compaction works against the object store; use scratch disk + # so the job does not contend with the server's hot cache. + env: + {{- include "gitmirrorcache.env" (dict "ctx" . "cacheRoot" "/tmp/compaction-cache") | nindent 16 }} + {{- with .Values.compaction.resources }} + resources: + {{- toYaml . | nindent 16 }} + {{- end }} + volumeMounts: + - name: scratch + mountPath: /tmp/compaction-cache + volumes: + - name: scratch + emptyDir: {} +{{- end }} diff --git a/deploy/helm/gitmirrorcache/templates/ingress.yaml b/deploy/helm/gitmirrorcache/templates/ingress.yaml new file mode 100644 index 0000000..b33fd84 --- /dev/null +++ b/deploy/helm/gitmirrorcache/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "gitmirrorcache.fullname" . }} + labels: + {{- include "gitmirrorcache.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- 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: {{ include "gitmirrorcache.fullname" $ }} + port: + name: http + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/gitmirrorcache/templates/service.yaml b/deploy/helm/gitmirrorcache/templates/service.yaml new file mode 100644 index 0000000..28196a6 --- /dev/null +++ b/deploy/helm/gitmirrorcache/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gitmirrorcache.fullname" . }} + labels: + {{- include "gitmirrorcache.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "gitmirrorcache.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/gitmirrorcache/templates/serviceaccount.yaml b/deploy/helm/gitmirrorcache/templates/serviceaccount.yaml new file mode 100644 index 0000000..1e779a3 --- /dev/null +++ b/deploy/helm/gitmirrorcache/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "gitmirrorcache.serviceAccountName" . }} + labels: + {{- include "gitmirrorcache.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/gitmirrorcache/templates/statefulset.yaml b/deploy/helm/gitmirrorcache/templates/statefulset.yaml new file mode 100644 index 0000000..de04810 --- /dev/null +++ b/deploy/helm/gitmirrorcache/templates/statefulset.yaml @@ -0,0 +1,104 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "gitmirrorcache.fullname" . }} + labels: + {{- include "gitmirrorcache.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + serviceName: {{ include "gitmirrorcache.fullname" . }} + selector: + matchLabels: + {{- include "gitmirrorcache.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- if .Values.configFile }} + checksum/config: {{ .Values.configFile | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "gitmirrorcache.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "gitmirrorcache.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: git-cache-api + image: {{ include "gitmirrorcache.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ (split ":" .Values.config.bindAddr)._1 | int }} + protocol: TCP + env: + {{- include "gitmirrorcache.env" (dict "ctx" .) | nindent 12 }} + {{- if .Values.configFile }} + - name: GIT_CACHE_CONFIG + value: /etc/gitmirrorcache/config.toml + {{- end }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: cache + mountPath: {{ .Values.config.cacheRoot }} + {{- if .Values.configFile }} + - name: config + mountPath: /etc/gitmirrorcache + readOnly: true + {{- end }} + volumes: + {{- if .Values.configFile }} + - name: config + configMap: + name: {{ include "gitmirrorcache.fullname" . }} + {{- end }} + {{- if not .Values.persistence.enabled }} + - name: cache + emptyDir: {} + {{- 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 .Values.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: cache + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 10 }} + {{- end }} + spec: + accessModes: + {{- toYaml .Values.persistence.accessModes | nindent 10 }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} + {{- end }} diff --git a/deploy/helm/gitmirrorcache/values.yaml b/deploy/helm/gitmirrorcache/values.yaml new file mode 100644 index 0000000..726cd08 --- /dev/null +++ b/deploy/helm/gitmirrorcache/values.yaml @@ -0,0 +1,164 @@ +replicaCount: 1 + +image: + repository: ghcr.io/0lut/gitmirrorcache + pullPolicy: IfNotPresent + # Defaults to the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + # Useful for EKS IRSA, e.g. + # eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/gitmirrorcache + annotations: {} + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + fsGroup: 999 + +securityContext: + runAsNonRoot: true + # The image runs as the system user "git-cache" (uid/gid 999). A numeric + # runAsUser is required for runAsNonRoot verification with named users. + runAsUser: 999 + runAsGroup: 999 + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: git-cache.example.com + paths: + - path: / + pathType: Prefix + tls: [] + +# Serving upload-pack spawns git pack-objects children (CPU- and memory-heavy, +# up to config.maxConcurrentGitProcesses at once), so do not run this small. +# The production ECS deployment uses 8 vCPU / 24 GiB (docs/deployment.md); +# treat ~2 vCPU / 4 GiB as the floor for light workloads and scale CPU with +# concurrent clone traffic. +resources: + requests: + cpu: "2" + memory: 4Gi + limits: + memory: 8Gi + +nodeSelector: {} +tolerations: [] +affinity: {} + +# Persistent hot cache mounted at /cache. The cache is disposable from a +# correctness perspective (S3 is the durable source); losing the volume only +# forces rehydration. Disable persistence to fall back to an emptyDir. +persistence: + enabled: true + storageClass: "" + accessModes: + - ReadWriteOnce + size: 100Gi + annotations: {} + +# Application configuration. Each field maps to a GIT_CACHE_* environment +# variable understood by the server and CLI. +config: + bindAddr: "0.0.0.0:8080" + cacheRoot: /cache + gitBinary: git + gitTimeoutSeconds: 120 + maxGitOutputBytes: 16777216 + rateLimitPerMinute: 120 + allowedUpstreamHosts: + - github.com + maxConcurrentGitProcesses: 64 + logLevel: info + objectStore: + # "s3" or "local" + kind: s3 + s3: + bucket: "" + # Runtime appends the v2 schema suffix (e.g. "repos" stores under repos-v2). + prefix: repos + # Optional custom endpoint for S3-compatible stores (MinIO, R2, ...). + endpoint: "" + # Only used when kind is "local"; stored on the cache volume. + local: + root: /cache/object-store + disk: + quotaBytes: 107374182400 # 100 GiB; keep below the persistence size + minFreeBytes: 10737418240 # 10 GiB + gitRemote: + # Serve git clone/fetch directly via /git/{host}/{owner}/{repo}.git + enabled: true + compaction: + chainDepthThreshold: 10 + inline: false + # Extra environment variables appended verbatim to the container, e.g. to + # set GIT_CACHE_* knobs not surfaced above. + extraEnv: [] + # extraEnv: + # - name: GIT_CACHE_GIT_REMOTE_PROXY_ON_MISS_BY_DEFAULT + # value: "false" + +# Optional full TOML config mounted into the server pod and pointed to by +# GIT_CACHE_CONFIG. When set, the server reads its ENTIRE config from this +# file and ignores GIT_CACHE_* env vars (see config/production.example.toml). +# The compaction CronJob always uses env-var config so it can keep its +# scratch cache root. +configFile: "" + +# Token used to authenticate against upstream hosts when warming/serving +# private repositories. +upstreamAuth: + # Name of an existing Secret containing the token. + existingSecret: "" + secretKey: token + # Environment variable the server reads the token from. + tokenEnv: GITHUB_TOKEN + +# Static AWS credentials for S3. Prefer IRSA / pod identity via +# serviceAccount.annotations and leave existingSecret empty. +aws: + region: "" + # Existing Secret with AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY keys. + existingSecret: "" + +# Hourly generation-chain compaction (git-cache compact --all), mirroring the +# EventBridge rule used by the AWS deployment. +compaction: + enabled: true + schedule: "0 * * * *" + resources: {} + # Concurrency-safe: publish uses conditional PUT on the generation head. + restartPolicy: Never + backoffLimit: 0 + +livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + +readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 2 + periodSeconds: 5 diff --git a/docs/deployment.md b/docs/deployment.md index c056d43..341be1b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,5 +1,14 @@ # Deployment Notes +## Kubernetes (Helm) + +A Helm chart for deploying to any Kubernetes cluster lives at +[`deploy/helm/gitmirrorcache`](../deploy/helm/gitmirrorcache/README.md). It runs +the server as a StatefulSet with a persistent `/cache` volume, an hourly +`git-cache compact --all` CronJob, and S3 (or any S3-compatible store) as the +durable source. Container images are published to +`ghcr.io/0lut/gitmirrorcache` by the `Publish Docker Image` workflow. + ## Current AWS Deployment Path Use **ECS on Graviton EC2 with host-mounted EBS** for large repository deployments.