From 08f66e83ea1e6205ed6499dbcbe7ef3b71b866fd Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Fri, 21 Aug 2026 21:23:04 +0100 Subject: Add docker container and helm chart --- .dockerignore | 9 ++ .github/workflows/build.yml | 108 ++++++++++++++++++++++- .gitignore | 1 + Dockerfile | 29 ++++++ REUSE.toml | 1 + charts/recluse/Chart.yaml | 10 +++ charts/recluse/templates/_helpers.tpl | 37 ++++++++ charts/recluse/templates/configmap.yaml | 46 ++++++++++ charts/recluse/templates/deployment.yaml | 97 ++++++++++++++++++++ charts/recluse/templates/httproute.yaml | 26 ++++++ charts/recluse/templates/pvc.yaml | 22 +++++ charts/recluse/templates/service.yaml | 19 ++++ charts/recluse/values.yaml | 78 ++++++++++++++++ mise.toml | 2 +- 14 files changed, 482 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 charts/recluse/Chart.yaml create mode 100644 charts/recluse/templates/_helpers.tpl create mode 100644 charts/recluse/templates/configmap.yaml create mode 100644 charts/recluse/templates/deployment.yaml create mode 100644 charts/recluse/templates/httproute.yaml create mode 100644 charts/recluse/templates/pvc.yaml create mode 100644 charts/recluse/templates/service.yaml create mode 100644 charts/recluse/values.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..90a5968 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +.git +.github +.recluse-state +dist +target +*.lcov diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1ff37a..419c209 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,15 @@ jobs: - name: Build packages run: | PKG_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "recluse") | .version') - VERSION="${PKG_VERSION}~nightly.$(git log -1 --format=%ct)" + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + VERSION="${GITHUB_REF_NAME#v}" + if [[ "$VERSION" != "$PKG_VERSION" ]]; then + echo "Tag version $VERSION does not match Cargo.toml version $PKG_VERSION" >&2 + exit 1 + fi + else + VERSION="$PKG_VERSION-nightly.$(git log -1 --format=%ct)" + fi mise run package -- \ --version "$VERSION" \ @@ -99,7 +107,6 @@ jobs: name: Smoke tests runs-on: ubuntu-24.04 needs: [build] - if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -193,3 +200,100 @@ jobs: S3_PUBLIC_URL: ${{ vars.S3_PUBLIC_URL }} S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} + + oci: + name: OCI artifacts + needs: [smoke] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3 + with: + version: 2026.7.5 + experimental: true + install: false + + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + pattern: binary-* + path: .container + + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + + - name: Determine artifact version + id: artifact + shell: bash + run: | + base_version=$(python3 -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["workspace"]["package"]["version"])') + if [[ "$GITHUB_REF" == refs/heads/main ]]; then + version="$base_version-nightly.$(git log -1 --format=%ct)" + elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then + version="${GITHUB_REF_NAME#v}" + if [[ "$version" != "$base_version" ]]; then + echo "Tag version $version does not match Cargo.toml version $base_version" >&2 + exit 1 + fi + else + version="$base_version-pr.$GITHUB_RUN_NUMBER" + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log Helm in to GHCR + if: github.event_name != 'pull_request' + env: + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + printf '%s' "$GHCR_TOKEN" | mise x helm@4.1.1 -- \ + helm registry login ghcr.io --username "$GITHUB_ACTOR" --password-stdin + + - name: Build and publish OCI artifacts + shell: bash + run: | + version="${{ steps.artifact.outputs.version }}" + image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/recluse" + tags=( + --tag "$image:sha-$GITHUB_SHA" + --tag "$image:$version" + ) + labels=( + --label "org.opencontainers.image.revision=$GITHUB_SHA" + --label "org.opencontainers.image.source=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" + --label "org.opencontainers.image.version=$version" + ) + image_output=() + chart_output=() + + if [[ "$GITHUB_REF" == refs/heads/main ]]; then + tags+=(--tag "$image:nightly") + elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then + tags+=(--tag "$image:latest") + fi + if [[ "${{ github.event_name }}" != pull_request ]]; then + image_output+=(--push) + chart_output+=(--push "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/charts") + fi + + mise run container -- \ + --context . \ + --file Dockerfile \ + --platform linux/amd64,linux/arm64 \ + --cache-scope recluse \ + "${tags[@]}" "${labels[@]}" "${image_output[@]}" + + mise run chart -- \ + --chart charts/recluse \ + --version "$version" \ + --app-version "$version" \ + "${chart_output[@]}" diff --git a/.gitignore b/.gitignore index 8b1a3f5..aa377c6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /target /dist +/.container /.recluse-state mise.local.toml mise.*.local.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e4f4141 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +ARG ALPINE_VERSION=3.23.5 +ARG ALPINE_DIGEST=sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40 +ARG DISTROLESS_DIGEST=sha256:a77defd6fedbb3392b175ba8ea3d1c22be963c1597c248c3ba987ddd80bfb512 + +FROM --platform=$BUILDPLATFORM docker.io/library/alpine:${ALPINE_VERSION}@${ALPINE_DIGEST} AS rootfs +RUN install -d -m 0750 /rootfs/var/lib/recluse + +FROM gcr.io/distroless/cc-debian13:nonroot@${DISTROLESS_DIGEST} + +ARG TARGETARCH + +LABEL org.opencontainers.image.source="https://github.com/dimidiumlabs/recluse" \ + org.opencontainers.image.licenses="AGPL-3.0-or-later" + +COPY --chown=root:root --chmod=0755 .container/binary-${TARGETARCH}/recluse /usr/local/bin/recluse +COPY --from=rootfs --chown=10000:10000 /rootfs/var/lib/recluse /var/lib/recluse +COPY --chown=root:root pkg/recluse.toml /etc/recluse.toml +COPY --chown=root:root LICENSE README.md /usr/share/doc/recluse/ + +USER 10000:10000 +EXPOSE 2000 +VOLUME ["/var/lib/recluse"] + +ENTRYPOINT ["/usr/local/bin/recluse"] +CMD ["--config=/etc/recluse.toml"] diff --git a/REUSE.toml b/REUSE.toml index 5b14da9..717f144 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -10,6 +10,7 @@ path = [ "mise.lock", "pkg/*.svg", + "charts/recluse/templates/*", "crates/recluse/src/assets/*.svg", "crates/recluse/src/assets/*.ico", diff --git a/charts/recluse/Chart.yaml b/charts/recluse/Chart.yaml new file mode 100644 index 0000000..ff40446 --- /dev/null +++ b/charts/recluse/Chart.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +apiVersion: v2 +type: application +version: 0.1.0 + +name: recluse +home: https://github.com/dimidiumlabs/recluse +description: Tiny package caching proxy diff --git a/charts/recluse/templates/_helpers.tpl b/charts/recluse/templates/_helpers.tpl new file mode 100644 index 0000000..4da058b --- /dev/null +++ b/charts/recluse/templates/_helpers.tpl @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +{{- define "recluse.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "recluse.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name (include "recluse.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "recluse.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +app.kubernetes.io/name: {{ include "recluse.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "recluse.selectorLabels" -}} +app.kubernetes.io/name: {{ include "recluse.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "recluse.image" -}} +{{- if .Values.image.digest -}} +{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}} +{{- else -}} +{{- printf "%s:%s" .Values.image.repository (default .Chart.AppVersion .Values.image.tag) -}} +{{- end -}} +{{- end -}} + +# vim: set filetype=helm: diff --git a/charts/recluse/templates/configmap.yaml b/charts/recluse/templates/configmap.yaml new file mode 100644 index 0000000..d5dbf18 --- /dev/null +++ b/charts/recluse/templates/configmap.yaml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +{{- if not .Values.config.existingConfigMap }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "recluse.fullname" . }} + labels: + {{- include "recluse.labels" . | nindent 4 }} +data: + recluse.toml: | + appname = {{ .Values.config.appname | quote }} + dirname = "/var/lib/recluse" + + [server] + max_body_size = {{ .Values.config.server.maxBodySize | quote }} + max_concurrent_requests = {{ .Values.config.server.maxConcurrentRequests }} + + rate_limit_period = {{ .Values.config.server.rateLimitPeriod }} + rate_limit_burst_size = {{ .Values.config.server.rateLimitBurstSize }} + + request_timeout = {{ .Values.config.server.requestTimeout }} + shutdown_timeout = {{ .Values.config.server.shutdownTimeout }} + + [[listen]] + addr = "0.0.0.0:2000" + hostnames = {{ .Values.config.hostnames | toJson }} + + {{- range $name, $backend := .Values.config.backends }} + [backends.{{ $name }}] + enabled = {{ $backend.enabled }} + refresh_interval = {{ $backend.refreshInterval }} + + {{- end }} + [telemetry.stdout] + enabled = {{ .Values.config.log.enabled }} + log_level = {{ .Values.config.log.level | quote }} + log_format = {{ .Values.config.log.format | quote }} + {{- with .Values.config.extra }} + +{{ . | indent 4 }} + {{- end }} +{{- end }} + +# vim: set filetype=helm: diff --git a/charts/recluse/templates/deployment.yaml b/charts/recluse/templates/deployment.yaml new file mode 100644 index 0000000..f82648c --- /dev/null +++ b/charts/recluse/templates/deployment.yaml @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "recluse.fullname" . }} + labels: + {{- include "recluse.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "recluse.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- if not .Values.config.existingConfigMap }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "recluse.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: recluse + image: {{ include "recluse.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + args: ["--config=/etc/recluse/recluse.toml"] + ports: + - name: http + containerPort: 2000 + protocol: TCP + startupProbe: + tcpSocket: + port: http + failureThreshold: 60 + periodSeconds: 5 + readinessProbe: + tcpSocket: + port: http + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + periodSeconds: 20 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /etc/recluse/recluse.toml + subPath: recluse.toml + readOnly: true + - name: state + mountPath: /var/lib/recluse + volumes: + - name: config + configMap: + name: {{ default (include "recluse.fullname" .) .Values.config.existingConfigMap }} + - name: state + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ default (include "recluse.fullname" .) .Values.persistence.existingClaim }} + {{- else }} + 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 }} + +# vim: set filetype=helm: diff --git a/charts/recluse/templates/httproute.yaml b/charts/recluse/templates/httproute.yaml new file mode 100644 index 0000000..5b7d341 --- /dev/null +++ b/charts/recluse/templates/httproute.yaml @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +{{- if .Values.route.enabled }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "recluse.fullname" . }} + labels: + {{- include "recluse.labels" . | nindent 4 }} +spec: + {{- with .Values.route.parentRefs }} + parentRefs: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.route.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + - backendRefs: + - name: {{ include "recluse.fullname" . }} + port: {{ .Values.service.port }} +{{- end }} + +# vim: set filetype=helm: diff --git a/charts/recluse/templates/pvc.yaml b/charts/recluse/templates/pvc.yaml new file mode 100644 index 0000000..2c563d1 --- /dev/null +++ b/charts/recluse/templates/pvc.yaml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "recluse.fullname" . }} + labels: + {{- include "recluse.labels" . | nindent 4 }} +spec: + accessModes: + {{- toYaml .Values.persistence.accessModes | nindent 4 }} + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- with .Values.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} +{{- end }} + +# vim: set filetype=helm: diff --git a/charts/recluse/templates/service.yaml b/charts/recluse/templates/service.yaml new file mode 100644 index 0000000..0367f03 --- /dev/null +++ b/charts/recluse/templates/service.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "recluse.fullname" . }} + labels: + {{- include "recluse.labels" . | nindent 4 }} +spec: + selector: + {{- include "recluse.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + +# vim: set filetype=helm: diff --git a/charts/recluse/values.yaml b/charts/recluse/values.yaml new file mode 100644 index 0000000..d6762d8 --- /dev/null +++ b/charts/recluse/values.yaml @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +replicaCount: 1 + +image: + repository: ghcr.io/dimidiumlabs/recluse + tag: "" + digest: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + + runAsUser: 10000 + runAsGroup: 10000 + + fsGroup: 10000 + fsGroupChangePolicy: OnRootMismatch + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: true + +service: + port: 2000 + +config: + existingConfigMap: "" + appname: recluse + hostnames: [] + server: + shutdownTimeout: 60 + requestTimeout: 30 + maxBodySize: 64 MiB + maxConcurrentRequests: 512 + rateLimitPeriod: 10 + rateLimitBurstSize: 50 + backends: + go: + enabled: true + refreshInterval: 3600 + zig: + enabled: true + refreshInterval: 3600 + log: + enabled: true + level: info + format: json + extra: "" + +persistence: + enabled: true + existingClaim: "" + accessModes: [ReadWriteOnce] + size: 20Gi + storageClass: "" + +route: + enabled: false + hostnames: [] + parentRefs: [] + +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} + +terminationGracePeriodSeconds: 70 diff --git a/mise.toml b/mise.toml index b3899cb..09fa32a 100644 --- a/mise.toml +++ b/mise.toml @@ -38,5 +38,5 @@ rust = { version = "1.97.1", profile = "minimal", components = [ [task_config] dir = "{{cwd}}" includes = [ - "git::https://github.com/dimidiumlabs/infra.git//tasks?ref=4a4589fdb968686a7c91b380ff369aba23bd8662", + "git::https://github.com/dimidiumlabs/infra.git//tasks?ref=8bc35fe8be889c50db2d1fb4425cc1b4097dc6b8", ] -- Gilti