diff --git a/kubernetes/docker-registry/Chart.yaml b/kubernetes/docker-registry/Chart.yaml new file mode 100644 index 00000000..a57bafba --- /dev/null +++ b/kubernetes/docker-registry/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +annotations: + category: registry +name: docker-registry +description: Docker registry +engine: gotpl + +dependencies: + - name: common + repository: https://svtechnmaa.github.io/charts/artifacthub/ + version: 1.x.x + +version: 1.0.0 +appVersion: "v1.0.0" diff --git a/kubernetes/docker-registry/README.md b/kubernetes/docker-registry/README.md new file mode 100644 index 00000000..c92c6a88 --- /dev/null +++ b/kubernetes/docker-registry/README.md @@ -0,0 +1,442 @@ +# Docker Private Registry — SeaweedFS S3 Backend + +## Table of Contents +- [Architecture Overview](#architecture-overview) +- [How It Works — Full Flow](#how-it-works--full-flow) + - [Push Flow (skopeo copy to registry)](#1-push-flow-skopeo-copy-to-registry) + - [Pull Flow (containerd pull image)](#2-pull-flow-containerd-pull-image) + - [Registry ↔ SeaweedFS S3 Interaction](#3-registry--seaweedfs-s3-interaction) +- [Inspect Registry via curl](#inspect-registry-via-curl) +- [Inspect Registry via SeaweedFS GUI](#inspect-registry-via-seaweedfs-gui) +- [Pull Images using crictl](#pull-images-using-crictl) +- [Data Layout on SeaweedFS](#data-layout-on-seaweedfs) + +--- + +## Architecture Overview + +### Component Diagram + +Three distinct services involved — each has a different role: + +``` + +-----------------------------------------------------------------------+ + | Kubernetes Cluster | + | | + | [ skopeo ] : CLI tool — copies images between registries | + | [ containerd ] : Container runtime — pulls images for pods | + | [ Docker Registry ] : Service — exposes image API on port 5000 | + | [ SeaweedFS S3 ] : Service — stores actual image data (blobs) | + | | + +-----------------------------------------------------------------------+ + + PUSH (skopeo) + +----------+ 1. copy image +-----------------+ S3 API +-------------+ + | ghcr.io |◄──────────────────| skopeo | | | + | source | (download blob) | | | SeaweedFS | + +----------+ | runs on any K8s | | S3 | + | node or host | | :8333 | + 2. push blob | | | | + ─────────────────►| Docker Registry |────────────► bucket: | + | ns: registry | | registry | + | :5000 | | | + +-----------------+ +-------------+ + + PULL (containerd — triggered via crictl) + + +--------------+ +-------------+ + | crictl | | | + | pull image | | SeaweedFS | + +--------------+ | S3 :8333 | + | | | + ▼ | bucket: | + +--------------+ Step 1. GET manifest +-----------------+ | registry | + | |─────────────────────────►| Docker Registry |──►| | + | containerd |◄─────────────────────────| ns: registry |◄─ | | + | (each node) | manifest JSON | :5000 | | | + | | | | | | + | | Step 2. GET blob | | | | + | |─────────────────────────►| | | | + | |◄─────────────────────────| | | | + | | 307 Redirect +-----------------+ | | + | | Location: http://seaweedfs-s3:8333/... | | + | | | | + | | Step 3. download blob directly (bypass Registry) | + | |───────────────────────────────────────────────►| | + | |◄───────────────────────────────────────────────| | + | | blob content (raw stream) | | + | | | | + | | (repeat Step 2→3 for each layer in manifest) | | + +--------------+ +-------------+ + + ----------------------------------------------------------------------- + Docker Registry : registry-docker-registry-service.registry.svc.cluster.local:5000 + SeaweedFS S3 : seaweedfs-s3.seaweedfs.svc.cluster.local:8333 + containerd alias: private.registry → points to Docker Registry :5000 + ----------------------------------------------------------------------- +``` + +> **Key point:** When pulling, containerd downloads blobs **directly from SeaweedFS** via a 307 redirect — the Registry only handles the manifest and redirect, it does not proxy the blob data itself. This keeps Registry resource usage low. + +--- + +### Flow Diagram + +#### PUSH — skopeo copies image from ghcr.io into private registry + +``` + ghcr.io skopeo Docker Registry SeaweedFS S3 + ────────── ───────── ─────────────── ──────────── + │ │ │ │ + │ Step 1 │ │ │ + │ Download │ │ │ + │ manifest ◄──│ │ │ + │ & blobs │ │ │ + │ │ │ │ + │ │ Step 2 │ │ + │ │ HEAD blob │ HeadObject │ + │ │ (already exist?)│──────────────────► │ + │ │◄─────────────────│◄────── 404 ────────│ + │ │ 404 not found │ (blob missing) │ + │ │ │ │ + │ │ Step 3 │ │ + │ │ POST │ PutObject │ + │ │ (init session) │ (create temp file)│ + │ │─────────────────►│──────────────────► │ + │ │◄─────────────────│◄────── 200 ────────│ + │ │ 202 + uuid │ │ + │ │ │ │ + │ │ Step 4 │ │ + │ │ PATCH × N │ PutObject │ + │ │ (upload chunks) │ (write each chunk)│ + │ │─────────────────►│──────────────────► │ + │ │◄─────────────────│◄────── 200 ────────│ + │ │ 202 accepted │ (repeat per chunk)│ + │ │ │ │ + │ │ Step 5 │ │ + │ │ PUT │ CopyObject │ + │ │ (finalize blob) │ _uploads/ → blobs/│ + │ │─────────────────►│──────────────────► │ + │ │◄─────────────────│◄────── 200 ────────│ + │ │ 201 created │ (blob committed) │ + │ │ │ │ + │ │ Step 6 │ │ + │ │ PUT manifest │ PutObject │ + │ │─────────────────►│──────────────────► │ + │ │◄─────────────────│◄────── 200 ────────│ + │ │ 201 created │ (manifest saved) │ + ─────────────────────────────────────────────────────────────────────────── +``` + +#### PULL — containerd pulls image from private registry + +``` + containerd Docker Registry SeaweedFS S3 + ──────────── ─────────────── ──────────── + │ │ │ + │ Step 1 │ │ + │ GET manifest │ GetObject │ + │─────────────────►│──────────────────► │ + │◄─────────────────│◄───────────────────│ + │ manifest JSON │ │ + │ │ │ + │ Step 2 │ │ + │ HEAD blob │ HeadObject │ + │ (check size) │──────────────────► │ + │◄─────────────────│◄───────────────────│ + │ 200 + size │ │ + │ │ │ + │ Step 3 │ │ + │ GET blob │ │ + │─────────────────►│ │ + │◄─────────────────│ │ + │ 307 Redirect │ │ + │ Location: http://seaweedfs-s3:8333/. │ + │ │ │ + │ Step 4 — bypass Registry, go direct │ + │──────────────────────────────────────►│ + │◄──────────────────────────────────────│ + │ blob content (raw stream) │ + │ │ │ + │ (repeat Step 2→4 for each layer) │ + ──────────────────────────────────────────────────────────── +``` + +--- + +## How It Works — Full Flow + +### 1. Push Flow (skopeo copy to registry) + +``` +skopeo copy docker://ghcr.io/svtechnmaa/arangodb:3.12.2 \ + docker://registry:5000/svtechnmaa/arangodb:3.12.2 +``` + +``` +skopeo Registry SeaweedFS S3 + │ │ │ + │─── GET /v2/ │ │ + │ ← 200 OK │ │ + │ │ │ + │ ① Fetch source manifest │ │ + │─── GET ghcr.io/manifests/tag │ │ + │ ← manifest list │ │ + │ (multi-arch) │ │ + │ │ │ + │ ② Check each blob exists │ │ + │─── HEAD /v2/.../blobs/sha256:111 + │ ← 404 (not found) ─┼──► HEAD s3://registry/.../sha256:111 + │ │ ← 404 (not found) │ + │ │ │ + │ ③ Init upload session │ │ + │─── POST /v2/.../blobs/uploads/ + │ ─┼──► PUT s3://_uploads//data (empty) + │ ← 202 + uuid ─┼◄── 200 OK │ + │ │ │ + │ ④ Upload blob in chunks │ │ + │─── PATCH /uploads/ │ │ + │ [chunk 1: 0→128MB] ─┼──► PUT s3://_uploads//data + │ ← 202 ─┼◄── 200 OK │ + │─── PATCH /uploads/ │ │ + │ [chunk 2: 128→256MB] ─┼──► PUT s3://_uploads//data + │ ← 202 ─┼◄── 200 OK │ + │ │ │ + │ ⑤ Finalize upload │ │ + │─── PUT /uploads/ │ │ + │ ?digest=sha256:111 ─┼──► CopyObject │ + │ │ src: _uploads//data │ + │ │ dest: blobs/sha256:111 │ + │ │─── Delete _uploads//data │ + │ ← 201 Created │ │ + │ │ │ + │ ⑥ Push manifest │ │ + │─── PUT /v2/.../manifests/tag │ │ + │ ─┼──► PUT s3://.../manifests/tag │ + │ ← 201 Created │ │ + ─────────────────────────────────────────────────────────────────────────── +``` + +--- + +### 2. Pull Flow (containerd pull image) + +``` +crictl pull private.registry/svtechnmaa/arangodb:3.12.2 +``` + +``` +containerd Registry SeaweedFS S3 + │ │ │ + │ ① Fetch manifest │ │ + │─── GET /v2/.../manifests/3.12.2 + │ ─┼──► GET s3://.../manifests/3.12.2 + │ ← manifest JSON ─┼◄── blob content │ + │ │ │ + │ ② Check each layer │ │ + │─── HEAD /v2/.../blobs/sha256:111 + │ ─┼──► HEAD s3://.../blobs/sha256:111 + │ ← 200 + size ─┼◄── 200 OK │ + │ │ │ + │ ③ Download blob │ │ + │─── GET /v2/.../blobs/sha256:111 + │ │ │ + │ ← 307 Redirect │ (redirect: disable: false) │ + │ Location: http://seaweedfs-s3:8333/registry/... │ + │ │ │ + │─────────────────────────────────────────────────────────────► │ + │ GET http://seaweedfs-s3:8333/registry/.../blobs/sha256:111 │ + │ ← blob content (streamed directly from SeaweedFS) │ + │◄───────────────────────────────────────────────────────────── │ + │ │ │ + │ ④ Repeat for each layer │ │ + ─────────────────────────────────────────────────────────────────────────── +``` + +> **Note:** `redirect: disable: false` → containerd downloads blobs **directly from SeaweedFS** instead of going through the Registry. The Registry only returns a redirect URL and does not proxy the blob data — this significantly reduces CPU and memory usage on the Registry pod. + +--- + +### 3. Registry ↔ SeaweedFS S3 Interaction + +The Registry never stores data locally. Every read/write goes through the S3 driver to SeaweedFS. The table below maps each Registry API action to the exact S3 API call it triggers: + +| Triggered by | Registry API | S3 API called | What happens on SeaweedFS | +|---|---|---|---| +| skopeo HEAD blob | `HEAD /v2/.../blobs/` | `HeadObject` | Check if blob file exists in `blobs/sha256/` | +| skopeo POST init | `POST /v2/.../blobs/uploads/` | `PutObject` (empty) | Create empty temp file at `_uploads//data` | +| skopeo PATCH chunk | `PATCH /v2/.../blobs/uploads/` | `PutObject` (append) | Write chunk data into `_uploads//data` | +| skopeo PUT finalize (blob < 1GB) | `PUT /v2/.../blobs/uploads/` | `CopyObject` (single call) | Move `_uploads//data` → `blobs/sha256/` in one operation | +| skopeo PUT finalize (blob > 1GB) | `PUT /v2/.../blobs/uploads/` | `CreateMultipartUpload` → `CopyPart` × N → `CompleteMultipartUpload` | Move `_uploads//data` → `blobs/sha256/` in multiple parts | +| skopeo PUT manifest | `PUT /v2/.../manifests/` | `PutObject` | Write manifest JSON to `repositories//_manifests/` | +| containerd GET manifest | `GET /v2/.../manifests/` | `GetObject` | Read manifest JSON from `repositories//_manifests/` | +| containerd GET blob | `GET /v2/.../blobs/` | — (redirect) | Registry returns `307 Redirect` to pre-signed SeaweedFS URL; containerd downloads directly | +| curl DELETE image | `DELETE /v2/.../manifests/` | `DeleteObject` | Remove manifest file — blob files remain until garbage collection runs | +| curl GET catalog | `GET /v2/_catalog` | `ListObjects` | List all directories under `repositories/` | + +--- + +## Inspect Registry via curl + +### Catalog & Tags + +```bash +# List all repositories (images) available in the registry +curl -s http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2/_catalog | jq . + +# Example response: +# { +# "repositories": [ +# "svtechnmaa/arangodb", +# "svtechnmaa/redis", +# ... +# ] +# } +``` + +```bash +# List all tags of a specific image +curl -s http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2//tags/list | jq . + +# Example: +curl -s http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2/svtechnmaa/arangodb/tags/list | jq . + +# Example response: +# { +# "name": "svtechnmaa/arangodb", +# "tags": ["3.12.2"] +# } +``` + +--- + +### Manifest + +```bash +# Get the manifest of an image (returns manifest list if multi-arch) +curl -s \ + -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \ + -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2//manifests/ | jq . + +# Example: +curl -s \ + -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \ + -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2/svtechnmaa/arangodb/manifests/3.12.2 | jq . +``` + +```bash +# Get the SHA256 digest of a manifest (needed for deletion) +curl -sI \ + -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2//manifests/ \ + | grep -i "docker-content-digest" + +# Example response: +# docker-content-digest: sha256:a1b2c3d4... +``` + +--- + +### Blob + +```bash +# Check whether a blob exists and get its size +curl -sI \ + http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2//blobs/ + +# Example: +curl -sI \ + http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2/svtechnmaa/arangodb/blobs/sha256:abc123... + +# Response if blob exists: +# HTTP/1.1 200 OK +# Content-Length: 52428800 +# Docker-Content-Digest: sha256:abc123... + +# Response if blob does not exist: +# HTTP/1.1 404 Not Found +``` + +--- + +### Registry Health & Info + +```bash +# Check if registry is alive +curl -s http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2/ +# Expected response: {} → registry is healthy + +# Check registry API version +curl -sI http://registry-docker-registry-service.registry.svc.cluster.local:5000/v2/ \ + | grep -i "docker-distribution-api-version" +# Expected response: docker-distribution-api-version: registry/2.0 +``` +--- + +## Inspect Registry via SeaweedFS GUI + +1. Expose the SeaweedFS filer as a NodePort service: +```bash +kubectl -n seaweedfs expose svc/seaweedfs-filer \ + --name=seaweedfs-filer-nodeport \ + --type=NodePort \ + --port=8888 \ + --target-port=8888 + +# Get the assigned NodePort +kubectl -n seaweedfs get svc seaweedfs-filer-nodeport +# Example output: +# NAME TYPE CLUSTER-IP PORT(S) +# seaweedfs-filer-nodeport NodePort 10.96.x.x 8888:3XXXX/TCP +``` + +2. Open in browser: +``` +http://:/buckets/registry/docker/registry/v2/repositories/ +``` + +Data layout on SeaweedFS: +``` +buckets/registry/docker/registry/v2/ +├── blobs/ +│ └── sha256/ +│ ├── 11/sha256:111... ← layer blob (tar.gz compressed filesystem) +│ ├── 22/sha256:222... ← layer blob (tar.gz compressed filesystem) +│ └── ab/sha256:abc... ← config blob (image metadata JSON) +│ +└── repositories/ + └── svtechnmaa/ + ├── arangodb/ + │ ├── _manifests/ ← manifest index by tag and digest + │ ├── _layers/ ← links pointing back to blobs/ + │ └── _uploads/ ← temporary upload dir (empty when no upload is running) + └── redis/ + ├── _manifests/ + ├── _layers/ + └── _uploads/ +``` + +--- + +## Pull Images using crictl + +```bash +crictl pull private.registry/svtechnmaa/: +``` + +> **`private.registry`** is an alias configured in containerd that points to `registry-docker-registry-service.registry.svc.cluster.local:5000`. + +Configuration at `/etc/containerd/certs.d/private.registry/hosts.toml`: + +```toml +server = "http://registry-docker-registry-service.registry.svc.cluster.local:5000" + +[host."http://registry-docker-registry-service.registry.svc.cluster.local:5000"] + capabilities = ["pull", "resolve"] + skip_verify = true +``` + +> **Why this is required:** containerd defaults to HTTPS for all registries. This registry runs on plain HTTP — the `hosts.toml` file explicitly tells containerd to allow HTTP and skip TLS verification for this alias. + diff --git a/kubernetes/docker-registry/charts/common-1.4.3.tgz b/kubernetes/docker-registry/charts/common-1.4.3.tgz new file mode 100644 index 00000000..cc15bbae Binary files /dev/null and b/kubernetes/docker-registry/charts/common-1.4.3.tgz differ diff --git a/kubernetes/docker-registry/templates/_hepers.tpl b/kubernetes/docker-registry/templates/_hepers.tpl new file mode 100644 index 00000000..c618700c --- /dev/null +++ b/kubernetes/docker-registry/templates/_hepers.tpl @@ -0,0 +1,10 @@ +{{/* +SeaweedFS S3 endpoint +*/}} +{{- define "registry.s3.endpoint" -}} +{{- $protocol := .Values.seaweedfs.protocol | default "http" -}} +{{- $svc := .Values.seaweedfs.serviceName | default "seaweedfs-s3" -}} +{{- $ns := .Values.seaweedfs.namespace | default .Release.Namespace -}} +{{- $port := .Values.seaweedfs.port | default 8333 -}} +{{- printf "%s://%s.%s.svc.cluster.local:%v" $protocol $svc $ns $port -}} +{{- end }} \ No newline at end of file diff --git a/kubernetes/docker-registry/templates/config.yaml b/kubernetes/docker-registry/templates/config.yaml new file mode 100644 index 00000000..a5076dc4 --- /dev/null +++ b/kubernetes/docker-registry/templates/config.yaml @@ -0,0 +1,118 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "%s-configuration" (include "common.names.fullname" .) }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: docker-registry +data: + config.yml: | + # Docker Distribution Registry configuration + # Docs: https://distribution.github.io/distribution/about/configuration/ + version: 0.1 + log: + # Log level: error | warn | info | debug + level: {{ .Values.registry.log.level | default "info" }} + storage: + cache: + # Cache blob metadata (digest, size, mediatype) in memory + # → Reduces HEAD requests to SeaweedFS S3 when skopeo checks if blob already exists + # → Does NOT cache blob content, only metadata + # ⚠ If running multiple registry replicas, cache is NOT shared between them + blobdescriptor: inmemory + {{- $endpoint := .Values.registry.s3.endpointOverride | default (include "registry.s3.endpoint" .) }} + s3: + # Name of the bucket on SeaweedFS where all registry data is stored + # Bucket must exist before registry starts + bucket: {{ .Values.registry.s3.bucket | required "registry.s3.bucket is required" }} + + # S3 access key configured in SeaweedFS s3 identity config + accesskey: {{ .Values.registry.s3.accesskey | required "registry.s3.accesskey is required" }} + + # S3 secret key configured in SeaweedFS s3 identity config + secretkey: {{ .Values.registry.s3.secretkey | required "registry.s3.secretkey is required" }} + + # AWS region — required by AWS SDK even when not using real AWS + # SeaweedFS ignores this value, any string is accepted + region: {{ .Values.registry.s3.region | default "us-east-1" }} + + # Override default AWS S3 endpoint to point at SeaweedFS S3 + # Required when using any S3-compatible service instead of real AWS + regionendpoint: {{ $endpoint }} + + # Use HTTP instead of HTTPS when talking to SeaweedFS + # Set to true if SeaweedFS is configured with TLS + secure: {{ .Values.registry.s3.secure | default false }} + + # Use AWS Signature Version 4 to sign all S3 requests + # SeaweedFS requires v4 — setting this to false will cause auth errors + v4auth: {{ .Values.registry.s3.v4auth | default true }} + + # Root path prefix inside the bucket where all registry files are stored + # All data will be written to: s3://registry//docker/registry/v2/... + # ⚠ Changing this after data exists will make existing images invisible to registry + rootdirectory: {{ .Values.registry.s3.rootdirectory | default "/" }} + + # Force path-style S3 URLs instead of virtual-hosted-style + # Virtual-hosted-style (forcepathstyle: false — default): + # http://registry.seaweedfs:8333/docker/registry/v2/... + # ↑ bucket name embedded in hostname → SeaweedFS cannot resolve + # + # Path-style (forcepathstyle: true): + # http://seaweedfs:8333/registry/docker/registry/v2/... + # ↑ bucket name in URL path → SeaweedFS resolves correctly + # + # ⚠ REQUIRED when using regionendpoint with SeaweedFS + # Without this → error: "Copy Source must mention the source bucket and key" + forcepathstyle: {{ .Values.registry.s3.forcepathstyle | default true }} + + # Size of each chunk when uploading a blob to S3 via multipart upload + # Default is 10MB → too many PATCH requests → increases chance of session loss + # 128MB → fewer PATCH requests per blob → more stable with SeaweedFS + # Minimum allowed by S3 spec: 5MB + # Formula: blob 256MB / chunksize 128MB = 2 PATCH requests total + chunksize: {{ .Values.registry.s3.chunksize | default 134217728 | int64 }} + + # Blobs SMALLER than this value use a single S3 CopyObject call to finalize + # Blobs LARGER than this value use S3 Multipart Copy (multiple CopyObject calls) + # Default is 32MB → almost all blobs trigger Multipart Copy → hits SeaweedFS CopyObject bug + # 1GB → most image layers (which are typically < 1GB) use single copy → avoids the bug + # Error prevented: "NoSuchUpload" and "Copy Source must mention source bucket and key" + multipartcopythresholdsize: {{ .Values.registry.s3.multipartcopythresholdsize | default 1073741824 | int64 }} + + # Size of each part when performing a Multipart Copy on SeaweedFS + # Only applies to blobs larger than multipartcopythresholdsize (> 1GB) + # Set equal to chunksize for consistency + multipartcopyChunksize: {{ .Values.registry.s3.multipartcopyChunksize | default 134217728 | int64 }} + + # Maximum number of concurrent Multipart Copy operations + # Default is 100 → 100 parallel CopyObject calls → SeaweedFS race condition + # 1 → serial execution → slower but stable with SeaweedFS + # Only affects blobs larger than multipartcopythresholdsize (> 1GB) + multipartcopyMaxConcurrency: {{ .Values.registry.s3.multipartcopyMaxConcurrency | default 1 | int }} + + # Disable S3 pre-signed URL redirect for blob downloads + # When false (redirect enabled): registry returns a pre-signed S3 URL + # → client downloads blob directly from SeaweedFS + # → works only if the client can reach the S3 endpoint + # When true (redirect disabled): registry proxies the blob content itself + # → required when SeaweedFS is only accessible inside the cluster + redirect: + disable: {{ .Values.registry.storage.redirect.disable | default false }} + http: + # Address and port the registry listens on + # ":5000" = 0.0.0.0:5000 → all network interfaces + # Use "127.0.0.1:5000" to restrict to localhost only + addr: {{ .Values.registry.http.addr | default ":5000" }} + + # Secret key used to encrypt and decrypt upload session state (_state parameter) + # The _state token encodes: { UUID, Offset, StartedAt } for each active upload + # ⚠ Must be identical across all registry replicas in HA setup + # → if replicas have different secrets, PATCH/PUT from one replica cannot decrypt _state created by another → upload fails + # ⚠ Do not change while uploads are in progress + # → existing _state tokens cannot be decrypted → active uploads fail + secret: {{ .Values.registry.http.secret | required "registry.http.secret is required" }} + + headers: + X-Content-Type-Options: {{ .Values.registry.http.headers.XContentTypeOptions | default "[nosniff]" }} diff --git a/kubernetes/docker-registry/templates/image_pull_secret.yaml b/kubernetes/docker-registry/templates/image_pull_secret.yaml new file mode 100644 index 00000000..3e5fa1dc --- /dev/null +++ b/kubernetes/docker-registry/templates/image_pull_secret.yaml @@ -0,0 +1,17 @@ +# templates/imagepullsecret.yaml +{{- if .Values.imagePullSecret.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "common.names.fullname" . }}-pull-secret + namespace: {{ .Release.Namespace }} + labels: + {{- include "common.labels.standard" . | nindent 4 }} + annotations: + helm.sh/hook: pre-install,pre-upgrade + helm.sh/hook-weight: "-10" + helm.sh/hook-delete-policy: before-hook-creation +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ printf `{"auths":{"%s":{"username":"%s","password":"%s","auth":"%s"}}}` .Values.imagePullSecret.registry .Values.imagePullSecret.username .Values.imagePullSecret.password (printf "%s:%s" .Values.imagePullSecret.username .Values.imagePullSecret.password | b64enc) | b64enc }} +{{- end }} \ No newline at end of file diff --git a/kubernetes/docker-registry/templates/job_create_bucket.yaml b/kubernetes/docker-registry/templates/job_create_bucket.yaml new file mode 100644 index 00000000..cb30bc80 --- /dev/null +++ b/kubernetes/docker-registry/templates/job_create_bucket.yaml @@ -0,0 +1,58 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "common.names.fullname" . }}-create-bucket + namespace: {{ .Release.Namespace }} + labels: + {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: create-bucket + annotations: + helm.sh/hook: pre-install,pre-upgrade + helm.sh/hook-weight: "-5" + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed +spec: + {{- if .Values.createBucket.activeDeadlineSeconds }} + activeDeadlineSeconds: {{ .Values.createBucket.activeDeadlineSeconds }} + {{- end }} + template: + metadata: + labels: + {{- include "common.labels.standard" . | nindent 8 }} + app.kubernetes.io/component: create-bucket + spec: + restartPolicy: OnFailure + {{- if .Values.imagePullSecret.enabled }} + imagePullSecrets: + - name: {{ include "common.names.fullname" . }}-pull-secret + {{- end }} + containers: + - name: create-bucket + image: {{ .Values.createBucket.image.registry }}/{{ .Values.createBucket.image.repository }}:{{ .Values.createBucket.image.tag }} + imagePullPolicy: {{ .Values.createBucket.image.pullPolicy }} + {{- with .Values.createBucket.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- $endpoint := .Values.registry.s3.endpointOverride | default (include "registry.s3.endpoint" .) }} + env: + - name: S3_ENDPOINT + value: {{ $endpoint | quote }} + - name: S3_BUCKET + value: {{ .Values.registry.s3.bucket | quote }} + - name: S3_ACCESS_KEY + value: {{ .Values.registry.s3.accesskey | quote }} + - name: S3_SECRET_KEY + value: {{ .Values.registry.s3.secretkey | quote }} + command: + - /bin/sh + - -c + - | + set -e + + echo "Setting up mc alias..." + mc alias set seaweedfs "${S3_ENDPOINT}" "${S3_ACCESS_KEY}" "${S3_SECRET_KEY}" + + echo "Creating bucket ${S3_BUCKET} if not exists..." + mc mb --ignore-existing "seaweedfs/${S3_BUCKET}" + + echo "Done!" \ No newline at end of file diff --git a/kubernetes/docker-registry/templates/registry_daemonset.yaml b/kubernetes/docker-registry/templates/registry_daemonset.yaml new file mode 100644 index 00000000..3d82194a --- /dev/null +++ b/kubernetes/docker-registry/templates/registry_daemonset.yaml @@ -0,0 +1,91 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "common.names.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: docker-registry +spec: + selector: + matchLabels: + {{- include "common.labels.matchLabels" . | nindent 6 }} + app.kubernetes.io/component: docker-registry + template: + metadata: + labels: + {{- include "common.labels.standard" . | nindent 8 }} + app.kubernetes.io/component: docker-registry + {{- with .Values.registry.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.registry.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- if .Values.imagePullSecret.enabled }} + imagePullSecrets: + - name: {{ include "common.names.fullname" . }}-pull-secret + {{- end }} + containers: + - name: registry + image: {{ include "common.images.image" (dict "imageRoot" .Values.image "global" .Values.global) }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + + ports: + - name: http + containerPort: 5000 + protocol: TCP + + {{- with .Values.registry.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + + {{- if .Values.registry.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: {{ .Values.registry.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.registry.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.registry.livenessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.registry.livenessProbe.failureThreshold }} + {{- end }} + + {{- if .Values.registry.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: {{ .Values.registry.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.registry.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.registry.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.registry.readinessProbe.failureThreshold }} + {{- end }} + + volumeMounts: + - name: config + mountPath: /etc/docker/registry + readOnly: true + + volumes: + - name: config + configMap: + name: {{ printf "%s-configuration" (include "common.names.fullname" .) }} + + {{- with .Values.registry.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + + {{- with .Values.registry.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + + {{- with .Values.registry.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/kubernetes/docker-registry/templates/service.yaml b/kubernetes/docker-registry/templates/service.yaml new file mode 100644 index 00000000..592ce206 --- /dev/null +++ b/kubernetes/docker-registry/templates/service.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "common.names.fullname" . }}-service + namespace: {{ .Release.Namespace }} +spec: + selector: + {{- include "common.labels.matchLabels" . | nindent 4 }} + app.kubernetes.io/component: docker-registry + ports: + - name: http + port: 5000 + targetPort: 5000 + type: ClusterIP + diff --git a/kubernetes/docker-registry/values.yaml b/kubernetes/docker-registry/values.yaml new file mode 100644 index 00000000..c42deef7 --- /dev/null +++ b/kubernetes/docker-registry/values.yaml @@ -0,0 +1,118 @@ +global: + imageRegistry: "" + +imagePullSecret: + enabled: true + registry: ghcr.io + username: "" + password: "" + +image: + registry: ghcr.io + repository: svtechnmaa/registry + tag: 2.8 + pullPolicy: IfNotPresent + +# --------------------------------------------------------------------------- +# Registry Configuration +# --------------------------------------------------------------------------- +registry: + log: + level: info + + s3: + bucket: registry + region: us-east-1 + secure: false + rootdirectory: / + endpointOverride: "" + accesskey: "" + secretkey: "" + v4auth: true + forcepathstyle: true + chunksize: 134217728 + multipartcopythresholdsize: 1073741824 + multipartcopyChunksize: 134217728 + multipartcopyMaxConcurrency: 1 + + storage: + redirect: + disable: false + + http: + addr: ":5000" + secret: "registry_key_2026" + headers: + X-Content-Type-Options: "[nosniff]" + + # Liveness / Readiness + livenessProbe: + enabled: true + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + + readinessProbe: + enabled: true + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + + # Resource limits + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + + # Node scheduling + nodeSelector: {} + tolerations: [] + affinity: {} + + # Extra annotations/labels cho pods + podAnnotations: {} + podLabels: {} + +# --------------------------------------------------------------------------- +# Service +# --------------------------------------------------------------------------- +service: + type: ClusterIP + port: 5000 + nodePort: "" + annotations: {} + +# --------------------------------------------------------------------------- +# Job: Create Bucket +# --------------------------------------------------------------------------- +createBucket: + image: + registry: ghcr.io + repository: svtechnmaa/minio-client + tag: "2025-08-13" + pullPolicy: IfNotPresent + + # Timeout job (seconds) + activeDeadlineSeconds: 120 + + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 1Gi + +# --------------------------------------------------------------------------- +# SeaweedFS (reference) +# --------------------------------------------------------------------------- +seaweedfs: + serviceName: seaweedfs-s3 + namespace: seaweedfs + port: 8333 + protocol: http \ No newline at end of file